Skip to content

feat(oid4vc): mDoc support, OID4VCI 1.0 and DCQL#2871

Draft
burdettadam wants to merge 1 commit intoopenwallet-foundation:mainfrom
Indicio-tech:chore/sync-indicio-to-upstream
Draft

feat(oid4vc): mDoc support, OID4VCI 1.0 and DCQL#2871
burdettadam wants to merge 1 commit intoopenwallet-foundation:mainfrom
Indicio-tech:chore/sync-indicio-to-upstream

Conversation

@burdettadam
Copy link
Copy Markdown
Contributor

This PR brings the Indicio-tech fork changes into upstream as a single squashed commit with a clean git history.

Summary

  • mDoc / mso_mdoc: Full mDoc credential format support using isomdl-uniffi, trust anchor registry, signing key management, status list revocation, and presentation verification
  • OID4VCI 1.0 Compliance: Token endpoint updated to final spec + HAIP profile, DPoP support, backward compatibility for draft clients, OAuth discovery endpoint
  • OID4VP / Verification: OID4VP Final with x5c key binding, JAR fixes, did:jwk client_id, UUID4 presentation definition IDs
  • DCQL: Expanded query language support with multi-credential flows
  • Routes Refactoring: Split monolithic public_routes.py and routes.py into focused submodules
  • SD-JWT VC and JWT VC JSON: Selective disclosure fixes, OID4VCI 1.0 pattern alignment
  • Integration Testing: Playwright E2E suites, OID4VCI conformance runner, Credo/Sphereon wallet interop, cross-wallet/revocation/validation/negative tests, demo stack
  • CI/Build: Conformance tests workflow, PR linting workflow, Python 3.13 + ACA-Py 1.5.1 Dockerfile
  • status_list: Endianness fix with regression test

317 files changed, 60,129 insertions(+), 24,141 deletions(-)

@burdettadam
Copy link
Copy Markdown
Contributor Author

🔑 Request for Feedback: Key Management, Trust Anchor, and Trust Registry Architecture

This PR introduces a new trust infrastructure layer for the mso_mdoc credential format. We'd appreciate feedback on the design — especially from anyone with opinions on key lifecycle management, X.509 trust chain validation, or multi-tenant wallet scoping.

Current Architecture

Admin API                        Storage (Askar)                Signing / Verification
───────────────────────────────  ─────────────────────────────  ────────────────────────────
POST /mso-mdoc/trust-anchors     TrustAnchorRecord              _get_trust_anchors()
GET  /mso-mdoc/trust-anchors      → scoped to wallet profile       → MsoMdocCredVerifier
DELETE /mso-mdoc/trust-anchors     → tagged by doctype/purpose      → MsoMdocPresVerifier
                                                                     → isomdl_uniffi (Rust)

POST /mso-mdoc/signing-keys      MdocSigningKeyRecord           _resolve_signing_key()
POST /mso-mdoc/signing-keys/      → private_key_pem stored,        → isomdl mdoc signing
     import                          never returned via API          → isomdl_uniffi (Rust)
PUT  /mso-mdoc/signing-keys/{id}  → tagged by doctype/label
GET  /mso-mdoc/signing-keys
DELETE /mso-mdoc/signing-keys/{id}

Trust Anchor Management

oid4vc/mso_mdoc/trust_anchor.pyTrustAnchorRecord is a BaseRecord storing trusted X.509 CA certificates for verifying mDoc issuer signatures.

  • Records are scoped to purpose (iaca for Issuer Authority CA, reader_auth for reader authentication) and optionally scoped to doctype (e.g., org.iso.18013.5.1.mDL). Anchors without a doctype are treated as wildcards.
  • PEM chains are supported (concatenated multi-cert PEM blocks) — flatten_trust_anchors() in mdoc/utils.py splits these before passing to the Rust library.
  • Trust anchors are immutable after creation — no PUT endpoint; updating requires delete + recreate.
  • Verification fails closed: if no trust anchors are registered, verification errors explicitly rather than accepting self-signed credentials.

oid4vc/mso_mdoc/trust_anchor_routes.py — Full CRUD for both TrustAnchorRecord and MdocSigningKeyRecord. All endpoints enforce @tenant_authentication.

Signing Key Management

oid4vc/mso_mdoc/signing_key.pyMdocSigningKeyRecord stores the issuer's EC private key and X.509 certificate for signing mDoc credentials.

  • Two enrollment workflows:
    1. Generate (POST /mso-mdoc/signing-keys): Server generates an EC P-256 key pair. Only the public_key_pem is returned — the caller submits this to their IACA for certificate signing, then attaches the certificate via PUT.
    2. Import (POST /mso-mdoc/signing-keys/import): For pre-existing key+cert pairs already registered with a trust registry.
  • Certificate-key consistency is validated via validate_cert_matches_private_key() at both create and update time.
  • Private key is never returned via APIMdocSigningKeyRecordSchema intentionally excludes private_key_pem and only exposes a computed public_key_pem property.
  • Certificate expiry is checked at issuance time in cred_processor.py (check_certificate_not_expired()).

Signing Key Resolution Order

In cred_processor.py, _resolve_signing_key() uses this fallback chain:

  1. Explicit signing_key_id in SupportedCredential.vc_additional_data → retrieve by ID
  2. Doctype-filtered MdocSigningKeyRecord query → first record with both private_key_pem and certificate_pem
  3. Wildcard MdocSigningKeyRecord query (no doctype tag)
  4. Error if nothing found

Verification Flow

mdoc/cred_verifier.py and mdoc/pres_verifier.py are thin wrappers over isomdl_uniffi (Rust via UniFFI):

  • _get_trust_anchors(profile, doctype) queries only TrustAnchorRecord with purpose=iaca
  • Presentation verifier includes a legacy format fallback: if OID4VP 2024 spec format fails, retries with 2023 format
  • A PreverifiedMdocClaims sentinel class prevents double-verification when presentation verification has already validated the credential

Design Decisions We'd Like Feedback On

  1. Trust anchors as BaseRecord vs. a dedicated storage subsystem — We chose BaseRecord for simplicity and automatic multi-tenant isolation (profile-scoped storage in Askar). Is this the right abstraction, or would a more purpose-built trust store be preferred?

  2. Immutable trust anchors (no PUT) — Trust anchors are delete-and-recreate by design (CA certificates don't change). Is this the right UX, or should we add update support for consistency?

  3. Key generation hardcoded to EC P-256 — The generate endpoint always produces P-256 keys. The import path technically allows other curves, but there's no validation that imported keys match credential_signing_alg_values_supported. Should we add curve validation on import, or support additional curves in generation?

  4. Private key storage model — Private keys are stored as PEM strings in Askar record_value (encrypted at rest by profile encryption). They are not stored as native Askar key entries. This means no HSM/key-wrapping support. Is this acceptable for the initial implementation, or should we use Askar's key management APIs?

  5. Signing key resolution picks the first match — When multiple signing keys exist for the same doctype, selection order depends on storage query order (effectively arbitrary). Should we add explicit priority/selection mechanisms?

  6. Legacy trust_anchors field in vc_additional_data — The routes.py credential config API still accepts trust_anchors (PEM list) in SupportedCredential.vc_additional_data, but _get_trust_anchors() exclusively reads from TrustAnchorRecord. The stored PEMs are silently ignored during verification. Should we remove this field, or wire it up as a convenience alias?

Any feedback on these points — or anything else in the trust infrastructure — would be greatly appreciated.

@timbl-ont
Copy link
Copy Markdown
Contributor

@burdettadam This looks great on first glance. Thank you.

Answers to the questions from my perspective.

  1. We are looking at using an external HSM and are in the early architecture design stages. Initial thought is to extend the BaseRecord, but have also thought an independent KMS (much like Kanon for storage) might be useful. The interface to the HSM will be PKCS#11

  2. In the external HSM use case there may be a requirement to re-import a keyID or certificate. In general keys being immutable is good.

  3. AMVAA mandates P-256 for mDL but that is just one profile. When looking at mDOC and specifically the ISO/IEC 23220 series for other mDOC profiles it might be good to support other NIST formats over time at least Ed25519 for now? In the future you have post quantum and the Google Longfellow ZKP (ISO 18013-5 V2)

  4. As per 1. we are looking at HSM with only direct storage of Key IDs in Askar.

5 and 6. @weiiv can you provide some feedback.

Comments:

  • For now can we keep the old demo in addition the new demo. It is useful for quick dev tests with external wallets and there will be a PR incoming to update it for OID4VCI 1.0. The original demo is a bit long in the tooth and needed to be refactored but It can be phased out over time.

  • Question for others, the existing native mDOC written in python is being replaced by SpruceIDs rust implementation which is outside of OWF. It is clear that the spruceID code is more mature (and I believe in large scale production), so I see this as a net benefit from a functionality point of view. Any concerns from an OWF perspective?

  • We have a planned PR in flight to fix some issues (some of which are now possibly moot) and to support attestation (https://datatracker.ietf.org/doc/draft-ietf-oauth-attestation-based-client-auth/). If possible can we coordinate on this?

  • Near term plans include the addition of dPOP to the Auth Server and issuer - just waiting for dPOP support to be merged in https://github.com/authlib/authlib

Has this PR been tested with Bifold (3.0) or Animo Paradym wallet yet? I will try the demo ASAP. Getting some errors on initial start.

@burdettadam burdettadam force-pushed the chore/sync-indicio-to-upstream branch from b1109ab to 7df101c Compare April 1, 2026 18:45
…efactoring

This PR brings the Indicio-tech fork changes into upstream as a single squashed
commit with a clean git history.

## Summary

- mDoc / mso_mdoc: Full mDoc credential format support using isomdl-uniffi, trust anchor
  registry, signing key management, status list revocation, and presentation verification
- OID4VCI 1.0 Compliance: Token endpoint updated to final spec + HAIP profile, DPoP support,
  backward compatibility for draft clients, OAuth discovery endpoint
- OID4VP / Verification: OID4VP Final with x5c key binding, JAR fixes, did:jwk client_id, UUID4
  presentation definition IDs
- DCQL: Expanded query language support with multi-credential flows
- Routes Refactoring: Split monolithic public_routes.py and routes.py into focused submodules
- SD-JWT VC and JWT VC JSON: Selective disclosure fixes, OID4VCI 1.0 pattern alignment
- status_list: Endianness fix

Note: Demo, integration tests, unit tests, CI workflows, docker files, poetry.lock
files, and changes to non-oid4vc plugins are excluded from this PR and will be
submitted separately.

Signed-off-by: Adam Burdett <burdettadam@gmail.com>
@burdettadam burdettadam force-pushed the chore/sync-indicio-to-upstream branch from 7df101c to b802b07 Compare April 1, 2026 18:55
@burdettadam burdettadam changed the title feat(oid4vc): mDoc support, OID4VCI 1.0 compliance, DCQL, and integration test infrastructure feat(oid4vc): mDoc support, OID4VCI 1.0 and DCQL Apr 1, 2026
@burdettadam
Copy link
Copy Markdown
Contributor Author

@timbl-ont Thank you for the detailed feedback — really helpful context on where things are heading.

I've reduced the draft PR to just the core source changes (no demo, integration tests, or CI workflows) to make it easier to review the functional changes in isolation. It sounds like we've diverged a bit on testing strategies — I think we can find common ground once we align on the core implementation direction.

Responding to your answers:

On HSM support (Q1 & Q4): Good to know PKCS#11 is the target interface. Rather than blocking the core mDoc functionality on that decision, I'm actively updating this draft to introduce a pluggable signing backend interface that allows Key-ID-only storage in Askar and delegates signing to an external HSM — so both models are supported without either blocking the other.

On key curves (Q3): Agreed — P-256 is the right default for mDL/AMVAA compliance but import validation should be flexible. I'll add curve validation on import, with Ed25519 as a near-term target and the Longfellow ZKP / post-quantum curves tracked for the future.

On immutability (Q2): Makes sense that HSM workflows may require re-import of a certificate against an existing key ID. I'll keep immutable-by-default but add a certificate re-import path.

On the attestation PR: Could you share a link to your draft PR? I'd like to track it directly and rebase as needed rather than risk conflicts at merge time.

On dPOP: We already have dPOP support wired in. The token endpoint (public_routes/token.py) accepts both Bearer and DPoP authorization schemes, and the OAuth discovery metadata (public_routes/metadata.py) advertises dpop_signing_alg_values_supported. Full proof binding can be layered on once authlib ships it upstream.

On wallet testing: We've had success with Credo and Sphereon. Interoperability always comes with caveats depending on which draft of OID4VCI/OID4VP the wallet implements. Bifold and Paradym are on our list.


Action items:

  1. Add curve validation on import (Ed25519 first)
  2. Add certificate re-import path for existing key IDs
  3. Introduce pluggable signing backend interface / HSM support (actively in progress on this draft)
  4. Track timbl-ont's attestation PR once linked
  5. Move tests out of the plugin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants