Add WASM/JS support for doing full lookups using DoH
authorMatt Corallo <git@bluematt.me>
Mon, 12 Feb 2024 03:04:51 +0000 (03:04 +0000)
committerMatt Corallo <git@bluematt.me>
Mon, 12 Feb 2024 03:04:51 +0000 (03:04 +0000)
src/query.rs
wasmpack/Cargo.toml
wasmpack/doh_lookup.js [new file with mode: 0644]
wasmpack/src/lib.rs

index 8c478e629274639388773cd5dc05a404672f2fa9..07954b3cb9fccc348803c00a5c229b20be1eca76 100644 (file)
@@ -93,14 +93,12 @@ impl ops::DerefMut for QueryBuf {
        }
 }
 
-// We don't care about transaction IDs as we're only going to accept signed data. Thus, we use
-// this constant instead of a random value.
-const TXID: u16 = 0x4242;
+// We don't care about transaction IDs as we're only going to accept signed data.
+// Further, if we're querying over DoH, the RFC says we SHOULD use a transaction ID of 0 here.
+const TXID: u16 = 0;
 
 fn build_query(domain: &Name, ty: u16) -> QueryBuf {
        let mut query = QueryBuf::new_zeroed(0);
-       let query_msg_len: u16 = 2 + 2 + 8 + 2 + 2 + name_len(domain) + 11;
-       query.extend_from_slice(&query_msg_len.to_be_bytes());
        query.extend_from_slice(&TXID.to_be_bytes());
        query.extend_from_slice(&[0x01, 0x20]); // Flags: Recursive, Authenticated Data
        query.extend_from_slice(&[0, 1, 0, 0, 0, 0, 0, 1]); // One question, One additional
@@ -271,12 +269,14 @@ impl ProofBuilder {
 
 #[cfg(feature = "std")]
 fn send_query(stream: &mut TcpStream, query: &[u8]) -> Result<(), Error> {
+       stream.write_all(&(query.len() as u16).to_be_bytes())?;
        stream.write_all(&query)?;
        Ok(())
 }
 
 #[cfg(feature = "tokio")]
 async fn send_query_async(stream: &mut TokioTcpStream, query: &[u8]) -> Result<(), Error> {
+       stream.write_all(&(query.len() as u16).to_be_bytes()).await?;
        stream.write_all(&query).await?;
        Ok(())
 }
index 780a4dc0707d1acdc56c34555bfbe83b1a00c093..6360d8fcc731165188dff0a76f38afe3d857147e 100644 (file)
@@ -15,3 +15,7 @@ wee_alloc = { version = "0.4", default-features = false }
 
 [lib]
 crate-type = ["cdylib", "rlib"]
+
+[profile.release]
+lto = true
+codegen-units = 1
diff --git a/wasmpack/doh_lookup.js b/wasmpack/doh_lookup.js
new file mode 100644 (file)
index 0000000..17c5bbf
--- /dev/null
@@ -0,0 +1,61 @@
+import init from './dnssec_prover_wasm.js';
+import * as wasm from './dnssec_prover_wasm.js';
+
+/**
+* Asynchronously resolves a given domain and type using the provided DoH endpoint, then verifies
+* the returned DNSSEC data and ultimately returns a JSON-encoded list of validated records.
+*/
+export async function lookup_doh(domain, ty, doh_endpoint) {
+       await init();
+
+       if (!domain.endsWith(".")) domain += ".";
+       if (ty.toLowerCase() == "txt") {
+               ty = 16;
+       } else if (ty.toLowerCase() == "tlsa") {
+               ty = 52;
+       } else if (ty.toLowerCase() == "a") {
+               ty = 1;
+       } else if (ty.toLowerCase() == "aaaa") {
+               ty = 28;
+       }
+       if (typeof(ty) == "number") {
+               var builder = wasm.init_proof_builder(domain, ty);
+               if (builder == null) {
+                       return "{\"error\":\"Bad domain\"}";
+               } else {
+                       var queries_pending = 0;
+                       var send_next_query;
+                       send_next_query = async function() {
+                               var query = wasm.get_next_query(builder);
+                               if (query != null) {
+                                       queries_pending += 1;
+                                       var b64 = btoa(String.fromCodePoint(...query));
+                                       var b64url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
+                                       try {
+                                               var resp = await fetch(doh_endpoint + "?dns=" + b64url,
+                                                       {headers: {"accept": "application/dns-message"}});
+                                               if (!resp.ok) { throw "Query returned HTTP " + resp.status; }
+                                               var array = await resp.arrayBuffer();
+                                               var buf = new Uint8Array(array);
+                                               wasm.process_query_response(builder, buf);
+                                               queries_pending -= 1;
+                                       } catch (e) {
+                                               return "{\"error\":\"DoH Query failed: " + e + "\"}";
+                                       }
+                                       return await send_next_query();
+                               } else if (queries_pending == 0) {
+                                       var proof = wasm.get_unverified_proof(builder);
+                                       if (proof != null) {
+                                               var result = wasm.verify_byte_stream(proof);
+                                               return JSON.stringify(JSON.parse(result), null, 1);
+                                       } else {
+                                               return "{\"error\":\"Failed to build proof\"}";
+                                       }
+                               }
+                       }
+                       return await send_next_query();
+               }
+       } else {
+               return "{\"error\":\"Unsupported Type\"}";
+       }
+}
index 952e036c191617a66bb0b34661ca487224b73983..57fe0a03a89b9b4a7c6b1ba3f0829ccefff2fe65 100644 (file)
@@ -2,14 +2,75 @@
 
 use dnssec_prover::ser::parse_rr_stream;
 use dnssec_prover::validation::{verify_rr_stream, ValidationError};
+use dnssec_prover::query::{ProofBuilder, QueryBuf};
 
 use wasm_bindgen::prelude::wasm_bindgen;
 
+extern crate alloc;
+use alloc::collections::VecDeque;
+
 use core::fmt::Write;
 
 #[global_allocator]
 static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
 
+#[wasm_bindgen]
+pub struct WASMProofBuilder(ProofBuilder, VecDeque<QueryBuf>);
+
+#[wasm_bindgen]
+/// Builds a proof builder which can generate a proof for records of the given `ty`pe at the given
+/// `name`.
+///
+/// After calling this [`get_next_query`] should be called to fetch the initial query.
+pub fn init_proof_builder(mut name: String, ty: u16) -> Option<WASMProofBuilder> {
+       if !name.ends_with('.') { name.push('.'); }
+       if let Ok(qname) = name.try_into() {
+               let (builder, initial_query) = ProofBuilder::new(&qname, ty);
+               let mut queries = VecDeque::with_capacity(4);
+               queries.push_back(initial_query);
+               Some(WASMProofBuilder(builder, queries))
+       } else {
+               None
+       }
+}
+
+#[wasm_bindgen]
+/// Processes a response to a query previously fetched from [`get_next_query`].
+///
+/// After calling this, [`get_next_query`] should be called until pending queries are exhausted and
+/// no more pending queries exist, at which point [`get_unverified_proof`] should be called.
+pub fn process_query_response(proof_builder: &mut WASMProofBuilder, response: Vec<u8>) {
+       if response.len() < u16::MAX as usize {
+               let mut answer = QueryBuf::new_zeroed(response.len() as u16);
+               answer.copy_from_slice(&response);
+               if let Ok(queries) = proof_builder.0.process_response(&answer) {
+                       for query in queries {
+                               proof_builder.1.push_back(query);
+                       }
+               }
+       }
+}
+
+
+#[wasm_bindgen]
+/// Gets the next query (if any) that should be sent to the resolver for the given proof builder.
+///
+/// Once the resolver responds [`process_query_response`] should be called with the response.
+pub fn get_next_query(proof_builder: &mut WASMProofBuilder) -> Option<Vec<u8>> {
+       if let Some(query) = proof_builder.1.pop_front() {
+               Some(query.into_vec())
+       } else {
+               None
+       }
+}
+
+#[wasm_bindgen]
+/// Gets the final, unverified, proof once all queries fetched via [`get_next_query`] have
+/// completed and their responses passed to [`process_query_response`].
+pub fn get_unverified_proof(proof_builder: WASMProofBuilder) -> Option<Vec<u8>> {
+       proof_builder.0.finish_proof().ok().map(|(proof, _ttl)| proof)
+}
+
 #[wasm_bindgen]
 /// Verifies an RFC 9102-formatted proof and returns the [`VerifiedRRStream`] in JSON form.
 pub fn verify_byte_stream(stream: Vec<u8>) -> String {