diff --git a/matcher-rs/.gitignore b/matcher-rs/.gitignore new file mode 100644 index 0000000..b8f0008 --- /dev/null +++ b/matcher-rs/.gitignore @@ -0,0 +1,5 @@ +/target +**/*.rs.bk +/bin/ +pkg/ +wasm-pack.log diff --git a/matcher-rs/Cargo.lock b/matcher-rs/Cargo.lock new file mode 100644 index 0000000..a75e65c --- /dev/null +++ b/matcher-rs/Cargo.lock @@ -0,0 +1,319 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lol_alloc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e5106554cabc97552dcadf54f57560ae6af3276652f82ca2be06120dc4c5dc" +dependencies = [ + "spin", +] + +[[package]] +name = "matcher-rs" +version = "0.1.0" +dependencies = [ + "bindgen", + "lol_alloc", + "serde", + "serde_json", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/matcher-rs/Cargo.toml b/matcher-rs/Cargo.toml new file mode 100644 index 0000000..aa7c445 --- /dev/null +++ b/matcher-rs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "matcher-rs" +version = "0.1.0" +authors = ["rensiyuan"] +edition = "2024" +build = "build.rs" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +lol_alloc = "0.4.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.145" + +[dev-dependencies] + +[build-dependencies] +bindgen = "0.72.1" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" +lto = true diff --git a/matcher-rs/build.rs b/matcher-rs/build.rs new file mode 100644 index 0000000..f822d9b --- /dev/null +++ b/matcher-rs/build.rs @@ -0,0 +1,20 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=../matcher/credentialmanager.h"); + println!("cargo:rerun-if-env-changed=TARGET"); + + let builder = bindgen::Builder::default() + .header("../matcher/credentialmanager.h") + .clang_arg("-I/usr/include") + .clang_arg("-fvisibility=default") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())); + + let bindings = builder.generate().expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/matcher-rs/src/bin/issuance.rs b/matcher-rs/src/bin/issuance.rs new file mode 100644 index 0000000..01dda1c --- /dev/null +++ b/matcher-rs/src/bin/issuance.rs @@ -0,0 +1,12 @@ +use matcher_rs::{credman::CredmanApiImpl, issuance::issuance_main}; + +fn main() { + issuance_main(&mut CredmanApiImpl {}).unwrap(); +} + +// Credman expects this as the entry point, but it isn't there if the target is wasm32-unknown-unknown. +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +#[unsafe(no_mangle)] +extern "C" fn _start() { + main(); +} diff --git a/matcher-rs/src/bindings.rs b/matcher-rs/src/bindings.rs new file mode 100644 index 0000000..04d76a4 --- /dev/null +++ b/matcher-rs/src/bindings.rs @@ -0,0 +1,6 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/matcher-rs/src/credman.rs b/matcher-rs/src/credman.rs new file mode 100644 index 0000000..2c53c9d --- /dev/null +++ b/matcher-rs/src/credman.rs @@ -0,0 +1,69 @@ +use std::{ffi::CStr, os::raw::c_void}; + +use crate::bindings::{ + AddStringIdEntry, GetCredentialsSize, GetRequestBuffer, GetRequestSize, ReadCredentialsBuffer, +}; + +pub trait CredmanApi { + fn get_request_buffer(&self) -> Vec; + fn get_registered_data(&self) -> Vec; + fn add_string_id_entry( + &mut self, + entry_id: &CStr, + icon: Option<&[u8]>, + title: Option<&CStr>, + subtitle: Option<&CStr>, + disclaimer: Option<&CStr>, + warning: Option<&CStr>, + ); +} + +pub struct CredmanApiImpl; + +impl CredmanApi for CredmanApiImpl { + fn get_request_buffer(&self) -> Vec { + let mut size: u32 = 0; + unsafe { + GetRequestSize(&mut size); + } + let mut r = vec![0; size as usize]; + unsafe { + GetRequestBuffer(r.as_mut_ptr() as *mut c_void); + } + r + } + fn get_registered_data(&self) -> Vec { + let mut size: u32 = 0; + unsafe { + GetCredentialsSize(&mut size); + } + let mut r = vec![0; size.try_into().unwrap()]; + unsafe { + ReadCredentialsBuffer(r.as_mut_ptr() as *mut c_void, 0, size as usize); + } + r + } + fn add_string_id_entry( + &mut self, + entry_id: &CStr, + icon: Option<&[u8]>, + title: Option<&CStr>, + subtitle: Option<&CStr>, + disclaimer: Option<&CStr>, + warning: Option<&CStr>, + ) { + let icon_bytes = icon.map_or(std::ptr::null(), |x| x.as_ptr()) as *const i8; + let icon_length = icon.map_or(0, |x| x.len()); + unsafe { + AddStringIdEntry( + entry_id.as_ptr(), + icon_bytes, + icon_length, + title.map_or(std::ptr::null(), |x| x.as_ptr()), + subtitle.map_or(std::ptr::null(), |x| x.as_ptr()), + disclaimer.map_or(std::ptr::null(), |x| x.as_ptr()), + warning.map_or(std::ptr::null(), |x| x.as_ptr()), + ); + } + } +} diff --git a/matcher-rs/src/issuance.rs b/matcher-rs/src/issuance.rs new file mode 100644 index 0000000..9a55f3b --- /dev/null +++ b/matcher-rs/src/issuance.rs @@ -0,0 +1,345 @@ +use crate::{ + credman::CredmanApi, + issuance_matcher::IssuanceMatcherData, + openid4vci::{DigitalCredentialCreationRequest, RegularizedOpenId4VciRequestData}, +}; + +const ALLOWED_PROTOCOLS: [&str; 4] = [ + "openid4vci-1.0", + "openid4vci1.0", + "openid4vci-1.1", + "openid4vci1.1", +]; + +pub fn issuance_main(credman: &mut impl CredmanApi) -> Result<(), Box> { + let matcher_data_buffer = credman.get_registered_data(); + let json_start = u32::from_le_bytes(matcher_data_buffer[..size_of::()].try_into()?); + let matcher_data: IssuanceMatcherData = + serde_json::from_slice(&matcher_data_buffer[json_start.try_into()?..])?; + let request: DigitalCredentialCreationRequest = + serde_json::from_slice(&credman.get_request_buffer())?; + if request.requests.iter().any(|r| { + ALLOWED_PROTOCOLS.iter().any(|s| r.protocol == *s) + && matcher_data + .filter + .matches(&RegularizedOpenId4VciRequestData::from(&r.data)) + }) { + let icon = &matcher_data_buffer[matcher_data.icon.0..matcher_data.icon.1]; + credman.add_string_id_entry( + matcher_data.entry_id.as_c_str(), + if icon.is_empty() { None } else { Some(icon) }, + matcher_data.title.as_deref(), + matcher_data.subtitle.as_deref(), + None, + None, + ); + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use std::ffi::{CStr, CString}; + + struct AddedEntry { + entry_id: CString, + icon: Option>, + title: Option, + subtitle: Option, + disclaimer: Option, + warning: Option, + } + + struct FakeCredman { + request_json: &'static str, + registered_json: &'static str, + icon: Vec, + added_entries: Vec, + } + + impl CredmanApi for FakeCredman { + fn get_request_buffer(&self) -> Vec { + self.request_json.as_bytes().into() + } + + fn get_registered_data(&self) -> Vec { + let mut result = Vec::with_capacity(4 + self.icon.len() + self.registered_json.len()); + result.extend_from_slice(&u32::to_le_bytes(4 + self.icon.len() as u32)); + result.extend_from_slice(&self.icon); + result.extend_from_slice(self.registered_json.as_bytes()); + result + } + + fn add_string_id_entry( + &mut self, + entry_id: &CStr, + icon: Option<&[u8]>, + title: Option<&CStr>, + subtitle: Option<&CStr>, + disclaimer: Option<&CStr>, + warning: Option<&CStr>, + ) { + self.added_entries.push(AddedEntry { + entry_id: entry_id.to_owned(), + icon: icon.map(|i| i.to_vec()), + title: title.map(|c| c.to_owned()), + subtitle: subtitle.map(|c| c.to_owned()), + disclaimer: disclaimer.map(|c| c.to_owned()), + warning: warning.map(|c| c.to_owned()), + }); + } + } + + #[test] + fn match_case1() { + let mut credman = FakeCredman { + request_json: r#" +{ + "requests": [ + { + "protocol": "openid4vci-1.1", + "data": { + "credential_offer": { + "credential_issuer": "https://issuer.my", + "credential_configuration_ids": [ + "US_SOCIAL_SECURITY_NUMBER" + ], + "grants": { + "authorization_code": {} + } + }, + "credential_issuer_metadata": { + "nonce_endpoint": "https://nonce.my" + } + } + } + ] +}"#, + registered_json: r#" + { + "entry_id": "C", + "title": "TTTT", + "subtitle": "SSSSS", + "icon": [0, 0], + "filter": { + "And": { + "filters": [{ + "ConfigurationIdAllowlist": { + "configuration_ids": ["US_SOCIAL_SECURITY_NUMBER", "EU_AGE"] + } + }, { + "IssuerAllowlist": { + "issuers": ["ccb", "https://issuer.my"] + } + }] + } + } + }"#, + icon: Vec::new(), + added_entries: Vec::new(), + }; + + issuance_main(&mut credman).unwrap(); + + assert_eq!(credman.added_entries.len(), 1); + let entry = &credman.added_entries[0]; + assert_eq!(entry.entry_id, c"C"); + assert_eq!(entry.title.as_ref().unwrap(), c"TTTT"); + assert_eq!(entry.subtitle.as_ref().unwrap(), c"SSSSS"); + assert!(entry.icon.is_none()); + } + + #[test] + fn invalid_json() { + let mut credman = FakeCredman { + request_json: r#" +{ + "requests": [ + { + "protocol": "openid4vci-1.1", + "data": { + "credential_offer": { + "credential_issuer": "https://issuer.my", + "credential_configuration_ids": [ + "US_SOCIAL_SECURITY_NUMBER" + ], + "grants": { + "authorization_code": {} + } + }, + "credential_issuer_metadata": { + "nonce_endpoint": "https://nonce.my" + } + } + } + ] +"#, + registered_json: r#" + { + "entry_id": "C", + "title": "TTTT", + "subtitle": "SSSSS", + "icon": [0, 0], + "filter": {"Unit": {}}"#, + icon: Vec::new(), + added_entries: Vec::new(), + }; + + let errmsg = format!("{:?}", issuance_main(&mut credman).unwrap_err()); + assert!(errmsg.contains("EOF while parsing an object")); + } + + #[test] + fn nomatch_case1() { + let mut credman = FakeCredman { + request_json: r#" +{ + "requests": [ + { + "protocol": "openid4vci-1.1", + "data": { + "credential_offer": { + "credential_issuer": "https://issuer.my", + "credential_configuration_ids": [ + "US_SOCIAL_SECURITY_NUMBER" + ], + "grants": { + "authorization_code": {} + } + }, + "credential_issuer_metadata": { + "nonce_endpoint": "https://nonce.my" + } + } + } + ] +}"#, + registered_json: r#" +{ + "entry_id": "C", + "title": "TTTT", + "subtitle": "SSSSS", + "icon": [ + 0, + 0 + ], + "filter": { + "And": { + "filters": [ + { + "ConfigurationIdAllowlist": { + "configuration_ids": [ + "US_SOCIAL_SECURITY_NUMBER", + "EU_AGE" + ] + } + }, + { + "IssuerAllowlist": { + "issuers": [ + "ccb", + "https://issuer.my" + ] + } + }, + { + "Not": { + "filter": { + "SupportsNonceEndpoint": { + } + } + } + } + ] + } + } +}"#, + icon: Vec::new(), + added_entries: Vec::new(), + }; + + issuance_main(&mut credman).unwrap(); + + assert_eq!(credman.added_entries.len(), 0); + } + + #[test] + fn match_mdoc_doctype() { + let mut credman = FakeCredman { + request_json: r#" +{ + "requests": [ + { + "protocol": "openid4vci-1.1", + "data": { + "credential_offer": { + "credential_issuer": "https://issuer.my", + "credential_configuration_ids": [ + "FICTITIOUS_STATE_MDL" + ], + "grants": { + "authorization_code": {} + } + }, + "credential_issuer_metadata": { + "nonce_endpoint": "https://nonce.my", + "credential_configurations_supported": { + "FICTITIOUS_STATE_MDL": { + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL" + } + } + } + } + } + ] +}"#, + registered_json: r#" +{ + "entry_id": "C", + "title": "TTTT", + "subtitle": "SSSSS", + "icon": [ + 0, + 0 + ], + "filter": { + "Or": { + "filters": [ + { + "ConfigurationIdAllowlist": { + "configuration_ids": [ + "US_SOCIAL_SECURITY_NUMBER", + "EU_AGE" + ] + } + }, + { + "IssuerAllowlist": { + "issuers": [ + "ccb" + ] + } + }, + { + "SupportsMdocDoctype": { + "doctypes": [ + "org.iso.18013.5.1.mDL" + ] + } + } + ] + } + } +}"#, + icon: Vec::new(), + added_entries: Vec::new(), + }; + + issuance_main(&mut credman).unwrap(); + + assert_eq!(credman.added_entries.len(), 1); + } +} diff --git a/matcher-rs/src/issuance_matcher.rs b/matcher-rs/src/issuance_matcher.rs new file mode 100644 index 0000000..a9edc31 --- /dev/null +++ b/matcher-rs/src/issuance_matcher.rs @@ -0,0 +1,90 @@ +use std::{collections::HashSet, ffi::CString}; + +use serde::Deserialize; + +use crate::openid4vci::RegularizedOpenId4VciRequestData; + +#[derive(Deserialize, Debug)] +pub enum OpenId4VciFilter { + Unit {}, // A placeholder that always matches + And { filters: Vec }, + Or { filters: Vec }, + Not { filter: Box }, + IssuerAllowlist { issuers: HashSet }, + ConfigurationIdAllowlist { configuration_ids: HashSet }, + SupportsAuthCodeFlow {}, + SupportsPreAuthFlow {}, + SupportsNonceEndpoint {}, + SupportsDeferredCredentialEndpoint {}, + SupportsNotificationEndpoint {}, + RequiresBatchIssuance { min_batch_size: u32 }, + SupportsMdocDoctype { doctypes: HashSet }, + SupportsSdJwtVct { vcts: HashSet }, +} + +impl Default for OpenId4VciFilter { + fn default() -> Self { + Self::Unit {} + } +} + +impl OpenId4VciFilter { + pub fn matches(&self, request: &RegularizedOpenId4VciRequestData) -> bool { + match &self { + Self::Unit {} => true, + Self::And { filters } => filters.iter().all(|f| f.matches(request)), + Self::Or { filters } => filters.iter().any(|f| f.matches(request)), + Self::Not { filter } => !filter.matches(request), + Self::IssuerAllowlist { issuers } => { + issuers.contains(&request.credential_offer.credential_issuer) + } + Self::ConfigurationIdAllowlist { configuration_ids } => request + .credential_offer + .credential_configuration_ids + .iter() + .any(|id| configuration_ids.contains(id)), + Self::SupportsAuthCodeFlow {} => request + .credential_offer + .grants + .contains_key("authorization_code"), + Self::SupportsPreAuthFlow {} => request + .credential_offer + .grants + .contains_key("urn:ietf:params:oauth:grant-type:pre-authorized_code"), + Self::SupportsNonceEndpoint {} => request + .credential_issuer_metadata + .is_some_and(|m| !m.nonce_endpoint.is_empty()), + Self::SupportsDeferredCredentialEndpoint {} => request + .credential_issuer_metadata + .is_some_and(|m| !m.deferred_credential_endpoint.is_empty()), + Self::SupportsNotificationEndpoint {} => request + .credential_issuer_metadata + .is_some_and(|m| !m.notification_endpoint.is_empty()), + Self::RequiresBatchIssuance { min_batch_size } => { + request.credential_issuer_metadata.is_some_and(|m| { + m.batch_credential_issuance + .as_ref() + .is_some_and(|b| b.batch_size >= *min_batch_size) + }) + } + Self::SupportsMdocDoctype { doctypes } => request + .credential_configurations + .iter() + .any(|c| doctypes.contains(&c.doctype)), + Self::SupportsSdJwtVct { vcts } => request + .credential_configurations + .iter() + .any(|c| vcts.contains(&c.vct)), + } + } +} + +#[derive(Deserialize, Debug, Default)] +#[serde(default)] +pub struct IssuanceMatcherData { + pub entry_id: CString, + pub icon: (usize, usize), + pub title: Option, + pub subtitle: Option, + pub filter: OpenId4VciFilter, +} diff --git a/matcher-rs/src/lib.rs b/matcher-rs/src/lib.rs new file mode 100644 index 0000000..fb23de0 --- /dev/null +++ b/matcher-rs/src/lib.rs @@ -0,0 +1,10 @@ +#[cfg(target_arch = "wasm32")] +#[global_allocator] +static ALLOCATOR: lol_alloc::AssumeSingleThreaded = + unsafe { lol_alloc::AssumeSingleThreaded::new(lol_alloc::LeakingAllocator::new()) }; + +pub mod bindings; +pub mod credman; +pub mod issuance; +pub mod issuance_matcher; +pub mod openid4vci; diff --git a/matcher-rs/src/openid4vci.rs b/matcher-rs/src/openid4vci.rs new file mode 100644 index 0000000..1fffef7 --- /dev/null +++ b/matcher-rs/src/openid4vci.rs @@ -0,0 +1,170 @@ +#![allow(unused)] + +use serde::Deserialize; + +#[derive(Deserialize, Debug, Default)] +#[serde(default)] +pub struct DigitalCredentialCreationRequest { + pub requests: Vec, +} + +#[derive(Deserialize, Debug, Default)] +#[serde(default)] +pub struct OpenId4VciRequest { + pub protocol: String, + pub data: OpenId4VciRequestData, +} + +#[derive(Deserialize, Debug, Default)] +#[serde(default)] +pub struct OpenId4VciRequestData { + pub credential_offer: credential_offer::CredentialOffer, + pub credential_issuer_metadata: Option, +} + +/** + * This correlates the `credential_configuration_ids` in the offer and `credential_configurations_supported` in the issuer metadata. + */ +pub struct RegularizedOpenId4VciRequestData<'a> { + // Borrow the offer + pub credential_offer: &'a credential_offer::CredentialOffer, + // Borrow the metadata + pub credential_issuer_metadata: + Option<&'a credential_issuer_metadata::CredentialIssuerMetadata>, + pub credential_configurations: Vec<&'a credential_issuer_metadata::CredentialConfiguration>, +} + +// Implement From on a Reference +impl<'a> From<&'a OpenId4VciRequestData> for RegularizedOpenId4VciRequestData<'a> { + fn from(value: &'a OpenId4VciRequestData) -> Self { + let mut configurations = + Vec::with_capacity(value.credential_offer.credential_configuration_ids.len()); + + if let Some(metadata) = &value.credential_issuer_metadata { + for id in &value.credential_offer.credential_configuration_ids { + if let Some(config) = metadata.credential_configurations_supported.get(id) { + configurations.push(config); + } + } + } + + Self { + credential_offer: &value.credential_offer, + credential_issuer_metadata: value.credential_issuer_metadata.as_ref(), + credential_configurations: configurations, + } + } +} + +pub mod credential_offer { + use serde::Deserialize; + use std::collections::HashMap; + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct CredentialOffer { + pub credential_issuer: String, + pub credential_configuration_ids: Vec, + pub grants: HashMap, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct Grant {} +} + +mod credential_issuer_metadata { + use serde::Deserialize; + use std::collections::{HashMap, HashSet}; + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct CredentialIssuerMetadata { + // pub credential_issuer: String, + // pub authorization_servers: Option>, + // pub credential_endpoint: String, + pub nonce_endpoint: String, + pub deferred_credential_endpoint: String, + pub notification_endpoint: String, + // pub credential_request_encryption: Option, + // pub credential_response_encryption: Option, + pub batch_credential_issuance: Option, + // pub display: Option>, + pub credential_configurations_supported: HashMap, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct CredentialRequestEncryption { + // pub jwks: serde_json::Value, + pub enc_values_supported: HashSet, + // pub zip_values_supported: Option>, + pub encryption_required: bool, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct CredentialResponseEncryption { + pub alg_values_supported: HashSet, + pub enc_values_supported: HashSet, + // pub zip_values_supported: Vec, + pub encryption_required: bool, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct BatchCredentialIssuance { + pub batch_size: u32, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct Display { + pub name: String, + pub locale: String, + pub logo: Option, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct Logo { + pub uri: String, + pub alt_text: String, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct CredentialConfiguration { + pub format: String, + pub scope: String, + pub doctype: String, + pub vct: String, + pub credential_signing_alg_values_supported: SiginingAlgs, + pub cryptographic_binding_methods_supported: Option>, + pub proof_types_supported: HashMap, + } + + #[derive(Deserialize, Debug)] + #[serde(untagged)] + #[derive(Default)] + pub enum SiginingAlgs { + #[default] + Unspecified, + SringAlgs(Vec), + IntAlgs(Vec), + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct ProofType { + pub proof_signing_alg_values_supported: Vec, + pub key_attestations_required: Option, + } + + #[derive(Deserialize, Debug, Default)] + #[serde(default)] + pub struct KeyAttestationsRequired { + pub key_storage: Option>, + pub user_authentication: Option>, + } +} diff --git a/matcher/credentialmanager.h b/matcher/credentialmanager.h index ca21af7..bd7b18f 100644 --- a/matcher/credentialmanager.h +++ b/matcher/credentialmanager.h @@ -8,52 +8,55 @@ #if defined(__wasm__) __attribute__((import_module("credman"), import_name("AddEntry"))) #endif -void AddEntry(long long cred_id, char* icon, size_t icon_len, char *title, char *subtitle, char *disclaimer, char *warning); +void AddEntry(long long cred_id, const char* icon, size_t icon_len, const char* title, const char* subtitle, const char* disclaimer, const char* warning); // Deprecated. Use AddFieldForStringIdEntry instead. #if defined(__wasm__) __attribute__((import_module("credman"), import_name("AddField"))) #endif -void AddField(long long cred_id, char *field_display_name, char *field_display_value); +void AddField(long long cred_id, const char* field_display_name, const char* field_display_value); #if defined(__wasm__) __attribute__((import_module("credman_v2"), import_name("AddEntrySet"))) #endif -void AddEntrySet(char *set_id, int set_length); +void AddEntrySet(const char* set_id, int set_length); #if defined(__wasm__) __attribute__((import_module("credman_v2"), import_name("AddEntryToSet"))) #endif -void AddEntryToSet(char *cred_id, char* icon, size_t icon_len, char *title, char *subtitle, char *disclaimer, char *warning, char *metadata, char *set_id, int set_index); +void AddEntryToSet(const char* cred_id, const char* icon, size_t icon_len, const char* title, const char* subtitle, const char* disclaimer, const char* warning, const char* metadata, const char* set_id, int set_index); #if defined(__wasm__) __attribute__((import_module("credman_v2"), import_name("AddFieldToEntrySet"))) #endif -void AddFieldToEntrySet(char *cred_id, char *field_display_name, char *field_display_value, char *set_id, int set_index); +void AddFieldToEntrySet(const char* cred_id, const char* field_display_name, const char* field_display_value, const char* set_id, int set_index); #if defined(__wasm__) __attribute__((import_module("credman_v2"), import_name("AddPaymentEntryToSet"))) #endif -void AddPaymentEntryToSet(char *cred_id, char *merchant_name, char *payment_method_name, char *payment_method_subtitle, char* payment_method_icon, size_t payment_method_icon_len, char *transaction_amount, char* bank_icon, size_t bank_icon_len, char* payment_provider_icon, size_t payment_provider_icon_len, char *metadata, char *set_id, int set_index); +void AddPaymentEntryToSet(const char* cred_id, const char* merchant_name, const char* payment_method_name, const char* payment_method_subtitle, const char* payment_method_icon, size_t payment_method_icon_len, const char* transaction_amount, const char* bank_icon, size_t bank_icon_len, const char* payment_provider_icon, size_t payment_provider_icon_len, const char* metadata, const char* set_id, int set_index); #if defined(__wasm__) __attribute__((import_module("credman_v2"), import_name("AddPaymentEntryToSetV2"))) #endif -void AddPaymentEntryToSetV2(char *cred_id, char *merchant_name, char *payment_method_name, char *payment_method_subtitle, char* payment_method_icon, size_t payment_method_icon_len, char *transaction_amount, char* bank_icon, size_t bank_icon_len, char* payment_provider_icon, size_t payment_provider_icon_len, char *additional_info, char *metadata, char *set_id, int set_index); +void AddPaymentEntryToSetV2(const char* cred_id, const char* merchant_name, const char* payment_method_name, const char* payment_method_subtitle, const char* payment_method_icon, size_t payment_method_icon_len, const char* transaction_amount, const char* bank_icon, size_t bank_icon_len, const char* payment_provider_icon, size_t payment_provider_icon_len, const char* additional_info, const char* metadata, const char* set_id, int set_index); #if defined(__wasm__) __attribute__((import_module("credman"), import_name("AddStringIdEntry"))) #endif -void AddStringIdEntry(char *cred_id, char* icon, size_t icon_len, char *title, char *subtitle, char *disclaimer, char *warning); +void AddStringIdEntry(const char* cred_id, const char* icon, size_t icon_len, const char* title, const char* subtitle, const char* disclaimer, const char* warning); #if defined(__wasm__) __attribute__((import_module("credman"), import_name("AddFieldForStringIdEntry"))) #endif -void AddFieldForStringIdEntry(char *cred_id, char *field_display_name, char *field_display_value); +void AddFieldForStringIdEntry(const char* cred_id, const char* field_display_name, const char* field_display_value); #if defined(__wasm__) __attribute__((import_module("credman"), import_name("GetRequestBuffer"))) #endif +#if defined(__rust_bindgen__) +__attribute__ ((visibility("default"))) +#endif void GetRequestBuffer(void* buffer); #if defined(__wasm__) @@ -79,17 +82,17 @@ void GetWasmVersion(uint32_t* version); #if defined(__wasm__) __attribute__((import_module("credman"), import_name("AddPaymentEntry"))) #endif -void AddPaymentEntry(char *cred_id, char *merchant_name, char *payment_method_name, char *payment_method_subtitle, char* payment_method_icon, size_t payment_method_icon_len, char *transaction_amount, char* bank_icon, size_t bank_icon_len, char* payment_provider_icon, size_t payment_provider_icon_len); +void AddPaymentEntry(const char* cred_id, const char* merchant_name, const char* payment_method_name, const char* payment_method_subtitle, const char* payment_method_icon, size_t payment_method_icon_len, const char* transaction_amount, const char* bank_icon, size_t bank_icon_len, const char* payment_provider_icon, size_t payment_provider_icon_len); #if defined(__wasm__) __attribute__((import_module("credman"), import_name("AddInlineIssuanceEntry"))) #endif -void AddInlineIssuanceEntry(char *cred_id, char* icon, size_t icon_len, char *title, char *subtitle); +void AddInlineIssuanceEntry(const char* cred_id, const char* icon, size_t icon_len, const char* title, const char* subtitle); #if defined(__wasm__) __attribute__((import_module("credman"), import_name("SetAdditionalDisclaimerAndUrlForVerificationEntry"))) #endif -void SetAdditionalDisclaimerAndUrlForVerificationEntry(char *cred_id, char *secondary_disclaimer, char *url_display_text, char *url_value); +void SetAdditionalDisclaimerAndUrlForVerificationEntry(const char* cred_id, const char* secondary_disclaimer, const char* url_display_text, const char* url_value); typedef struct CallingAppInfo { char package_name[256]; @@ -106,6 +109,6 @@ void GetCallingAppInfo(CallingAppInfo* info); #if defined(__wasm__) __attribute__((import_module("credman_v4"), import_name("SelfDeclarePackageInfo"))) #endif -void SelfDeclarePackageInfo(char *package_display_name, char* package_icon, size_t package_icon_len); +void SelfDeclarePackageInfo(const char* package_display_name, const char* package_icon, size_t package_icon_len); #endif diff --git a/matcher/testharness.c b/matcher/testharness.c index 291e2ee..d1d51c8 100644 --- a/matcher/testharness.c +++ b/matcher/testharness.c @@ -36,10 +36,10 @@ size_t ReadCredentialsBuffer(void* buffer, size_t offset, size_t len) { return bytes_read; } -void AddStringIdEntry(char *cred_id, char* icon, size_t icon_len, char *title, char *subtitle, char *disclaimer, char *warning) { +void AddStringIdEntry(const char* cred_id, const char* icon, size_t icon_len, const char* title, const char* subtitle, const char* disclaimer, const char* warning) { printf("AddStringIdEntry id:%s title:%s subtitle:%s\n", cred_id, title, subtitle); } -void AddFieldForStringIdEntry(char *cred_id, char *field_display_name, char *field_display_value) { +void AddFieldForStringIdEntry(const char* cred_id, const char* field_display_name, const char* field_display_value) { printf("AddFieldForStringIdEntry id:%s field_display_name:%s\n", cred_id, field_display_name); }