Add auto-updating VICAL support for mDL trust verification - #288
spruceduncan wants to merge 3 commits into
Conversation
Fetch, verify, and parse the AAMVA VICAL (Verified Issuer Certificate Authority List) to dynamically build IACA trust anchor registries for mDL verification across all participating US states. Exports four UniFFI functions: - fetch_vical: scrapes AAMVA website for the latest VICAL binary - build_trust_anchor_registry_from_vical: parses cached VICAL bytes into PEM trust anchors, with optional additional IACA certs - fetch_and_build_trust_anchors: convenience combining fetch + build with fallback to cached bytes - establish_session_with_vical: wraps establish_session with automatic VICAL-based trust anchor resolution Includes hardcoded AAMVA root and intermediate CA certificates as fallbacks when the network is unavailable. Every step in the pipeline has graceful degradation: network failures fall back to cached bytes, signature verification failures fall back to unverified parsing, and parse failures fall back to additional IACA PEMs only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ryanmtate
left a comment
There was a problem hiding this comment.
Nice work! The only major issue I saw was the unused _verified flag for adding an unverified root to the trust anchor. We should provide a parameter to use this flag to either handle an error, or explicitly allow this to pass through (e.g. dev purposes, etc.)
|
|
||
| if let Some(bytes) = vical_bytes { | ||
| match verify_or_parse_vical(&bytes) { | ||
| Ok((vical, _verified)) => { |
There was a problem hiding this comment.
We don't use the _verified flag here, allowing potentially an unverified (untrusted) root to be used in the trust anchor. Can we add an option that gates this, something like allow_untrusted: bool, that will deny unverified VICALs from being added to the trust registry?
| verified = v; | ||
| if final_name.is_none() { | ||
| final_name = Some(vical.vical_provider.clone()); | ||
| } | ||
| if final_date.is_none() { | ||
| final_date = Some(format!("{:?}", vical.date)); | ||
| } | ||
| let registry = vical.to_trust_anchor_registry(); | ||
| all_pems.extend(trust_anchors_to_pems(®istry)); |
There was a problem hiding this comment.
Similar issue to the above
| let options = ValidationOptions::default(); | ||
|
|
||
| // Try verified parse (async, use block_on) | ||
| match crate::mdl::block_on(VerifiedVical::from_bytes_with_options( |
There was a problem hiding this comment.
Do we need to use block_on here? should we maybe expose the async runtime in the uniffi export function to make that method async?
There was a problem hiding this comment.
#[uniffi::export(async_runtime = "tokio")]
Fetch, verify, and parse the AAMVA VICAL (Verified Issuer Certificate Authority List) to dynamically build IACA trust anchor registries for mDL verification across all participating US states.
Description
SpruceKit Mobile currently hardcodes a small set of SpruceID-specific IACA certificates for mDL trust verification. To verify mDLs from any US state, the verifier needs IACA root certificates from all participating states — which AAMVA publishes as a VICAL at
vical.dts.aamva.org.AAMVA is expected to eventually provide a proper REST endpoint for VICAL retrieval, but the timeline is unknown. In the interim, we scrape the HTML from their website to fetch the latest VICAL. The system falls back gracefully at every step and supports states not in the VICAL (like California) via additional raw IACA certificates.
New UniFFI API
Four exported functions, each usable independently:
fetch_vical— Scrapes the AAMVA website for the latest VICAL binary. Returns raw CBOR bytes + metadata for the app to cache. Does not parse or verify.build_trust_anchor_registry_from_vical— Takes optional cached VICAL bytes + optional additional IACA PEM strings. Tries verified parse, falls back to unverified. Returns PEM strings compatible withestablish_session.fetch_and_build_trust_anchors— Convenience combining fetch + build with fallback to cached bytes. Returns PEMs + updated VICAL bytes for caching.establish_session_with_vical— Wrapsestablish_sessionwith automatic VICAL-based trust anchor resolution.Fallback strategy
The
verifiedfield in results tells the caller whether signature verification succeeded.App-side caching model
Rust never reads or writes to disk. The app controls storage:
Other changes
pemfeature tox509-certdependency to enableEncodePem/DecodePemtraits.rust/src/trusted_roots/as network fallbacks.Optional section
vical/mod.rsmodule includes comprehensive tests (scraping unit tests with HTML fixtures, fallback path tests, and#[ignore]live integration tests against the real AAMVA website).str::findbased, no regex) but will break if AAMVA changes their page structure. The fallback to cached bytes mitigates this.Tested
Unit tests (10, run in CI):
Live integration tests (4,
#[ignore], require network):Run with
cargo test vical -- --ignored --nocapturelive_fetch_vical— fetches raw VICAL from AAMVAlive_fetch_and_build— end-to-end fetch + verify + buildlive_fetch_parse_and_list_issuers— fetches, verifies COSE signature, lists all 20 IACA certs with issuing authority/jurisdiction, then round-trips the PEMs throughTrustAnchorRegistry::from_pem_certificates(the same code pathestablish_sessionuses)live_cache_round_trip— verifies cached bytes produce identical anchors without networkLive test output confirms: 20 IACA root certificates extracted from verified VICAL, covering MD, UT, VA, CO, GA, AK, ND, AZ, MT, IL.