├── .github └── workflows │ ├── rust.yml │ └── semgrep.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── odoh_bench.rs ├── examples └── cli.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── decrypt.rs ├── src ├── lib.rs └── protocol.rs └── tests └── test-vectors.json /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Formatting 20 | run: cargo fmt -- --check 21 | - name: Clippy 22 | run: cargo clippy -- -D warnings 23 | - name: Build 24 | run: cargo build --verbose 25 | - name: Run tests 26 | run: cargo test --verbose 27 | - name: Run benches 28 | run: cargo bench 29 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | schedule: 9 | - cron: '0 0 * * *' 10 | name: Semgrep config 11 | jobs: 12 | semgrep: 13 | name: semgrep/ci 14 | runs-on: ubuntu-latest 15 | env: 16 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 17 | SEMGREP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 20 | container: 21 | image: semgrep/semgrep 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "odoh-rs" 3 | version = "1.0.3" 4 | authors = ["Tanya Verma "] 5 | edition = "2018" 6 | license = "BSD-2-Clause" 7 | description = "Rust implementation of the Oblivious DNS over HTTPS (ODoH) protocol version 1" 8 | repository = "https://github.com/cloudflare/odoh-rs/" 9 | keywords = ["odoh", "protocols", "dns", "doh", "privacy"] 10 | categories = ["network-programming", "cryptography"] 11 | include = ["/src", "/examples", "README.md", "LICENSE"] 12 | 13 | [dependencies] 14 | aes-gcm = { version = "0.10", features = [ "std" ] } 15 | bytes = "1.0" 16 | hkdf = "0.12" 17 | hpke = { version = "0.11", features = [ "std", "x25519" ], default-features = false } 18 | rand = { version = "0.8", features = [ "std_rng" ], default-features = false } 19 | thiserror = "1.0" 20 | 21 | [dev-dependencies] 22 | anyhow = "1" 23 | base64 = "0.21.0" 24 | clap = { version = "3.2", features = ["derive"] } 25 | criterion = "0.3" 26 | domain = "0.6" 27 | env_logger = "0.9" 28 | hex = "0.4" 29 | log = "0.4" 30 | rand = "0.8" 31 | reqwest = "0.11" 32 | serde = { version = "1.0", features = ["derive"] } 33 | serde_json = "1.0" 34 | tokio = { version = "1", features = [ "full" ] } 35 | 36 | [[bench]] 37 | name = "odoh_bench" 38 | path = "benches/odoh_bench.rs" 39 | harness = false 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Cloudflare, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odoh-rs 2 | 3 | [![Latest Version]][crates.io] 4 | [![docs.rs](https://docs.rs/odoh-rs/badge.svg)](https://docs.rs/odoh-rs) 5 | 6 | [Latest Version]: https://img.shields.io/crates/v/odoh-rs.svg 7 | [crates.io]: https://crates.io/crates/odoh-rs 8 | 9 | [odoh-rs] is a library that implements [RFC 9230] Oblivious DNS over HTTPS protocol in Rust. 10 | 11 | It can be used to implement an ODoH client or server (target). 12 | [odoh-client-rs] uses `odoh-rs` to implement its functionality, and is a good source of API usage examples, along with the tests in `odoh-rs`, in particular [test_vectors_for_odoh]. 13 | 14 | This library is interoperable with [odoh-go]. 15 | 16 | `odoh-rs` uses [hpke] as the underlying HPKE implementation. It supports the default Oblivious DoH ciphersuite 17 | `(KEM: X25519HkdfSha256, KDF: HkdfSha256, AEAD: AesGcm128)`. 18 | 19 | It does not provide full crypto agility. 20 | 21 | [odoh-rs]: https://github.com/cloudflare/odoh-rs/ 22 | [RFC 9230]: https://datatracker.ietf.org/doc/rfc9230/ 23 | [odoh-client-rs]: https://github.com/cloudflare/odoh-client-rs/ 24 | [odoh-go]: https://github.com/cloudflare/odoh-go 25 | [test_vectors_for_odoh]: https://github.com/cloudflare/odoh-rs/blob/master/tests/test-vectors.json 26 | [hpke]: https://github.com/rozbb/rust-hpke 27 | 28 | # Example API Usage 29 | 30 | This example outlines the steps necessary for a successful ODoH query. 31 | 32 | ```rust 33 | // Use a seed to initialize a RNG. *Note* you should rely on some 34 | // random source. 35 | let mut rng = StdRng::from_seed([0; 32]); 36 | 37 | // Generate a key pair on server side. 38 | let key_pair = ObliviousDoHKeyPair::new(&mut rng); 39 | 40 | // Create client configs from the key pair. It can be distributed 41 | // to the clients. 42 | let public_key = key_pair.public().clone(); 43 | let client_configs: ObliviousDoHConfigs = vec![ObliviousDoHConfig::from(public_key)].into(); 44 | let client_configs_bytes = compose(&client_configs).unwrap().freeze(); 45 | 46 | // ... distributing client_configs_bytes ... 47 | 48 | // Parse and extract first supported config from client configs on client side. 49 | let client_configs: ObliviousDoHConfigs = parse(&mut client_configs_bytes.clone()).unwrap(); 50 | let client_config = client_configs.into_iter().next().unwrap(); 51 | let config_contents = client_config.into(); 52 | 53 | // This is a example client request. This library doesn't validate 54 | // DNS message. 55 | let query = ObliviousDoHMessagePlaintext::new(b"What's the IP of one.one.one.one?", 0); 56 | 57 | // Encrypt the above request. The client_secret returned will be 58 | // used later to decrypt server's response. 59 | let (query_enc, cli_secret) = encrypt_query(&query, &config_contents, &mut rng).unwrap(); 60 | 61 | // ... sending query_enc to the server ... 62 | 63 | // Server decrypt request. 64 | let (query_dec, srv_secret) = decrypt_query(&query_enc, &key_pair).unwrap(); 65 | assert_eq!(query, query_dec); 66 | 67 | // Server could now resolve the decrypted query, and compose a response. 68 | let response = ObliviousDoHMessagePlaintext::new(b"The IP is 1.1.1.1", 0); 69 | 70 | // server encrypt response 71 | let nonce = ResponseNonce::default(); 72 | let response_enc = encrypt_response(&query_dec, &response, srv_secret, nonce).unwrap(); 73 | 74 | // ... sending response_enc back to the client ... 75 | 76 | // client descrypt response 77 | let response_dec = decrypt_response(&query, &response_enc, cli_secret).unwrap(); 78 | assert_eq!(response, response_dec); 79 | ``` 80 | -------------------------------------------------------------------------------- /benches/odoh_bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use odoh_rs::*; 3 | use rand::rngs::StdRng; 4 | use rand::SeedableRng; 5 | 6 | pub fn bench_steps(c: &mut Criterion) { 7 | // generate all the data for this test 8 | let mut rng = StdRng::from_seed([0; 32]); 9 | let key_pair = ObliviousDoHKeyPair::new(&mut rng); 10 | 11 | let query = ObliviousDoHMessagePlaintext::new(b"What's the IP of one.one.one.one?", 0); 12 | let query_bytes = compose(&query).unwrap().freeze(); 13 | 14 | let response = ObliviousDoHMessagePlaintext::new(b"The IP is 1.1.1.1", 0); 15 | let response_bytes = compose(&response).unwrap().freeze(); 16 | 17 | let (query_enc, cli_secret) = encrypt_query(&query, key_pair.public(), &mut rng).unwrap(); 18 | let query_enc_bytes = compose(&query_enc).unwrap().freeze(); 19 | 20 | let (query_dec, srv_secret) = decrypt_query(&query_enc, &key_pair).unwrap(); 21 | //let query_dec_bytes = compose(&query_dec).unwrap().freeze(); 22 | 23 | let nonce = ResponseNonce::default(); 24 | let response_enc = encrypt_response(&query_dec, &response, srv_secret, nonce).unwrap(); 25 | let response_enc_bytes = compose(&response_enc).unwrap().freeze(); 26 | 27 | c.bench_function("step_encrypt_query", |b| { 28 | b.iter(|| { 29 | black_box({ 30 | let query = parse(&mut query_bytes.clone()).unwrap(); 31 | encrypt_query(&query, key_pair.public(), &mut rng).unwrap() 32 | }) 33 | }) 34 | }); 35 | 36 | c.bench_function("step_decrypt_query", |b| { 37 | b.iter(|| { 38 | black_box({ 39 | let query_enc = parse(&mut query_enc_bytes.clone()).unwrap(); 40 | decrypt_query(&query_enc, &key_pair).unwrap() 41 | }) 42 | }) 43 | }); 44 | 45 | c.bench_function("step_encrypt_response", |b| { 46 | b.iter(|| { 47 | black_box({ 48 | let nonce = ResponseNonce::default(); 49 | let response = parse(&mut response_bytes.clone()).unwrap(); 50 | encrypt_response(&response, &response, srv_secret, nonce).unwrap() 51 | }) 52 | }) 53 | }); 54 | 55 | c.bench_function("step_decrypt_response", |b| { 56 | b.iter(|| { 57 | black_box({ 58 | let response_enc = parse(&mut response_enc_bytes.clone()).unwrap(); 59 | decrypt_response(&query, &response_enc, cli_secret).unwrap() 60 | }) 61 | }) 62 | }); 63 | } 64 | 65 | criterion_group!(benches, bench_steps,); 66 | criterion_main!(benches); 67 | -------------------------------------------------------------------------------- /examples/cli.rs: -------------------------------------------------------------------------------- 1 | // A basic client example. 2 | 3 | use anyhow::{Context, Result}; 4 | use base64::{engine::general_purpose, Engine as _}; 5 | use clap::Parser; 6 | use domain::base::{Dname as DnameO, Message, MessageBuilder, ParsedDname, Rtype}; 7 | use domain::rdata::AllRecordData; 8 | use log::{info, trace}; 9 | use odoh_rs::*; 10 | use rand::rngs::StdRng; 11 | use rand::{Rng, SeedableRng}; 12 | use reqwest::{Client, RequestBuilder, Url}; 13 | 14 | type Dname = DnameO>; 15 | 16 | const WELL_KNOWN_CONF_PATH: &str = "/.well-known/odohconfigs"; 17 | 18 | #[derive(Parser, Debug)] 19 | #[clap(version)] 20 | struct Opts { 21 | #[clap( 22 | short, 23 | long, 24 | help = "Specific query domain", 25 | default_value = "cloudflare.com" 26 | )] 27 | domain: Dname, 28 | #[clap( 29 | name = "type", 30 | short, 31 | long, 32 | help = "Specific query type", 33 | default_value = "AAAA" 34 | )] 35 | rtype: Rtype, 36 | #[clap( 37 | short, 38 | long, 39 | help = "Target base URL", 40 | default_value = "https://odoh.cloudflare-dns.com" 41 | )] 42 | service: Url, 43 | #[clap(short, long, help = "Use user provided config, encoded in hexstring")] 44 | configs: Option, 45 | #[clap( 46 | name = "header", 47 | long, 48 | help = "Extra header to add, in \"key:value\" format, can appear multiple times", 49 | value_parser 50 | )] 51 | headers: Vec, 52 | #[clap( 53 | name = "step", 54 | long, 55 | help = "Dump encrypted query and read the encrypted response from user instead", 56 | value_parser 57 | )] 58 | step: bool, 59 | } 60 | 61 | #[tokio::main] 62 | async fn main() -> Result<()> { 63 | env_logger::init(); 64 | let opts: Opts = Opts::parse(); 65 | 66 | let cli = Client::new(); 67 | let configs_bytes = if let Some(s) = opts.configs { 68 | info!("Use user provided configs"); 69 | hex::decode(s).context("Invalid hex value of configs")? 70 | } else { 71 | trace!("Retrieving ODoH configs"); 72 | let conf_url = opts 73 | .service 74 | .join(WELL_KNOWN_CONF_PATH) 75 | .context("Failed to compose well-known config path")?; 76 | 77 | let mut req_builder = cli.get(conf_url); 78 | req_builder = add_headers(req_builder, &opts.headers); 79 | let body = req_builder 80 | .send() 81 | .await 82 | .context("failed to make request for config")? 83 | .bytes() 84 | .await 85 | .context("failed to get body")?; 86 | body.to_vec() 87 | }; 88 | 89 | let configs: ObliviousDoHConfigs = 90 | parse(&mut (configs_bytes.as_ref())).context("invalid configs")?; 91 | let config = configs 92 | .into_iter() 93 | .next() 94 | .context("no available config")? 95 | .into(); 96 | 97 | trace!("Creating DNS message"); 98 | let mut msg = MessageBuilder::new_vec(); 99 | msg.header_mut().set_rd(true); 100 | let mut msg = msg.question(); 101 | msg.push((opts.domain, opts.rtype)) 102 | .context("failed to push question")?; 103 | let msg = msg.finish(); 104 | 105 | let mut rng = StdRng::from_entropy(); 106 | 107 | // add a random padding for testing purpose 108 | let padding_len = rng.gen_range(0..10); 109 | let query = ObliviousDoHMessagePlaintext::new(&msg, padding_len); 110 | trace!( 111 | "Encrypting DNS message with {} bytes of padding", 112 | padding_len 113 | ); 114 | let (query_enc, cli_secret) = 115 | encrypt_query(&query, &config, &mut rng).context("failed to encrypt query")?; 116 | let query_body = compose(&query_enc) 117 | .context("failed to compose query body")? 118 | .freeze(); 119 | 120 | let mut resp_body = if opts.step { 121 | println!( 122 | "* Encrypted request in base64: {}\n* Paste the encrypted response in base64 below:", 123 | general_purpose::URL_SAFE.encode(&query_body) 124 | ); 125 | let mut buffer = String::new(); 126 | std::io::stdin().read_line(&mut buffer)?; 127 | println!("decoding [{}]", buffer.trim()); 128 | general_purpose::URL_SAFE.decode(&buffer.trim())?.into() 129 | } else { 130 | trace!("Exchanging with server"); 131 | let mut req_builder = cli 132 | .post(opts.service.join("/dns-query")?) 133 | .header("content-type", ODOH_HTTP_HEADER) 134 | .header("accept", ODOH_HTTP_HEADER); 135 | req_builder = add_headers(req_builder, &opts.headers); 136 | 137 | req_builder 138 | .body(query_body) 139 | .send() 140 | .await 141 | .context("failed to query target server")? 142 | .bytes() 143 | .await 144 | .context("failed to get response body")? 145 | }; 146 | 147 | trace!("Decrypting DNS message"); 148 | let response_enc = parse(&mut resp_body).context("failed to parse response body")?; 149 | let response_dec = decrypt_response(&query, &response_enc, cli_secret) 150 | .context("failed to decrypt resopnse")?; 151 | 152 | let msg_bytes = response_dec.into_msg(); 153 | let msg = 154 | Message::from_octets(msg_bytes.as_ref()).context("failed to parse response message")?; 155 | 156 | trace!("Printing DNS response"); 157 | for (rr, _) in msg.for_slice().iter().filter_map(|r| r.ok()) { 158 | if rr.rtype() == Rtype::Opt { 159 | return Ok(()); 160 | } 161 | 162 | if let Ok(Some(rr)) = rr.to_record::>>() { 163 | println!( 164 | "{}\t{}\t{}\t{}\t{}", 165 | rr.owner(), 166 | rr.ttl(), 167 | rr.class(), 168 | rr.rtype(), 169 | rr.data() 170 | ) 171 | } else { 172 | println!( 173 | "{}\t{}\t{}\t{}", 174 | rr.owner(), 175 | rr.ttl(), 176 | rr.class(), 177 | rr.rtype() 178 | ) 179 | } 180 | } 181 | 182 | Ok(()) 183 | } 184 | 185 | fn add_headers(mut builder: RequestBuilder, headers: &[String]) -> RequestBuilder { 186 | for header in headers { 187 | if let Some((k, v)) = header.split_once(':') { 188 | builder = builder.header(k.trim(), v.trim()); 189 | } 190 | } 191 | builder 192 | } 193 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "odoh-rs-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2018" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | libfuzzer-sys = "0.4" 12 | rand = "0.8.5" 13 | 14 | [dependencies.odoh-rs] 15 | path = ".." 16 | 17 | # Prevent this from interfering with workspaces 18 | [workspace] 19 | members = ["."] 20 | 21 | [profile.release] 22 | debug = 1 23 | 24 | [[bin]] 25 | name = "decrypt" 26 | path = "fuzz_targets/decrypt.rs" 27 | test = false 28 | doc = false 29 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/decrypt.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use odoh_rs::{decrypt_query, Deserialize, ObliviousDoHKeyPair, ObliviousDoHMessage}; 5 | use rand::rngs::StdRng; 6 | use rand::SeedableRng; 7 | use std::hint::black_box; 8 | 9 | fuzz_target!(|data: Vec| { 10 | let mut rng = StdRng::from_seed([0; 32]); 11 | let key_pair = ObliviousDoHKeyPair::new(&mut rng); 12 | 13 | let slice = &mut data.as_slice(); 14 | let Ok(msg) = ObliviousDoHMessage::deserialize(slice) else { 15 | return; 16 | }; 17 | 18 | let _ = black_box(decrypt_query(&msg, &key_pair)); 19 | }); 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | [odoh-rs] is a library that implements [RFC 9230] Oblivious DNS over HTTPS protocol in Rust. 4 | 5 | It can be used to implement an ODoH client or server (target). 6 | [odoh-client-rs] uses `odoh-rs` to implement its functionality, and is a good source of API usage examples, along with the tests in `odoh-rs`, in particular [test_vectors_for_odoh]. 7 | 8 | This library is interoperable with [odoh-go]. 9 | 10 | `odoh-rs` uses [hpke] as the underlying HPKE implementation. It supports the default Oblivious DoH ciphersuite 11 | `(KEM: X25519HkdfSha256, KDF: HkdfSha256, AEAD: AesGcm128)`. 12 | 13 | It does not provide full crypto agility. 14 | 15 | [odoh-rs]: https://github.com/cloudflare/odoh-rs/ 16 | [RFC 9230]: https://datatracker.ietf.org/doc/rfc9230/ 17 | [odoh-client-rs]: https://github.com/cloudflare/odoh-client-rs/ 18 | [odoh-go]: https://github.com/cloudflare/odoh-go 19 | [test_vectors_for_odoh]: https://github.com/cloudflare/odoh-rs/blob/master/tests/test-vectors.json 20 | [hpke]: https://github.com/rozbb/rust-hpke 21 | 22 | # Example API usage 23 | 24 | This example outlines the steps necessary for a successful ODoH query. 25 | 26 | ``` 27 | # use crate::odoh_rs::*; 28 | # use rand::{rngs::StdRng, SeedableRng}; 29 | 30 | // Use a seed to initialize a RNG. *Note* you should rely on some 31 | // random source. 32 | let mut rng = StdRng::from_seed([0; 32]); 33 | 34 | // Generate a key pair on server side. 35 | let key_pair = ObliviousDoHKeyPair::new(&mut rng); 36 | 37 | // Create client configs from the key pair. It can be distributed 38 | // to the clients. 39 | let public_key = key_pair.public().clone(); 40 | let client_configs: ObliviousDoHConfigs = vec![ObliviousDoHConfig::from(public_key)].into(); 41 | let client_configs_bytes = compose(&client_configs).unwrap().freeze(); 42 | 43 | // ... distributing client_configs_bytes ... 44 | 45 | // Parse and extract first supported config from client configs on client side. 46 | let client_configs: ObliviousDoHConfigs = parse(&mut client_configs_bytes.clone()).unwrap(); 47 | let client_config = client_configs.into_iter().next().unwrap(); 48 | let config_contents = client_config.into(); 49 | 50 | // This is a example client request. This library doesn't validate 51 | // DNS message. 52 | let query = ObliviousDoHMessagePlaintext::new(b"What's the IP of one.one.one.one?", 0); 53 | 54 | // Encrypt the above request. The client_secret returned will be 55 | // used later to decrypt server's response. 56 | let (query_enc, cli_secret) = encrypt_query(&query, &config_contents, &mut rng).unwrap(); 57 | 58 | // ... sending query_enc to the server ... 59 | 60 | // Server decrypt request. 61 | let (query_dec, srv_secret) = decrypt_query(&query_enc, &key_pair).unwrap(); 62 | assert_eq!(query, query_dec); 63 | 64 | // Server could now resolve the decrypted query, and compose a response. 65 | let response = ObliviousDoHMessagePlaintext::new(b"The IP is 1.1.1.1", 0); 66 | 67 | // server encrypt response 68 | let nonce = ResponseNonce::default(); 69 | let response_enc = encrypt_response(&query_dec, &response, srv_secret, nonce).unwrap(); 70 | 71 | // ... sending response_enc back to the client ... 72 | 73 | // client descrypt response 74 | let response_dec = decrypt_response(&query, &response_enc, cli_secret).unwrap(); 75 | assert_eq!(response, response_dec); 76 | ``` 77 | */ 78 | 79 | mod protocol; 80 | 81 | pub use protocol::*; 82 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | //! API for protocol functionality such as creating and parsing ODoH queries and responses. 2 | 3 | #![deny(missing_docs)] 4 | 5 | use aes_gcm::aead::generic_array::GenericArray; 6 | use aes_gcm::aead::{AeadInPlace, KeyInit}; 7 | use aes_gcm::Aes128Gcm; 8 | use bytes::{Buf, BufMut, Bytes, BytesMut}; 9 | use hkdf::Hkdf; 10 | use hpke::aead::{Aead as AeadTrait, AesGcm128}; 11 | use hpke::kdf::{HkdfSha256, Kdf as KdfTrait}; 12 | use hpke::kem::X25519HkdfSha256; 13 | use hpke::{Deserializable, HpkeError, Kem as KemTrait, OpModeR, OpModeS, Serializable}; 14 | use rand::{CryptoRng, RngCore}; 15 | use std::convert::{TryFrom, TryInto}; 16 | use thiserror::Error as ThisError; 17 | 18 | // Extra info string used by various crypto routines. 19 | const LABEL_QUERY: &[u8] = b"odoh query"; 20 | const LABEL_KEY: &[u8] = b"odoh key"; 21 | const LABEL_NONCE: &[u8] = b"odoh nonce"; 22 | const LABEL_KEY_ID: &[u8] = b"odoh key id"; 23 | const LABEL_RESPONSE: &[u8] = b"odoh response"; 24 | 25 | // The fixed HPKE ciphersuite this crate supports, and their associated constants 26 | type Kem = X25519HkdfSha256; 27 | type Aead = AesGcm128; 28 | type Kdf = HkdfSha256; 29 | const KEM_ID: u16 = Kem::KEM_ID; 30 | const KDF_ID: u16 = Kdf::KDF_ID; 31 | const AEAD_ID: u16 = Aead::AEAD_ID; 32 | 33 | /// For the selected KDF: SHA256 34 | const KDF_OUTPUT_SIZE: usize = 32; 35 | const AEAD_KEY_SIZE: usize = 16; 36 | const AEAD_NONCE_SIZE: usize = 12; 37 | 38 | /// This is the maximum of `AEAD_KEY_SIZE` and `AEAD_NONCE_SIZE` 39 | const RESPONSE_NONCE_SIZE: usize = 16; 40 | 41 | /// Length of public key used in config 42 | const PUBLIC_KEY_SIZE: usize = 32; 43 | 44 | type AeadKey = [u8; AEAD_KEY_SIZE]; 45 | type AeadNonce = [u8; AEAD_NONCE_SIZE]; 46 | 47 | /// Secret used in encrypt/decrypt API. 48 | pub type OdohSecret = [u8; AEAD_KEY_SIZE]; 49 | 50 | /// Response nonce needed by [`encrypt_response`](fn.encrypt_response.html) 51 | pub type ResponseNonce = [u8; RESPONSE_NONCE_SIZE]; 52 | 53 | /// HTTP content-type header required for sending queries and responses 54 | pub const ODOH_HTTP_HEADER: &str = "application/oblivious-dns-message"; 55 | 56 | /// ODoH version supported by this library 57 | pub const ODOH_VERSION: u16 = 0x0001; 58 | 59 | /// Errors generated by this crate. 60 | #[derive(ThisError, Debug, Clone, PartialEq, Eq)] 61 | pub enum Error { 62 | /// Input data is too short. 63 | #[error("Input data is too short")] 64 | ShortInput, 65 | /// Input data has incorrect length. 66 | #[error("Input data has incorrect length")] 67 | InvalidInputLength, 68 | /// Padding is not zero. 69 | #[error("Padding is not zero")] 70 | InvalidPadding, 71 | /// Config parameter is invalid. 72 | #[error("Config parameter is invalid")] 73 | InvalidParameter, 74 | /// Type byte in ObliviousDoHMessage is invalid. 75 | #[error("Type byte in ObliviousDoHMessage is invalid")] 76 | InvalidMessageType, 77 | /// Message key_id does not match public key. 78 | #[error("Message key_id does not match public key")] 79 | KeyIdMismatch, 80 | /// Response nonce is not equal to max(key, nonce) size. 81 | #[error("Response nonce is not equal to max(key, nonce) size")] 82 | InvalidResponseNonceLength, 83 | 84 | /// Errors from hpke crate. 85 | #[error(transparent)] 86 | Hpke(#[from] HpkeError), 87 | 88 | /// Errors from aes-gcm crate. 89 | #[error(transparent)] 90 | AesGcm(#[from] aes_gcm::Error), 91 | 92 | /// Unexpected internal error. 93 | #[error("Unexpected internal error")] 94 | Internal, 95 | } 96 | 97 | type Result = std::result::Result; 98 | 99 | /// Serialize to IETF wireformat that is similar to [XDR](https://tools.ietf.org/html/rfc1014) 100 | pub trait Serialize { 101 | /// Serialize the provided struct into the buf. 102 | fn serialize(self, buf: &mut B) -> Result<()>; 103 | } 104 | 105 | /// Deserialize from IETF wireformat that is similar to [XDR](https://tools.ietf.org/html/rfc1014) 106 | pub trait Deserialize { 107 | /// Deserialize a struct from the buf. 108 | fn deserialize(buf: &mut B) -> Result 109 | where 110 | Self: Sized; 111 | } 112 | 113 | /// Convenient function to deserialize a structure from Bytes. 114 | pub fn parse(buf: &mut B) -> Result { 115 | D::deserialize(buf) 116 | } 117 | 118 | /// Convenient function to serialize a structure into a new BytesMut. 119 | pub fn compose(s: S) -> Result { 120 | let mut buf = BytesMut::new(); 121 | s.serialize(&mut buf)?; 122 | Ok(buf) 123 | } 124 | 125 | fn read_lengthed(b: &mut B) -> Result { 126 | if b.remaining() < 2 { 127 | return Err(Error::ShortInput); 128 | } 129 | 130 | let len = b.get_u16() as usize; 131 | 132 | if len > b.remaining() { 133 | return Err(Error::InvalidInputLength); 134 | } 135 | 136 | Ok(b.copy_to_bytes(len)) 137 | } 138 | 139 | /// Supplies config information to the client. 140 | /// 141 | /// It contains one or more `ObliviousDoHConfig` structures in 142 | /// decreasing order of preference. This allows a server to support multiple versions 143 | /// of ODoH and multiple sets of ODoH HPKE suite parameters. 144 | /// 145 | /// This information is designed to be disseminated via [DNS HTTPS 146 | /// records](https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-03), 147 | /// using the param `odohconfig`. 148 | #[derive(Debug, Clone)] 149 | pub struct ObliviousDoHConfigs { 150 | // protocol: length prefix 151 | configs: Vec, 152 | } 153 | 154 | impl ObliviousDoHConfigs { 155 | /// Filter the list of configs, leave ones matches ODOH_VERSION. 156 | pub fn supported(self) -> Vec { 157 | self.into_iter().collect() 158 | } 159 | } 160 | 161 | type VecIter = std::vec::IntoIter; 162 | impl IntoIterator for ObliviousDoHConfigs { 163 | type Item = ObliviousDoHConfig; 164 | type IntoIter = std::iter::Filter bool>; 165 | 166 | fn into_iter(self) -> Self::IntoIter { 167 | self.configs 168 | .into_iter() 169 | .filter(|c| c.version == ODOH_VERSION) 170 | } 171 | } 172 | 173 | impl From> for ObliviousDoHConfigs { 174 | fn from(configs: Vec) -> Self { 175 | Self { configs } 176 | } 177 | } 178 | 179 | impl Serialize for &ObliviousDoHConfigs { 180 | fn serialize(self, buf: &mut B) -> Result<()> { 181 | // calculate total length 182 | let mut len = 0; 183 | for c in self.configs.iter() { 184 | // 2 bytes of version and 2 bytes of length 185 | len += 2 + 2 + c.length; 186 | } 187 | 188 | buf.put_u16(len); 189 | for c in self.configs.iter() { 190 | c.serialize(buf)?; 191 | } 192 | 193 | Ok(()) 194 | } 195 | } 196 | 197 | impl Deserialize for ObliviousDoHConfigs { 198 | fn deserialize(buf: &mut B) -> Result { 199 | let mut buf = read_lengthed(buf)?; 200 | 201 | let mut configs = Vec::new(); 202 | loop { 203 | if buf.is_empty() { 204 | break; 205 | } 206 | let c = parse(&mut buf)?; 207 | configs.push(c); 208 | } 209 | 210 | Ok(Self { configs }) 211 | } 212 | } 213 | 214 | /// Contains version and encryption information. Based on the version specified, 215 | /// the contents can differ. 216 | /// 217 | /// For `ODOH_VERSION = 0x0001`, `ObliviousDoHConfig::contents` 218 | /// deserializes into 219 | /// [ObliviousDoHConfigContents](./../struct.ObliviousDoHConfigContents.html). 220 | #[derive(Debug, Clone, PartialEq, Eq)] 221 | pub struct ObliviousDoHConfig { 222 | version: u16, 223 | length: u16, 224 | contents: ObliviousDoHConfigContents, 225 | } 226 | 227 | impl Serialize for &ObliviousDoHConfig { 228 | fn serialize(self, buf: &mut B) -> Result<()> { 229 | buf.put_u16(self.version); 230 | buf.put_u16(self.length); 231 | self.contents.serialize(buf) 232 | } 233 | } 234 | 235 | impl Deserialize for ObliviousDoHConfig { 236 | fn deserialize(mut buf: &mut B) -> Result { 237 | if buf.remaining() < 2 { 238 | return Err(Error::ShortInput); 239 | } 240 | let version = buf.get_u16(); 241 | let mut contents = read_lengthed(&mut buf)?; 242 | let length = contents.len() as u16; 243 | 244 | Ok(Self { 245 | version, 246 | length, 247 | contents: parse(&mut contents)?, 248 | }) 249 | } 250 | } 251 | 252 | impl From for ObliviousDoHConfigContents { 253 | fn from(c: ObliviousDoHConfig) -> Self { 254 | c.contents 255 | } 256 | } 257 | 258 | impl From for ObliviousDoHConfig { 259 | fn from(c: ObliviousDoHConfigContents) -> Self { 260 | Self { 261 | version: ODOH_VERSION, 262 | length: c.len() as u16, 263 | contents: c, 264 | } 265 | } 266 | } 267 | 268 | /// Contains the HPKE suite parameters and the 269 | /// resolver (target's) public key. 270 | #[derive(Debug, Clone, PartialEq, Eq)] 271 | pub struct ObliviousDoHConfigContents { 272 | kem_id: u16, 273 | kdf_id: u16, 274 | aead_id: u16, 275 | // protocol: length prefix 276 | public_key: Bytes, 277 | } 278 | 279 | impl ObliviousDoHConfigContents { 280 | /// Creates a KeyID for an `ObliviousDoHConfigContents` struct 281 | pub fn identifier(&self) -> Result> { 282 | let buf = compose(self)?; 283 | 284 | let key_id_info = LABEL_KEY_ID.to_vec(); 285 | let prk = Hkdf::<::HashImpl>::new(None, &buf); 286 | let mut key_id = [0; KDF_OUTPUT_SIZE]; 287 | prk.expand(&key_id_info, &mut key_id) 288 | .map_err(|_| Error::from(HpkeError::KdfOutputTooLong))?; 289 | Ok(key_id.to_vec()) 290 | } 291 | 292 | fn len(&self) -> usize { 293 | 2 + 2 + 2 + 2 + self.public_key.len() 294 | } 295 | } 296 | 297 | impl Serialize for &ObliviousDoHConfigContents { 298 | fn serialize(self, buf: &mut B) -> Result<()> { 299 | buf.put_u16(self.kem_id); 300 | buf.put_u16(self.kdf_id); 301 | buf.put_u16(self.aead_id); 302 | 303 | buf.put_u16(to_u16(self.public_key.len())?); 304 | buf.put(self.public_key.clone()); 305 | Ok(()) 306 | } 307 | } 308 | 309 | impl Deserialize for ObliviousDoHConfigContents { 310 | fn deserialize(mut buf: &mut B) -> Result { 311 | if buf.remaining() < 2 + 2 + 2 { 312 | return Err(Error::ShortInput); 313 | } 314 | 315 | let kem_id = buf.get_u16(); 316 | let kdf_id = buf.get_u16(); 317 | let aead_id = buf.get_u16(); 318 | 319 | if kem_id != KEM_ID || kdf_id != KDF_ID || aead_id != AEAD_ID { 320 | return Err(Error::InvalidParameter); 321 | } 322 | 323 | let public_key = read_lengthed(&mut buf)?; 324 | if public_key.len() != PUBLIC_KEY_SIZE { 325 | return Err(Error::InvalidInputLength); 326 | } 327 | 328 | Ok(Self { 329 | kem_id, 330 | kdf_id, 331 | aead_id, 332 | public_key, 333 | }) 334 | } 335 | } 336 | 337 | /// `ObliviousDoHMessageType` is supplied at the beginning of every ODoH message. 338 | /// It is used to specify whether a message is a query or a response. 339 | #[derive(Debug, Clone, Eq, PartialEq, Copy)] 340 | enum ObliviousDoHMessageType { 341 | Query = 1, 342 | Response = 2, 343 | } 344 | 345 | impl TryFrom for ObliviousDoHMessageType { 346 | type Error = Error; 347 | fn try_from(n: u8) -> Result { 348 | match n { 349 | 1 => Ok(Self::Query), 350 | 2 => Ok(Self::Response), 351 | _ => Err(Error::InvalidMessageType), 352 | } 353 | } 354 | } 355 | 356 | /// Main structure used to transfer queries and responses. 357 | /// 358 | /// It specifies a message type, an identifier of the corresponding `ObliviousDoHConfigContents` 359 | /// structure being used, and the encrypted message for the target resolver, or a DNS response 360 | /// message for the client. 361 | pub struct ObliviousDoHMessage { 362 | msg_type: ObliviousDoHMessageType, 363 | // protocol: length prefix 364 | key_id: Bytes, 365 | // protocol: length prefix 366 | encrypted_msg: Bytes, 367 | } 368 | 369 | impl ObliviousDoHMessage { 370 | /// Returns the key ID contained in this message. 371 | pub fn key_id(&self) -> &[u8] { 372 | self.key_id.as_ref() 373 | } 374 | } 375 | 376 | impl Deserialize for ObliviousDoHMessage { 377 | fn deserialize(mut buf: &mut B) -> Result { 378 | if !buf.has_remaining() { 379 | return Err(Error::ShortInput); 380 | } 381 | 382 | let msg_type = buf.get_u8().try_into()?; 383 | let key_id = read_lengthed(&mut buf)?; 384 | let encrypted_msg = read_lengthed(&mut buf)?; 385 | 386 | Ok(Self { 387 | msg_type, 388 | key_id, 389 | encrypted_msg, 390 | }) 391 | } 392 | } 393 | 394 | impl Serialize for &ObliviousDoHMessage { 395 | fn serialize(self, buf: &mut B) -> Result<()> { 396 | buf.put_u8(self.msg_type as u8); 397 | buf.put_u16(to_u16(self.key_id.len())?); 398 | buf.put(self.key_id.clone()); 399 | buf.put_u16(to_u16(self.encrypted_msg.len())?); 400 | buf.put(self.encrypted_msg.clone()); 401 | Ok(()) 402 | } 403 | } 404 | 405 | /// Structure holding unencrypted dns message and padding. 406 | #[derive(Debug, Clone, Eq, PartialEq)] 407 | pub struct ObliviousDoHMessagePlaintext { 408 | // protocol: length prefix 409 | dns_msg: Bytes, 410 | // protocol: length prefix 411 | padding: Bytes, 412 | } 413 | 414 | impl ObliviousDoHMessagePlaintext { 415 | /// Create a new [`ObliviousDoHMessagePlaintext`] from DNS message 416 | /// bytes and an optional padding. 417 | /// 418 | /// [`ObliviousDoHMessagePlaintext`]: struct.ObliviousDoHMessagePlaintext.html 419 | pub fn new>(msg: M, padding_len: usize) -> Self { 420 | Self { 421 | dns_msg: msg.as_ref().to_vec().into(), 422 | padding: vec![0; padding_len].into(), 423 | } 424 | } 425 | 426 | /// Consume the struct, return the inner DNS message bytes. 427 | pub fn into_msg(self) -> Bytes { 428 | self.dns_msg 429 | } 430 | 431 | /// Return the length of padding. 432 | pub fn padding_len(&self) -> usize { 433 | self.padding.len() 434 | } 435 | } 436 | 437 | impl Deserialize for ObliviousDoHMessagePlaintext { 438 | fn deserialize(buf: &mut B) -> Result { 439 | let dns_msg = read_lengthed(buf)?; 440 | let padding = read_lengthed(buf)?; 441 | 442 | if !padding.iter().all(|&x| x == 0x00) { 443 | return Err(Error::InvalidPadding); 444 | } 445 | 446 | Ok(Self { dns_msg, padding }) 447 | } 448 | } 449 | 450 | impl Serialize for &ObliviousDoHMessagePlaintext { 451 | fn serialize(self, buf: &mut B) -> Result<()> { 452 | if !self.padding.iter().all(|&x| x == 0x00) { 453 | return Err(Error::InvalidPadding); 454 | } 455 | 456 | buf.put_u16(to_u16(self.dns_msg.len())?); 457 | buf.put(self.dns_msg.clone()); 458 | 459 | buf.put_u16(to_u16(self.padding.len())?); 460 | buf.put(self.padding.clone()); 461 | 462 | Ok(()) 463 | } 464 | } 465 | 466 | /// `ObliviousDoHKeyPair` supplies relevant encryption/decryption information 467 | /// required by the target resolver to process DNS queries. 468 | #[derive(Clone)] 469 | pub struct ObliviousDoHKeyPair { 470 | private_key: ::PrivateKey, 471 | public_key: ObliviousDoHConfigContents, 472 | } 473 | 474 | impl ObliviousDoHKeyPair { 475 | /// Generate a new keypair from given RNG. 476 | pub fn new(mut rng: &mut R) -> Self { 477 | let (private_key, public_key) = Kem::gen_keypair(&mut rng); 478 | 479 | let contents = ObliviousDoHConfigContents { 480 | kem_id: KEM_ID, 481 | kdf_id: KDF_ID, 482 | aead_id: AEAD_ID, 483 | public_key: public_key.to_bytes().to_vec().into(), 484 | }; 485 | 486 | Self { 487 | private_key, 488 | public_key: contents, 489 | } 490 | } 491 | 492 | /// Create a key pair from provided parameters. 493 | pub fn from_parameters(kem_id: u16, kdf_id: u16, aead_id: u16, ikm: &[u8]) -> Self { 494 | // derive keypair from ikm 495 | let (private_key, public_key) = Kem::derive_keypair(ikm); 496 | Self { 497 | private_key, 498 | public_key: ObliviousDoHConfigContents { 499 | kem_id, 500 | kdf_id, 501 | aead_id, 502 | public_key: public_key.to_bytes().to_vec().into(), 503 | }, 504 | } 505 | } 506 | 507 | /// Return a reference of the private key. 508 | pub fn private(&self) -> &::PrivateKey { 509 | &self.private_key 510 | } 511 | 512 | /// Return a reference of the public key. 513 | pub fn public(&self) -> &ObliviousDoHConfigContents { 514 | &self.public_key 515 | } 516 | } 517 | 518 | /// Encrypt a client DNS query with a proper config, return the 519 | /// encrypted query and client secret. 520 | pub fn encrypt_query( 521 | query: &ObliviousDoHMessagePlaintext, 522 | config: &ObliviousDoHConfigContents, 523 | rng: &mut R, 524 | ) -> Result<(ObliviousDoHMessage, OdohSecret)> { 525 | let server_pk = ::PublicKey::from_bytes(&config.public_key)?; 526 | let (encapped_key, mut send_ctx) = 527 | hpke::setup_sender::(&OpModeS::Base, &server_pk, LABEL_QUERY, rng)?; 528 | 529 | let key_id = config.identifier()?; 530 | let aad = build_aad(ObliviousDoHMessageType::Query, &key_id)?; 531 | 532 | let mut odoh_secret = OdohSecret::default(); 533 | send_ctx.export(LABEL_RESPONSE, &mut odoh_secret)?; 534 | 535 | let mut buf = compose(query)?; 536 | 537 | let tag = send_ctx.seal_in_place_detached(&mut buf, &aad)?; 538 | 539 | let result = [ 540 | encapped_key.to_bytes().as_slice(), 541 | &buf, 542 | tag.to_bytes().as_slice(), 543 | ] 544 | .concat(); 545 | 546 | let msg = ObliviousDoHMessage { 547 | msg_type: ObliviousDoHMessageType::Query, 548 | key_id: key_id.to_vec().into(), 549 | encrypted_msg: result.into(), 550 | }; 551 | 552 | Ok((msg, odoh_secret)) 553 | } 554 | 555 | /// Decrypt a DNS response from the server. 556 | pub fn decrypt_response( 557 | query: &ObliviousDoHMessagePlaintext, 558 | response: &ObliviousDoHMessage, 559 | secret: OdohSecret, 560 | ) -> Result { 561 | if response.msg_type != ObliviousDoHMessageType::Response { 562 | return Err(Error::InvalidMessageType); 563 | } 564 | 565 | let response_nonce = response 566 | .key_id 567 | .as_ref() 568 | .try_into() 569 | .map_err(|_| Error::InvalidResponseNonceLength)?; 570 | let (key, nonce) = derive_secrets(secret, query, response_nonce)?; 571 | let cipher = Aes128Gcm::new(GenericArray::from_slice(&key)); 572 | let mut data = response.encrypted_msg.to_vec(); 573 | 574 | let aad = build_aad(ObliviousDoHMessageType::Response, &response.key_id)?; 575 | 576 | cipher.decrypt_in_place(GenericArray::from_slice(&nonce), &aad, &mut data)?; 577 | 578 | let response_decrypted = parse(&mut Bytes::from(data))?; 579 | Ok(response_decrypted) 580 | } 581 | 582 | /// Decrypt a client query. 583 | pub fn decrypt_query( 584 | query: &ObliviousDoHMessage, 585 | key_pair: &ObliviousDoHKeyPair, 586 | ) -> Result<(ObliviousDoHMessagePlaintext, OdohSecret)> { 587 | if query.msg_type != ObliviousDoHMessageType::Query { 588 | return Err(Error::InvalidMessageType); 589 | } 590 | 591 | let key_id = key_pair.public().identifier()?; 592 | let key_id_recv = &query.key_id; 593 | 594 | if !key_id_recv.eq(&key_id) { 595 | return Err(Error::KeyIdMismatch); 596 | } 597 | 598 | let server_sk = key_pair.private(); 599 | let key_size = ::PublicKey::size(); 600 | if key_size > query.encrypted_msg.len() { 601 | return Err(Error::InvalidInputLength); 602 | } 603 | let (enc, ct) = query.encrypted_msg.split_at(key_size); 604 | 605 | let encapped_key = ::EncappedKey::from_bytes(enc)?; 606 | 607 | let mut recv_ctx = hpke::setup_receiver::( 608 | &OpModeR::Base, 609 | server_sk, 610 | &encapped_key, 611 | LABEL_QUERY, 612 | )?; 613 | 614 | // Open the payload 615 | let aad = build_aad(ObliviousDoHMessageType::Query, &key_id)?; 616 | let plaintext = recv_ctx.open(ct, &aad)?; 617 | 618 | let mut odoh_secret = OdohSecret::default(); 619 | recv_ctx.export(LABEL_RESPONSE, &mut odoh_secret)?; 620 | 621 | let query_decrypted = parse(&mut Bytes::from(plaintext))?; 622 | Ok((query_decrypted, odoh_secret)) 623 | } 624 | 625 | /// Encrypt a server response. 626 | pub fn encrypt_response( 627 | query: &ObliviousDoHMessagePlaintext, 628 | response: &ObliviousDoHMessagePlaintext, 629 | secret: OdohSecret, 630 | response_nonce: ResponseNonce, 631 | ) -> Result { 632 | let (key, nonce) = derive_secrets(secret, query, response_nonce)?; 633 | let cipher = Aes128Gcm::new(GenericArray::from_slice(&key)); 634 | let aad = build_aad(ObliviousDoHMessageType::Response, &response_nonce)?; 635 | 636 | let mut buf = Vec::new(); 637 | response.serialize(&mut buf)?; 638 | cipher.encrypt_in_place(GenericArray::from_slice(&nonce), &aad, &mut buf)?; 639 | 640 | Ok(ObliviousDoHMessage { 641 | msg_type: ObliviousDoHMessageType::Response, 642 | key_id: response_nonce.to_vec().into(), 643 | encrypted_msg: buf.into(), 644 | }) 645 | } 646 | 647 | // TODO: try to use a static buffer for aad building 648 | fn build_aad(t: ObliviousDoHMessageType, key_id: &[u8]) -> Result> { 649 | let mut aad = vec![t as u8]; 650 | aad.extend(&to_u16(key_id.len())?.to_be_bytes()); 651 | aad.extend(key_id); 652 | Ok(aad) 653 | } 654 | 655 | /// Derives a key and nonce pair using the odoh secret and 656 | /// response_nonce. 657 | fn derive_secrets( 658 | odoh_secret: OdohSecret, 659 | query: &ObliviousDoHMessagePlaintext, 660 | response_nonce: ResponseNonce, 661 | ) -> Result<(AeadKey, AeadNonce)> { 662 | let buf = compose(query)?; 663 | let salt = [ 664 | buf.as_ref(), 665 | &to_u16(response_nonce.len())?.to_be_bytes(), 666 | &response_nonce, 667 | ] 668 | .concat(); 669 | 670 | let h_key = Hkdf::<::HashImpl>::new(Some(&salt), &odoh_secret); 671 | let mut key = AeadKey::default(); 672 | h_key 673 | .expand(LABEL_KEY, &mut key) 674 | .map_err(|_| Error::from(HpkeError::KdfOutputTooLong))?; 675 | 676 | let h_nonce = Hkdf::<::HashImpl>::new(Some(&salt), &odoh_secret); 677 | let mut nonce = AeadNonce::default(); 678 | h_nonce 679 | .expand(LABEL_NONCE, &mut nonce) 680 | .map_err(|_| Error::from(HpkeError::KdfOutputTooLong))?; 681 | 682 | Ok((key, nonce)) 683 | } 684 | 685 | #[inline] 686 | fn to_u16(n: usize) -> Result { 687 | n.try_into().map_err(|_| Error::InvalidInputLength) 688 | } 689 | 690 | #[cfg(test)] 691 | mod tests { 692 | use super::*; 693 | use rand::rngs::StdRng; 694 | use rand::SeedableRng; 695 | 696 | #[test] 697 | fn configs() { 698 | // parse 699 | let configs_hex = "002c000100280020000100010020bbd80565312cff62c44020a60c511711a6754425d5f42be1de3bca6b9bb3c50f"; 700 | let mut configs_bin: Bytes = hex::decode(configs_hex).unwrap().into(); 701 | let configs: ObliviousDoHConfigs = parse(&mut configs_bin).unwrap(); 702 | assert_eq!(configs.configs.len(), 1); 703 | // check all bytes have been consumed 704 | assert!(configs_bin.is_empty()); 705 | 706 | // compose 707 | let buf = compose(&configs).unwrap(); 708 | assert_eq!(configs_hex, hex::encode(&buf)); 709 | 710 | // check support 711 | let mut c1 = configs.configs[0].clone(); 712 | let mut c2 = c1.clone(); 713 | c1.version = 0xff; 714 | let supported = ObliviousDoHConfigs::from(vec![c1.clone(), c2.clone()]).supported(); 715 | assert_eq!(supported[0], c2); 716 | 717 | c2.version = 0xff; 718 | let supported = ObliviousDoHConfigs::from(vec![c1, c2]).supported(); 719 | assert!(supported.is_empty()); 720 | } 721 | 722 | #[test] 723 | fn pubkey() { 724 | // parse 725 | let key_hex = 726 | "0020000100010020aacc53b3df0c6eb2d7d5ce4ddf399593376c9903ba6a52a52c3a2340f97bb764"; 727 | let mut key_bin: Bytes = hex::decode(key_hex).unwrap().into(); 728 | let key: ObliviousDoHConfigContents = parse(&mut key_bin).unwrap(); 729 | assert!(key_bin.is_empty()); 730 | 731 | // compose 732 | let buf = compose(&key).unwrap(); 733 | assert_eq!(key_hex, hex::encode(&buf)); 734 | } 735 | 736 | #[test] 737 | fn exchange() { 738 | // Use a seed to initialize a RNG. *Note* you should rely on some 739 | // random source. 740 | let mut rng = StdRng::from_seed([0; 32]); 741 | 742 | // Generate a key pair on server side. 743 | let key_pair = ObliviousDoHKeyPair::new(&mut rng); 744 | 745 | // Create client configs from the key pair. It can be distributed 746 | // to the clients. 747 | let public_key = key_pair.public().clone(); 748 | let client_configs: ObliviousDoHConfigs = vec![ObliviousDoHConfig::from(public_key)].into(); 749 | let mut client_configs_bytes = compose(&client_configs).unwrap().freeze(); 750 | 751 | // ... distributing client_configs_bytes ... 752 | 753 | // Parse and extract first supported config from client configs on client side. 754 | let client_configs: ObliviousDoHConfigs = parse(&mut client_configs_bytes).unwrap(); 755 | let config_contents = client_configs.supported()[0].clone().into(); 756 | 757 | // This is a example client request. This library doesn't validate 758 | // DNS message. 759 | let query = ObliviousDoHMessagePlaintext::new(b"What's the IP of one.one.one.one?", 0); 760 | 761 | // Encrypt the above request. The client_secret returned will be 762 | // used later to decrypt server's response. 763 | let (query_enc, cli_secret) = encrypt_query(&query, &config_contents, &mut rng).unwrap(); 764 | 765 | // ... sending query_enc to the server ... 766 | 767 | // Server decrypt request. 768 | let (query_dec, srv_secret) = decrypt_query(&query_enc, &key_pair).unwrap(); 769 | assert_eq!(query, query_dec); 770 | 771 | // Server could now resolve the decrypted query, and compose a response. 772 | let response = ObliviousDoHMessagePlaintext::new(b"The IP is 1.1.1.1", 0); 773 | 774 | // server encrypt response 775 | let nonce = ResponseNonce::default(); 776 | let response_enc = encrypt_response(&query_dec, &response, srv_secret, nonce).unwrap(); 777 | 778 | // ... sending response_enc back to the client ... 779 | 780 | // client descrypt response 781 | let response_dec = decrypt_response(&query, &response_enc, cli_secret).unwrap(); 782 | assert_eq!(response, response_dec); 783 | } 784 | 785 | #[test] 786 | fn test_vector() { 787 | use super::*; 788 | use serde::Deserialize as SerdeDeserialize; 789 | 790 | const TEST_VECTORS: &str = std::include_str!("../tests/test-vectors.json"); 791 | 792 | #[derive(SerdeDeserialize, Debug, Clone)] 793 | pub struct TestVector { 794 | pub aead_id: u16, 795 | pub kdf_id: u16, 796 | pub kem_id: u16, 797 | pub key_id: String, 798 | pub odohconfigs: String, 799 | pub public_key_seed: String, 800 | pub transactions: Vec, 801 | } 802 | 803 | #[derive(SerdeDeserialize, Debug, Clone)] 804 | #[serde(rename_all = "camelCase")] 805 | pub struct Transaction { 806 | pub oblivious_query: String, 807 | pub oblivious_response: String, 808 | pub query: String, 809 | pub response: String, 810 | pub query_padding_length: usize, 811 | pub response_padding_length: usize, 812 | } 813 | 814 | let test_vectors: Vec = serde_json::from_str(TEST_VECTORS).unwrap(); 815 | for tv in test_vectors { 816 | let ikm_bytes = hex::decode(tv.public_key_seed).unwrap(); 817 | let (secret_key, _) = Kem::derive_keypair(&ikm_bytes); 818 | 819 | let mut configs_bytes: Bytes = hex::decode(tv.odohconfigs).unwrap().into(); 820 | let configs: ObliviousDoHConfigs = parse(&mut configs_bytes).unwrap(); 821 | let odoh_public_key: ObliviousDoHConfigContents = 822 | configs.supported().into_iter().next().unwrap().into(); 823 | 824 | assert_eq!( 825 | odoh_public_key.identifier().unwrap(), 826 | hex::decode(tv.key_id).unwrap(), 827 | ); 828 | 829 | let key_pair = ObliviousDoHKeyPair { 830 | private_key: secret_key, 831 | public_key: odoh_public_key, 832 | }; 833 | 834 | for t in tv.transactions { 835 | let query = ObliviousDoHMessagePlaintext::new( 836 | &hex::decode(t.query).unwrap(), 837 | t.query_padding_length, 838 | ); 839 | 840 | let mut odoh_query_bytes: Bytes = hex::decode(t.oblivious_query).unwrap().into(); 841 | let odoh_query = parse(&mut odoh_query_bytes).unwrap(); 842 | 843 | // decrypt oblivious_query from test should match its query 844 | let (odoh_query_dec, srv_secret) = decrypt_query(&odoh_query, &key_pair).unwrap(); 845 | assert_eq!(odoh_query_dec, query); 846 | 847 | let odoh_response_bytes: Bytes = hex::decode(t.oblivious_response).unwrap().into(); 848 | let odoh_response: ObliviousDoHMessage = 849 | parse(&mut odoh_response_bytes.clone()).unwrap(); 850 | 851 | let response = ObliviousDoHMessagePlaintext::new( 852 | &hex::decode(t.response).unwrap(), 853 | t.response_padding_length, 854 | ); 855 | 856 | // assert with fixed response nonce to make sure the 857 | // right hpke version is being used 858 | let response_enc = encrypt_response( 859 | &query, 860 | &response, 861 | srv_secret, 862 | odoh_response.key_id[..16].try_into().unwrap(), 863 | ) 864 | .unwrap(); 865 | 866 | // encrypted response is the same as the one parsed from test 867 | let response_enc_bytes = compose(&response_enc).unwrap(); 868 | assert_eq!(response_enc_bytes.as_ref(), odoh_response_bytes.as_ref(),); 869 | } 870 | } 871 | } 872 | 873 | #[test] 874 | fn padding() { 875 | let query = ObliviousDoHMessagePlaintext::new(&[], 0); 876 | assert_eq!(query.padding_len(), 0); 877 | 878 | let query = ObliviousDoHMessagePlaintext::new(&[], 2); 879 | assert_eq!(query.padding_len(), 2); 880 | 881 | let mut query_bytes = compose(&query).unwrap(); 882 | let last = query_bytes.len() - 1; 883 | query_bytes[last] = 0x01; 884 | assert_eq!( 885 | Error::InvalidPadding, 886 | parse::(&mut query_bytes.freeze()).unwrap_err() 887 | ); 888 | 889 | let mut query = query; 890 | query.padding = vec![1, 2].into(); 891 | assert_eq!(Error::InvalidPadding, compose(&query).unwrap_err()); 892 | } 893 | 894 | #[test] 895 | fn parse_encapsulated_key() { 896 | // Use a seed to initialize a RNG. *Note* you should rely on some 897 | // random source. 898 | let mut rng = StdRng::from_seed([0; 32]); 899 | let key_pair = ObliviousDoHKeyPair::new(&mut rng); 900 | 901 | // Construct a malformed payload. Parsing the encrypted message should fail because it is 902 | // too short to include the encapsulated key. 903 | let query_enc = ObliviousDoHMessage { 904 | msg_type: ObliviousDoHMessageType::Query, 905 | key_id: key_pair.public().identifier().unwrap().to_vec().into(), 906 | encrypted_msg: b"too short".to_vec().into(), 907 | }; 908 | assert!(decrypt_query(&query_enc, &key_pair).is_err()); 909 | } 910 | } 911 | -------------------------------------------------------------------------------- /tests/test-vectors.json: -------------------------------------------------------------------------------- 1 | [{"kem_id":32,"kdf_id":1,"aead_id":1,"odohconfigs":"002c000100280020000100010020c6a793bedbd601c25970b1cc46bea80fdb1a8ec51540d79e4f9f17b8baa9da33","public_key_seed":"c9d84d04e6369fccb8a4d5a264001491221f1b97d9b80dd32c35834bb4462383","key_id":"9265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b9","transactions":[{"query":"9db1072b0ab473d1e4b74b09637d5f8f253ad4047426ab4dfcc58350bb67b60c","queryPaddingLength":0,"response":"9db1072b0ab473d1e4b74b09637d5f8f253ad4047426ab4dfcc58350bb67b60c9db1072b0ab473d1e4b74b09637d5f8f253ad4047426ab4dfcc58350bb67b60c","responsePaddingLength":0,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b90054655d2fa2b2271e40b5a78745e41e6d6c5c181fd1fcffcc30fc451d5ec7fdcc5a6ae0da1cba2bd379da93d02d0e42a3849ec6ba53a54c7c8216f0d3cc2cef30ac54f824f5d8b57657d8c7b95e2c0276580b3851d9","obliviousResponse":"0200100f474d14998a841b15f84388a8af1881005413556bcd8d86194fb47a51982b715b7f253f4f3ea14d89a5dd9d2b67e6c13b0b7eb7ae740c09d915ef77461956ba8acac5c5f1d8965ca0888b1c0aa2a0084b4c2c375b74e1c3d4e51ad3fe8080b7941fd5719df5"},{"query":"44f20987ac22db1994d3bb73826e2a20e24e5ca3e98d13fcf664a96c59fab7a0","queryPaddingLength":0,"response":"44f20987ac22db1994d3bb73826e2a20e24e5ca3e98d13fcf664a96c59fab7a044f20987ac22db1994d3bb73826e2a20e24e5ca3e98d13fcf664a96c59fab7a0","responsePaddingLength":64,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900540af79ff8441b04b98ae2e433879a6aa315eeb9325140fc43f3bbcd1617de271cc08906d35de8c575c61ba3d989e3c1663b6e9a727a97c9326f06d11a9720e89b5f5a7513ad6fbd73ce4d996d6ce2b1c202836691","obliviousResponse":"02001033e1570b05a7a3001041a94bba0c77130094678dfb7e2dbb456ca05a4af5d9f7c2e82564cde42ec37a904d8fb57fb6bdf7661bd9a32df37d2dfe1686ca56544e1b7f435a29aff10ccbf9bc9c996cea7aa69b6a8e123f652b86938d79a7883b756d45f9ca6e0f38ddf8b9e5dac088480f6187a1287b788d3dc4991b532f36736188e1a9e3d7a615cf1b61396652502400bd740e35265357876a9345ea7efe4c7f19a1081dd886"},{"query":"0e8a6efce38ba8b5eb32722ce6ec7a54e8f85774d6e5cb4dadd9b46fe2d0a4e7","queryPaddingLength":0,"response":"0e8a6efce38ba8b5eb32722ce6ec7a54e8f85774d6e5cb4dadd9b46fe2d0a4e70e8a6efce38ba8b5eb32722ce6ec7a54e8f85774d6e5cb4dadd9b46fe2d0a4e7","responsePaddingLength":192,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900546f5d6fdd16247adec53f69d811d92962d107c9307e5596a6284acb0cc177676399ef7402f9f04005d21a2a3f1a92cf5b3d9d3bd11b02eb9c21093cdcc6a9752a34397e7083bde83fc5cc416068c318721b87840c","obliviousResponse":"0200100cc3da3808387c8bac7b6660c017a72d01142de493b3e02d32f513a249e116f0b0b10a839504c19c137d43cb18942526f41952e3b5696c3c06e54bd424c6a2e5530a75ee1432231c461f253e5fb14259d7392dc0eb679cbbca162d82841a90c48275ab63d7e016d5cc88d7b8640d3e2bc3c92dc7ec9110cb9ad72f808d3d44668a0381c1eb094276079a81ed1666bbcff9b89caae5182c086951d53a2021413c9acb9c23666187547e51050cbd768a2f1f46133fbbdbd4ff6d1c25d76a9c31923db2c085d8d4a6fb2ff367bf075fd255a48848bff9fa996abd8ad551402954df99aee06a54406c0d075f5e88e741c20c41dca0de3656df39a5e63e8a8eea0676eab7ecec2e3b3d27a5a2e93837e9ceb33725b7ad16b2b82d36b54d1775b1d46d6a2bb5bb8bd8"},{"query":"03ae8f2dac3c83fcde93b2124da8657092f1172e471514aeddd5507c759171b8","queryPaddingLength":0,"response":"03ae8f2dac3c83fcde93b2124da8657092f1172e471514aeddd5507c759171b803ae8f2dac3c83fcde93b2124da8657092f1172e471514aeddd5507c759171b8","responsePaddingLength":404,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b9005439d4bcd57371463a7fdc7c934619f72766b8179237315588709cff69040ca338b4509a92028ffe59e37e6afa4cc9751a4cb23d206290c950b1e95244fb7ca9e3b9741f7175637e59d40d377dbde8dd3f05354c82","obliviousResponse":"020010a5996cdc5120e2d0e3907a0a2f9ac85201e8f80fb0154d1b66122ca45ab29663b981bb46c20e09d709bd1bcac6c9a0f95c46c308bfbf2b95b690c7f53e26ce6f7b250b8a2580da30e4f0696401d3c37a14ba905be7d38fd3f4bd99c503238b1625e39f92dede0362f25d2b4c8ec6a6417cdc977a0f5ca969f47a5b8ea9b808918e9ac6af255b7e8230e3d690fd21e6e4930254cf3864c91f962bb51daf4628311d1eca84babc91bf8aed503521d91dbe9aa57c88e98a2d3eb194581fdaf4a5c6f79435200d959bd49611d88f91a1925bd170ad7751c11c5e698aef559f8dec1951db1fec4c5eb6c6a840b9d9dfd6894e2f70bd785da6a0ff6dd871ad3f965a47eef53142c98bb3558266c34830fe56bae6b0b0f7051f58c1e52fff09a2d0526f7b31e54accce358d5cebb5381147e342e10fe17a90fc6192354716530f31d8427d656965b0cd9cbce3ad88aa87eae3610da5abef5e1f4897dac8c77a6a4407ea073a7c45f50439b52d6166d8b05e4e5fe9c8360415d85fd131f3d8d12b1d69ee3f22a6c3347ebf92e811dfed2a373bbdc32d8085f0c0b2accd2887b80393d5faf727d2a350cdd9c983107d418a9e0feed96efd1dd6a1e53d03eef3d89bb485c8a67edecbf35f9ee406fa9eb7d7329b8548cb177306c766f964fa08551abec24c75afd5677d16525ee1fcfd6ff18547db4b26398e673953153a82"},{"query":"2491c01a8d9d41d2c346925dbebf34280a490ea45f5d40caab57402ae2e00c5b","queryPaddingLength":32,"response":"2491c01a8d9d41d2c346925dbebf34280a490ea45f5d40caab57402ae2e00c5b2491c01a8d9d41d2c346925dbebf34280a490ea45f5d40caab57402ae2e00c5b","responsePaddingLength":0,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900748384db49bd8f414ff22a525822120c93f1324ee7ca11ab2a942d7a9cd9b1736b4b7fe470508509ed9115d11eeeefab17042be3633c9896fdfcb2325092a57842fe27ce701519dbc0b9bef228ff6fcccacc5245eba80d652f7214f993fb2eaa87348d9d203e97e77f488a3a1f03379bb7b971daaa","obliviousResponse":"0200105db7f417de4b62f692e024dd21a7d8a700549b59c20fb7d2975963837cb103c6e19d8ca2eba8513ab2f976be1f9a3b616ceaef3fc39d32dbbe2346963880079e73e731aa5521cd8196f8372a3df83909e372a61625764bcd55782cdd822900fc88b38812f203"},{"query":"6537c42600c6a3c6db735f8fb9e8e3618acf7508bb315a8862360c4b18dc83b8","queryPaddingLength":32,"response":"6537c42600c6a3c6db735f8fb9e8e3618acf7508bb315a8862360c4b18dc83b86537c42600c6a3c6db735f8fb9e8e3618acf7508bb315a8862360c4b18dc83b8","responsePaddingLength":64,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b90074e743c3dcfadd8b146103a69f59544d25eeb7de64772910b4413c94c5716ae94743655e725a6d5e00e29e05fa812108b03d9913450b08b0ab04a7eeec65e13ab52adcfac71b1ea280cbfd9c5865022835addc74f6f71d5c28358b121fae3150470324d4f4ecd0e49b729b74e525bed627e2668aa0","obliviousResponse":"0200104463598990890a8bf9051685d6694597009499a1d75c78516db57ef3ffcfddc137ac801acf4a8632a1238ca4b21facded26bc60fe132a1d44725f42fff07b11a10d00760592200c8aebd441a16b4902506c529a11e40af23634645e9d1be643274a5ab65cf4fff9346f9a47cc752b885d242b65ebf62b6ace2ae6d8544cbe64310fcfdb85ad0272b142d6a39de262577bdd6c7b573ccb6e9f194c91e1034b40b57e10700a130"},{"query":"bc389e2d2a103b4b016c54480bbc0c730369d5ab49d084650e24de30fd92167a","queryPaddingLength":32,"response":"bc389e2d2a103b4b016c54480bbc0c730369d5ab49d084650e24de30fd92167abc389e2d2a103b4b016c54480bbc0c730369d5ab49d084650e24de30fd92167a","responsePaddingLength":192,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900740a95e5d3623112d9b254b1bb82a547c0164e292b770566d77ea5cba094855701c57f4b9aa09a251360c1845d75a59527b40adf6b90aec8625b14189e3ede90646a8fecf57230e2aca75f8f49a36807d0a7c6dc5b27960cc3d336944ff093a05186908a4361e09a84f5c754732b997d9a80136040","obliviousResponse":"0200108ec1808e931a5ec92aaf363e1eafabae011489b8306746c0780ff2464ab05664329fc03a9d2844ee94664a7f8d468fbb4d43f54730e13558c44416723f18373c10302a042588a5e4fc8fa13f6b81e4cbf0a927e8c16426e81108927dd13cf6c549c1d82d691c8729342fcc6e4e55cd74f238263cba16d2a173f271975953b259b0bf36371a78a465727d1c5c09f67dbe814e956e444af018b46e449f41d6a24b4941a3cb00bda9fe164f9f06d55795a2dafe3e0e51e75582e46f62805ddc384d2321e7d77bcbd23a24306fcc91f6a3dbff3b0141b81a56919660a6a7c326376123a9d4cc34c8b1ac5e0954699d4937db134e930ebb2990cecef6f831163fe4abeaf5eb0cb86e2f64bdcf03eec2cbb5a4b1fc31b50ef365e62766de9952807c161ebdf27dd904"},{"query":"a8566d7387143cea9ceab709bd111db388575e503ddc0df9d82305d8eb70912d","queryPaddingLength":32,"response":"a8566d7387143cea9ceab709bd111db388575e503ddc0df9d82305d8eb70912da8566d7387143cea9ceab709bd111db388575e503ddc0df9d82305d8eb70912d","responsePaddingLength":404,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b9007484d408fdae2844e39f2453fc7230cd465f09756ad93f133350393abf20bada17518d0fb5258dfa417257c591153f940545fdefef666d350b9cd21b2480d7037d7ef00072add3294a6a2b35a50d386549f23178740059c9572a805cb7a3054dfc9506ddb7233e6b77abe8c4b9bfb38ee2c5f6f2c1","obliviousResponse":"02001012027ae831e3828598fb2e1088c975b401e850a72f31aec8c83965f2c05d9eadd46f76e93a898eadd7968a6c8554fc8caf04c70ecd5f7eb2d7bfaeb22d19273b32f47fd1556b5b848b5e683bce2527cec2179a3c859f0ef22bd4d0651edbbd490a54e4a775bd78b1521900af69c4415105ae8a75e659c2ca54d2486435cc6269d1883146e165ac6102478481456c656c326e9a3769d08d1624e02c0feecd707776cec2646947911368404b258e5eb69b9553a84c7ab18b23fde54e39b8b190046db49e58a4004b25149b427c70baef06b62b523942444d5cdddaa5f4b589086a68502bc5ae045594829c8acb78271641d54609de63999dc58cf789ca22804381bdb2a8add3d2040566873ccd109451e0da3a0f724dfaa73fd1d6498161bd8905ad8df83bbd73ed4c2ea30f32db57cf86e2a45827355b1ce3600f3c9ed9211a069a008affc1d1cccd10e31e996ab49a7225d0c37dd2807b97613d332831a38129df04bfc81113e779f1dafae5ca8368aaeb59b08d8269ae16e1de1f1dac80cf5fd9e255cbbe1cf93e1e2ffb4f5dd16e738ea8f0411d2d6741f7c0316f8dc0790c8bba2d717e427f1cea9aafd143fd1a2f899dc6e7a869612d405ae9bb2f8a259e83a83792173e4ef9b582cf6c9792889e082a4b0d0c6c1c5c9a2a8b6c41c793aa0a18774f2041d42882fa950eb0270a3fb92b8c2c8069bb7e6d23"},{"query":"80bdac7b284c4743ea8d665523cd5e538e0edb00df39f4eeadf99564ef34825e","queryPaddingLength":32,"response":"80bdac7b284c4743ea8d665523cd5e538e0edb00df39f4eeadf99564ef34825e80bdac7b284c4743ea8d665523cd5e538e0edb00df39f4eeadf99564ef34825e","responsePaddingLength":0,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900742db0830746e710c6bbc235b08dcb9a2a7c5693b392d911f504f5e1e3e10dd24ef9e91a01ca1ec2b07934a382c7f204afe8ce479d233a7fb8d089e0238ac164d660a11235d7be568d8b4b9f758264b63ab35aa81d4a5f5587fefc30bff990a394e89956a6894e1fe2f8f80c2668de804457eb61b1","obliviousResponse":"0200106cf1ea20eb2346d36117982dd26cbe0b005474c8f2a09c5e94231a605865bdf3bd90b83447a89d050b7f8a903cd3742dd6e8d1e9a18c4c2cc881e7c34599e3d2eb761087844b840514a001785ded23c44e11801464fd38d496996dfb810712b6d3f41384570d"},{"query":"e3ad820972fb3eef6ac0601315738e07d95c8a5c999605fd4c9c17f53783a37b","queryPaddingLength":32,"response":"e3ad820972fb3eef6ac0601315738e07d95c8a5c999605fd4c9c17f53783a37be3ad820972fb3eef6ac0601315738e07d95c8a5c999605fd4c9c17f53783a37b","responsePaddingLength":64,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b90074614b51e9510423fc548f9e395add95867d0b1cc1655b475e2473a762332e361630b0dbb60f7fad5d67be43ec8ffd766199c84905de3df910a54fc6bdf1f819b70984a502ecd1a6affa20d2bb3e8673bcf14c94b51d8e6afb48bbfb5a5714f5f55dd7c13e8926c21003fadcad02e704d594b24c84","obliviousResponse":"020010725e7b46034aaefb468bcf1331fd6e8d0094a90d99eae107c97948be7b10b6f59e6aef1592f76a32b96183c6d2bf14dbf432d0a516ae5e7214fd4a3542b9dcd2a4065a4e42629a7be9e1bbab3ebc355d4460b9462c7f4b8c6213f57adad123de5c4ed065c4c11517b385347ff52455d45e7c92644ce74af5a5b6f2561bb81ea7f9cf02b8f0b68bb44249649c9f725ffcfed41ff242017b86b132b9836ef9fe794896297d470c"},{"query":"6248b8ba8abdd45b4d4f80bcca2a44dbcf1318d0b485c1c09b52e569e0f38490","queryPaddingLength":32,"response":"6248b8ba8abdd45b4d4f80bcca2a44dbcf1318d0b485c1c09b52e569e0f384906248b8ba8abdd45b4d4f80bcca2a44dbcf1318d0b485c1c09b52e569e0f38490","responsePaddingLength":192,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b90074cf7bc59c372e42765bdc1112d1688e06fa0e39b077ab0533b57f34548872df5120db38bfa04adf4c267b278665ef54ddd55d7325b628814f8e1fddaa38ca25832a870cc516016d25001a7e8ca4b32c98d08976df42545b743a6611ac230bedeb9a9c771a79116a2116cde48733b29456bdf1cc7b","obliviousResponse":"02001073fbe6d68b77673566a83b1653c4fa830114e3a281fabc8270f196d0d711ca483df0d44689698a5de66a62a06fe63390ba9f2ff5e35bcc135457d6dca26e32264d4b146aad88568291d9c82775c1f8550bc4e04e9ddede679afd4e2f4862dee5584c8f428bfaeb90363a139145fae371c0135e66db202c27af6b8c86f16aaa15c020be915124cde1115d8d6df8e1de0efe06e7b20d16d345d508f0af1214b15faf54aa9881d256488b445958d96217e0eca99e1f68a34c27c9670910afc6605daeddbdf748a75e4bb7e96487922cf0968542d649906ceba42bc8d31cb192d739552b1658703e0d82f92c4782ff861b6ed1370b72ccea9f5735b3653f88f8e4d363f9952c993ad9d96f8af95800f52f5ac7f6b2c0a107e275b4ab22510e60ed411cb80dfaa03f"},{"query":"fedc4f95704d25ccd1cceda39dddf74c61881b81be055b792ad4b31fc6ca5766","queryPaddingLength":32,"response":"fedc4f95704d25ccd1cceda39dddf74c61881b81be055b792ad4b31fc6ca5766fedc4f95704d25ccd1cceda39dddf74c61881b81be055b792ad4b31fc6ca5766","responsePaddingLength":404,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b9007450b3e91ad6df76e518cf0d14b552a38110bd157d598cecace84bfdacddfce7522ed04ad2751a266371f7d2f82f2cbd747ef4f64ebc0a58437c063a0221a147c5d595cf28707e4a03fe868b0ecc265a98a6288d4e748b1fdea6c2f27c3e8a640bf35ae85ae9d37108c4959994b21bd45f1236cb68","obliviousResponse":"020010ac3b91d15f2854a632b08acfcf9b0a5c01e8a36de291200a38378f3410769606dbadb3d27c992f2c26b3200d59bc293530a8b8046be5a7f9930154130ead268a30f5b69299bf9410d5969ac8959165dd24427ead9ea356aff47fa43598db301c55d0efdcb7d94fdcb6845db1cd162a5f69766c04c3ffe234c94bfb67c61317d2adcd418bd0c002d6b47d83e196cee520fe30a23899e0bde7c304de40d25f1667a90bd12d5f1299ccc0186df85c4214a9d272420fb8e54c6e69ea7754ffa4c142b9a4561f09db8cd0e50bb22963ca0b5ef9beb11e1e3ff21bf82285789df129c8f9a0371057f0638532c13e997cb85e19988424c59833edd54db46e75676fe498fa35b7837b90468123fc1bb21f5627c958ea2f8f44a2a77c15dddda08f489e71915241bc1dc74e1a0cb58ca0a28eedf6e0e74f1b3d37078a7132b12b0e1a34a7cdbb6126236a5205af34ceb3955f4a0233f74e48eca06dc5a7d658c451295922f1c456d21bd409b54fb27ce1392445a688fe81bea8ba422b741bec40a3362a1bf23490405d17d0e7b6035228b0a79ea0240d7ba1231c0ec657abb3c4661cb0c27668552fbe361542f23f0960c514cd56b83e0a37550ae8b579f710079c81f8fa8e39e1fb41c94b4e673524c26a506610822af94aa0c4ea8703d068dd5b0f7c816523fc20806427de2007dcc31e0c0ed6dcdcd42e1a0c70e5651a"},{"query":"3c61a330a885add39b3035b811abc6a950d2f538ace30633485b5a824ed7e91d","queryPaddingLength":96,"response":"3c61a330a885add39b3035b811abc6a950d2f538ace30633485b5a824ed7e91d3c61a330a885add39b3035b811abc6a950d2f538ace30633485b5a824ed7e91d","responsePaddingLength":0,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900b4b731c47830e69f40ab4e0799ea9a09e7686b0a656e5ea3ae7237758f79f1e34ef040ffea243113c4ca3621ae8d8e94717abc02f62db012654f395fe1fa8ee333828f7324e636d78cdb9ef612076fcb19c29f93ff9026f753e2908ac98e1de3dd86b1ecc02ef15ee48cecbcf5edef3f3ee36e0294bd1475d52a4c8d6e02b0bb9f4f173a26148320543760e57abbad20f452c0db3cac59b2273d417f85a40b6dea11fab49ed8f620cce4ff36c9e88d8de68c2a8b9f","obliviousResponse":"020010fff22c9dd6c145f49bcfdb7c9575b57100541ed3d9b2ab6c94e1de9055089bf301cae735dcd116521acffe32e12fb5e8aec084b2f2bda949ccd3b4486f49c470798c38ea69b69a22aae5aa595848d26ce5dba5a68f6b8c07e557751786d81d8d6ddd7a6c4d65"},{"query":"57054db24b335e900b400d13a13c51f5a5686551cba5a2716392e63ff5e3ee83","queryPaddingLength":96,"response":"57054db24b335e900b400d13a13c51f5a5686551cba5a2716392e63ff5e3ee8357054db24b335e900b400d13a13c51f5a5686551cba5a2716392e63ff5e3ee83","responsePaddingLength":64,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900b4828a21fe82b0e7e1a638793d0d48c9bdddf8d437a3228709b329d0d84d912e04bd197e8dee3a5fc928889ff16068b0c971b9527a75bf9ca1edf5fdc705c160d718b328805c47973d0df94921046d83e1d780f1945e287e7d50daf9da242d0aa0b3c95c54b8ad8c482e1d377ab55807db51a17bc609914dfaa1b2681faab814b9fa2424aadc7d48856e30560997b49600c49a92a02dc0fbc3e53464292f0d80f4b7f7101f8f647087f73befb2920810760210b45e","obliviousResponse":"02001055cceebcda0eec6f3b9d5a6506cfcdf70094da984782c222915c312c393394ca5d318cd77c62595e7a9310683b422d4096b249d4f2d40585f79b4919948d3dade740eff71ad85bac1b019be3a6f4ab06fc182c2359390b731b4cca45244978b6473059fb02f2477fbc53a68b23869ff45c9a1467f1dfc96d682bb8a9698703a06f44a7483ff13cb2a3c2b86e0d4c31396d036f28f400688b2ef54f11824799900a7b46609e32"},{"query":"d2f385705b564739e6ece8955af34c3ff4e2e1e9bfafc82867eeac807f6c7c81","queryPaddingLength":96,"response":"d2f385705b564739e6ece8955af34c3ff4e2e1e9bfafc82867eeac807f6c7c81d2f385705b564739e6ece8955af34c3ff4e2e1e9bfafc82867eeac807f6c7c81","responsePaddingLength":192,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900b484100fa15b975f9b747b573dbeaa5e86fb57a7a0f92f180a8efdf9d650ee8126d382e9171757bbf7227bb5149aef5047630bf1a6310b841fc3a6485783a7b610cf34c335fb3f93ce8821d465d755e65a85e04bb438cb8dd7db98f1cdb6a35dd2c26be255a32a485ade757512dd1f023e6aa637c471a3634d4961273e33f52cb736c2e67853974416003b6e099c7bfeaee4fb40dddcd3f8d1ddab2dc8062ae83db35d5e3b61752806f0403390b485aac005ccf610","obliviousResponse":"020010ef6318d8c63d3571d0e2284b6c38f86d0114561dd986901c2c58b9d104a4cd74384dc39841ebb9781263eee117ef3e25ee9f650b23f66695a09601b95f5f1c1e9c171253a185669d40cc5b07c725fa663800cd0ad4fd2c7e50fb6ba53886d963efa90256098da57bf55c0b43cbc16e5caedc65e0de0bc60771820189c4078b26a0ef404669716214646f095f9beb058e5a5e170d2e76063214731132bb97b91ad58e5bc4a3f524998622367d7cdbf75dc8cb785c810feccd4a2ef80e9e4ce45eaa1a623311138f22bcc066b1bbeeb531ddfdcb8d4bd520fca8c7cc4b2fa6ea595105cafc08d59405ac655289d40b151ff1490da36cfb9fb00c1c89eea97e7862402c039c4f4b57174ea22ff678ad22afc53a3bf86339b29906f382030f9952e720062f9ba2f1"},{"query":"2486d426a22fd2d56fa027ea800e676cf15698792e878c8366aa35211f9faceb","queryPaddingLength":96,"response":"2486d426a22fd2d56fa027ea800e676cf15698792e878c8366aa35211f9faceb2486d426a22fd2d56fa027ea800e676cf15698792e878c8366aa35211f9faceb","responsePaddingLength":404,"obliviousQuery":"0100209265d14d640ff991b31892f36326ab601ea84d61964fc7a9c7f981a5313e58b900b49f8ab654e033fcf4b421441f67c0a792690ef3fcafb26e8684c8bd335dc6d561d9355baf8b1365da53b4d39617e51ee7814831b02841b1c618dd93de6205f90adf312079a9b93ac57eaec5ffdd53c2354992f0f3231307e009380f171f2228c639bf4c1af262c0e41552d44f9a5a08cebc13df248cfddea8e77e6ac43cf99bdc18c46fff587141c57dd66b3723df4cfa03501693751ff19ee82aad5bcb3760080d871a83e3690a7a724f49816d2bcc6861a331b9","obliviousResponse":"020010a1a577d296df4c780daecb27de39babc01e858e92949d238e774070c07fe36b9c479176ae8511ee6e5cccd0d653a5c5400bb8411488914db3c40cd808c746c1cf5cc0fde2f4eb966b02c823ec0282c30c9dcd5fc619505d163454f45a189db3c1e7fda97dc55608c2adca39402f643f00f19a29aaa4975f8249f0804cb61231142c4ef93b6a06663db8bd55423f7c29c5a49fe7d510b9c1e2f5c81485c4bc4dd97b17983d45615b16c2f38719b8859a34d553d4e9e989b3dfbd063c89de6139efdf6ceed9a5ed364237a283e6932420700d11da4cac143c49e5095b3772993f915d32c3db880040964b1fc0c4a69e14d40d3d8ee9c2f68dc1d836aae2b02b0cf3bcdd6896890d34c71291cc0e1cb44e60ab03e92713035a84da93d34270077ecc3a735690f94a0ae698663ed53b713ba48c12d3167a46898b70ebfb1e604454d0a77c7ae75cfe7dc7ca7b1ceca00df3ed35b43d75975d8ebc79a8d31bee409101bd3100ff55cfa9eb3e5aee46b39fda19666d2587bd755c69e90beb93effb8214fe34f9551d67b083efdd9e4032fe0b8ba1549e07ea215611fd7a0bdb87393cf28f5175a6e46a6ca65184ad161c79eba089522b5d60e510b85c60b130d9dd6359f0fe984cebf2b8943c1b5b6c96f0c9e8338c7d9d12b9986d2fa40b2d0866521c28bf43c5940d2a91d3da5630a5493ce574121ec504e3eb68d9f"}]}] --------------------------------------------------------------------------------