Expose uniffi bindings for building and verifying proofs
authorMatt Corallo <git@bluematt.me>
Sun, 14 Apr 2024 17:21:52 +0000 (17:21 +0000)
committerMatt Corallo <git@bluematt.me>
Sun, 14 Apr 2024 17:27:27 +0000 (17:27 +0000)
uniffi/Cargo.toml [new file with mode: 0644]
uniffi/build.rs [new file with mode: 0644]
uniffi/src/interface.udl [new file with mode: 0644]
uniffi/src/lib.rs [new file with mode: 0644]

diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml
new file mode 100644 (file)
index 0000000..8a223a8
--- /dev/null
@@ -0,0 +1,23 @@
+[package]
+name = "dnssec-prover-uniffi"
+version = "0.1.0"
+authors = ["Matt Corallo"]
+license = "MIT OR Apache-2.0"
+repository = "https://git.bitcoin.ninja/index.cgi?p=dnssec-prover"
+description = "A simple crate which allows for the creation and validation of transferrable proofs of entries in the DNS."
+edition = "2021"
+build = "build.rs"
+
+[dependencies]
+dnssec-prover = { path = "../", default-features = false, features = ["validation"] }
+uniffi = { version = "0.27" }
+
+[build-dependencies]
+uniffi = { version = "0.27", features = [ "build" ] }
+
+[lib]
+crate-type = ["cdylib"]
+
+[profile.release]
+lto = true
+codegen-units = 1
diff --git a/uniffi/build.rs b/uniffi/build.rs
new file mode 100644 (file)
index 0000000..4e14e57
--- /dev/null
@@ -0,0 +1,3 @@
+fn main() {
+       uniffi::generate_scaffolding("src/interface.udl").unwrap();
+}
diff --git a/uniffi/src/interface.udl b/uniffi/src/interface.udl
new file mode 100644 (file)
index 0000000..a6f02be
--- /dev/null
@@ -0,0 +1,10 @@
+namespace dnssec_prover {
+       string verify_byte_stream(bytes stream, string name_to_resolve);
+       ProofBuilder? init_proof_builder(string name, u16 ty);
+};
+
+interface ProofBuilder {
+       void process_query_response(bytes response);
+       bytes? get_next_query();
+       bytes? get_unverified_proof();
+};
diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs
new file mode 100644 (file)
index 0000000..109584b
--- /dev/null
@@ -0,0 +1,97 @@
+//! UniFFI-compatible verification wrappers
+
+uniffi::include_scaffolding!("interface");
+
+use dnssec_prover::ser::parse_rr_stream;
+use dnssec_prover::validation::{verify_rr_stream, ValidationError};
+use dnssec_prover::rr::Name;
+use dnssec_prover::query::ProofBuilder as NativeProofBuilder;
+use dnssec_prover::query::{QueryBuf};
+
+use std::collections::VecDeque;
+use std::fmt::Write;
+use std::sync::{Arc, Mutex};
+
+pub struct ProofBuilder(Mutex<(NativeProofBuilder, VecDeque<QueryBuf>)>);
+
+/// 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<Arc<ProofBuilder>> {
+       if !name.ends_with('.') { name.push('.'); }
+       if let Ok(qname) = name.try_into() {
+               let (builder, initial_query) = NativeProofBuilder::new(&qname, ty);
+               let mut queries = VecDeque::with_capacity(4);
+               queries.push_back(initial_query);
+               Some(Arc::new(ProofBuilder(Mutex::new((builder, queries)))))
+       } else {
+               None
+       }
+}
+
+impl ProofBuilder {
+       /// 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(&self, 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);
+                       let mut us = self.0.lock().unwrap();
+                       if let Ok(queries) = us.0.process_response(&answer) {
+                               for query in queries {
+                                       us.1.push_back(query);
+                               }
+                       }
+               }
+       }
+
+       /// 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(&self) -> Option<Vec<u8>> {
+               if let Some(query) = self.0.lock().unwrap().1.pop_front() {
+                       Some(query.into_vec())
+               } else {
+                       None
+               }
+       }
+
+       /// 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(&self) -> Option<Vec<u8>> {
+               self.0.lock().unwrap().0.clone().finish_proof().ok().map(|(proof, _ttl)| proof)
+       }
+}
+
+/// Verifies an RFC 9102-formatted proof and returns verified records matching the given name
+/// (resolving any C/DNAMEs as required).
+pub fn verify_byte_stream(stream: Vec<u8>, name_to_resolve: String) -> String {
+       let name = match Name::try_from(name_to_resolve) {
+               Ok(name) => name,
+               Err(()) => return "{\"error\":\"Bad name to resolve\"}".to_string(),
+       };
+       match do_verify_byte_stream(stream, name) {
+               Ok(r) => r,
+               Err(e) => format!("{{\"error\":\"{:?}\"}}", e),
+       }
+}
+
+fn do_verify_byte_stream(stream: Vec<u8>, name_to_resolve: Name) -> Result<String, ValidationError> {
+       let rrs = parse_rr_stream(&stream).map_err(|()| ValidationError::Invalid)?;
+       let verified_rrs = verify_rr_stream(&rrs)?;
+       let resolved_rrs = verified_rrs.resolve_name(&name_to_resolve);
+       let mut resp = String::new();
+       write!(&mut resp, "{}",
+               format_args!("{{\"valid_from\": {}, \"expires\": {}, \"max_cache_ttl\": {}, \"verified_rrs\": [",
+               verified_rrs.valid_from, verified_rrs.expires, verified_rrs.max_cache_ttl)
+       ).expect("Write to a String shouldn't fail");
+       for (idx, rr) in resolved_rrs.iter().enumerate() {
+               write!(&mut resp, "{}{}", if idx != 0 { ", " } else { "" }, rr.json())
+                       .expect("Write to a String shouldn't fail");
+       }
+       resp += "]}";
+       Ok(resp)
+}