From 1302e5cd6a3f9618e197ca67af53b94310585a96 Mon Sep 17 00:00:00 2001 From: Dzejkop Date: Wed, 2 Sep 2026 18:59:06 +0200 Subject: [PATCH 1/9] feat: add attested DeepFace matching --- Cargo.lock | 442 ++++++++++++++-- Cargo.toml | 5 + .../tests/authenticator_integration.rs | 2 +- crates/walletkit-deepface/Cargo.toml | 26 + crates/walletkit-deepface/src/lib.rs | 493 ++++++++++++++++++ flake.lock | 6 +- rust-toolchain.toml | 2 +- 7 files changed, 942 insertions(+), 34 deletions(-) create mode 100644 crates/walletkit-deepface/Cargo.toml create mode 100644 crates/walletkit-deepface/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 90e3097a..868113cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "aes" version = "0.8.4" @@ -61,24 +71,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] +[[package]] +name = "aes" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", "subtle", ] +[[package]] +name = "aes-gcm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" +dependencies = [ + "aead 0.6.1", + "aes 0.9.3", + "cipher 0.5.2", + "ctr 0.10.1", + "ctutils", + "ghash 0.6.0", +] + [[package]] name = "ahash" version = "0.8.12" @@ -1512,6 +1547,29 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attested-channel" +version = "0.1.0" +source = "git+https://github.com/worldcoin/embedding-verifier?rev=3c130d9d9e3780eeb8b93dda3f482f5a75f95c83#3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" +dependencies = [ + "aes-gcm 0.11.1", + "aws-nitro-enclaves-nsm-api", + "base64 0.22.1", + "ciborium", + "coset", + "getrandom 0.4.3", + "hex", + "hkdf 0.13.0", + "hpke 0.14.0", + "p384", + "serde", + "sha2 0.11.0", + "thiserror 2.0.18", + "webpki", + "x509-cert", + "zeroize", +] + [[package]] name = "auto_impl" version = "1.3.0" @@ -1552,6 +1610,19 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "aws-nitro-enclaves-nsm-api" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92c1f4471b33f6a7af9ea421b249ed18a11c71156564baf6293148fa6ad1b09" +dependencies = [ + "libc", + "log", + "serde", + "serde_bytes", + "serde_cbor", +] + [[package]] name = "backon" version = "1.6.0" @@ -1993,7 +2064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -2014,9 +2085,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", + "aead 0.5.2", "chacha20 0.9.1", - "cipher", + "cipher 0.4.4", "poly1305", "zeroize", ] @@ -2059,7 +2130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half", + "half 2.7.1", ] [[package]] @@ -2069,10 +2140,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", - "inout", + "inout 0.1.4", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "circom-witness-rs" version = "0.2.3" @@ -2144,6 +2226,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -2339,6 +2427,12 @@ dependencies = [ "ciborium-io", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2487,7 +2581,25 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] @@ -2499,7 +2611,21 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "fiat-crypto", + "fiat-crypto 0.2.9", + "rustc_version 0.4.1", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "fiat-crypto 0.3.0", "rustc_version 0.4.1", "subtle", ] @@ -2585,6 +2711,41 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "deepface-client" +version = "0.1.0" +source = "git+https://github.com/worldcoin/embedding-verifier?rev=3c130d9d9e3780eeb8b93dda3f482f5a75f95c83#3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" +dependencies = [ + "attested-channel", + "base64 0.22.1", + "deepface-protocol", + "getrandom 0.4.3", + "hex", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "deepface-protocol" +version = "0.1.0" +source = "git+https://github.com/worldcoin/embedding-verifier?rev=3c130d9d9e3780eeb8b93dda3f482f5a75f95c83#3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" +dependencies = [ + "aes-gcm 0.11.1", + "ark-ff 0.5.0", + "attested-channel", + "ciborium", + "coset", + "serde", + "serde_bytes", + "taceo-ark-babyjubjub", + "taceo-eddsa-babyjubjub", + "taceo-poseidon2", + "zeroize", +] + [[package]] name = "delegate-display" version = "3.0.1" @@ -2606,10 +2767,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "der_derive", + "flagset", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "deranged" version = "0.5.8" @@ -2705,6 +2879,7 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -2979,6 +3154,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "filetime" version = "0.2.29" @@ -3013,6 +3194,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -3233,7 +3420,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval", + "polyval 0.6.2", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval 0.7.3", ] [[package]] @@ -3331,6 +3527,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "half" version = "2.7.1" @@ -3432,6 +3634,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -3463,7 +3668,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -3475,23 +3689,50 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "hpke" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f65d16b699dd1a1fa2d851c970b0c971b388eeeb40f744252b8de48860980c8f" dependencies = [ - "aead", - "aes-gcm", + "aead 0.5.2", + "aes-gcm 0.10.3", "chacha20poly1305", "digest 0.10.7", "generic-array", - "hkdf", - "hmac", + "hkdf 0.12.4", + "hmac 0.12.1", "rand_core 0.9.5", "sha2 0.10.9", "subtle", - "x25519-dalek", + "x25519-dalek 2.0.1", + "zeroize", +] + +[[package]] +name = "hpke" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5130e119706b4d8c2180da6126f7e60b6c38c2d340d539219f57051f0a7af7" +dependencies = [ + "aead 0.6.1", + "aes-gcm 0.11.1", + "getrandom 0.4.3", + "hkdf 0.13.0", + "hybrid-array", + "rand_core 0.10.1", + "sha2 0.11.0", + "subtle", + "x25519-dalek 3.0.0", "zeroize", ] @@ -3862,6 +4103,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "inplace-vec-builder" version = "0.1.1" @@ -4650,13 +4900,13 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41a03aaaf57495c75ce66aee6a7c3b21abf046c9d4cca3d45b22cdbf0de1bfba" dependencies = [ - "aead", - "aes-gcm", + "aead 0.5.2", + "aes-gcm 0.10.3", "byteorder", "chacha20poly1305", "hex", - "hkdf", - "hpke", + "hkdf 0.12.4", + "hpke 0.13.0", "log", "rand 0.9.4", "serde", @@ -4881,6 +5131,18 @@ dependencies = [ "transpose", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -5065,7 +5327,7 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] @@ -5077,7 +5339,18 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", ] [[package]] @@ -6205,12 +6478,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 1.0.8", ] @@ -6258,7 +6533,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -7073,6 +7348,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -8021,6 +8316,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "tokio" version = "1.52.3" @@ -8587,6 +8903,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -8742,7 +9068,7 @@ dependencies = [ "eyre", "getrandom 0.3.4", "hex", - "hkdf", + "hkdf 0.12.4", "log", "mockito", "rand 0.8.6", @@ -8793,6 +9119,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "walletkit-deepface" +version = "0.21.4" +dependencies = [ + "async-trait", + "attested-channel", + "deepface-client", + "deepface-protocol", + "thiserror 2.0.18", + "tokio", + "zeroize", +] + [[package]] name = "walletkit-sqlite" version = "0.21.4" @@ -8946,6 +9285,19 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -8980,6 +9332,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-root-certs" version = "0.26.11" @@ -9527,10 +9889,32 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", ] +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek 5.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", + "tls_codec", +] + [[package]] name = "xattr" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index 3596fc34..1909bae8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/uniffi-bindgen", "crates/walletkit-core", + "crates/walletkit-deepface", "crates/walletkit", "crates/walletkit-db", "crates/walletkit-sqlite", @@ -30,6 +31,8 @@ alloy = { version = "2", default-features = false } alloy-core = { version = "1", default-features = false, features = [ "sol-types", ] } +attested-channel = { version = "0.1.0", git = "https://github.com/worldcoin/embedding-verifier", rev = "3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" } +async-trait = "0.1" backon = "1.6" base64 = "0.22" cc = "1" @@ -40,6 +43,8 @@ clap = "4" ctor = "0.2" dirs = "6" dotenvy = "0.15.7" +deepface-client = { version = "0.1.0", git = "https://github.com/worldcoin/embedding-verifier", rev = "3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" } +deepface-protocol = { version = "0.1.0", git = "https://github.com/worldcoin/embedding-verifier", rev = "3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" } eyre = "0.6" getrandom = "0.3" hex = "0.4" diff --git a/crates/walletkit-core/tests/authenticator_integration.rs b/crates/walletkit-core/tests/authenticator_integration.rs index b1cf3815..6239f55e 100644 --- a/crates/walletkit-core/tests/authenticator_integration.rs +++ b/crates/walletkit-core/tests/authenticator_integration.rs @@ -88,5 +88,5 @@ async fn test_authenticator_integration() { .await .unwrap(); let packed_account_data = authenticator.packed_account_data(); - println!("Created World ID with packed account data: {packed_account_data:?}",); + println!("Created World ID with packed account data: {packed_account_data:?}"); } diff --git a/crates/walletkit-deepface/Cargo.toml b/crates/walletkit-deepface/Cargo.toml new file mode 100644 index 00000000..5902bc87 --- /dev/null +++ b/crates/walletkit-deepface/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "walletkit-deepface" +description = "Attested DeepFace matching for WalletKit." +publish = false + +version.workspace = true +edition.workspace = true +rust-version = "1.97" +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-trait = { workspace = true } +attested-channel = { workspace = true, features = ["attestation"] } +deepface-client = { workspace = true } +deepface-protocol = { workspace = true } +thiserror = { workspace = true } +zeroize = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt"] } + +[lints] +workspace = true diff --git a/crates/walletkit-deepface/src/lib.rs b/crates/walletkit-deepface/src/lib.rs new file mode 100644 index 00000000..02a926c1 --- /dev/null +++ b/crates/walletkit-deepface/src/lib.rs @@ -0,0 +1,493 @@ +#![cfg_attr(target_arch = "wasm32", allow(missing_docs))] +#![cfg(not(target_arch = "wasm32"))] + +//! Attested `DeepFace` matching in preparation for zero-knowledge proof generation. +//! +//! This module deliberately knows nothing about Orb PCP storage. Its caller supplies the live +//! image and the credential material obtained through the platform's Oxide/OrbKit adapter. The +//! module owns assignment, attestation verification, sealing, transport, response opening, and +//! match-token verification. + +use std::time::SystemTime; + +use async_trait::async_trait; +use attested_channel::channel::CHANNEL_VERSION; +use deepface_client::{ClientError, Config, FaceVerifierClient, VerifiedAssignment}; +use deepface_protocol::messages::{FailureReason, MatchInputs, MatchResult}; +use thiserror::Error; +use zeroize::Zeroize; + +/// Inputs for one attested `DeepFace` match. +/// +/// `credential_image` and `hashes_json` must come from the same enrolled Orb PCP. In particular, +/// `hashes_json` must contain the exact archive bytes, not parsed and reserialized JSON. +#[derive(Debug)] +pub struct DeepFaceMatchRequest { + /// Raw liveness image bytes captured for this request. + pub live_image: Vec, + /// Raw `thumbnail.png` bytes decrypted from the enrolled Orb PCP. + pub credential_image: Vec, + /// Exact raw `hashes.json` bytes extracted from the enrolled Orb PCP. + pub hashes_json: Vec, + /// Opaque identifier used by the TEE host to locate the RP's encrypted challenge image. + pub challenge_image_id: String, + /// AES-256-GCM key supplied by the RP for the challenge image. + pub challenge_image_key: [u8; 32], + /// AES-256-GCM nonce supplied by the RP for the challenge image. + pub challenge_image_iv: [u8; 12], + /// Minimum similarity required by the RP. Must be finite and between zero and one. + pub match_threshold: f32, +} + +impl DeepFaceMatchRequest { + fn validate(&self) -> Result<(), DeepFaceError> { + for (attribute, bytes) in [ + ("live_image", self.live_image.as_slice()), + ("credential_image", self.credential_image.as_slice()), + ("hashes_json", self.hashes_json.as_slice()), + ] { + if bytes.is_empty() { + return Err(DeepFaceError::InvalidInput { + attribute, + reason: "must not be empty".to_string(), + }); + } + } + + if self.challenge_image_id.trim().is_empty() { + return Err(DeepFaceError::InvalidInput { + attribute: "challenge_image_id", + reason: "must not be empty".to_string(), + }); + } + + if !self.match_threshold.is_finite() + || !(0.0..=1.0).contains(&self.match_threshold) + { + return Err(DeepFaceError::InvalidInput { + attribute: "match_threshold", + reason: "must be finite and between 0 and 1 inclusive".to_string(), + }); + } + + Ok(()) + } + + fn into_sensitive_inputs(mut self) -> SensitiveMatchInputs { + SensitiveMatchInputs { + challenge_image_id: std::mem::take(&mut self.challenge_image_id), + inputs: MatchInputs { + version: CHANNEL_VERSION, + live_image: std::mem::take(&mut self.live_image), + credential_image: std::mem::take(&mut self.credential_image), + hashes_json: std::mem::take(&mut self.hashes_json), + challenge_image_key: std::mem::take(&mut self.challenge_image_key), + challenge_image_iv: std::mem::take(&mut self.challenge_image_iv), + match_threshold: self.match_threshold, + }, + } + } +} + +impl Drop for DeepFaceMatchRequest { + fn drop(&mut self) { + self.live_image.zeroize(); + self.credential_image.zeroize(); + self.hashes_json.zeroize(); + self.challenge_image_key.zeroize(); + self.challenge_image_iv.zeroize(); + } +} + +/// A match token whose sealed response, signing-key attestation, and signature were verified. +/// +/// This is the internal handoff to `DeepFace` proof generation. It is intentionally not a +/// `UniFFI` type: the foreign side should eventually receive only the generated `DeepFace` proof. +#[derive(Debug)] +pub struct VerifiedMatchToken(Vec); + +impl VerifiedMatchToken { + /// Borrows the encoded COSE/CBOR token for proof generation. + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl Drop for VerifiedMatchToken { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +/// A sealed match rejection returned by the attested enclave. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeepFaceMatchRejection { + /// The sealed inputs were malformed. + MalformedInputs, + /// The channel version was not supported by the enclave. + UnsupportedVersion, + /// The PCP hashes file was invalid or did not contain the thumbnail commitment. + InvalidHashesJson, + /// The credential image did not match the PCP thumbnail commitment. + ThumbnailHashMismatch, + /// At least one comparison scored below the requested threshold. + MatchBelowThreshold, + /// The enclave could not obtain a usable comparison score from the images. + ImageAnalysisFailed, + /// The RP's challenge image could not be authenticated with the supplied key and nonce. + ChallengeDecryptFailed, +} + +impl From for DeepFaceMatchRejection { + fn from(value: FailureReason) -> Self { + match value { + FailureReason::MalformedInputs => Self::MalformedInputs, + FailureReason::UnsupportedVersion => Self::UnsupportedVersion, + FailureReason::InvalidHashesJson => Self::InvalidHashesJson, + FailureReason::ThumbnailHashMismatch => Self::ThumbnailHashMismatch, + FailureReason::MatchBelowThreshold => Self::MatchBelowThreshold, + FailureReason::ImageAnalysisFailed => Self::ImageAnalysisFailed, + FailureReason::ChallengeDecryptFailed => Self::ChallengeDecryptFailed, + } + } +} + +/// The verified outcome of the TEE match phase. +#[derive(Debug)] +pub enum DeepFaceMatchOutcome { + /// The enclave issued a token and `WalletKit` verified it against an attested signing key. + Matched(VerifiedMatchToken), + /// The enclave opened the request but declined to issue a token. + Rejected(DeepFaceMatchRejection), +} + +/// Failures before `WalletKit` obtains an authoritative sealed match outcome. +#[derive(Debug, Error)] +pub enum DeepFaceError { + /// A caller-supplied value cannot form a valid match request. + #[error("invalid {attribute}: {reason}")] + InvalidInput { + /// Name of the invalid field. + attribute: &'static str, + /// Why the value was rejected. + reason: String, + }, + /// The verifier configuration was not valid. + #[error("invalid DeepFace verifier configuration: {0}")] + Configuration(String), + /// Assignment, attestation, transport, channel opening, or token verification failed. + #[error("DeepFace verifier request failed: {0}")] + Verifier(String), +} + +/// `WalletKit`'s attested `DeepFace` match module. +/// +/// Keep this value alive across requests so the underlying HTTP client can retain transport state. +/// No WalletKit-owned sealing key is persisted: each verified assignment supplies the enclave's +/// attested public key, and the client creates fresh HPKE sealing material for the request. +#[derive(Debug)] +pub struct DeepFaceMatcher { + client: FaceVerifierClient, +} + +impl DeepFaceMatcher { + /// Builds a matcher from the embedding verifier's JSON configuration. + /// + /// The configuration pins the accepted Nitro PCR measurement sets and the TEE host URL. + /// + /// # Errors + /// + /// Returns [`DeepFaceError::Configuration`] when the configuration is invalid, or + /// [`DeepFaceError::Verifier`] when the HTTP client cannot be constructed. + pub fn from_config_json(config_json: &str) -> Result { + let config = Config::from_json(config_json) + .map_err(|error| DeepFaceError::Configuration(error.to_string()))?; + let client = FaceVerifierClient::new(config) + .map_err(|error| DeepFaceError::Verifier(error.to_string()))?; + + Ok(Self { client }) + } + + /// Performs the attested TEE match phase. + /// + /// A stale assignment is retried exactly once with a fresh assignment and freshly sealed + /// ciphertext. A sealed rejection is an authoritative outcome, not a transport failure. + /// + /// # Errors + /// + /// Returns [`DeepFaceError::InvalidInput`] before making a network request when a caller value + /// is unusable. Other failures are returned as [`DeepFaceError::Verifier`]. + pub async fn perform_match( + &self, + request: DeepFaceMatchRequest, + ) -> Result { + // TODO(DEEPFACE): consume `VerifiedMatchToken` in the DeepFace proof generator and expose + // only the generated proof across the foreign-language seam. + perform_match(&self.client, request, SystemTime::now).await + } +} + +struct SensitiveMatchInputs { + challenge_image_id: String, + inputs: MatchInputs, +} + +impl Drop for SensitiveMatchInputs { + fn drop(&mut self) { + self.inputs.live_image.zeroize(); + self.inputs.credential_image.zeroize(); + self.inputs.hashes_json.zeroize(); + self.inputs.challenge_image_key.zeroize(); + self.inputs.challenge_image_iv.zeroize(); + } +} + +#[async_trait] +trait MatchClient: Sync { + type Assignment: Send + Sync; + + async fn request_assignment( + &self, + now: SystemTime, + ) -> Result; + + async fn request_match( + &self, + assignment: &Self::Assignment, + inputs: &MatchInputs, + challenge_image_id: &str, + now: SystemTime, + ) -> Result; +} + +#[async_trait] +impl MatchClient for FaceVerifierClient { + type Assignment = VerifiedAssignment; + + async fn request_assignment( + &self, + now: SystemTime, + ) -> Result { + Self::request_assignment(self, now).await + } + + async fn request_match( + &self, + assignment: &Self::Assignment, + inputs: &MatchInputs, + challenge_image_id: &str, + now: SystemTime, + ) -> Result { + Self::request_match(self, assignment, inputs, challenge_image_id, now).await + } +} + +async fn perform_match SystemTime>( + client: &C, + request: DeepFaceMatchRequest, + now: F, +) -> Result { + request.validate()?; + let request = request.into_sensitive_inputs(); + let mut reassigned = false; + + loop { + let assignment = client + .request_assignment(now()) + .await + .map_err(|error| verifier_error(&error))?; + + match client + .request_match( + &assignment, + &request.inputs, + &request.challenge_image_id, + now(), + ) + .await + { + Ok(MatchResult::Success(token)) => { + return Ok(DeepFaceMatchOutcome::Matched(VerifiedMatchToken( + token.into_bytes(), + ))); + } + Ok(MatchResult::Failed(reason)) => { + return Ok(DeepFaceMatchOutcome::Rejected(reason.into())); + } + Err(ClientError::ReassignRequired) if !reassigned => reassigned = true, + Err(error) => return Err(verifier_error(&error)), + } + } +} + +fn verifier_error(error: &ClientError) -> DeepFaceError { + DeepFaceError::Verifier(error.to_string()) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::VecDeque, + sync::{ + atomic::{AtomicUsize, Ordering}, + Mutex, + }, + time::SystemTime, + }; + + use deepface_client::ClientError; + use deepface_protocol::{ + match_token::MatchToken, + messages::{FailureReason, MatchInputs, MatchResult}, + }; + + use super::{ + perform_match, DeepFaceError, DeepFaceMatchOutcome, DeepFaceMatchRejection, + DeepFaceMatchRequest, MatchClient, + }; + + struct FakeClient { + assignments: AtomicUsize, + results: Mutex>>, + } + + impl FakeClient { + fn new( + results: impl IntoIterator>, + ) -> Self { + Self { + assignments: AtomicUsize::new(0), + results: Mutex::new(results.into_iter().collect()), + } + } + } + + #[async_trait::async_trait] + impl MatchClient for FakeClient { + type Assignment = usize; + + async fn request_assignment( + &self, + _now: SystemTime, + ) -> Result { + Ok(self.assignments.fetch_add(1, Ordering::Relaxed)) + } + + async fn request_match( + &self, + _assignment: &Self::Assignment, + _inputs: &MatchInputs, + _challenge_image_id: &str, + _now: SystemTime, + ) -> Result { + self.results + .lock() + .expect("fake result lock should not be poisoned") + .pop_front() + .expect("test should provide one result per request") + } + } + + fn request() -> DeepFaceMatchRequest { + DeepFaceMatchRequest { + live_image: b"live".to_vec(), + credential_image: b"credential".to_vec(), + hashes_json: br#"{"thumbnail.png":"00"}"#.to_vec(), + challenge_image_id: "challenge-id".to_string(), + challenge_image_key: [7; 32], + challenge_image_iv: [9; 12], + match_threshold: 0.7, + } + } + + #[tokio::test] + async fn returns_a_verified_token_after_the_client_verifies_success() { + let client = FakeClient::new([Ok(MatchResult::Success( + MatchToken::from_bytes(b"signed-token".to_vec()), + ))]); + + let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + .await + .expect("match should succeed"); + + let DeepFaceMatchOutcome::Matched(token) = outcome else { + panic!("expected a matched outcome"); + }; + assert_eq!(token.as_bytes(), b"signed-token"); + assert_eq!(client.assignments.load(Ordering::Relaxed), 1); + } + + #[tokio::test] + async fn returns_a_typed_sealed_rejection() { + let client = FakeClient::new([Ok(MatchResult::Failed( + FailureReason::ThumbnailHashMismatch, + ))]); + + let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + .await + .expect("a sealed rejection is an outcome"); + + assert!(matches!( + outcome, + DeepFaceMatchOutcome::Rejected( + DeepFaceMatchRejection::ThumbnailHashMismatch + ) + )); + } + + #[tokio::test] + async fn reassigns_and_reseals_exactly_once() { + let client = FakeClient::new([ + Err(ClientError::ReassignRequired), + Ok(MatchResult::Failed(FailureReason::MatchBelowThreshold)), + ]); + + let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + .await + .expect("fresh assignment should recover the match request"); + + assert!(matches!( + outcome, + DeepFaceMatchOutcome::Rejected(DeepFaceMatchRejection::MatchBelowThreshold) + )); + assert_eq!(client.assignments.load(Ordering::Relaxed), 2); + } + + #[tokio::test] + async fn does_not_retry_a_second_stale_assignment() { + let client = FakeClient::new([ + Err(ClientError::ReassignRequired), + Err(ClientError::ReassignRequired), + ]); + + let error = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + .await + .expect_err("a second stale assignment should be surfaced"); + + assert!(matches!(error, DeepFaceError::Verifier(_))); + assert_eq!(client.assignments.load(Ordering::Relaxed), 2); + } + + #[tokio::test] + async fn rejects_a_non_finite_threshold_before_assignment() { + let client = FakeClient::new([]); + let mut request = request(); + request.match_threshold = f32::NAN; + + let error = perform_match(&client, request, || SystemTime::UNIX_EPOCH) + .await + .expect_err( + "NaN would bypass enclave comparisons and must be rejected locally", + ); + + assert!(matches!( + error, + DeepFaceError::InvalidInput { + attribute: "match_threshold", + .. + } + )); + assert_eq!(client.assignments.load(Ordering::Relaxed), 0); + } +} diff --git a/flake.lock b/flake.lock index cfb7a8b6..9986492f 100644 --- a/flake.lock +++ b/flake.lock @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1777605393, - "narHash": "sha256-Hjp0VOOHgHcTrX23iVvnfAudPcuCmfkfpQNFwv2v/ks=", + "lastModified": 1788332415, + "narHash": "sha256-BTFrmyh0oaVDsvA9NNw0YpbbeppTwU0t70iVx672Ew8=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "ff88db34cfa486fc4964a6991cab1678d82eee8c", + "rev": "860d7c835ab91bfc8972b67092f5f2db8e9390a0", "type": "github" }, "original": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9dd24e12..e0c6158e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.94.1" +channel = "1.97.0" profile = "default" components = ["rustfmt", "clippy", "rust-analyzer"] targets = [ From da7db84affb2db3e3e977ed338c58cb59b2634a2 Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 03:19:42 +0200 Subject: [PATCH 2/9] Integrate Flamingo into walletkit-core --- Cargo.lock | 184 ++++++++++++------ Cargo.toml | 7 +- crates/walletkit-core/Cargo.toml | 4 + .../lib.rs => walletkit-core/src/flamingo.rs} | 175 ++++++++--------- crates/walletkit-core/src/lib.rs | 4 + crates/walletkit-deepface/Cargo.toml | 26 --- 6 files changed, 217 insertions(+), 183 deletions(-) rename crates/{walletkit-deepface/src/lib.rs => walletkit-core/src/flamingo.rs} (72%) delete mode 100644 crates/walletkit-deepface/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 868113cd..7ee383c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1550,7 +1550,8 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attested-channel" version = "0.1.0" -source = "git+https://github.com/worldcoin/embedding-verifier?rev=3c130d9d9e3780eeb8b93dda3f482f5a75f95c83#3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4ff80bd27b9ba7c0a7bc0ce7b7cc199c19445cc6b19a9a628e9fdbff31cb1a" dependencies = [ "aes-gcm 0.11.1", "aws-nitro-enclaves-nsm-api", @@ -2312,7 +2313,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2401,6 +2402,35 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -2711,41 +2741,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" -[[package]] -name = "deepface-client" -version = "0.1.0" -source = "git+https://github.com/worldcoin/embedding-verifier?rev=3c130d9d9e3780eeb8b93dda3f482f5a75f95c83#3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" -dependencies = [ - "attested-channel", - "base64 0.22.1", - "deepface-protocol", - "getrandom 0.4.3", - "hex", - "reqwest 0.12.28", - "serde", - "serde_json", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "deepface-protocol" -version = "0.1.0" -source = "git+https://github.com/worldcoin/embedding-verifier?rev=3c130d9d9e3780eeb8b93dda3f482f5a75f95c83#3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" -dependencies = [ - "aes-gcm 0.11.1", - "ark-ff 0.5.0", - "attested-channel", - "ciborium", - "coset", - "serde", - "serde_bytes", - "taceo-ark-babyjubjub", - "taceo-eddsa-babyjubjub", - "taceo-poseidon2", - "zeroize", -] - [[package]] name = "delegate-display" version = "3.0.1" @@ -2910,7 +2905,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2935,6 +2930,15 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -3085,7 +3089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3200,6 +3204,51 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" +[[package]] +name = "flamingo-verifier-api-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fbd7b70857bcd594ba7dc3e2e8e9ccfedcc4d3cda34a569797a4a2da9c66e34" +dependencies = [ + "serde", +] + +[[package]] +name = "flamingo-verifier-client" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be6c11550cf0763066886d9fcf818da3b239d89cc141d5fef63a8600711f25c5" +dependencies = [ + "attested-channel", + "base64 0.22.1", + "flamingo-verifier-api-types", + "flamingo-verifier-protocol", + "getrandom 0.4.3", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "flamingo-verifier-protocol" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5111797c3fc3dbc08fbeda86c3d0174ff1b0499127ded390fb113b3e74919f" +dependencies = [ + "ark-ff 0.5.0", + "attested-channel", + "ciborium", + "coset", + "serde", + "serde_bytes", + "taceo-ark-babyjubjub", + "taceo-eddsa-babyjubjub", + "taceo-poseidon2", + "zeroize", +] + [[package]] name = "flate2" version = "1.1.9" @@ -4135,7 +4184,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4468,6 +4517,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -4800,7 +4855,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6127,6 +6182,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -6187,7 +6258,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6456,6 +6527,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", "futures-channel", "futures-core", "futures-util", @@ -6717,7 +6790,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6797,7 +6870,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8126,10 +8199,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9058,6 +9131,8 @@ version = "0.21.4" dependencies = [ "alloy", "alloy-core", + "async-trait", + "attested-channel", "backon", "base64 0.22.1", "chacha20poly1305", @@ -9066,6 +9141,8 @@ dependencies = [ "ctor", "dotenvy", "eyre", + "flamingo-verifier-client", + "flamingo-verifier-protocol", "getrandom 0.3.4", "hex", "hkdf 0.12.4", @@ -9119,19 +9196,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "walletkit-deepface" -version = "0.21.4" -dependencies = [ - "async-trait", - "attested-channel", - "deepface-client", - "deepface-protocol", - "thiserror 2.0.18", - "tokio", - "zeroize", -] - [[package]] name = "walletkit-sqlite" version = "0.21.4" @@ -9415,7 +9479,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1909bae8..2da6a2df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "crates/uniffi-bindgen", "crates/walletkit-core", - "crates/walletkit-deepface", "crates/walletkit", "crates/walletkit-db", "crates/walletkit-sqlite", @@ -31,7 +30,7 @@ alloy = { version = "2", default-features = false } alloy-core = { version = "1", default-features = false, features = [ "sol-types", ] } -attested-channel = { version = "0.1.0", git = "https://github.com/worldcoin/embedding-verifier", rev = "3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" } +attested-channel = "0.1.0" async-trait = "0.1" backon = "1.6" base64 = "0.22" @@ -43,8 +42,8 @@ clap = "4" ctor = "0.2" dirs = "6" dotenvy = "0.15.7" -deepface-client = { version = "0.1.0", git = "https://github.com/worldcoin/embedding-verifier", rev = "3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" } -deepface-protocol = { version = "0.1.0", git = "https://github.com/worldcoin/embedding-verifier", rev = "3c130d9d9e3780eeb8b93dda3f482f5a75f95c83" } +flamingo-verifier-client = "0.1.0" +flamingo-verifier-protocol = "0.1.0" eyre = "0.6" getrandom = "0.3" hex = "0.4" diff --git a/crates/walletkit-core/Cargo.toml b/crates/walletkit-core/Cargo.toml index fb1919e1..9091be45 100644 --- a/crates/walletkit-core/Cargo.toml +++ b/crates/walletkit-core/Cargo.toml @@ -57,7 +57,11 @@ getrandom = { workspace = true, features = ["wasm_js"] } # Native-only dependencies (not available on wasm32) [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-trait = { workspace = true } +attested-channel = { workspace = true, features = ["attestation"] } ctor = { workspace = true } +flamingo-verifier-client = { workspace = true } +flamingo-verifier-protocol = { workspace = true } reqwest = { workspace = true, features = ["brotli", "rustls-tls"] } rustls = { workspace = true, features = ["ring"] } diff --git a/crates/walletkit-deepface/src/lib.rs b/crates/walletkit-core/src/flamingo.rs similarity index 72% rename from crates/walletkit-deepface/src/lib.rs rename to crates/walletkit-core/src/flamingo.rs index 02a926c1..07bd618a 100644 --- a/crates/walletkit-deepface/src/lib.rs +++ b/crates/walletkit-core/src/flamingo.rs @@ -1,7 +1,4 @@ -#![cfg_attr(target_arch = "wasm32", allow(missing_docs))] -#![cfg(not(target_arch = "wasm32"))] - -//! Attested `DeepFace` matching in preparation for zero-knowledge proof generation. +//! Attested `Flamingo` matching in preparation for zero-knowledge proof generation. //! //! This module deliberately knows nothing about Orb PCP storage. Its caller supplies the live //! image and the credential material obtained through the platform's Oxide/OrbKit adapter. The @@ -12,59 +9,66 @@ use std::time::SystemTime; use async_trait::async_trait; use attested_channel::channel::CHANNEL_VERSION; -use deepface_client::{ClientError, Config, FaceVerifierClient, VerifiedAssignment}; -use deepface_protocol::messages::{FailureReason, MatchInputs, MatchResult}; +use flamingo_verifier_client::{ + ClientError, Config, FaceVerifierClient, VerifiedAssignment, +}; +use flamingo_verifier_protocol::messages::{FailureReason, MatchInputs, MatchResult}; use thiserror::Error; use zeroize::Zeroize; -/// Inputs for one attested `DeepFace` match. +/// Inputs for one attested `Flamingo` match. /// /// `credential_image` and `hashes_json` must come from the same enrolled Orb PCP. In particular, /// `hashes_json` must contain the exact archive bytes, not parsed and reserialized JSON. #[derive(Debug)] -pub struct DeepFaceMatchRequest { +pub struct FlamingoMatchRequest { /// Raw liveness image bytes captured for this request. pub live_image: Vec, /// Raw `thumbnail.png` bytes decrypted from the enrolled Orb PCP. pub credential_image: Vec, /// Exact raw `hashes.json` bytes extracted from the enrolled Orb PCP. pub hashes_json: Vec, - /// Opaque identifier used by the TEE host to locate the RP's encrypted challenge image. - pub challenge_image_id: String, - /// AES-256-GCM key supplied by the RP for the challenge image. - pub challenge_image_key: [u8; 32], - /// AES-256-GCM nonce supplied by the RP for the challenge image. - pub challenge_image_iv: [u8; 12], + /// Optional second liveness frame for the `LightGuard` flow. + pub light_guard_image: Option>, + /// Raw challenge image bytes downloaded from the relying party. + pub challenge_image: Vec, /// Minimum similarity required by the RP. Must be finite and between zero and one. pub match_threshold: f32, } -impl DeepFaceMatchRequest { - fn validate(&self) -> Result<(), DeepFaceError> { +impl FlamingoMatchRequest { + fn validate(&self) -> Result<(), FlamingoError> { for (attribute, bytes) in [ ("live_image", self.live_image.as_slice()), ("credential_image", self.credential_image.as_slice()), ("hashes_json", self.hashes_json.as_slice()), ] { if bytes.is_empty() { - return Err(DeepFaceError::InvalidInput { + return Err(FlamingoError::InvalidInput { attribute, reason: "must not be empty".to_string(), }); } } - if self.challenge_image_id.trim().is_empty() { - return Err(DeepFaceError::InvalidInput { - attribute: "challenge_image_id", + if self.challenge_image.is_empty() { + return Err(FlamingoError::InvalidInput { + attribute: "challenge_image", reason: "must not be empty".to_string(), }); } + if self.light_guard_image.as_ref().is_some_and(Vec::is_empty) { + return Err(FlamingoError::InvalidInput { + attribute: "light_guard_image", + reason: "must not be empty when provided".to_string(), + }); + } + if !self.match_threshold.is_finite() || !(0.0..=1.0).contains(&self.match_threshold) { - return Err(DeepFaceError::InvalidInput { + return Err(FlamingoError::InvalidInput { attribute: "match_threshold", reason: "must be finite and between 0 and 1 inclusive".to_string(), }); @@ -75,34 +79,33 @@ impl DeepFaceMatchRequest { fn into_sensitive_inputs(mut self) -> SensitiveMatchInputs { SensitiveMatchInputs { - challenge_image_id: std::mem::take(&mut self.challenge_image_id), inputs: MatchInputs { version: CHANNEL_VERSION, live_image: std::mem::take(&mut self.live_image), credential_image: std::mem::take(&mut self.credential_image), + light_guard_image: std::mem::take(&mut self.light_guard_image), hashes_json: std::mem::take(&mut self.hashes_json), - challenge_image_key: std::mem::take(&mut self.challenge_image_key), - challenge_image_iv: std::mem::take(&mut self.challenge_image_iv), + challenge_image: std::mem::take(&mut self.challenge_image), match_threshold: self.match_threshold, }, } } } -impl Drop for DeepFaceMatchRequest { +impl Drop for FlamingoMatchRequest { fn drop(&mut self) { self.live_image.zeroize(); self.credential_image.zeroize(); self.hashes_json.zeroize(); - self.challenge_image_key.zeroize(); - self.challenge_image_iv.zeroize(); + self.light_guard_image.zeroize(); + self.challenge_image.zeroize(); } } /// A match token whose sealed response, signing-key attestation, and signature were verified. /// -/// This is the internal handoff to `DeepFace` proof generation. It is intentionally not a -/// `UniFFI` type: the foreign side should eventually receive only the generated `DeepFace` proof. +/// This is the internal handoff to `Flamingo` proof generation. It is intentionally not a +/// `UniFFI` type: the foreign side should eventually receive only the generated `Flamingo` proof. #[derive(Debug)] pub struct VerifiedMatchToken(Vec); @@ -122,7 +125,7 @@ impl Drop for VerifiedMatchToken { /// A sealed match rejection returned by the attested enclave. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeepFaceMatchRejection { +pub enum FlamingoMatchRejection { /// The sealed inputs were malformed. MalformedInputs, /// The channel version was not supported by the enclave. @@ -135,11 +138,9 @@ pub enum DeepFaceMatchRejection { MatchBelowThreshold, /// The enclave could not obtain a usable comparison score from the images. ImageAnalysisFailed, - /// The RP's challenge image could not be authenticated with the supplied key and nonce. - ChallengeDecryptFailed, } -impl From for DeepFaceMatchRejection { +impl From for FlamingoMatchRejection { fn from(value: FailureReason) -> Self { match value { FailureReason::MalformedInputs => Self::MalformedInputs, @@ -148,23 +149,22 @@ impl From for DeepFaceMatchRejection { FailureReason::ThumbnailHashMismatch => Self::ThumbnailHashMismatch, FailureReason::MatchBelowThreshold => Self::MatchBelowThreshold, FailureReason::ImageAnalysisFailed => Self::ImageAnalysisFailed, - FailureReason::ChallengeDecryptFailed => Self::ChallengeDecryptFailed, } } } /// The verified outcome of the TEE match phase. #[derive(Debug)] -pub enum DeepFaceMatchOutcome { +pub enum FlamingoMatchOutcome { /// The enclave issued a token and `WalletKit` verified it against an attested signing key. Matched(VerifiedMatchToken), /// The enclave opened the request but declined to issue a token. - Rejected(DeepFaceMatchRejection), + Rejected(FlamingoMatchRejection), } /// Failures before `WalletKit` obtains an authoritative sealed match outcome. #[derive(Debug, Error)] -pub enum DeepFaceError { +pub enum FlamingoError { /// A caller-supplied value cannot form a valid match request. #[error("invalid {attribute}: {reason}")] InvalidInput { @@ -174,37 +174,37 @@ pub enum DeepFaceError { reason: String, }, /// The verifier configuration was not valid. - #[error("invalid DeepFace verifier configuration: {0}")] + #[error("invalid Flamingo verifier configuration: {0}")] Configuration(String), /// Assignment, attestation, transport, channel opening, or token verification failed. - #[error("DeepFace verifier request failed: {0}")] + #[error("Flamingo verifier request failed: {0}")] Verifier(String), } -/// `WalletKit`'s attested `DeepFace` match module. +/// `WalletKit`'s attested `Flamingo` match module. /// /// Keep this value alive across requests so the underlying HTTP client can retain transport state. /// No WalletKit-owned sealing key is persisted: each verified assignment supplies the enclave's /// attested public key, and the client creates fresh HPKE sealing material for the request. #[derive(Debug)] -pub struct DeepFaceMatcher { +pub struct FlamingoMatcher { client: FaceVerifierClient, } -impl DeepFaceMatcher { +impl FlamingoMatcher { /// Builds a matcher from the embedding verifier's JSON configuration. /// /// The configuration pins the accepted Nitro PCR measurement sets and the TEE host URL. /// /// # Errors /// - /// Returns [`DeepFaceError::Configuration`] when the configuration is invalid, or - /// [`DeepFaceError::Verifier`] when the HTTP client cannot be constructed. - pub fn from_config_json(config_json: &str) -> Result { + /// Returns [`FlamingoError::Configuration`] when the configuration is invalid, or + /// [`FlamingoError::Verifier`] when the HTTP client cannot be constructed. + pub fn from_config_json(config_json: &str) -> Result { let config = Config::from_json(config_json) - .map_err(|error| DeepFaceError::Configuration(error.to_string()))?; + .map_err(|error| FlamingoError::Configuration(error.to_string()))?; let client = FaceVerifierClient::new(config) - .map_err(|error| DeepFaceError::Verifier(error.to_string()))?; + .map_err(|error| FlamingoError::Verifier(error.to_string()))?; Ok(Self { client }) } @@ -216,20 +216,17 @@ impl DeepFaceMatcher { /// /// # Errors /// - /// Returns [`DeepFaceError::InvalidInput`] before making a network request when a caller value - /// is unusable. Other failures are returned as [`DeepFaceError::Verifier`]. + /// Returns [`FlamingoError::InvalidInput`] before making a network request when a caller value + /// is unusable. Other failures are returned as [`FlamingoError::Verifier`]. pub async fn perform_match( &self, - request: DeepFaceMatchRequest, - ) -> Result { - // TODO(DEEPFACE): consume `VerifiedMatchToken` in the DeepFace proof generator and expose - // only the generated proof across the foreign-language seam. + request: FlamingoMatchRequest, + ) -> Result { perform_match(&self.client, request, SystemTime::now).await } } struct SensitiveMatchInputs { - challenge_image_id: String, inputs: MatchInputs, } @@ -237,9 +234,9 @@ impl Drop for SensitiveMatchInputs { fn drop(&mut self) { self.inputs.live_image.zeroize(); self.inputs.credential_image.zeroize(); + self.inputs.light_guard_image.zeroize(); self.inputs.hashes_json.zeroize(); - self.inputs.challenge_image_key.zeroize(); - self.inputs.challenge_image_iv.zeroize(); + self.inputs.challenge_image.zeroize(); } } @@ -256,7 +253,6 @@ trait MatchClient: Sync { &self, assignment: &Self::Assignment, inputs: &MatchInputs, - challenge_image_id: &str, now: SystemTime, ) -> Result; } @@ -276,18 +272,17 @@ impl MatchClient for FaceVerifierClient { &self, assignment: &Self::Assignment, inputs: &MatchInputs, - challenge_image_id: &str, now: SystemTime, ) -> Result { - Self::request_match(self, assignment, inputs, challenge_image_id, now).await + Self::request_match(self, assignment, inputs, now).await } } async fn perform_match SystemTime>( client: &C, - request: DeepFaceMatchRequest, + request: FlamingoMatchRequest, now: F, -) -> Result { +) -> Result { request.validate()?; let request = request.into_sensitive_inputs(); let mut reassigned = false; @@ -299,21 +294,16 @@ async fn perform_match SystemTime>( .map_err(|error| verifier_error(&error))?; match client - .request_match( - &assignment, - &request.inputs, - &request.challenge_image_id, - now(), - ) + .request_match(&assignment, &request.inputs, now()) .await { - Ok(MatchResult::Success(token)) => { - return Ok(DeepFaceMatchOutcome::Matched(VerifiedMatchToken( - token.into_bytes(), + Ok(MatchResult::Success(statement)) => { + return Ok(FlamingoMatchOutcome::Matched(VerifiedMatchToken( + statement.token.into_bytes(), ))); } Ok(MatchResult::Failed(reason)) => { - return Ok(DeepFaceMatchOutcome::Rejected(reason.into())); + return Ok(FlamingoMatchOutcome::Rejected(reason.into())); } Err(ClientError::ReassignRequired) if !reassigned => reassigned = true, Err(error) => return Err(verifier_error(&error)), @@ -321,8 +311,8 @@ async fn perform_match SystemTime>( } } -fn verifier_error(error: &ClientError) -> DeepFaceError { - DeepFaceError::Verifier(error.to_string()) +fn verifier_error(error: &ClientError) -> FlamingoError { + FlamingoError::Verifier(error.to_string()) } #[cfg(test)] @@ -336,15 +326,15 @@ mod tests { time::SystemTime, }; - use deepface_client::ClientError; - use deepface_protocol::{ + use flamingo_verifier_client::ClientError; + use flamingo_verifier_protocol::{ match_token::MatchToken, - messages::{FailureReason, MatchInputs, MatchResult}, + messages::{AttestedStatement, FailureReason, MatchInputs, MatchResult}, }; use super::{ - perform_match, DeepFaceError, DeepFaceMatchOutcome, DeepFaceMatchRejection, - DeepFaceMatchRequest, MatchClient, + perform_match, FlamingoError, FlamingoMatchOutcome, FlamingoMatchRejection, + FlamingoMatchRequest, MatchClient, }; struct FakeClient { @@ -378,7 +368,6 @@ mod tests { &self, _assignment: &Self::Assignment, _inputs: &MatchInputs, - _challenge_image_id: &str, _now: SystemTime, ) -> Result { self.results @@ -389,29 +378,29 @@ mod tests { } } - fn request() -> DeepFaceMatchRequest { - DeepFaceMatchRequest { + fn request() -> FlamingoMatchRequest { + FlamingoMatchRequest { live_image: b"live".to_vec(), credential_image: b"credential".to_vec(), hashes_json: br#"{"thumbnail.png":"00"}"#.to_vec(), - challenge_image_id: "challenge-id".to_string(), - challenge_image_key: [7; 32], - challenge_image_iv: [9; 12], + light_guard_image: None, + challenge_image: b"challenge".to_vec(), match_threshold: 0.7, } } #[tokio::test] async fn returns_a_verified_token_after_the_client_verifies_success() { - let client = FakeClient::new([Ok(MatchResult::Success( - MatchToken::from_bytes(b"signed-token".to_vec()), - ))]); + let client = FakeClient::new([Ok(MatchResult::Success(AttestedStatement { + token: MatchToken::from_bytes(b"signed-token".to_vec()), + signing_key_attestation: Vec::new(), + }))]); let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) .await .expect("match should succeed"); - let DeepFaceMatchOutcome::Matched(token) = outcome else { + let FlamingoMatchOutcome::Matched(token) = outcome else { panic!("expected a matched outcome"); }; assert_eq!(token.as_bytes(), b"signed-token"); @@ -430,8 +419,8 @@ mod tests { assert!(matches!( outcome, - DeepFaceMatchOutcome::Rejected( - DeepFaceMatchRejection::ThumbnailHashMismatch + FlamingoMatchOutcome::Rejected( + FlamingoMatchRejection::ThumbnailHashMismatch ) )); } @@ -449,7 +438,7 @@ mod tests { assert!(matches!( outcome, - DeepFaceMatchOutcome::Rejected(DeepFaceMatchRejection::MatchBelowThreshold) + FlamingoMatchOutcome::Rejected(FlamingoMatchRejection::MatchBelowThreshold) )); assert_eq!(client.assignments.load(Ordering::Relaxed), 2); } @@ -465,7 +454,7 @@ mod tests { .await .expect_err("a second stale assignment should be surfaced"); - assert!(matches!(error, DeepFaceError::Verifier(_))); + assert!(matches!(error, FlamingoError::Verifier(_))); assert_eq!(client.assignments.load(Ordering::Relaxed), 2); } @@ -483,7 +472,7 @@ mod tests { assert!(matches!( error, - DeepFaceError::InvalidInput { + FlamingoError::InvalidInput { attribute: "match_threshold", .. } diff --git a/crates/walletkit-core/src/lib.rs b/crates/walletkit-core/src/lib.rs index 1f231bf4..ea9d3998 100644 --- a/crates/walletkit-core/src/lib.rs +++ b/crates/walletkit-core/src/lib.rs @@ -102,6 +102,10 @@ pub enum Region { Ap, } +/// Attested Flamingo matching in preparation for zero-knowledge proof generation. +#[cfg(not(target_arch = "wasm32"))] +pub mod flamingo; + /// Contains error outputs from `WalletKit` pub mod error; diff --git a/crates/walletkit-deepface/Cargo.toml b/crates/walletkit-deepface/Cargo.toml deleted file mode 100644 index 5902bc87..00000000 --- a/crates/walletkit-deepface/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "walletkit-deepface" -description = "Attested DeepFace matching for WalletKit." -publish = false - -version.workspace = true -edition.workspace = true -rust-version = "1.97" -authors.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -async-trait = { workspace = true } -attested-channel = { workspace = true, features = ["attestation"] } -deepface-client = { workspace = true } -deepface-protocol = { workspace = true } -thiserror = { workspace = true } -zeroize = { workspace = true } - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt"] } - -[lints] -workspace = true From e4d5c287dc754c338c7deb44218a4b41e6feee2f Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 03:33:09 +0200 Subject: [PATCH 3/9] Clarify webpki license --- deny.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deny.toml b/deny.toml index 213d40d2..a5a02e7b 100644 --- a/deny.toml +++ b/deny.toml @@ -36,6 +36,12 @@ allow = [ "Zlib", ] +[[licenses.clarify]] +name = "webpki" +version = "0.22.4" +expression = "ISC" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + # Ignore unmaintained required crates warning [advisories] ignore = [ From c3b3a275aa4d7c7706a3f59c11caeae8b8b8433f Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 16:54:52 +0200 Subject: [PATCH 4/9] update --- Cargo.lock | 19 +- Cargo.toml | 6 +- crates/walletkit-cli/Cargo.toml | 3 + crates/walletkit-cli/src/commands/flamingo.rs | 423 ++++++++++++++++++ crates/walletkit-cli/src/commands/mod.rs | 7 + crates/walletkit-core/src/flamingo.rs | 247 +++++++--- 6 files changed, 622 insertions(+), 83 deletions(-) create mode 100644 crates/walletkit-cli/src/commands/flamingo.rs diff --git a/Cargo.lock b/Cargo.lock index 7ee383c9..fd60cb2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1549,9 +1549,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attested-channel" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4ff80bd27b9ba7c0a7bc0ce7b7cc199c19445cc6b19a9a628e9fdbff31cb1a" +checksum = "acd6a0e6e8507e9f3f9f6e18229cae802967caa99a532d833b71ca44074a54ad" dependencies = [ "aes-gcm 0.11.1", "aws-nitro-enclaves-nsm-api", @@ -3206,18 +3206,18 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flamingo-verifier-api-types" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbd7b70857bcd594ba7dc3e2e8e9ccfedcc4d3cda34a569797a4a2da9c66e34" +checksum = "a0af5f5bb0c7431f542d821b9ac65aa1b1a97da440d81c1490146ad8a059cccc" dependencies = [ "serde", ] [[package]] name = "flamingo-verifier-client" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6c11550cf0763066886d9fcf818da3b239d89cc141d5fef63a8600711f25c5" +checksum = "dd7afb7c9d6facc6d60cca971c19f3dbaaf03a095d835e6213b40857df7b5bd1" dependencies = [ "attested-channel", "base64 0.22.1", @@ -3233,9 +3233,9 @@ dependencies = [ [[package]] name = "flamingo-verifier-protocol" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5111797c3fc3dbc08fbeda86c3d0174ff1b0499127ded390fb113b3e74919f" +checksum = "d3f61c0d32322c20786c5cd9075224e6663fbc66cf1683db23c92074500b02ee" dependencies = [ "ark-ff 0.5.0", "attested-channel", @@ -9106,15 +9106,18 @@ name = "walletkit-cli" version = "0.21.4" dependencies = [ "alloy-core", + "attested-channel", "base64 0.22.1", "ciborium", "clap", "dirs", "eyre", + "flamingo-verifier-protocol", "hex", "rand 0.8.6", "serde", "serde_json", + "sha2 0.10.9", "tempfile", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 2da6a2df..8ffaacf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ alloy = { version = "2", default-features = false } alloy-core = { version = "1", default-features = false, features = [ "sol-types", ] } -attested-channel = "0.1.0" +attested-channel = "0.2.0" async-trait = "0.1" backon = "1.6" base64 = "0.22" @@ -42,8 +42,8 @@ clap = "4" ctor = "0.2" dirs = "6" dotenvy = "0.15.7" -flamingo-verifier-client = "0.1.0" -flamingo-verifier-protocol = "0.1.0" +flamingo-verifier-client = "0.2.0" +flamingo-verifier-protocol = "0.2.0" eyre = "0.6" getrandom = "0.3" hex = "0.4" diff --git a/crates/walletkit-cli/Cargo.toml b/crates/walletkit-cli/Cargo.toml index 6562bdbe..0277d088 100644 --- a/crates/walletkit-cli/Cargo.toml +++ b/crates/walletkit-cli/Cargo.toml @@ -14,7 +14,10 @@ path = "src/main.rs" [dependencies] alloy-core = { workspace = true } +attested-channel = { workspace = true, features = ["attestation"] } base64 = { workspace = true } +flamingo-verifier-protocol = { workspace = true } +sha2 = { workspace = true } ciborium = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } dirs = { workspace = true } diff --git a/crates/walletkit-cli/src/commands/flamingo.rs b/crates/walletkit-cli/src/commands/flamingo.rs new file mode 100644 index 00000000..c3ed2705 --- /dev/null +++ b/crates/walletkit-cli/src/commands/flamingo.rs @@ -0,0 +1,423 @@ +//! Image-to-attested-statement demo over the verifier host's HTTP API. + +use std::{ + fs, + io::Write as _, + path::{Path, PathBuf}, + time::{Instant, SystemTime}, +}; + +use attested_channel::nitro::{EnclaveAttestationVerifier, PcrMeasurement}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use clap::{Args, Subcommand}; +use eyre::{WrapErr as _, ensure}; +use flamingo_verifier_protocol::match_token::{ + self, EdDSAPublicKey, MatchClaims, MatchToken, +}; +use serde_json::{Value, json}; +use sha2::{Digest as _, Sha256}; +use walletkit_core::flamingo::{ + FlamingoMatchOutcome, FlamingoMatchRequest, FlamingoMatcher, +}; + +use super::Cli; +use crate::output; + +#[derive(Subcommand)] +pub enum FlamingoCommand { + /// Compare three image files and save a verified statement (not a ZK proof). + Match(MatchArgs), +} + +#[derive(Args)] +pub struct MatchArgs { + /// Forwarded HTTP host URL, e.g. . + #[arg(long, default_value = "http://127.0.0.1:8000")] + host_url: String, + /// Trusted PCR0/1/2 JSON. Omit to use WalletKit's hardcoded measurements. + #[arg(long)] + measurements: Option, + /// Reference image (the enrolled thumbnail when using a real PCP). + #[arg(long)] + credential_image: PathBuf, + /// Live capture image. + #[arg(long)] + live_image: PathBuf, + /// RP challenge image. + #[arg(long)] + challenge_image: PathBuf, + /// Exact hashes.json from a PCP; omitted means generate a demo commitment. + #[arg(long)] + hashes_json: Option, + /// Optional second live frame for servers supporting `LightGuard`. + #[arg(long)] + light_guard_image: Option, + /// Similarity threshold in [0, 1]. + #[arg(long, default_value = "0.9", value_parser = parse_threshold)] + threshold: f32, + /// Statement JSON destination. Existing files are never overwritten. + #[arg(long, default_value = "flamingo-statement.json")] + output: PathBuf, +} + +fn parse_threshold(value: &str) -> Result { + let value: f32 = value.parse().map_err(|_| "threshold must be a number")?; + if value.is_finite() && (0.0..=1.0).contains(&value) { + Ok(value) + } else { + Err("threshold must be finite and between 0 and 1".to_string()) + } +} + +pub async fn run(cli: &Cli, command: &FlamingoCommand) -> eyre::Result<()> { + let FlamingoCommand::Match(args) = command; + let measurements = args + .measurements + .as_ref() + .map(|path| { + let bytes = fs::read(path) + .wrap_err_with(|| format!("cannot read {}", path.display()))?; + let json = serde_json::from_slice(&bytes) + .wrap_err_with(|| format!("invalid JSON in {}", path.display()))?; + parse_measurements(&json) + .wrap_err_with(|| format!("invalid measurements in {}", path.display())) + }) + .transpose()?; + let credential_image = fs::read(&args.credential_image) + .wrap_err_with(|| format!("cannot read {}", args.credential_image.display()))?; + let hashes_json = match &args.hashes_json { + Some(path) => fs::read(path) + .wrap_err_with(|| format!("cannot read {}", path.display()))?, + None => demo_hashes_json(&credential_image), + }; + let request = FlamingoMatchRequest { + live_image: fs::read(&args.live_image) + .wrap_err_with(|| format!("cannot read {}", args.live_image.display()))?, + credential_image, + hashes_json, + light_guard_image: args + .light_guard_image + .as_deref() + .map(|path| { + fs::read(path) + .wrap_err_with(|| format!("cannot read {}", path.display())) + }) + .transpose()?, + challenge_image: fs::read(&args.challenge_image).wrap_err_with(|| { + format!("cannot read {}", args.challenge_image.display()) + })?, + match_threshold: args.threshold, + }; + + validate_thumbnail(&request.credential_image, &request.hashes_json)?; + let expected = ExpectedClaims { + live: Sha256::digest(&request.live_image).into(), + credential: Sha256::digest(&request.hashes_json).into(), + challenge: Sha256::digest(&request.challenge_image).into(), + threshold: args.threshold, + }; + + let matcher = FlamingoMatcher::with_measurements(&args.host_url, measurements)?; + + if !cli.json { + eprintln!("Attested face-match demo — no ZK proof or enrollment verification."); + eprintln!( + "Credential commitment: {}", + if args.hashes_json.is_some() { + "provided PCP hashes.json" + } else { + "generated from reference image" + } + ); + eprintln!( + "Calling {} (PCR0/1/2 verification, encrypted request and response)...", + args.host_url + ); + } + + let started = Instant::now(); + let result = matcher + .perform_match(request) + .await + .wrap_err("attested match failed")?; + + let statement = match result { + FlamingoMatchOutcome::Matched(statement) => statement, + FlamingoMatchOutcome::Rejected(reason) => { + eyre::bail!("enclave rejected the match: {reason:?}") + } + }; + let measurements = + measurements.unwrap_or_else(FlamingoMatcher::default_measurements); + let claims = verify_statement(&statement, &measurements, &expected)?; + let artifact = statement_artifact(&statement, &claims); + save_statement(&args.output, &artifact)?; + + if cli.json { + output::print_json_data( + &json!({"matched": true, "statement_path": args.output, + "claims": artifact["claims"], "elapsed_seconds": started.elapsed().as_secs_f64()}), + true, + ); + } else { + println!( + "{} Enclave attestations and token signature verified", + output::pass_label() + ); + println!("{} All three input commitments match", output::pass_label()); + println!( + "Live similarity: {:.6} (requested threshold: {:.6})", + claims.match_coefficient, args.threshold + ); + println!( + "Statement + signing-key attestation: {}", + args.output.display() + ); + println!("Completed in {:.2}s", started.elapsed().as_secs_f64()); + } + + Ok(()) +} + +fn parse_measurements(value: &Value) -> eyre::Result<[[u8; 48]; 3]> { + // Accept both the build script's flat JSON and nitro-cli's Measurements envelope. + let value = value.get("Measurements").unwrap_or(value); + let mut result = [[0; 48]; 3]; + for (index, name) in ["PCR0", "PCR1", "PCR2"].iter().enumerate() { + let hex = value + .get(name) + .and_then(Value::as_str) + .ok_or_else(|| eyre::eyre!("measurements must contain {name}"))?; + let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex)) + .wrap_err_with(|| format!("invalid hex for {name}"))?; + result[index] = bytes.try_into().map_err(|_| { + eyre::eyre!("{name} must be exactly 48 bytes (96 hex characters)") + })?; + ensure!( + result[index].iter().any(|byte| *byte != 0), + "{name} is zero; debug enclave measurements are not accepted" + ); + } + Ok(result) +} + +fn demo_hashes_json(credential_image: &[u8]) -> Vec { + format!( + r#"{{"thumbnail.png":"{}"}}"#, + hex::encode(Sha256::digest(credential_image)) + ) + .into_bytes() +} + +fn validate_thumbnail(image: &[u8], hashes: &[u8]) -> eyre::Result<()> { + let hashes: Value = + serde_json::from_slice(hashes).wrap_err("invalid hashes.json")?; + let hash = hashes + .get("thumbnail.png") + .and_then(Value::as_str) + .ok_or_else(|| eyre::eyre!("hashes.json is missing thumbnail.png"))?; + let committed = hex::decode(hash).wrap_err("invalid thumbnail.png hash")?; + ensure!( + committed.as_slice() == Sha256::digest(image).as_slice(), + "credential image does not match hashes.json thumbnail.png" + ); + Ok(()) +} + +struct ExpectedClaims { + live: [u8; 32], + credential: [u8; 32], + challenge: [u8; 32], + threshold: f32, +} + +fn verify_statement( + statement: &walletkit_core::flamingo::VerifiedMatchToken, + measurements: &[[u8; 48]; 3], + expected: &ExpectedClaims, +) -> eyre::Result { + // Independently read the signed claims and bind the returned artifact to these input files. + let policy = EnclaveAttestationVerifier::new( + vec![vec![ + PcrMeasurement::new(0, measurements[0]), + PcrMeasurement::new(1, measurements[1]), + PcrMeasurement::new(2, measurements[2]), + ]], + 3_600_000, + ); + let attestation = + policy.verify(statement.signing_key_attestation(), SystemTime::now())?; + let key: [u8; 32] = attestation + .enclave_public_key + .try_into() + .map_err(|_| eyre::eyre!("attested signing key is not 32 bytes"))?; + let key = EdDSAPublicKey::from_compressed_bytes(key) + .map_err(|error| eyre::eyre!("invalid signing key: {error:?}"))?; + let claims = match_token::verify( + &MatchToken::from_bytes(statement.as_bytes().to_vec()), + &key, + ) + .map_err(|error| eyre::eyre!("invalid match token: {error:?}"))?; + expected.check(&claims)?; + Ok(claims) +} + +impl ExpectedClaims { + fn check(&self, claims: &MatchClaims) -> eyre::Result<()> { + ensure!( + claims.live_image_hash == self.live, + "signed live image hash differs from submitted image" + ); + ensure!( + claims.credential_claim == self.credential, + "signed credential commitment differs from submitted hashes.json" + ); + ensure!( + claims.challenger_image_hash == self.challenge, + "signed challenge image hash differs from the original challenge" + ); + ensure!( + claims.match_coefficient.is_finite() + && claims.match_coefficient >= self.threshold, + "signed live similarity is below the requested threshold" + ); + Ok(()) + } +} + +fn statement_artifact( + statement: &walletkit_core::flamingo::VerifiedMatchToken, + claims: &MatchClaims, +) -> Value { + json!({ + "format": "walletkit-flamingo-demo-v1", + "token_base64": STANDARD.encode(statement.as_bytes()), + "signing_key_attestation_base64": STANDARD.encode(statement.signing_key_attestation()), + "claims": { + "live_image_hash": hex::encode(claims.live_image_hash), + "credential_claim": hex::encode(claims.credential_claim), + "challenger_image_hash": hex::encode(claims.challenger_image_hash), + "match_coefficient": claims.match_coefficient, + }, + }) +} + +fn save_statement(path: impl AsRef, statement: &Value) -> eyre::Result<()> { + let path = path.as_ref(); + let encoded = serde_json::to_vec_pretty(statement)?; + let mut options = fs::OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt as _; + options.mode(0o600); + } + let mut file = options + .open(path) + .wrap_err_with(|| format!("cannot open {} for writing", path.display()))?; + file.write_all(&encoded) + .wrap_err("cannot write statement")?; + file.write_all(b"\n")?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn measurements() -> Value { + json!({"PCR0": "42".repeat(48), "PCR1": "43".repeat(48), "PCR2": "44".repeat(48)}) + } + + #[test] + fn requires_all_three_nonzero_full_length_measurements() { + assert_eq!( + parse_measurements(&measurements()).unwrap(), + [[0x42; 48], [0x43; 48], [0x44; 48]] + ); + assert_eq!( + parse_measurements(&json!({"Measurements": measurements()})).unwrap(), + parse_measurements(&measurements()).unwrap() + ); + for pcr in ["PCR0", "PCR1", "PCR2"] { + for invalid in [ + Value::Null, + json!("00".repeat(48)), + json!("42".repeat(47)), + json!("gg".repeat(48)), + ] { + let mut value = measurements(); + value[pcr] = invalid; + assert!(parse_measurements(&value).is_err()); + } + } + } + + #[test] + fn generated_hashes_bind_only_the_supplied_reference() { + let hashes = demo_hashes_json(b"reference"); + validate_thumbnail(b"reference", &hashes).unwrap(); + assert!(validate_thumbnail(b"other reference", &hashes).is_err()); + } + + #[test] + fn detects_substituted_inputs_and_insufficient_score() { + let expected = ExpectedClaims { + live: [1; 32], + credential: [2; 32], + challenge: [3; 32], + threshold: 0.7, + }; + let claims = MatchClaims { + live_image_hash: expected.live, + credential_claim: expected.credential, + challenger_image_hash: expected.challenge, + match_coefficient: 0.8, + }; + expected.check(&claims).unwrap(); + for altered in [ + MatchClaims { + live_image_hash: [4; 32], + ..claims + }, + MatchClaims { + credential_claim: [4; 32], + ..claims + }, + MatchClaims { + challenger_image_hash: [4; 32], + ..claims + }, + MatchClaims { + match_coefficient: 0.6, + ..claims + }, + MatchClaims { + match_coefficient: f32::NAN, + ..claims + }, + ] { + assert!(expected.check(&altered).is_err()); + } + } + + #[test] + fn statement_output_overwrites_existing_files() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("statement.json"); + save_statement(&path, &json!({"token": "longer original token"})).unwrap(); + save_statement(&path, &json!({"token": "second"})).unwrap(); + assert_eq!( + serde_json::from_slice::(&fs::read(path).unwrap()).unwrap()["token"], + "second" + ); + } + + #[test] + fn rejects_nonfinite_and_out_of_range_thresholds() { + for input in ["NaN", "inf", "-0.1", "1.1"] { + assert!(parse_threshold(input).is_err()); + } + assert!(parse_threshold("0.9").is_ok()); + } +} diff --git a/crates/walletkit-cli/src/commands/mod.rs b/crates/walletkit-cli/src/commands/mod.rs index 8e2e351a..d4899b61 100644 --- a/crates/walletkit-cli/src/commands/mod.rs +++ b/crates/walletkit-cli/src/commands/mod.rs @@ -2,6 +2,7 @@ mod auth; mod credential; +mod flamingo; mod proof; mod recovery_agent; mod recovery_binding; @@ -99,6 +100,11 @@ pub struct Cli { #[derive(Subcommand)] pub enum Command { + /// Attested Flamingo face matching against a forwarded enclave host. + Flamingo { + #[command(subcommand)] + action: flamingo::FlamingoCommand, + }, /// Local wallet setup and inspection. Wallet { #[command(subcommand)] @@ -339,6 +345,7 @@ pub async fn init_authenticator( /// Top-level command dispatch. pub async fn run(cli: Cli) -> eyre::Result<()> { match &cli.command { + Command::Flamingo { action } => flamingo::run(&cli, action).await, Command::Wallet { action } => wallet::run(&cli, action).await, Command::Auth { action } => auth::run(&cli, action).await, Command::Credential { action } => credential::run(&cli, action).await, diff --git a/crates/walletkit-core/src/flamingo.rs b/crates/walletkit-core/src/flamingo.rs index 07bd618a..d70fde73 100644 --- a/crates/walletkit-core/src/flamingo.rs +++ b/crates/walletkit-core/src/flamingo.rs @@ -5,22 +5,27 @@ //! module owns assignment, attestation verification, sealing, transport, response opening, and //! match-token verification. -use std::time::SystemTime; +use std::{sync::Arc, time::SystemTime}; use async_trait::async_trait; -use attested_channel::channel::CHANNEL_VERSION; +use attested_channel::{channel::CHANNEL_VERSION, nitro::PcrMeasurement}; use flamingo_verifier_client::{ ClientError, Config, FaceVerifierClient, VerifiedAssignment, }; use flamingo_verifier_protocol::messages::{FailureReason, MatchInputs, MatchResult}; use thiserror::Error; -use zeroize::Zeroize; + +// TODO: Replace all three PCRs with measurements from the approved enclave release. +// TODO: These should likely originate from one of the flamingo crates +const EXPECTED_ENCLAVE_PCR0: [u8; 48] = [0x42; 48]; +const EXPECTED_ENCLAVE_PCR1: [u8; 48] = [0x43; 48]; +const EXPECTED_ENCLAVE_PCR2: [u8; 48] = [0x44; 48]; /// Inputs for one attested `Flamingo` match. /// /// `credential_image` and `hashes_json` must come from the same enrolled Orb PCP. In particular, /// `hashes_json` must contain the exact archive bytes, not parsed and reserialized JSON. -#[derive(Debug)] +#[derive(Debug, uniffi::Record)] pub struct FlamingoMatchRequest { /// Raw liveness image bytes captured for this request. pub live_image: Vec, @@ -45,7 +50,7 @@ impl FlamingoMatchRequest { ] { if bytes.is_empty() { return Err(FlamingoError::InvalidInput { - attribute, + attribute: attribute.to_string(), reason: "must not be empty".to_string(), }); } @@ -53,14 +58,14 @@ impl FlamingoMatchRequest { if self.challenge_image.is_empty() { return Err(FlamingoError::InvalidInput { - attribute: "challenge_image", + attribute: "challenge_image".to_string(), reason: "must not be empty".to_string(), }); } if self.light_guard_image.as_ref().is_some_and(Vec::is_empty) { return Err(FlamingoError::InvalidInput { - attribute: "light_guard_image", + attribute: "light_guard_image".to_string(), reason: "must not be empty when provided".to_string(), }); } @@ -69,7 +74,7 @@ impl FlamingoMatchRequest { || !(0.0..=1.0).contains(&self.match_threshold) { return Err(FlamingoError::InvalidInput { - attribute: "match_threshold", + attribute: "match_threshold".to_string(), reason: "must be finite and between 0 and 1 inclusive".to_string(), }); } @@ -77,54 +82,45 @@ impl FlamingoMatchRequest { Ok(()) } - fn into_sensitive_inputs(mut self) -> SensitiveMatchInputs { - SensitiveMatchInputs { - inputs: MatchInputs { - version: CHANNEL_VERSION, - live_image: std::mem::take(&mut self.live_image), - credential_image: std::mem::take(&mut self.credential_image), - light_guard_image: std::mem::take(&mut self.light_guard_image), - hashes_json: std::mem::take(&mut self.hashes_json), - challenge_image: std::mem::take(&mut self.challenge_image), - match_threshold: self.match_threshold, - }, + fn into_inputs(self) -> MatchInputs { + MatchInputs { + version: CHANNEL_VERSION, + live_image: self.live_image, + credential_image: self.credential_image, + light_guard_image: self.light_guard_image, + hashes_json: self.hashes_json, + challenge_image: self.challenge_image, + match_threshold: self.match_threshold, } } } -impl Drop for FlamingoMatchRequest { - fn drop(&mut self) { - self.live_image.zeroize(); - self.credential_image.zeroize(); - self.hashes_json.zeroize(); - self.light_guard_image.zeroize(); - self.challenge_image.zeroize(); - } -} - /// A match token whose sealed response, signing-key attestation, and signature were verified. /// -/// This is the internal handoff to `Flamingo` proof generation. It is intentionally not a -/// `UniFFI` type: the foreign side should eventually receive only the generated `Flamingo` proof. -#[derive(Debug)] -pub struct VerifiedMatchToken(Vec); +/// Foreign callers receive an opaque handle. The token and signing-key attestation remain +/// together in Rust for proof generation and eventual relay of the attestation to the RP. +#[derive(Debug, uniffi::Object)] +pub struct VerifiedMatchToken { + token: Vec, + signing_key_attestation: Vec, +} impl VerifiedMatchToken { /// Borrows the encoded COSE/CBOR token for proof generation. #[must_use] pub fn as_bytes(&self) -> &[u8] { - &self.0 + &self.token } -} -impl Drop for VerifiedMatchToken { - fn drop(&mut self) { - self.0.zeroize(); + /// Borrows the signing-key attestation to relay alongside the generated proof. + #[must_use] + pub fn signing_key_attestation(&self) -> &[u8] { + &self.signing_key_attestation } } /// A sealed match rejection returned by the attested enclave. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] pub enum FlamingoMatchRejection { /// The sealed inputs were malformed. MalformedInputs, @@ -154,22 +150,22 @@ impl From for FlamingoMatchRejection { } /// The verified outcome of the TEE match phase. -#[derive(Debug)] +#[derive(Debug, uniffi::Enum)] pub enum FlamingoMatchOutcome { /// The enclave issued a token and `WalletKit` verified it against an attested signing key. - Matched(VerifiedMatchToken), + Matched(Arc), /// The enclave opened the request but declined to issue a token. Rejected(FlamingoMatchRejection), } /// Failures before `WalletKit` obtains an authoritative sealed match outcome. -#[derive(Debug, Error)] +#[derive(Debug, Error, uniffi::Error)] pub enum FlamingoError { /// A caller-supplied value cannot form a valid match request. #[error("invalid {attribute}: {reason}")] InvalidInput { /// Name of the invalid field. - attribute: &'static str, + attribute: String, /// Why the value was rejected. reason: String, }, @@ -186,27 +182,25 @@ pub enum FlamingoError { /// Keep this value alive across requests so the underlying HTTP client can retain transport state. /// No WalletKit-owned sealing key is persisted: each verified assignment supplies the enclave's /// attested public key, and the client creates fresh HPKE sealing material for the request. -#[derive(Debug)] +#[derive(Debug, uniffi::Object)] pub struct FlamingoMatcher { client: FaceVerifierClient, } +#[uniffi::export(async_runtime = "tokio")] impl FlamingoMatcher { - /// Builds a matcher from the embedding verifier's JSON configuration. + /// Builds a matcher for a TEE host using `WalletKit`'s pinned enclave measurements. /// - /// The configuration pins the accepted Nitro PCR measurement sets and the TEE host URL. + /// PCR0, PCR1, and PCR2 are placeholders that must be replaced before real matches can succeed. + /// Rust callers can supply custom pins through [`Self::with_measurements`]. /// /// # Errors /// /// Returns [`FlamingoError::Configuration`] when the configuration is invalid, or /// [`FlamingoError::Verifier`] when the HTTP client cannot be constructed. - pub fn from_config_json(config_json: &str) -> Result { - let config = Config::from_json(config_json) - .map_err(|error| FlamingoError::Configuration(error.to_string()))?; - let client = FaceVerifierClient::new(config) - .map_err(|error| FlamingoError::Verifier(error.to_string()))?; - - Ok(Self { client }) + #[uniffi::constructor] + pub fn new(host_url: &str) -> Result { + Self::with_measurements(host_url, None) } /// Performs the attested TEE match phase. @@ -226,18 +220,57 @@ impl FlamingoMatcher { } } -struct SensitiveMatchInputs { - inputs: MatchInputs, +impl FlamingoMatcher { + /// Returns the hardcoded PCR0/1/2 measurements used when no override is supplied. + #[must_use] + pub const fn default_measurements() -> [[u8; 48]; 3] { + [ + EXPECTED_ENCLAVE_PCR0, + EXPECTED_ENCLAVE_PCR1, + EXPECTED_ENCLAVE_PCR2, + ] + } + + /// Builds a matcher with custom PCR0/1/2 measurements, or the hardcoded defaults. + /// + /// Supply measurements from a trusted enclave build. `None` uses the same pins as + /// [`Self::new`]. All three measurements must be nonzero; debug enclaves are rejected. + /// + /// # Errors + /// Returns an error if a measurement is zero or the host/client configuration is invalid. + pub fn with_measurements( + host_url: &str, + measurements: Option<[[u8; 48]; 3]>, + ) -> Result { + let config = matcher_config(host_url, measurements)?; + let client = FaceVerifierClient::new(config) + .map_err(|error| FlamingoError::Verifier(error.to_string()))?; + Ok(Self { client }) + } } -impl Drop for SensitiveMatchInputs { - fn drop(&mut self) { - self.inputs.live_image.zeroize(); - self.inputs.credential_image.zeroize(); - self.inputs.light_guard_image.zeroize(); - self.inputs.hashes_json.zeroize(); - self.inputs.challenge_image.zeroize(); +fn matcher_config( + host_url: &str, + measurements: Option<[[u8; 48]; 3]>, +) -> Result { + let measurements = + measurements.unwrap_or_else(FlamingoMatcher::default_measurements); + for (index, measurement) in measurements.iter().enumerate() { + if measurement.iter().all(|byte| *byte == 0) { + return Err(FlamingoError::Configuration(format!( + "PCR{index} must be nonzero; debug enclaves are not accepted" + ))); + } } + Config::new( + host_url, + vec![vec![ + PcrMeasurement::new(0, measurements[0]), + PcrMeasurement::new(1, measurements[1]), + PcrMeasurement::new(2, measurements[2]), + ]], + ) + .map_err(|error| FlamingoError::Configuration(error.to_string())) } #[async_trait] @@ -284,7 +317,7 @@ async fn perform_match SystemTime>( now: F, ) -> Result { request.validate()?; - let request = request.into_sensitive_inputs(); + let request = request.into_inputs(); let mut reassigned = false; loop { @@ -293,13 +326,13 @@ async fn perform_match SystemTime>( .await .map_err(|error| verifier_error(&error))?; - match client - .request_match(&assignment, &request.inputs, now()) - .await - { + match client.request_match(&assignment, &request, now()).await { Ok(MatchResult::Success(statement)) => { - return Ok(FlamingoMatchOutcome::Matched(VerifiedMatchToken( - statement.token.into_bytes(), + return Ok(FlamingoMatchOutcome::Matched(Arc::new( + VerifiedMatchToken { + token: statement.token.into_bytes(), + signing_key_attestation: statement.signing_key_attestation, + }, ))); } Ok(MatchResult::Failed(reason)) => { @@ -389,11 +422,80 @@ mod tests { } } + #[test] + fn configuration_pins_the_release_and_disallows_debug_enclaves() { + let config = + super::matcher_config("https://verifier.example.com", None).unwrap(); + let json = serde_json::to_value(config).unwrap(); + assert_eq!( + json["allowed_pcr_configs"], + serde_json::json!([[{ + "index": 0, + "value": hex::encode(super::EXPECTED_ENCLAVE_PCR0), + }, { + "index": 1, + "value": hex::encode(super::EXPECTED_ENCLAVE_PCR1), + }, { + "index": 2, + "value": hex::encode(super::EXPECTED_ENCLAVE_PCR2), + }]]) + ); + assert_eq!(json["allow_debug_measurements"], false); + for measurement in [ + super::EXPECTED_ENCLAVE_PCR0, + super::EXPECTED_ENCLAVE_PCR1, + super::EXPECTED_ENCLAVE_PCR2, + ] { + assert!(measurement.iter().any(|byte| *byte != 0)); + } + } + + #[test] + fn custom_measurements_replace_all_default_pins() { + let measurements = [[1; 48], [2; 48], [3; 48]]; + let config = + super::matcher_config("https://verifier.example.com", Some(measurements)) + .unwrap(); + let json = serde_json::to_value(config).unwrap(); + assert_eq!(json["allowed_pcr_configs"].as_array().unwrap().len(), 1); + for (index, measurement) in measurements.iter().enumerate() { + assert_eq!(json["allowed_pcr_configs"][0][index]["index"], index); + assert_eq!( + json["allowed_pcr_configs"][0][index]["value"], + hex::encode(measurement) + ); + } + assert_eq!(json["allow_debug_measurements"], false); + } + + #[test] + fn rejects_zero_custom_measurements() { + for index in 0..3 { + let mut measurements = [[1; 48], [2; 48], [3; 48]]; + measurements[index] = [0; 48]; + assert!(matches!( + super::FlamingoMatcher::with_measurements( + "https://verifier.example.com", + Some(measurements) + ), + Err(FlamingoError::Configuration(_)) + )); + } + } + + #[test] + fn rejects_an_invalid_host_url() { + assert!(matches!( + super::FlamingoMatcher::new("not a URL"), + Err(FlamingoError::Configuration(_)) + )); + } + #[tokio::test] async fn returns_a_verified_token_after_the_client_verifies_success() { let client = FakeClient::new([Ok(MatchResult::Success(AttestedStatement { token: MatchToken::from_bytes(b"signed-token".to_vec()), - signing_key_attestation: Vec::new(), + signing_key_attestation: b"signing-key-attestation".to_vec(), }))]); let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) @@ -404,6 +506,7 @@ mod tests { panic!("expected a matched outcome"); }; assert_eq!(token.as_bytes(), b"signed-token"); + assert_eq!(token.signing_key_attestation(), b"signing-key-attestation"); assert_eq!(client.assignments.load(Ordering::Relaxed), 1); } @@ -473,9 +576,9 @@ mod tests { assert!(matches!( error, FlamingoError::InvalidInput { - attribute: "match_threshold", + attribute, .. - } + } if attribute == "match_threshold" )); assert_eq!(client.assignments.load(Ordering::Relaxed), 0); } From 002ff760fb788ccb3b34d7c7fd2cac27752b8b16 Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 16:55:19 +0200 Subject: [PATCH 5/9] fmt --- crates/walletkit-cli/src/commands/flamingo.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/walletkit-cli/src/commands/flamingo.rs b/crates/walletkit-cli/src/commands/flamingo.rs index c3ed2705..e318aef4 100644 --- a/crates/walletkit-cli/src/commands/flamingo.rs +++ b/crates/walletkit-cli/src/commands/flamingo.rs @@ -8,13 +8,13 @@ use std::{ }; use attested_channel::nitro::{EnclaveAttestationVerifier, PcrMeasurement}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; use clap::{Args, Subcommand}; -use eyre::{WrapErr as _, ensure}; +use eyre::{ensure, WrapErr as _}; use flamingo_verifier_protocol::match_token::{ self, EdDSAPublicKey, MatchClaims, MatchToken, }; -use serde_json::{Value, json}; +use serde_json::{json, Value}; use sha2::{Digest as _, Sha256}; use walletkit_core::flamingo::{ FlamingoMatchOutcome, FlamingoMatchRequest, FlamingoMatcher, From 7dee75f6b6c87d8a383bd6d99da8bec0ee84e41f Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 16:56:50 +0200 Subject: [PATCH 6/9] restore rust 1.94.1 --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e0c6158e..9dd24e12 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.97.0" +channel = "1.94.1" profile = "default" components = ["rustfmt", "clippy", "rust-analyzer"] targets = [ From 8e3aaac1c3162e6351a89bd2b88a871f80cf2bff Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 17:09:32 +0200 Subject: [PATCH 7/9] cleanup --- crates/walletkit-core/src/flamingo.rs | 290 ++++++++++++-------------- 1 file changed, 131 insertions(+), 159 deletions(-) diff --git a/crates/walletkit-core/src/flamingo.rs b/crates/walletkit-core/src/flamingo.rs index d70fde73..f1f1d8cb 100644 --- a/crates/walletkit-core/src/flamingo.rs +++ b/crates/walletkit-core/src/flamingo.rs @@ -21,6 +21,16 @@ const EXPECTED_ENCLAVE_PCR0: [u8; 48] = [0x42; 48]; const EXPECTED_ENCLAVE_PCR1: [u8; 48] = [0x43; 48]; const EXPECTED_ENCLAVE_PCR2: [u8; 48] = [0x44; 48]; +/// `WalletKit`'s attested `Flamingo` match module. +/// +/// Keep this value alive across requests so the underlying HTTP client can retain transport state. +/// No WalletKit-owned sealing key is persisted: each verified assignment supplies the enclave's +/// attested public key, and the client creates fresh HPKE sealing material for the request. +#[derive(Debug, uniffi::Object)] +pub struct FlamingoMatcher { + client: FaceVerifierClient, +} + /// Inputs for one attested `Flamingo` match. /// /// `credential_image` and `hashes_json` must come from the same enrolled Orb PCP. In particular, @@ -41,60 +51,6 @@ pub struct FlamingoMatchRequest { pub match_threshold: f32, } -impl FlamingoMatchRequest { - fn validate(&self) -> Result<(), FlamingoError> { - for (attribute, bytes) in [ - ("live_image", self.live_image.as_slice()), - ("credential_image", self.credential_image.as_slice()), - ("hashes_json", self.hashes_json.as_slice()), - ] { - if bytes.is_empty() { - return Err(FlamingoError::InvalidInput { - attribute: attribute.to_string(), - reason: "must not be empty".to_string(), - }); - } - } - - if self.challenge_image.is_empty() { - return Err(FlamingoError::InvalidInput { - attribute: "challenge_image".to_string(), - reason: "must not be empty".to_string(), - }); - } - - if self.light_guard_image.as_ref().is_some_and(Vec::is_empty) { - return Err(FlamingoError::InvalidInput { - attribute: "light_guard_image".to_string(), - reason: "must not be empty when provided".to_string(), - }); - } - - if !self.match_threshold.is_finite() - || !(0.0..=1.0).contains(&self.match_threshold) - { - return Err(FlamingoError::InvalidInput { - attribute: "match_threshold".to_string(), - reason: "must be finite and between 0 and 1 inclusive".to_string(), - }); - } - - Ok(()) - } - - fn into_inputs(self) -> MatchInputs { - MatchInputs { - version: CHANNEL_VERSION, - live_image: self.live_image, - credential_image: self.credential_image, - light_guard_image: self.light_guard_image, - hashes_json: self.hashes_json, - challenge_image: self.challenge_image, - match_threshold: self.match_threshold, - } - } -} - /// A match token whose sealed response, signing-key attestation, and signature were verified. /// /// Foreign callers receive an opaque handle. The token and signing-key attestation remain @@ -105,18 +61,13 @@ pub struct VerifiedMatchToken { signing_key_attestation: Vec, } -impl VerifiedMatchToken { - /// Borrows the encoded COSE/CBOR token for proof generation. - #[must_use] - pub fn as_bytes(&self) -> &[u8] { - &self.token - } - - /// Borrows the signing-key attestation to relay alongside the generated proof. - #[must_use] - pub fn signing_key_attestation(&self) -> &[u8] { - &self.signing_key_attestation - } +/// The verified outcome of the TEE match phase. +#[derive(Debug, uniffi::Enum)] +pub enum FlamingoMatchOutcome { + /// The enclave issued a token and `WalletKit` verified it against an attested signing key. + Matched(Arc), + /// The enclave opened the request but declined to issue a token. + Rejected(FlamingoMatchRejection), } /// A sealed match rejection returned by the attested enclave. @@ -136,28 +87,6 @@ pub enum FlamingoMatchRejection { ImageAnalysisFailed, } -impl From for FlamingoMatchRejection { - fn from(value: FailureReason) -> Self { - match value { - FailureReason::MalformedInputs => Self::MalformedInputs, - FailureReason::UnsupportedVersion => Self::UnsupportedVersion, - FailureReason::InvalidHashesJson => Self::InvalidHashesJson, - FailureReason::ThumbnailHashMismatch => Self::ThumbnailHashMismatch, - FailureReason::MatchBelowThreshold => Self::MatchBelowThreshold, - FailureReason::ImageAnalysisFailed => Self::ImageAnalysisFailed, - } - } -} - -/// The verified outcome of the TEE match phase. -#[derive(Debug, uniffi::Enum)] -pub enum FlamingoMatchOutcome { - /// The enclave issued a token and `WalletKit` verified it against an attested signing key. - Matched(Arc), - /// The enclave opened the request but declined to issue a token. - Rejected(FlamingoMatchRejection), -} - /// Failures before `WalletKit` obtains an authoritative sealed match outcome. #[derive(Debug, Error, uniffi::Error)] pub enum FlamingoError { @@ -177,14 +106,21 @@ pub enum FlamingoError { Verifier(String), } -/// `WalletKit`'s attested `Flamingo` match module. -/// -/// Keep this value alive across requests so the underlying HTTP client can retain transport state. -/// No WalletKit-owned sealing key is persisted: each verified assignment supplies the enclave's -/// attested public key, and the client creates fresh HPKE sealing material for the request. -#[derive(Debug, uniffi::Object)] -pub struct FlamingoMatcher { - client: FaceVerifierClient, +#[async_trait] +trait MatchClient: Sync { + type Assignment: Send + Sync; + + async fn request_assignment( + &self, + now: SystemTime, + ) -> Result; + + async fn request_match( + &self, + assignment: &Self::Assignment, + inputs: &MatchInputs, + now: SystemTime, + ) -> Result; } #[uniffi::export(async_runtime = "tokio")] @@ -249,45 +185,85 @@ impl FlamingoMatcher { } } -fn matcher_config( - host_url: &str, - measurements: Option<[[u8; 48]; 3]>, -) -> Result { - let measurements = - measurements.unwrap_or_else(FlamingoMatcher::default_measurements); - for (index, measurement) in measurements.iter().enumerate() { - if measurement.iter().all(|byte| *byte == 0) { - return Err(FlamingoError::Configuration(format!( - "PCR{index} must be nonzero; debug enclaves are not accepted" - ))); +impl FlamingoMatchRequest { + fn validate(&self) -> Result<(), FlamingoError> { + for (attribute, bytes) in [ + ("live_image", self.live_image.as_slice()), + ("credential_image", self.credential_image.as_slice()), + ("hashes_json", self.hashes_json.as_slice()), + ] { + if bytes.is_empty() { + return Err(FlamingoError::InvalidInput { + attribute: attribute.to_string(), + reason: "must not be empty".to_string(), + }); + } + } + + if self.challenge_image.is_empty() { + return Err(FlamingoError::InvalidInput { + attribute: "challenge_image".to_string(), + reason: "must not be empty".to_string(), + }); + } + + if self.light_guard_image.as_ref().is_some_and(Vec::is_empty) { + return Err(FlamingoError::InvalidInput { + attribute: "light_guard_image".to_string(), + reason: "must not be empty when provided".to_string(), + }); + } + + if !self.match_threshold.is_finite() + || !(0.0..=1.0).contains(&self.match_threshold) + { + return Err(FlamingoError::InvalidInput { + attribute: "match_threshold".to_string(), + reason: "must be finite and between 0 and 1 inclusive".to_string(), + }); + } + + Ok(()) + } + + fn into_inputs(self) -> MatchInputs { + MatchInputs { + version: CHANNEL_VERSION, + live_image: self.live_image, + credential_image: self.credential_image, + light_guard_image: self.light_guard_image, + hashes_json: self.hashes_json, + challenge_image: self.challenge_image, + match_threshold: self.match_threshold, } } - Config::new( - host_url, - vec![vec![ - PcrMeasurement::new(0, measurements[0]), - PcrMeasurement::new(1, measurements[1]), - PcrMeasurement::new(2, measurements[2]), - ]], - ) - .map_err(|error| FlamingoError::Configuration(error.to_string())) } -#[async_trait] -trait MatchClient: Sync { - type Assignment: Send + Sync; +impl VerifiedMatchToken { + /// Borrows the encoded COSE/CBOR token for proof generation. + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + &self.token + } - async fn request_assignment( - &self, - now: SystemTime, - ) -> Result; + /// Borrows the signing-key attestation to relay alongside the generated proof. + #[must_use] + pub fn signing_key_attestation(&self) -> &[u8] { + &self.signing_key_attestation + } +} - async fn request_match( - &self, - assignment: &Self::Assignment, - inputs: &MatchInputs, - now: SystemTime, - ) -> Result; +impl From for FlamingoMatchRejection { + fn from(value: FailureReason) -> Self { + match value { + FailureReason::MalformedInputs => Self::MalformedInputs, + FailureReason::UnsupportedVersion => Self::UnsupportedVersion, + FailureReason::InvalidHashesJson => Self::InvalidHashesJson, + FailureReason::ThumbnailHashMismatch => Self::ThumbnailHashMismatch, + FailureReason::MatchBelowThreshold => Self::MatchBelowThreshold, + FailureReason::ImageAnalysisFailed => Self::ImageAnalysisFailed, + } + } } #[async_trait] @@ -298,7 +274,7 @@ impl MatchClient for FaceVerifierClient { &self, now: SystemTime, ) -> Result { - Self::request_assignment(self, now).await + self.request_assignment(now).await } async fn request_match( @@ -307,10 +283,34 @@ impl MatchClient for FaceVerifierClient { inputs: &MatchInputs, now: SystemTime, ) -> Result { - Self::request_match(self, assignment, inputs, now).await + self.request_match(assignment, inputs, now).await } } +fn matcher_config( + host_url: &str, + measurements: Option<[[u8; 48]; 3]>, +) -> Result { + let measurements = + measurements.unwrap_or_else(FlamingoMatcher::default_measurements); + for (index, measurement) in measurements.iter().enumerate() { + if measurement.iter().all(|byte| *byte == 0) { + return Err(FlamingoError::Configuration(format!( + "PCR{index} must be nonzero; debug enclaves are not accepted" + ))); + } + } + Config::new( + host_url, + vec![vec![ + PcrMeasurement::new(0, measurements[0]), + PcrMeasurement::new(1, measurements[1]), + PcrMeasurement::new(2, measurements[2]), + ]], + ) + .map_err(|error| FlamingoError::Configuration(error.to_string())) +} + async fn perform_match SystemTime>( client: &C, request: FlamingoMatchRequest, @@ -422,34 +422,6 @@ mod tests { } } - #[test] - fn configuration_pins_the_release_and_disallows_debug_enclaves() { - let config = - super::matcher_config("https://verifier.example.com", None).unwrap(); - let json = serde_json::to_value(config).unwrap(); - assert_eq!( - json["allowed_pcr_configs"], - serde_json::json!([[{ - "index": 0, - "value": hex::encode(super::EXPECTED_ENCLAVE_PCR0), - }, { - "index": 1, - "value": hex::encode(super::EXPECTED_ENCLAVE_PCR1), - }, { - "index": 2, - "value": hex::encode(super::EXPECTED_ENCLAVE_PCR2), - }]]) - ); - assert_eq!(json["allow_debug_measurements"], false); - for measurement in [ - super::EXPECTED_ENCLAVE_PCR0, - super::EXPECTED_ENCLAVE_PCR1, - super::EXPECTED_ENCLAVE_PCR2, - ] { - assert!(measurement.iter().any(|byte| *byte != 0)); - } - } - #[test] fn custom_measurements_replace_all_default_pins() { let measurements = [[1; 48], [2; 48], [3; 48]]; From e79ee5ca404edc87a9592ae5c0aa02ef62903cbb Mon Sep 17 00:00:00 2001 From: dzejkop Date: Fri, 4 Sep 2026 17:13:54 +0200 Subject: [PATCH 8/9] clippy --- crates/walletkit-cli/src/commands/flamingo.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/walletkit-cli/src/commands/flamingo.rs b/crates/walletkit-cli/src/commands/flamingo.rs index e318aef4..fb9cc9fc 100644 --- a/crates/walletkit-cli/src/commands/flamingo.rs +++ b/crates/walletkit-cli/src/commands/flamingo.rs @@ -34,7 +34,7 @@ pub struct MatchArgs { /// Forwarded HTTP host URL, e.g. . #[arg(long, default_value = "http://127.0.0.1:8000")] host_url: String, - /// Trusted PCR0/1/2 JSON. Omit to use WalletKit's hardcoded measurements. + /// Trusted PCR0/1/2 JSON. Omit to use `WalletKit`'s hardcoded measurements. #[arg(long)] measurements: Option, /// Reference image (the enrolled thumbnail when using a real PCP). @@ -69,6 +69,7 @@ fn parse_threshold(value: &str) -> Result { } } +#[expect(clippy::too_many_lines)] pub async fn run(cli: &Cli, command: &FlamingoCommand) -> eyre::Result<()> { let FlamingoCommand::Match(args) = command; let measurements = args From d2fb2771790441908c47fab12797210caa1e810e Mon Sep 17 00:00:00 2001 From: Takis Kakalis <80459599+Takaros999@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:11:32 -0700 Subject: [PATCH 9/9] feat: migrate flamingo core to pontifex and split cli --- Cargo.lock | 329 ++++++++------ Cargo.toml | 7 +- crates/walletkit-cli/Cargo.toml | 3 - crates/walletkit-cli/src/commands/flamingo.rs | 424 ------------------ crates/walletkit-cli/src/commands/mod.rs | 7 - crates/walletkit-core/Cargo.toml | 2 +- crates/walletkit-core/src/flamingo.rs | 114 +++-- 7 files changed, 265 insertions(+), 621 deletions(-) delete mode 100644 crates/walletkit-cli/src/commands/flamingo.rs diff --git a/Cargo.lock b/Cargo.lock index fd60cb2c..a83e4c85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,17 +75,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "aes" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" -dependencies = [ - "cipher 0.5.2", - "cpubits", - "cpufeatures 0.3.0", -] - [[package]] name = "aes-gcm" version = "0.10.3" @@ -93,27 +82,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead 0.5.2", - "aes 0.8.4", + "aes", "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.1", + "ctr", + "ghash", "subtle", ] -[[package]] -name = "aes-gcm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" -dependencies = [ - "aead 0.6.1", - "aes 0.9.3", - "cipher 0.5.2", - "ctr 0.10.1", - "ctutils", - "ghash 0.6.0", -] - [[package]] name = "ahash" version = "0.8.12" @@ -1547,30 +1522,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "attested-channel" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd6a0e6e8507e9f3f9f6e18229cae802967caa99a532d833b71ca44074a54ad" -dependencies = [ - "aes-gcm 0.11.1", - "aws-nitro-enclaves-nsm-api", - "base64 0.22.1", - "ciborium", - "coset", - "getrandom 0.4.3", - "hex", - "hkdf 0.13.0", - "hpke 0.14.0", - "p384", - "serde", - "sha2 0.11.0", - "thiserror 2.0.18", - "webpki", - "x509-cert", - "zeroize", -] - [[package]] name = "auto_impl" version = "1.3.0" @@ -1611,19 +1562,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "aws-nitro-enclaves-nsm-api" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92c1f4471b33f6a7af9ea421b249ed18a11c71156564baf6293148fa6ad1b09" -dependencies = [ - "libc", - "log", - "serde", - "serde_bytes", - "serde_cbor", -] - [[package]] name = "backon" version = "1.6.0" @@ -1853,6 +1791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", + "zeroize", ] [[package]] @@ -2076,6 +2015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", + "cipher 0.5.2", "cpufeatures 0.3.0", "rand_core 0.10.1", ] @@ -2089,10 +2029,22 @@ dependencies = [ "aead 0.5.2", "chacha20 0.9.1", "cipher 0.4.4", - "poly1305", + "poly1305 0.8.0", "zeroize", ] +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead 0.6.1", + "chacha20 0.10.1", + "cipher 0.5.2", + "poly1305 0.9.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -2131,7 +2083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half 2.7.1", + "half", ] [[package]] @@ -2342,6 +2294,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + [[package]] name = "const-hex" version = "1.19.1" @@ -2457,12 +2415,6 @@ dependencies = [ "ciborium-io", ] -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2572,6 +2524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -2614,15 +2567,6 @@ dependencies = [ "cipher 0.4.4", ] -[[package]] -name = "ctr" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "ctutils" version = "0.4.2" @@ -2658,6 +2602,7 @@ dependencies = [ "fiat-crypto 0.3.0", "rustc_version 0.4.1", "subtle", + "zeroize", ] [[package]] @@ -2875,6 +2820,7 @@ dependencies = [ "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", + "zeroize", ] [[package]] @@ -3207,8 +3153,7 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flamingo-verifier-api-types" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0af5f5bb0c7431f542d821b9ac65aa1b1a97da440d81c1490146ad8a059cccc" +source = "git+https://github.com/worldcoin/flamingo?rev=f0f1814e58c83f772a6fc8f8e76476e1060f8f64#f0f1814e58c83f772a6fc8f8e76476e1060f8f64" dependencies = [ "serde", ] @@ -3216,14 +3161,14 @@ dependencies = [ [[package]] name = "flamingo-verifier-client" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7afb7c9d6facc6d60cca971c19f3dbaaf03a095d835e6213b40857df7b5bd1" +source = "git+https://github.com/worldcoin/flamingo?rev=f0f1814e58c83f772a6fc8f8e76476e1060f8f64#f0f1814e58c83f772a6fc8f8e76476e1060f8f64" dependencies = [ - "attested-channel", "base64 0.22.1", "flamingo-verifier-api-types", "flamingo-verifier-protocol", - "getrandom 0.4.3", + "flamingo-verifier-sealed-types", + "hex", + "pontifex", "reqwest 0.12.28", "serde", "serde_json", @@ -3234,18 +3179,26 @@ dependencies = [ [[package]] name = "flamingo-verifier-protocol" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f61c0d32322c20786c5cd9075224e6663fbc66cf1683db23c92074500b02ee" +source = "git+https://github.com/worldcoin/flamingo?rev=f0f1814e58c83f772a6fc8f8e76476e1060f8f64#f0f1814e58c83f772a6fc8f8e76476e1060f8f64" dependencies = [ "ark-ff 0.5.0", - "attested-channel", - "ciborium", "coset", "serde", "serde_bytes", "taceo-ark-babyjubjub", "taceo-eddsa-babyjubjub", "taceo-poseidon2", +] + +[[package]] +name = "flamingo-verifier-sealed-types" +version = "0.2.0" +source = "git+https://github.com/worldcoin/flamingo?rev=f0f1814e58c83f772a6fc8f8e76476e1060f8f64#f0f1814e58c83f772a6fc8f8e76476e1060f8f64" +dependencies = [ + "ciborium", + "flamingo-verifier-protocol", + "serde", + "serde_bytes", "zeroize", ] @@ -3469,16 +3422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval 0.6.2", -] - -[[package]] -name = "ghash" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" -dependencies = [ - "polyval 0.7.3", + "polyval", ] [[package]] @@ -3576,12 +3520,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" - [[package]] name = "half" version = "2.7.1" @@ -3754,8 +3692,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f65d16b699dd1a1fa2d851c970b0c971b388eeeb40f744252b8de48860980c8f" dependencies = [ "aead 0.5.2", - "aes-gcm 0.10.3", - "chacha20poly1305", + "aes-gcm", + "chacha20poly1305 0.10.1", "digest 0.10.7", "generic-array", "hkdf 0.12.4", @@ -3774,13 +3712,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5130e119706b4d8c2180da6126f7e60b6c38c2d340d539219f57051f0a7af7" dependencies = [ "aead 0.6.1", - "aes-gcm 0.11.1", - "getrandom 0.4.3", + "chacha20poly1305 0.11.0", "hkdf 0.13.0", "hybrid-array", + "ml-kem", "rand_core 0.10.1", "sha2 0.11.0", + "sha3 0.12.0", + "shake", "subtle", + "turboshake", + "x-wing", "x25519-dalek 3.0.0", "zeroize", ] @@ -3836,7 +3778,9 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ + "ctutils", "typenum", + "zeroize", ] [[package]] @@ -4457,6 +4401,16 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + [[package]] name = "konst" version = "0.2.20" @@ -4778,6 +4732,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "rand_core 0.10.1", + "sha3 0.11.0", + "zeroize", +] + [[package]] name = "mmap-rs" version = "0.6.1" @@ -4820,6 +4788,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", + "zeroize", +] + [[package]] name = "multimap" version = "0.10.1" @@ -4956,9 +4936,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41a03aaaf57495c75ce66aee6a7c3b21abf046c9d4cca3d45b22cdbf0de1bfba" dependencies = [ "aead 0.5.2", - "aes-gcm 0.10.3", + "aes-gcm", "byteorder", - "chacha20poly1305", + "chacha20poly1305 0.10.1", "hex", "hkdf 0.12.4", "hpke 0.13.0", @@ -5385,6 +5365,16 @@ dependencies = [ "universal-hash 0.5.1", ] +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures 0.3.0", + "universal-hash 0.6.1", +] + [[package]] name = "polyval" version = "0.6.2" @@ -5398,14 +5388,25 @@ dependencies = [ ] [[package]] -name = "polyval" -version = "0.7.3" +name = "pontifex" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +checksum = "1f05f626f4b534d7891a41b0a0ffc47e8e7eafa40c63f4dbacfa9b2828527195" dependencies = [ - "cpubits", - "cpufeatures 0.3.0", - "universal-hash 0.6.1", + "ciborium", + "const-fnv1a-hash", + "coset", + "p384", + "quantum-box", + "serde", + "serde_bytes", + "sha2 0.10.9", + "thiserror 2.0.18", + "tokio", + "tracing", + "webpki", + "x509-cert", + "zeroize", ] [[package]] @@ -6198,6 +6199,20 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quantum-box" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71a8fbba8d8939e7e5f503520a27bae93371df6c6ffe8de7f804de59bb3d29c0" +dependencies = [ + "base64 0.22.1", + "getrandom 0.4.3", + "hpke 0.14.0", + "thiserror 2.0.18", + "x-wing", + "zeroize", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -7431,16 +7446,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half 1.8.3", - "serde", -] - [[package]] name = "serde_core" version = "1.0.229" @@ -7621,6 +7626,17 @@ dependencies = [ "keccak 0.2.0", ] +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "sha3-asm" version = "0.1.8" @@ -7631,6 +7647,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -7784,6 +7811,15 @@ dependencies = [ "der", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" +dependencies = [ + "zeroize", +] + [[package]] name = "sqlite-wasm-rs" version = "0.5.5" @@ -8771,6 +8807,17 @@ dependencies = [ "utf-8", ] +[[package]] +name = "turboshake" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f892c6904b0bd5a9241eac848347abcbbbd2b9f3892bad23ac3e6efab8d6f06a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "typenum" version = "1.20.1" @@ -9106,18 +9153,15 @@ name = "walletkit-cli" version = "0.21.4" dependencies = [ "alloy-core", - "attested-channel", "base64 0.22.1", "ciborium", "clap", "dirs", "eyre", - "flamingo-verifier-protocol", "hex", "rand 0.8.6", "serde", "serde_json", - "sha2 0.10.9", "tempfile", "tokio", "tracing", @@ -9135,10 +9179,9 @@ dependencies = [ "alloy", "alloy-core", "async-trait", - "attested-channel", "backon", "base64 0.22.1", - "chacha20poly1305", + "chacha20poly1305 0.10.1", "chrono", "ciborium", "ctor", @@ -9146,6 +9189,7 @@ dependencies = [ "eyre", "flamingo-verifier-client", "flamingo-verifier-protocol", + "flamingo-verifier-sealed-types", "getrandom 0.3.4", "hex", "hkdf 0.12.4", @@ -9950,6 +9994,20 @@ dependencies = [ "tap", ] +[[package]] +name = "x-wing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51507b887016c3925c84591108dadb8c099f776eb04fec6a60ae3519fee856f" +dependencies = [ + "kem", + "ml-kem", + "sha3 0.12.0", + "shake", + "x25519-dalek 3.0.0", + "zeroize", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -9968,6 +10026,7 @@ checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ "curve25519-dalek 5.0.0", "rand_core 0.10.1", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8ffaacf3..bdc581ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ alloy = { version = "2", default-features = false } alloy-core = { version = "1", default-features = false, features = [ "sol-types", ] } -attested-channel = "0.2.0" async-trait = "0.1" backon = "1.6" base64 = "0.22" @@ -42,8 +41,10 @@ clap = "4" ctor = "0.2" dirs = "6" dotenvy = "0.15.7" -flamingo-verifier-client = "0.2.0" -flamingo-verifier-protocol = "0.2.0" +# TODO: Replace these pins with a Flamingo release containing PR #97. +flamingo-verifier-client = { git = "https://github.com/worldcoin/flamingo", rev = "f0f1814e58c83f772a6fc8f8e76476e1060f8f64" } +flamingo-verifier-protocol = { git = "https://github.com/worldcoin/flamingo", rev = "f0f1814e58c83f772a6fc8f8e76476e1060f8f64" } +flamingo-verifier-sealed-types = { git = "https://github.com/worldcoin/flamingo", rev = "f0f1814e58c83f772a6fc8f8e76476e1060f8f64" } eyre = "0.6" getrandom = "0.3" hex = "0.4" diff --git a/crates/walletkit-cli/Cargo.toml b/crates/walletkit-cli/Cargo.toml index 0277d088..6562bdbe 100644 --- a/crates/walletkit-cli/Cargo.toml +++ b/crates/walletkit-cli/Cargo.toml @@ -14,10 +14,7 @@ path = "src/main.rs" [dependencies] alloy-core = { workspace = true } -attested-channel = { workspace = true, features = ["attestation"] } base64 = { workspace = true } -flamingo-verifier-protocol = { workspace = true } -sha2 = { workspace = true } ciborium = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } dirs = { workspace = true } diff --git a/crates/walletkit-cli/src/commands/flamingo.rs b/crates/walletkit-cli/src/commands/flamingo.rs deleted file mode 100644 index fb9cc9fc..00000000 --- a/crates/walletkit-cli/src/commands/flamingo.rs +++ /dev/null @@ -1,424 +0,0 @@ -//! Image-to-attested-statement demo over the verifier host's HTTP API. - -use std::{ - fs, - io::Write as _, - path::{Path, PathBuf}, - time::{Instant, SystemTime}, -}; - -use attested_channel::nitro::{EnclaveAttestationVerifier, PcrMeasurement}; -use base64::{engine::general_purpose::STANDARD, Engine as _}; -use clap::{Args, Subcommand}; -use eyre::{ensure, WrapErr as _}; -use flamingo_verifier_protocol::match_token::{ - self, EdDSAPublicKey, MatchClaims, MatchToken, -}; -use serde_json::{json, Value}; -use sha2::{Digest as _, Sha256}; -use walletkit_core::flamingo::{ - FlamingoMatchOutcome, FlamingoMatchRequest, FlamingoMatcher, -}; - -use super::Cli; -use crate::output; - -#[derive(Subcommand)] -pub enum FlamingoCommand { - /// Compare three image files and save a verified statement (not a ZK proof). - Match(MatchArgs), -} - -#[derive(Args)] -pub struct MatchArgs { - /// Forwarded HTTP host URL, e.g. . - #[arg(long, default_value = "http://127.0.0.1:8000")] - host_url: String, - /// Trusted PCR0/1/2 JSON. Omit to use `WalletKit`'s hardcoded measurements. - #[arg(long)] - measurements: Option, - /// Reference image (the enrolled thumbnail when using a real PCP). - #[arg(long)] - credential_image: PathBuf, - /// Live capture image. - #[arg(long)] - live_image: PathBuf, - /// RP challenge image. - #[arg(long)] - challenge_image: PathBuf, - /// Exact hashes.json from a PCP; omitted means generate a demo commitment. - #[arg(long)] - hashes_json: Option, - /// Optional second live frame for servers supporting `LightGuard`. - #[arg(long)] - light_guard_image: Option, - /// Similarity threshold in [0, 1]. - #[arg(long, default_value = "0.9", value_parser = parse_threshold)] - threshold: f32, - /// Statement JSON destination. Existing files are never overwritten. - #[arg(long, default_value = "flamingo-statement.json")] - output: PathBuf, -} - -fn parse_threshold(value: &str) -> Result { - let value: f32 = value.parse().map_err(|_| "threshold must be a number")?; - if value.is_finite() && (0.0..=1.0).contains(&value) { - Ok(value) - } else { - Err("threshold must be finite and between 0 and 1".to_string()) - } -} - -#[expect(clippy::too_many_lines)] -pub async fn run(cli: &Cli, command: &FlamingoCommand) -> eyre::Result<()> { - let FlamingoCommand::Match(args) = command; - let measurements = args - .measurements - .as_ref() - .map(|path| { - let bytes = fs::read(path) - .wrap_err_with(|| format!("cannot read {}", path.display()))?; - let json = serde_json::from_slice(&bytes) - .wrap_err_with(|| format!("invalid JSON in {}", path.display()))?; - parse_measurements(&json) - .wrap_err_with(|| format!("invalid measurements in {}", path.display())) - }) - .transpose()?; - let credential_image = fs::read(&args.credential_image) - .wrap_err_with(|| format!("cannot read {}", args.credential_image.display()))?; - let hashes_json = match &args.hashes_json { - Some(path) => fs::read(path) - .wrap_err_with(|| format!("cannot read {}", path.display()))?, - None => demo_hashes_json(&credential_image), - }; - let request = FlamingoMatchRequest { - live_image: fs::read(&args.live_image) - .wrap_err_with(|| format!("cannot read {}", args.live_image.display()))?, - credential_image, - hashes_json, - light_guard_image: args - .light_guard_image - .as_deref() - .map(|path| { - fs::read(path) - .wrap_err_with(|| format!("cannot read {}", path.display())) - }) - .transpose()?, - challenge_image: fs::read(&args.challenge_image).wrap_err_with(|| { - format!("cannot read {}", args.challenge_image.display()) - })?, - match_threshold: args.threshold, - }; - - validate_thumbnail(&request.credential_image, &request.hashes_json)?; - let expected = ExpectedClaims { - live: Sha256::digest(&request.live_image).into(), - credential: Sha256::digest(&request.hashes_json).into(), - challenge: Sha256::digest(&request.challenge_image).into(), - threshold: args.threshold, - }; - - let matcher = FlamingoMatcher::with_measurements(&args.host_url, measurements)?; - - if !cli.json { - eprintln!("Attested face-match demo — no ZK proof or enrollment verification."); - eprintln!( - "Credential commitment: {}", - if args.hashes_json.is_some() { - "provided PCP hashes.json" - } else { - "generated from reference image" - } - ); - eprintln!( - "Calling {} (PCR0/1/2 verification, encrypted request and response)...", - args.host_url - ); - } - - let started = Instant::now(); - let result = matcher - .perform_match(request) - .await - .wrap_err("attested match failed")?; - - let statement = match result { - FlamingoMatchOutcome::Matched(statement) => statement, - FlamingoMatchOutcome::Rejected(reason) => { - eyre::bail!("enclave rejected the match: {reason:?}") - } - }; - let measurements = - measurements.unwrap_or_else(FlamingoMatcher::default_measurements); - let claims = verify_statement(&statement, &measurements, &expected)?; - let artifact = statement_artifact(&statement, &claims); - save_statement(&args.output, &artifact)?; - - if cli.json { - output::print_json_data( - &json!({"matched": true, "statement_path": args.output, - "claims": artifact["claims"], "elapsed_seconds": started.elapsed().as_secs_f64()}), - true, - ); - } else { - println!( - "{} Enclave attestations and token signature verified", - output::pass_label() - ); - println!("{} All three input commitments match", output::pass_label()); - println!( - "Live similarity: {:.6} (requested threshold: {:.6})", - claims.match_coefficient, args.threshold - ); - println!( - "Statement + signing-key attestation: {}", - args.output.display() - ); - println!("Completed in {:.2}s", started.elapsed().as_secs_f64()); - } - - Ok(()) -} - -fn parse_measurements(value: &Value) -> eyre::Result<[[u8; 48]; 3]> { - // Accept both the build script's flat JSON and nitro-cli's Measurements envelope. - let value = value.get("Measurements").unwrap_or(value); - let mut result = [[0; 48]; 3]; - for (index, name) in ["PCR0", "PCR1", "PCR2"].iter().enumerate() { - let hex = value - .get(name) - .and_then(Value::as_str) - .ok_or_else(|| eyre::eyre!("measurements must contain {name}"))?; - let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex)) - .wrap_err_with(|| format!("invalid hex for {name}"))?; - result[index] = bytes.try_into().map_err(|_| { - eyre::eyre!("{name} must be exactly 48 bytes (96 hex characters)") - })?; - ensure!( - result[index].iter().any(|byte| *byte != 0), - "{name} is zero; debug enclave measurements are not accepted" - ); - } - Ok(result) -} - -fn demo_hashes_json(credential_image: &[u8]) -> Vec { - format!( - r#"{{"thumbnail.png":"{}"}}"#, - hex::encode(Sha256::digest(credential_image)) - ) - .into_bytes() -} - -fn validate_thumbnail(image: &[u8], hashes: &[u8]) -> eyre::Result<()> { - let hashes: Value = - serde_json::from_slice(hashes).wrap_err("invalid hashes.json")?; - let hash = hashes - .get("thumbnail.png") - .and_then(Value::as_str) - .ok_or_else(|| eyre::eyre!("hashes.json is missing thumbnail.png"))?; - let committed = hex::decode(hash).wrap_err("invalid thumbnail.png hash")?; - ensure!( - committed.as_slice() == Sha256::digest(image).as_slice(), - "credential image does not match hashes.json thumbnail.png" - ); - Ok(()) -} - -struct ExpectedClaims { - live: [u8; 32], - credential: [u8; 32], - challenge: [u8; 32], - threshold: f32, -} - -fn verify_statement( - statement: &walletkit_core::flamingo::VerifiedMatchToken, - measurements: &[[u8; 48]; 3], - expected: &ExpectedClaims, -) -> eyre::Result { - // Independently read the signed claims and bind the returned artifact to these input files. - let policy = EnclaveAttestationVerifier::new( - vec![vec![ - PcrMeasurement::new(0, measurements[0]), - PcrMeasurement::new(1, measurements[1]), - PcrMeasurement::new(2, measurements[2]), - ]], - 3_600_000, - ); - let attestation = - policy.verify(statement.signing_key_attestation(), SystemTime::now())?; - let key: [u8; 32] = attestation - .enclave_public_key - .try_into() - .map_err(|_| eyre::eyre!("attested signing key is not 32 bytes"))?; - let key = EdDSAPublicKey::from_compressed_bytes(key) - .map_err(|error| eyre::eyre!("invalid signing key: {error:?}"))?; - let claims = match_token::verify( - &MatchToken::from_bytes(statement.as_bytes().to_vec()), - &key, - ) - .map_err(|error| eyre::eyre!("invalid match token: {error:?}"))?; - expected.check(&claims)?; - Ok(claims) -} - -impl ExpectedClaims { - fn check(&self, claims: &MatchClaims) -> eyre::Result<()> { - ensure!( - claims.live_image_hash == self.live, - "signed live image hash differs from submitted image" - ); - ensure!( - claims.credential_claim == self.credential, - "signed credential commitment differs from submitted hashes.json" - ); - ensure!( - claims.challenger_image_hash == self.challenge, - "signed challenge image hash differs from the original challenge" - ); - ensure!( - claims.match_coefficient.is_finite() - && claims.match_coefficient >= self.threshold, - "signed live similarity is below the requested threshold" - ); - Ok(()) - } -} - -fn statement_artifact( - statement: &walletkit_core::flamingo::VerifiedMatchToken, - claims: &MatchClaims, -) -> Value { - json!({ - "format": "walletkit-flamingo-demo-v1", - "token_base64": STANDARD.encode(statement.as_bytes()), - "signing_key_attestation_base64": STANDARD.encode(statement.signing_key_attestation()), - "claims": { - "live_image_hash": hex::encode(claims.live_image_hash), - "credential_claim": hex::encode(claims.credential_claim), - "challenger_image_hash": hex::encode(claims.challenger_image_hash), - "match_coefficient": claims.match_coefficient, - }, - }) -} - -fn save_statement(path: impl AsRef, statement: &Value) -> eyre::Result<()> { - let path = path.as_ref(); - let encoded = serde_json::to_vec_pretty(statement)?; - let mut options = fs::OpenOptions::new(); - options.write(true).create(true).truncate(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt as _; - options.mode(0o600); - } - let mut file = options - .open(path) - .wrap_err_with(|| format!("cannot open {} for writing", path.display()))?; - file.write_all(&encoded) - .wrap_err("cannot write statement")?; - file.write_all(b"\n")?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn measurements() -> Value { - json!({"PCR0": "42".repeat(48), "PCR1": "43".repeat(48), "PCR2": "44".repeat(48)}) - } - - #[test] - fn requires_all_three_nonzero_full_length_measurements() { - assert_eq!( - parse_measurements(&measurements()).unwrap(), - [[0x42; 48], [0x43; 48], [0x44; 48]] - ); - assert_eq!( - parse_measurements(&json!({"Measurements": measurements()})).unwrap(), - parse_measurements(&measurements()).unwrap() - ); - for pcr in ["PCR0", "PCR1", "PCR2"] { - for invalid in [ - Value::Null, - json!("00".repeat(48)), - json!("42".repeat(47)), - json!("gg".repeat(48)), - ] { - let mut value = measurements(); - value[pcr] = invalid; - assert!(parse_measurements(&value).is_err()); - } - } - } - - #[test] - fn generated_hashes_bind_only_the_supplied_reference() { - let hashes = demo_hashes_json(b"reference"); - validate_thumbnail(b"reference", &hashes).unwrap(); - assert!(validate_thumbnail(b"other reference", &hashes).is_err()); - } - - #[test] - fn detects_substituted_inputs_and_insufficient_score() { - let expected = ExpectedClaims { - live: [1; 32], - credential: [2; 32], - challenge: [3; 32], - threshold: 0.7, - }; - let claims = MatchClaims { - live_image_hash: expected.live, - credential_claim: expected.credential, - challenger_image_hash: expected.challenge, - match_coefficient: 0.8, - }; - expected.check(&claims).unwrap(); - for altered in [ - MatchClaims { - live_image_hash: [4; 32], - ..claims - }, - MatchClaims { - credential_claim: [4; 32], - ..claims - }, - MatchClaims { - challenger_image_hash: [4; 32], - ..claims - }, - MatchClaims { - match_coefficient: 0.6, - ..claims - }, - MatchClaims { - match_coefficient: f32::NAN, - ..claims - }, - ] { - assert!(expected.check(&altered).is_err()); - } - } - - #[test] - fn statement_output_overwrites_existing_files() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("statement.json"); - save_statement(&path, &json!({"token": "longer original token"})).unwrap(); - save_statement(&path, &json!({"token": "second"})).unwrap(); - assert_eq!( - serde_json::from_slice::(&fs::read(path).unwrap()).unwrap()["token"], - "second" - ); - } - - #[test] - fn rejects_nonfinite_and_out_of_range_thresholds() { - for input in ["NaN", "inf", "-0.1", "1.1"] { - assert!(parse_threshold(input).is_err()); - } - assert!(parse_threshold("0.9").is_ok()); - } -} diff --git a/crates/walletkit-cli/src/commands/mod.rs b/crates/walletkit-cli/src/commands/mod.rs index d4899b61..8e2e351a 100644 --- a/crates/walletkit-cli/src/commands/mod.rs +++ b/crates/walletkit-cli/src/commands/mod.rs @@ -2,7 +2,6 @@ mod auth; mod credential; -mod flamingo; mod proof; mod recovery_agent; mod recovery_binding; @@ -100,11 +99,6 @@ pub struct Cli { #[derive(Subcommand)] pub enum Command { - /// Attested Flamingo face matching against a forwarded enclave host. - Flamingo { - #[command(subcommand)] - action: flamingo::FlamingoCommand, - }, /// Local wallet setup and inspection. Wallet { #[command(subcommand)] @@ -345,7 +339,6 @@ pub async fn init_authenticator( /// Top-level command dispatch. pub async fn run(cli: Cli) -> eyre::Result<()> { match &cli.command { - Command::Flamingo { action } => flamingo::run(&cli, action).await, Command::Wallet { action } => wallet::run(&cli, action).await, Command::Auth { action } => auth::run(&cli, action).await, Command::Credential { action } => credential::run(&cli, action).await, diff --git a/crates/walletkit-core/Cargo.toml b/crates/walletkit-core/Cargo.toml index 9091be45..07483042 100644 --- a/crates/walletkit-core/Cargo.toml +++ b/crates/walletkit-core/Cargo.toml @@ -58,10 +58,10 @@ getrandom = { workspace = true, features = ["wasm_js"] } # Native-only dependencies (not available on wasm32) [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-trait = { workspace = true } -attested-channel = { workspace = true, features = ["attestation"] } ctor = { workspace = true } flamingo-verifier-client = { workspace = true } flamingo-verifier-protocol = { workspace = true } +flamingo-verifier-sealed-types = { workspace = true } reqwest = { workspace = true, features = ["brotli", "rustls-tls"] } rustls = { workspace = true, features = ["ring"] } diff --git a/crates/walletkit-core/src/flamingo.rs b/crates/walletkit-core/src/flamingo.rs index f1f1d8cb..f8fbe382 100644 --- a/crates/walletkit-core/src/flamingo.rs +++ b/crates/walletkit-core/src/flamingo.rs @@ -5,14 +5,16 @@ //! module owns assignment, attestation verification, sealing, transport, response opening, and //! match-token verification. -use std::{sync::Arc, time::SystemTime}; +use std::sync::Arc; use async_trait::async_trait; -use attested_channel::{channel::CHANNEL_VERSION, nitro::PcrMeasurement}; use flamingo_verifier_client::{ - ClientError, Config, FaceVerifierClient, VerifiedAssignment, + Config, Error as ClientError, FaceVerifierClient, PcrMeasurement, + VerifiedAssignment, +}; +use flamingo_verifier_sealed_types::{ + FailureReason, MatchInputs, MatchResult, MATCH_PROTOCOL_VERSION, }; -use flamingo_verifier_protocol::messages::{FailureReason, MatchInputs, MatchResult}; use thiserror::Error; // TODO: Replace all three PCRs with measurements from the approved enclave release. @@ -51,7 +53,7 @@ pub struct FlamingoMatchRequest { pub match_threshold: f32, } -/// A match token whose sealed response, signing-key attestation, and signature were verified. +/// A match token whose signing-key attestation and signature were verified. /// /// Foreign callers receive an opaque handle. The token and signing-key attestation remain /// together in Rust for proof generation and eventual relay of the attestation to the RP. @@ -61,16 +63,18 @@ pub struct VerifiedMatchToken { signing_key_attestation: Vec, } -/// The verified outcome of the TEE match phase. +/// The outcome of the TEE match phase. #[derive(Debug, uniffi::Enum)] pub enum FlamingoMatchOutcome { /// The enclave issued a token and `WalletKit` verified it against an attested signing key. Matched(Arc), - /// The enclave opened the request but declined to issue a token. + /// The response reported a rejection. An unsigned rejection does not authenticate its sender. Rejected(FlamingoMatchRejection), } -/// A sealed match rejection returned by the attested enclave. +/// A rejection reason reported in an encrypted match response. +/// +/// The reason is unsigned; it is not proof that the attested enclave issued it. #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] pub enum FlamingoMatchRejection { /// The sealed inputs were malformed. @@ -87,7 +91,7 @@ pub enum FlamingoMatchRejection { ImageAnalysisFailed, } -/// Failures before `WalletKit` obtains an authoritative sealed match outcome. +/// Failures while configuring or performing a match request. #[derive(Debug, Error, uniffi::Error)] pub enum FlamingoError { /// A caller-supplied value cannot form a valid match request. @@ -110,16 +114,12 @@ pub enum FlamingoError { trait MatchClient: Sync { type Assignment: Send + Sync; - async fn request_assignment( - &self, - now: SystemTime, - ) -> Result; + async fn request_assignment(&self) -> Result; async fn request_match( &self, assignment: &Self::Assignment, inputs: &MatchInputs, - now: SystemTime, ) -> Result; } @@ -142,7 +142,8 @@ impl FlamingoMatcher { /// Performs the attested TEE match phase. /// /// A stale assignment is retried exactly once with a fresh assignment and freshly sealed - /// ciphertext. A sealed rejection is an authoritative outcome, not a transport failure. + /// ciphertext. A reported rejection is returned without a retry. Only a successful match + /// carries a token verified against an attested signing key. /// /// # Errors /// @@ -152,7 +153,7 @@ impl FlamingoMatcher { &self, request: FlamingoMatchRequest, ) -> Result { - perform_match(&self.client, request, SystemTime::now).await + perform_match(&self.client, request).await } } @@ -228,7 +229,7 @@ impl FlamingoMatchRequest { fn into_inputs(self) -> MatchInputs { MatchInputs { - version: CHANNEL_VERSION, + version: MATCH_PROTOCOL_VERSION, live_image: self.live_image, credential_image: self.credential_image, light_guard_image: self.light_guard_image, @@ -270,20 +271,16 @@ impl From for FlamingoMatchRejection { impl MatchClient for FaceVerifierClient { type Assignment = VerifiedAssignment; - async fn request_assignment( - &self, - now: SystemTime, - ) -> Result { - self.request_assignment(now).await + async fn request_assignment(&self) -> Result { + self.request_assignment().await } async fn request_match( &self, assignment: &Self::Assignment, inputs: &MatchInputs, - now: SystemTime, ) -> Result { - self.request_match(assignment, inputs, now).await + self.request_match(assignment, inputs).await } } @@ -311,10 +308,9 @@ fn matcher_config( .map_err(|error| FlamingoError::Configuration(error.to_string())) } -async fn perform_match SystemTime>( +async fn perform_match( client: &C, request: FlamingoMatchRequest, - now: F, ) -> Result { request.validate()?; let request = request.into_inputs(); @@ -322,11 +318,11 @@ async fn perform_match SystemTime>( loop { let assignment = client - .request_assignment(now()) + .request_assignment() .await .map_err(|error| verifier_error(&error))?; - match client.request_match(&assignment, &request, now()).await { + match client.request_match(&assignment, &request).await { Ok(MatchResult::Success(statement)) => { return Ok(FlamingoMatchOutcome::Matched(Arc::new( VerifiedMatchToken { @@ -356,13 +352,12 @@ mod tests { atomic::{AtomicUsize, Ordering}, Mutex, }, - time::SystemTime, }; - use flamingo_verifier_client::ClientError; - use flamingo_verifier_protocol::{ - match_token::MatchToken, - messages::{AttestedStatement, FailureReason, MatchInputs, MatchResult}, + use flamingo_verifier_client::Error as ClientError; + use flamingo_verifier_protocol::match_token::MatchToken; + use flamingo_verifier_sealed_types::{ + AttestedStatement, FailureReason, MatchInputs, MatchResult, }; use super::{ @@ -390,10 +385,7 @@ mod tests { impl MatchClient for FakeClient { type Assignment = usize; - async fn request_assignment( - &self, - _now: SystemTime, - ) -> Result { + async fn request_assignment(&self) -> Result { Ok(self.assignments.fetch_add(1, Ordering::Relaxed)) } @@ -401,7 +393,6 @@ mod tests { &self, _assignment: &Self::Assignment, _inputs: &MatchInputs, - _now: SystemTime, ) -> Result { self.results .lock() @@ -437,7 +428,6 @@ mod tests { hex::encode(measurement) ); } - assert_eq!(json["allow_debug_measurements"], false); } #[test] @@ -463,6 +453,36 @@ mod tests { )); } + #[tokio::test] + async fn rejects_a_legacy_assignment_before_sending_images() { + let mut server = mockito::Server::new_async().await; + let assignment = server + .mock("POST", "/v1/enclave-assignment") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(r#"{"attestation":"YXR0ZXN0YXRpb24="}"#) + .expect(1) + .create_async() + .await; + let image_upload = server + .mock("POST", "/v1/matches") + .expect(0) + .create_async() + .await; + let matcher = super::FlamingoMatcher::with_measurements( + &server.url(), + Some([[1; 48], [2; 48], [3; 48]]), + ) + .unwrap(); + + let error = matcher.perform_match(request()).await.unwrap_err(); + + assert!(matches!(error, FlamingoError::Verifier(_))); + assignment.assert_async().await; + image_upload.assert_async().await; + drop(server); + } + #[tokio::test] async fn returns_a_verified_token_after_the_client_verifies_success() { let client = FakeClient::new([Ok(MatchResult::Success(AttestedStatement { @@ -470,7 +490,7 @@ mod tests { signing_key_attestation: b"signing-key-attestation".to_vec(), }))]); - let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + let outcome = perform_match(&client, request()) .await .expect("match should succeed"); @@ -488,7 +508,7 @@ mod tests { FailureReason::ThumbnailHashMismatch, ))]); - let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + let outcome = perform_match(&client, request()) .await .expect("a sealed rejection is an outcome"); @@ -507,7 +527,7 @@ mod tests { Ok(MatchResult::Failed(FailureReason::MatchBelowThreshold)), ]); - let outcome = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + let outcome = perform_match(&client, request()) .await .expect("fresh assignment should recover the match request"); @@ -525,7 +545,7 @@ mod tests { Err(ClientError::ReassignRequired), ]); - let error = perform_match(&client, request(), || SystemTime::UNIX_EPOCH) + let error = perform_match(&client, request()) .await .expect_err("a second stale assignment should be surfaced"); @@ -539,11 +559,9 @@ mod tests { let mut request = request(); request.match_threshold = f32::NAN; - let error = perform_match(&client, request, || SystemTime::UNIX_EPOCH) - .await - .expect_err( - "NaN would bypass enclave comparisons and must be rejected locally", - ); + let error = perform_match(&client, request).await.expect_err( + "NaN would bypass enclave comparisons and must be rejected locally", + ); assert!(matches!( error,