From dd184e8d30a332d4f6159caf2ed1afe107cc9976 Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Wed, 1 Apr 2026 19:11:59 -0600 Subject: [PATCH 01/15] feat(mso_mdoc): pluggable signing backend with prepare/complete flow Introduce a MdocSigningBackend ABC that decouples mDoc signing from key storage, enabling HSM or KMS backends without code changes. SoftwareSigningBackend (default) now uses the prepare/complete pattern from isomdl-uniffi: the mDoc COSE structure is prepared in Rust, the signature payload is signed in Python via the cryptography library, and the finalised mDoc is assembled back in Rust. Private keys never cross the FFI boundary. Changes: - signing_backend.py (new): MdocSigningBackend ABC + SoftwareSigningBackend using PreparedMdoc.new() / .signature_payload() / .complete() - signing_key.py: added validate_ec_p256_private_key(), ALLOWED_CURVES, restored generate_ec_p256_key_pem() and MdocSigningKeyCreateSchema, added private_key_pem to MdocSigningKeyUpdateSchema - trust_anchor_routes.py: both generate and import signing-key endpoints, curve validation on import, certificate re-import on update - cred_processor.py: delegates resolve + sign to MdocSigningBackend, falls back to SoftwareSigningBackend when none is injected - __init__.py: registers SoftwareSigningBackend on injection context Signed-off-by: Adam Burdett Signed-off-by: Adam Burdett --- oid4vc/mso_mdoc/__init__.py | 9 ++ oid4vc/mso_mdoc/cred_processor.py | 100 +++++------- oid4vc/mso_mdoc/signing_backend.py | 215 +++++++++++++++++++++++++ oid4vc/mso_mdoc/signing_key.py | 58 +++++-- oid4vc/mso_mdoc/trust_anchor_routes.py | 39 ++++- 5 files changed, 343 insertions(+), 78 deletions(-) create mode 100644 oid4vc/mso_mdoc/signing_backend.py diff --git a/oid4vc/mso_mdoc/__init__.py b/oid4vc/mso_mdoc/__init__.py index 966c81a2c..4ee15ae59 100644 --- a/oid4vc/mso_mdoc/__init__.py +++ b/oid4vc/mso_mdoc/__init__.py @@ -5,6 +5,7 @@ from acapy_agent.config.injection_context import InjectionContext from mso_mdoc.cred_processor import MsoMdocCredProcessor +from mso_mdoc.signing_backend import MdocSigningBackend, SoftwareSigningBackend from oid4vc.cred_processor import CredProcessors from . import routes as routes # noqa: F401 — triggers ACA-Py route discovery @@ -15,6 +16,14 @@ async def setup(context: InjectionContext): """Setup the plugin.""" LOGGER.info("Setting up MSO_MDOC plugin") + # Register signing backend (default: software PEM-based signing). + # Deployments can override by binding a different MdocSigningBackend + # implementation before this plugin loads. + if not context.inject_or(MdocSigningBackend): + context.injector.bind_instance( + MdocSigningBackend, SoftwareSigningBackend() + ) + processors = context.inject_or(CredProcessors) if not processors: processors = CredProcessors() diff --git a/oid4vc/mso_mdoc/cred_processor.py b/oid4vc/mso_mdoc/cred_processor.py index 6e7f13e4e..e567ce420 100644 --- a/oid4vc/mso_mdoc/cred_processor.py +++ b/oid4vc/mso_mdoc/cred_processor.py @@ -23,12 +23,12 @@ from oid4vc.models.supported_cred import SupportedCredential from oid4vc.pop_result import PopResult -from .mdoc.issuer import MDL_MANDATORY_FIELDS, isomdl_mdoc_sign +from .mdoc.issuer import MDL_MANDATORY_FIELDS from .mdoc.cred_verifier import MsoMdocCredVerifier from .mdoc.pres_verifier import MsoMdocPresVerifier from .payload import normalize_mdoc_result, prepare_mdoc_payload +from .signing_backend import MdocSigningBackend from .trust_anchor import TrustAnchorRecord -from .signing_key import MdocSigningKeyRecord __all__ = [ "MsoMdocCredProcessor", @@ -289,66 +289,38 @@ def _build_headers( headers["deviceKey"] = device_key_str return headers - async def _resolve_signing_key( + async def _resolve_signing_material( self, supported: SupportedCredential, profile: Optional[Profile] = None, ) -> Dict[str, Any]: - """Resolve the signing key for credential issuance. + """Resolve signing material via the pluggable signing backend. - Resolution order: - 1. ``signing_key_id`` in ``vc_additional_data`` — fetch a specific - ``MdocSigningKeyRecord`` by ID. - 2. ``MdocSigningKeyRecord`` query by doctype — first matching record. - - Returns: - Dict with ``private_key_pem`` and ``certificate_pem``. + Delegates to the ``MdocSigningBackend`` bound on the injection context. + Falls back to a ``SoftwareSigningBackend`` if nothing is bound. """ + if not profile: + raise CredProcessorError( + "Profile is required for signing material resolution" + ) + + backend = profile.inject_or(MdocSigningBackend) + if not backend: + from .signing_backend import SoftwareSigningBackend + backend = SoftwareSigningBackend() + additional = supported.vc_additional_data or {} doctype = (supported.format_data or {}).get("doctype") - - # 1. Explicit signing key record ID signing_key_id = additional.get("signing_key_id") - if signing_key_id and profile: - try: - async with profile.session() as session: - key_record = await MdocSigningKeyRecord.retrieve_by_id( - session, signing_key_id - ) - if key_record.private_key_pem and key_record.certificate_pem: - return { - "private_key_pem": key_record.private_key_pem, - "certificate_pem": key_record.certificate_pem, - } - except Exception as exc: - LOGGER.warning( - "Could not load MdocSigningKeyRecord %s: %s", signing_key_id, exc - ) - # 2. MdocSigningKeyRecord query by doctype (or any key if no doctype) - if profile: - try: - async with profile.session() as session: - tag_filter = {"doctype": doctype} if doctype else None - key_records = await MdocSigningKeyRecord.query( - session, tag_filter=tag_filter - ) - if not key_records and doctype: - # fall back to wildcard keys (no doctype set) - key_records = await MdocSigningKeyRecord.query(session) - for key_record in key_records: - if key_record.private_key_pem and key_record.certificate_pem: - return { - "private_key_pem": key_record.private_key_pem, - "certificate_pem": key_record.certificate_pem, - } - except Exception as exc: - LOGGER.debug("MdocSigningKeyRecord query failed: %s", exc) - - raise CredProcessorError( - "No mDoc signing key configured. " - "Create a MdocSigningKeyRecord via POST /mso-mdoc/signing-keys." - ) + try: + return await backend.resolve_signing_material( + profile, + signing_key_id=signing_key_id, + doctype=doctype, + ) + except ValueError as err: + raise CredProcessorError(str(err)) from err async def _assign_status_entry( self, @@ -463,13 +435,15 @@ async def issue( # Resolve signing key — check MdocSigningKeyRecord first, then # fall back to vc_additional_data and env vars - key_data = await self._resolve_signing_key(supported, context.profile) - private_key_pem = key_data["private_key_pem"] - certificate_pem = key_data["certificate_pem"] + signing_material = await self._resolve_signing_material( + supported, context.profile + ) + certificate_pem = signing_material.get("certificate_pem", "") # Validity-period guard: reject expired or not-yet-valid certificates - # before passing them to the Rust signing library. - check_certificate_not_expired(certificate_pem) + # before passing them to the signing backend. + if certificate_pem: + check_certificate_not_expired(certificate_pem) if not device_key_str and not pop.holder_jwk: raise CredProcessorError( @@ -504,7 +478,7 @@ async def issue( ) # Use cleaned JWK if available, otherwise fall back to # the device key extracted from holder_kid / verification_method. - # isomdl_mdoc_sign expects a dict-like JWK. + # The signing backend expects a dict-like JWK. signing_holder_key = holder_jwk_clean if signing_holder_key is None and device_key_str: try: @@ -524,8 +498,14 @@ async def issue( "Unable to resolve a holder JWK for device key binding." ) - mso_mdoc = isomdl_mdoc_sign( - signing_holder_key, headers, payload, certificate_pem, private_key_pem + # Sign via the pluggable backend + backend = context.profile.inject_or(MdocSigningBackend) + if not backend: + from .signing_backend import SoftwareSigningBackend + backend = SoftwareSigningBackend() + + mso_mdoc = await backend.sign_mdoc( + signing_material, signing_holder_key, headers, payload ) # Normalize mDoc result handling for robust string/bytes processing diff --git a/oid4vc/mso_mdoc/signing_backend.py b/oid4vc/mso_mdoc/signing_backend.py new file mode 100644 index 000000000..d1a0b2d87 --- /dev/null +++ b/oid4vc/mso_mdoc/signing_backend.py @@ -0,0 +1,215 @@ +"""Pluggable signing backend for mDoc credential issuance. + +Defines an abstract ``MdocSigningBackend`` interface that decouples signing-key +resolution and mDoc signing from the storage representation. The default +``SoftwareSigningBackend`` wraps the prepare/complete signing flow from +isomdl-uniffi — private keys never cross the FFI boundary. A future +``PKCS11SigningBackend`` can implement the same interface using HSM-backed +key material. + +Backends are registered on the injection context via ``CredProcessors`` or +directly on the ``InjectionContext`` so they can be swapped per deployment. +""" + +import abc +import json +import logging +from typing import Any, Dict, Mapping, Optional + +from acapy_agent.core.profile import Profile + +from .signing_key import MdocSigningKeyRecord + +LOGGER = logging.getLogger(__name__) + + +class MdocSigningBackend(abc.ABC): + """Abstract base class for mDoc signing backends. + + Implementations must provide: + - ``resolve_signing_material`` — locate key + cert for a given doctype + - ``sign_mdoc`` — produce a signed CBOR mDoc + + The split allows backends that hold key material externally (HSM, KMS) + to avoid ever exposing raw private keys. + """ + + @abc.abstractmethod + async def resolve_signing_material( + self, + profile: Profile, + *, + signing_key_id: Optional[str] = None, + doctype: Optional[str] = None, + ) -> Dict[str, Any]: + """Resolve signing material for credential issuance. + + Returns a dict that ``sign_mdoc`` can consume. The dict contents + are backend-specific — the software backend returns PEM strings + while an HSM backend might return a key handle / URI. + + Raises: + ValueError: If no suitable signing material is found. + """ + + @abc.abstractmethod + async def sign_mdoc( + self, + signing_material: Dict[str, Any], + holder_jwk: dict, + headers: Mapping[str, Any], + payload: Mapping[str, Any], + ) -> str: + """Produce a signed CBOR mDoc. + + Args: + signing_material: Output of ``resolve_signing_material``. + holder_jwk: The holder's public key (EC JWK dict). + headers: mDoc headers (must include ``doctype``). + payload: Namespace-keyed claim data. + + Returns: + The signed mDoc as a string (base64url or hex, depending on + the underlying library). + """ + + +class SoftwareSigningBackend(MdocSigningBackend): + """Default software-based signing using isomdl-uniffi with PEM keys. + + This mirrors the original ``_resolve_signing_key`` + ``isomdl_mdoc_sign`` + path that existed before the pluggable backend refactor. + """ + + async def resolve_signing_material( + self, + profile: Profile, + *, + signing_key_id: Optional[str] = None, + doctype: Optional[str] = None, + ) -> Dict[str, Any]: + """Resolve PEM key material from MdocSigningKeyRecord storage.""" + + # 1. Explicit signing key record ID + if signing_key_id: + try: + async with profile.session() as session: + key_record = await MdocSigningKeyRecord.retrieve_by_id( + session, signing_key_id + ) + if key_record.private_key_pem and key_record.certificate_pem: + return { + "private_key_pem": key_record.private_key_pem, + "certificate_pem": key_record.certificate_pem, + } + except Exception as exc: + LOGGER.warning( + "Could not load MdocSigningKeyRecord %s: %s", + signing_key_id, + exc, + ) + + # 2. Query by doctype (or any key if no doctype) + try: + async with profile.session() as session: + tag_filter = {"doctype": doctype} if doctype else None + key_records = await MdocSigningKeyRecord.query( + session, tag_filter=tag_filter + ) + if not key_records and doctype: + # fall back to wildcard keys (no doctype set) + key_records = await MdocSigningKeyRecord.query(session) + for key_record in key_records: + if key_record.private_key_pem and key_record.certificate_pem: + return { + "private_key_pem": key_record.private_key_pem, + "certificate_pem": key_record.certificate_pem, + } + except Exception as exc: + LOGGER.debug("MdocSigningKeyRecord query failed: %s", exc) + + raise ValueError( + "No mDoc signing key configured. " + "Import a signing key via POST /mso-mdoc/signing-keys." + ) + + async def sign_mdoc( + self, + signing_material: Dict[str, Any], + holder_jwk: dict, + headers: Mapping[str, Any], + payload: Mapping[str, Any], + ) -> str: + """Sign an mDoc using the prepare/complete flow with PEM key material. + + The mDoc structure is prepared in the Rust FFI layer, the signature + payload is signed in Python using the ``cryptography`` library, and + the mDoc is completed with the raw signature and certificate chain. + Private keys never cross the FFI boundary. + """ + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, + ) + from isomdl_uniffi import PreparedMdoc + + doctype = headers.get("doctype", "") + holder_jwk_str = ( + json.dumps(holder_jwk) + if isinstance(holder_jwk, dict) + else str(holder_jwk) + ) + + # Prepare namespaces — each element value is JSON-encoded. + namespaces: Dict[str, Dict[str, str]] = {} + + if doctype == "org.iso.18013.5.1.mDL": + mdl_ns = "org.iso.18013.5.1" + aamva_key = "org.iso.18013.5.1.aamva" + mdl_items: Dict[str, str] = {} + for k, v in payload.items(): + if k == aamva_key and isinstance(v, dict): + namespaces[aamva_key] = { + ak: json.dumps(av) for ak, av in v.items() + } + else: + mdl_items[k] = json.dumps(v) + mdl_items.setdefault("driving_privileges", json.dumps([])) + namespaces[mdl_ns] = mdl_items + else: + namespaces[doctype] = { + k: json.dumps(v) for k, v in payload.items() + } + + # Prepare the mDoc (builds COSE structure, returns payload to sign) + prepared = PreparedMdoc( + doc_type=doctype, + namespaces=namespaces, + holder_jwk=holder_jwk_str, + signature_algorithm="ES256", + ) + + sig_payload = prepared.signature_payload() + + # Sign with the DS private key via Python's cryptography library + private_key = serialization.load_pem_private_key( + signing_material["private_key_pem"].encode("utf-8"), + password=None, + ) + der_sig = private_key.sign(sig_payload, ec.ECDSA(hashes.SHA256())) + + # Convert DER-encoded ECDSA signature to raw r||s for COSE + r_int, s_int = decode_dss_signature(der_sig) + key_size = (private_key.key_size + 7) // 8 + raw_sig = r_int.to_bytes(key_size, byteorder="big") + s_int.to_bytes( + key_size, byteorder="big" + ) + + # Complete the mDoc with signature and certificate chain + mdoc = prepared.complete( + certificate_chain_pem=signing_material["certificate_pem"], + signature=raw_sig, + ) + + return mdoc.issuer_signed_b64() diff --git a/oid4vc/mso_mdoc/signing_key.py b/oid4vc/mso_mdoc/signing_key.py index 93d1f10a3..115d8e401 100644 --- a/oid4vc/mso_mdoc/signing_key.py +++ b/oid4vc/mso_mdoc/signing_key.py @@ -4,18 +4,19 @@ used by the issuer to sign mDoc credentials. Records are scoped to the current wallet session, giving multi-tenant isolation automatically. -The recommended workflow is: +Two key-creation workflows are supported: 1. **Generate** – ``POST /mso-mdoc/signing-keys`` creates a new EC P-256 - key pair server-side. The response includes the ``public_key_pem`` (and - optionally a CSR) so that the caller can submit the public key to an - IACA for certificate signing. -2. **Attach certificate** – ``PUT /mso-mdoc/signing-keys/{id}`` uploads the - CA-signed certificate once it has been obtained. - -For pre-existing keys already registered with a public trust registry, -use ``POST /mso-mdoc/signing-keys/import`` to load the private key and -certificate in a single step. + key pair server-side (following ACA-Py's convention of wallet-managed + key generation). The response includes the ``public_key_pem`` so the + caller can submit it to an IACA for certificate signing. +2. **Import** – ``POST /mso-mdoc/signing-keys/import`` loads a pre-existing + EC P-256 private key and (optionally) X.509 certificate in a single + step. Use this for keys produced by an HSM, IACA ceremony, or other + external tooling. + +In both cases, ``PUT /mso-mdoc/signing-keys/{id}`` can attach or replace +the CA-signed certificate once it has been obtained. """ from typing import Optional @@ -28,6 +29,10 @@ from marshmallow import fields +ALLOWED_CURVES = (ec.SECP256R1,) # P-256 only; extend as needed +"""EC curves accepted for mDoc signing keys (ISO 18013-5 mandates P-256).""" + + def generate_ec_p256_key_pem() -> str: """Generate an EC P-256 private key and return it as a PKCS8 PEM string.""" private_key = ec.generate_private_key(ec.SECP256R1()) @@ -38,6 +43,30 @@ def generate_ec_p256_key_pem() -> str: ).decode("utf-8") +def validate_ec_p256_private_key(private_key_pem: str) -> ec.EllipticCurvePrivateKey: + """Load a PEM private key and verify it is an EC key on an allowed curve. + + Returns the validated private key object. + Raises ``ValueError`` for non-EC keys or unsupported curves. + """ + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), password=None + ) + if not isinstance(private_key, ec.EllipticCurvePrivateKey): + raise ValueError( + "Only EC private keys are supported for mDoc signing; " + f"received {type(private_key).__name__}" + ) + if not any(isinstance(private_key.curve, c) for c in ALLOWED_CURVES): + curve_name = private_key.curve.name if private_key.curve else "unknown" + allowed = ", ".join(c.name for c in ALLOWED_CURVES) + raise ValueError( + f"EC curve '{curve_name}' is not supported for mDoc signing; " + f"allowed curves: {allowed}" + ) + return private_key + + def public_key_pem_from_private(private_key_pem: str) -> str: """Derive the PEM-encoded public key from a PEM-encoded private key.""" private_key = serialization.load_pem_private_key( @@ -244,6 +273,15 @@ class MdocSigningKeyImportSchema(OpenAPISchema): class MdocSigningKeyUpdateSchema(OpenAPISchema): """Request schema for ``PUT /mso-mdoc/signing-keys/{id}``.""" + private_key_pem = fields.Str( + required=False, + metadata={ + "description": ( + "PEM-encoded EC private key. Supply together with " + "certificate_pem to re-import after key rotation." + ) + }, + ) certificate_pem = fields.Str( required=False, metadata={ diff --git a/oid4vc/mso_mdoc/trust_anchor_routes.py b/oid4vc/mso_mdoc/trust_anchor_routes.py index 7c36b67d3..aebb7cc73 100644 --- a/oid4vc/mso_mdoc/trust_anchor_routes.py +++ b/oid4vc/mso_mdoc/trust_anchor_routes.py @@ -4,10 +4,9 @@ - ``TrustAnchorRecord``: X.509 CA certificates trusted for mDoc verification - ``MdocSigningKeyRecord``: EC private keys and certificates for mDoc issuance -These records replace the previous pattern of cramming signing material and -trust anchors into ``SupportedCredential.vc_additional_data``. Using -``BaseRecord`` gives multi-tenant isolation automatically through profile -session scoping. +Signing keys can be generated server-side (following ACA-Py's wallet-managed +key creation pattern) or imported from external sources (HSM, IACA ceremony). +Curve validation (EC P-256) is enforced on both paths. """ import logging @@ -35,6 +34,7 @@ MdocSigningKeyImportSchema, MdocSigningKeyUpdateSchema, generate_ec_p256_key_pem, + validate_ec_p256_private_key, validate_cert_matches_private_key, ) @@ -252,8 +252,9 @@ async def create_signing_key(request: web.Request): async def import_signing_key(request: web.Request): """Import a pre-existing EC signing key and optional certificate. - Use this for keys already registered with a public trust registry - (IACA, etc.) that cannot be regenerated. + The private key must be an EC key on the P-256 curve (as required by + ISO 18013-5). Use this for keys generated externally, e.g. via an + HSM, IACA key ceremony, or local tooling. """ context: AdminRequestContext = request["context"] body = await request.json() @@ -262,6 +263,11 @@ async def import_signing_key(request: web.Request): if not private_key_pem: raise web.HTTPBadRequest(reason="private_key_pem is required for import") + try: + validate_ec_p256_private_key(private_key_pem) + except ValueError as err: + raise web.HTTPBadRequest(reason=str(err)) from err + certificate_pem = body.get("certificate_pem") if certificate_pem: try: @@ -295,6 +301,11 @@ async def update_signing_key(request: web.Request): If ``certificate_pem`` is provided, the certificate's public key is validated against the stored private key to prevent mismatched pairs. + + To re-import a key and certificate together (e.g. after certificate + renewal with a new key), supply both ``private_key_pem`` and + ``certificate_pem``. The new private key is validated for curve + compliance (P-256). """ context: AdminRequestContext = request["context"] signing_key_id = request.match_info["signing_key_id"] @@ -304,11 +315,23 @@ async def update_signing_key(request: web.Request): async with context.profile.session() as session: record = await MdocSigningKeyRecord.retrieve_by_id(session, signing_key_id) + private_key_pem = body.get("private_key_pem") certificate_pem = body.get("certificate_pem") - if certificate_pem and record.private_key_pem: + + # If a new private key is supplied, validate its curve + if private_key_pem: + try: + validate_ec_p256_private_key(private_key_pem) + except ValueError as err: + raise web.HTTPBadRequest(reason=str(err)) from err + record.private_key_pem = private_key_pem + + # Validate cert matches whichever private key will be stored + effective_private_key = private_key_pem or record.private_key_pem + if certificate_pem and effective_private_key: try: validate_cert_matches_private_key( - record.private_key_pem, certificate_pem + effective_private_key, certificate_pem ) except ValueError as err: raise web.HTTPBadRequest(reason=str(err)) from err From 149ca2b6617e4afa01d293d6a7f47b67223d7ffa Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Wed, 1 Apr 2026 20:05:04 -0600 Subject: [PATCH 02/15] fix: move imports to top level, update ruff pre-commit to v0.15.8 Signed-off-by: Adam Burdett --- oid4vc/.pre-commit-config.yaml | 2 +- oid4vc/mso_mdoc/cred_processor.py | 16 ++++++++++------ oid4vc/mso_mdoc/signing_backend.py | 23 +++++++---------------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/oid4vc/.pre-commit-config.yaml b/oid4vc/.pre-commit-config.yaml index 6de2e7dcd..3e38c49cc 100644 --- a/oid4vc/.pre-commit-config.yaml +++ b/oid4vc/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: stages: [commit] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.285 + rev: v0.15.8 hooks: - id: ruff stages: [commit] diff --git a/oid4vc/mso_mdoc/cred_processor.py b/oid4vc/mso_mdoc/cred_processor.py index e567ce420..7079d135d 100644 --- a/oid4vc/mso_mdoc/cred_processor.py +++ b/oid4vc/mso_mdoc/cred_processor.py @@ -27,7 +27,7 @@ from .mdoc.cred_verifier import MsoMdocCredVerifier from .mdoc.pres_verifier import MsoMdocPresVerifier from .payload import normalize_mdoc_result, prepare_mdoc_payload -from .signing_backend import MdocSigningBackend +from .signing_backend import MdocSigningBackend, SoftwareSigningBackend from .trust_anchor import TrustAnchorRecord __all__ = [ @@ -306,7 +306,6 @@ async def _resolve_signing_material( backend = profile.inject_or(MdocSigningBackend) if not backend: - from .signing_backend import SoftwareSigningBackend backend = SoftwareSigningBackend() additional = supported.vc_additional_data or {} @@ -356,7 +355,9 @@ async def _assign_status_entry( return None try: - entry = await _status_handler.assign_status_list_entry(context, definition_id) + entry = await _status_handler.assign_status_list_entry( + context, definition_id + ) except Exception as exc: LOGGER.warning( "Failed to assign status list entry for definition %s: %s", @@ -501,7 +502,6 @@ async def issue( # Sign via the pluggable backend backend = context.profile.inject_or(MdocSigningBackend) if not backend: - from .signing_backend import SoftwareSigningBackend backend = SoftwareSigningBackend() mso_mdoc = await backend.sign_mdoc( @@ -521,7 +521,9 @@ async def issue( # Log full exception for debugging before raising a generic error LOGGER.exception("mso_mdoc issuance error: %s", ex) # Surface the underlying exception text in the CredProcessorError - raise CredProcessorError(f"Failed to issue mso_mdoc credential: {ex}") from ex + raise CredProcessorError( + f"Failed to issue mso_mdoc credential: {ex}" + ) from ex # issuer_signed_b64() already returns base64url without padding # (ISO 18013-5 §8.3 compliant) — exactly what OID4VCI 1.0 §7.3.1 requires. @@ -537,7 +539,9 @@ def _prepare_payload( def _normalize_mdoc_result(self, result: Any) -> str: return normalize_mdoc_result(result) - def validate_credential_subject(self, supported: SupportedCredential, subject: dict): + def validate_credential_subject( + self, supported: SupportedCredential, subject: dict + ): """Validate the credential subject.""" if not subject: raise CredProcessorError("Credential subject cannot be empty") diff --git a/oid4vc/mso_mdoc/signing_backend.py b/oid4vc/mso_mdoc/signing_backend.py index d1a0b2d87..452a2ae15 100644 --- a/oid4vc/mso_mdoc/signing_backend.py +++ b/oid4vc/mso_mdoc/signing_backend.py @@ -17,6 +17,10 @@ from typing import Any, Dict, Mapping, Optional from acapy_agent.core.profile import Profile +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from isomdl_uniffi import PreparedMdoc from .signing_key import MdocSigningKeyRecord @@ -147,18 +151,9 @@ async def sign_mdoc( the mDoc is completed with the raw signature and certificate chain. Private keys never cross the FFI boundary. """ - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import ec - from cryptography.hazmat.primitives.asymmetric.utils import ( - decode_dss_signature, - ) - from isomdl_uniffi import PreparedMdoc - doctype = headers.get("doctype", "") holder_jwk_str = ( - json.dumps(holder_jwk) - if isinstance(holder_jwk, dict) - else str(holder_jwk) + json.dumps(holder_jwk) if isinstance(holder_jwk, dict) else str(holder_jwk) ) # Prepare namespaces — each element value is JSON-encoded. @@ -170,17 +165,13 @@ async def sign_mdoc( mdl_items: Dict[str, str] = {} for k, v in payload.items(): if k == aamva_key and isinstance(v, dict): - namespaces[aamva_key] = { - ak: json.dumps(av) for ak, av in v.items() - } + namespaces[aamva_key] = {ak: json.dumps(av) for ak, av in v.items()} else: mdl_items[k] = json.dumps(v) mdl_items.setdefault("driving_privileges", json.dumps([])) namespaces[mdl_ns] = mdl_items else: - namespaces[doctype] = { - k: json.dumps(v) for k, v in payload.items() - } + namespaces[doctype] = {k: json.dumps(v) for k, v in payload.items()} # Prepare the mDoc (builds COSE structure, returns payload to sign) prepared = PreparedMdoc( From e30440d2a1a44999bb3bc72f8084b783948d666c Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 08:17:29 -0600 Subject: [PATCH 03/15] fix: apply ruff format to mso_mdoc files Signed-off-by: Adam Burdett --- oid4vc/mso_mdoc/__init__.py | 4 +--- oid4vc/mso_mdoc/cred_processor.py | 12 +++--------- oid4vc/mso_mdoc/trust_anchor_routes.py | 10 ++++++++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/oid4vc/mso_mdoc/__init__.py b/oid4vc/mso_mdoc/__init__.py index 4ee15ae59..3d3ed5efa 100644 --- a/oid4vc/mso_mdoc/__init__.py +++ b/oid4vc/mso_mdoc/__init__.py @@ -20,9 +20,7 @@ async def setup(context: InjectionContext): # Deployments can override by binding a different MdocSigningBackend # implementation before this plugin loads. if not context.inject_or(MdocSigningBackend): - context.injector.bind_instance( - MdocSigningBackend, SoftwareSigningBackend() - ) + context.injector.bind_instance(MdocSigningBackend, SoftwareSigningBackend()) processors = context.inject_or(CredProcessors) if not processors: diff --git a/oid4vc/mso_mdoc/cred_processor.py b/oid4vc/mso_mdoc/cred_processor.py index 7079d135d..da1df1686 100644 --- a/oid4vc/mso_mdoc/cred_processor.py +++ b/oid4vc/mso_mdoc/cred_processor.py @@ -355,9 +355,7 @@ async def _assign_status_entry( return None try: - entry = await _status_handler.assign_status_list_entry( - context, definition_id - ) + entry = await _status_handler.assign_status_list_entry(context, definition_id) except Exception as exc: LOGGER.warning( "Failed to assign status list entry for definition %s: %s", @@ -521,9 +519,7 @@ async def issue( # Log full exception for debugging before raising a generic error LOGGER.exception("mso_mdoc issuance error: %s", ex) # Surface the underlying exception text in the CredProcessorError - raise CredProcessorError( - f"Failed to issue mso_mdoc credential: {ex}" - ) from ex + raise CredProcessorError(f"Failed to issue mso_mdoc credential: {ex}") from ex # issuer_signed_b64() already returns base64url without padding # (ISO 18013-5 §8.3 compliant) — exactly what OID4VCI 1.0 §7.3.1 requires. @@ -539,9 +535,7 @@ def _prepare_payload( def _normalize_mdoc_result(self, result: Any) -> str: return normalize_mdoc_result(result) - def validate_credential_subject( - self, supported: SupportedCredential, subject: dict - ): + def validate_credential_subject(self, supported: SupportedCredential, subject: dict): """Validate the credential subject.""" if not subject: raise CredProcessorError("Credential subject cannot be empty") diff --git a/oid4vc/mso_mdoc/trust_anchor_routes.py b/oid4vc/mso_mdoc/trust_anchor_routes.py index aebb7cc73..47a4a05ae 100644 --- a/oid4vc/mso_mdoc/trust_anchor_routes.py +++ b/oid4vc/mso_mdoc/trust_anchor_routes.py @@ -69,7 +69,10 @@ class TrustAnchorQuerySchema(OpenAPISchema): doctype = fields.Str( required=False, - metadata={"description": "Filter by doctype", "example": "org.iso.18013.5.1.mDL"}, + metadata={ + "description": "Filter by doctype", + "example": "org.iso.18013.5.1.mDL", + }, ) purpose = fields.Str( required=False, @@ -82,7 +85,10 @@ class SigningKeyQuerySchema(OpenAPISchema): doctype = fields.Str( required=False, - metadata={"description": "Filter by doctype", "example": "org.iso.18013.5.1.mDL"}, + metadata={ + "description": "Filter by doctype", + "example": "org.iso.18013.5.1.mDL", + }, ) label = fields.Str( required=False, From 6a9d1ecf144b7de89fbaf99c1d08ebab9925a747 Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 08:19:45 -0600 Subject: [PATCH 04/15] ci: bump isomdl-uniffi to test13, point conformance at prepare-complete-signing branch Signed-off-by: Adam Burdett --- .github/workflows/oid4vc-conformance-tests.yaml | 4 ++-- .github/workflows/pr-linting-and-unit-tests.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/oid4vc-conformance-tests.yaml b/.github/workflows/oid4vc-conformance-tests.yaml index 5fbfdb0b3..2ee194fb2 100644 --- a/.github/workflows/oid4vc-conformance-tests.yaml +++ b/.github/workflows/oid4vc-conformance-tests.yaml @@ -53,7 +53,7 @@ jobs: tags: oid4vc-integration-acapy-issuer:latest build-args: | ACAPY_VERSION=1.4.0 - ISOMDL_BRANCH=fix/python-build-system + ISOMDL_BRANCH=feat/prepare-complete-signing cache-from: type=gha,scope=acapy-oid4vc cache-to: type=gha,mode=max,scope=acapy-oid4vc @@ -67,7 +67,7 @@ jobs: tags: oid4vc-integration-acapy-verifier:latest build-args: | ACAPY_VERSION=1.4.0 - ISOMDL_BRANCH=fix/python-build-system + ISOMDL_BRANCH=feat/prepare-complete-signing # Issuer + verifier share all layers; use same cache scope. cache-from: type=gha,scope=acapy-oid4vc diff --git a/.github/workflows/pr-linting-and-unit-tests.yaml b/.github/workflows/pr-linting-and-unit-tests.yaml index 350da9c98..fdeddc64d 100644 --- a/.github/workflows/pr-linting-and-unit-tests.yaml +++ b/.github/workflows/pr-linting-and-unit-tests.yaml @@ -82,7 +82,7 @@ jobs: if: contains(steps.changed-plugins.outputs.changed-plugins, 'oid4vc') run: | cd oid4vc - poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test12/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test13/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl cd .. #---------------------------------------------- # Lint plugins From cf1b8f14ff5184ce8fafba6efea9aeae32574d76 Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 08:59:16 -0600 Subject: [PATCH 05/15] fix(tests): update mso_mdoc unit tests for signing backend refactor Signed-off-by: Adam Burdett --- oid4vc/mso_mdoc/tests/test_cred_processor.py | 34 ++++++++++------- .../tests/test_expired_certificate.py | 37 ++++++++++++++----- oid4vc/mso_mdoc/tests/test_trust_registry.py | 8 ++-- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/oid4vc/mso_mdoc/tests/test_cred_processor.py b/oid4vc/mso_mdoc/tests/test_cred_processor.py index 940f9a3f1..91a11095c 100644 --- a/oid4vc/mso_mdoc/tests/test_cred_processor.py +++ b/oid4vc/mso_mdoc/tests/test_cred_processor.py @@ -134,17 +134,25 @@ async def test_issue_calls_signer_correctly( key_rec.private_key_pem = "test-priv-key" key_rec.certificate_pem = "test-cert" + mock_backend = MagicMock() + mock_backend.resolve_signing_material = AsyncMock( + return_value={ + "private_key_pem": key_rec.private_key_pem, + "certificate_pem": key_rec.certificate_pem, + } + ) + mock_backend.sign_mdoc = AsyncMock( + return_value="oLHC0-T1" # base64url without padding + ) + with ( - patch("mso_mdoc.cred_processor.isomdl_mdoc_sign") as mock_sign, patch("mso_mdoc.cred_processor.check_certificate_not_expired"), patch( - "mso_mdoc.cred_processor.MdocSigningKeyRecord.query", + "mso_mdoc.signing_backend.MdocSigningKeyRecord.query", AsyncMock(return_value=[key_rec]), ), ): - mock_sign.return_value = ( - "oLHC0-T1" # base64url without padding as returned by isomdl-uniffi - ) + mock_context.profile.inject_or = MagicMock(return_value=mock_backend) # Setup input ex_record = MagicMock(spec=OID4VCIExchangeRecord) @@ -172,11 +180,11 @@ async def test_issue_calls_signer_correctly( # Verify result: issuer_signed_b64() returns base64url directly assert result == "oLHC0-T1" - # Verify signer was called with correct arguments - mock_sign.assert_called_once() - call_args = mock_sign.call_args - assert call_args[0][0] == pop.holder_jwk # holder_jwk - assert call_args[0][1]["doctype"] == "org.iso.18013.5.1.mDL" # headers - assert call_args[0][2] == sample_body # payload - assert call_args[0][3] == "test-cert" # cert - assert call_args[0][4] == "test-priv-key" # priv key + # Verify backend was called + mock_backend.sign_mdoc.assert_called_once() + call_args = mock_backend.sign_mdoc.call_args + # signing_material, holder_jwk, headers, payload + assert call_args[0][0]["certificate_pem"] == "test-cert" + assert call_args[0][0]["private_key_pem"] == "test-priv-key" + assert call_args[0][1]["kty"] == "EC" # holder_jwk + assert call_args[0][2]["doctype"] == "org.iso.18013.5.1.mDL" # headers diff --git a/oid4vc/mso_mdoc/tests/test_expired_certificate.py b/oid4vc/mso_mdoc/tests/test_expired_certificate.py index aa1f0d5f2..87850194b 100644 --- a/oid4vc/mso_mdoc/tests/test_expired_certificate.py +++ b/oid4vc/mso_mdoc/tests/test_expired_certificate.py @@ -251,13 +251,23 @@ async def test_issue_raises_when_certificate_is_expired(self): ex_record = self._make_ex_record() pop = self._make_pop() + mock_backend = MagicMock() + mock_backend.resolve_signing_material = AsyncMock( + return_value={ + "private_key_pem": private_key_pem, + "certificate_pem": expired_cert_pem, + } + ) + mock_backend.sign_mdoc = AsyncMock() + with ( - patch("mso_mdoc.cred_processor.isomdl_mdoc_sign") as mock_sign, patch( - "mso_mdoc.cred_processor.MdocSigningKeyRecord.query", + "mso_mdoc.signing_backend.MdocSigningKeyRecord.query", AsyncMock(return_value=[key_rec]), ), ): + profile.inject_or = MagicMock(return_value=mock_backend) + context = self._make_admin_context(profile) processor = MsoMdocCredProcessor() with pytest.raises(CredProcessorError, match=r"(?i)expir"): @@ -269,8 +279,8 @@ async def test_issue_raises_when_certificate_is_expired(self): context=context, ) - # The Rust signer must NEVER have been called with an expired cert. - mock_sign.assert_not_called() + # The signing backend must NEVER have been called with an expired cert. + mock_backend.sign_mdoc.assert_not_called() @pytest.mark.asyncio async def test_issue_succeeds_when_certificate_is_valid(self): @@ -287,16 +297,25 @@ async def test_issue_succeeds_when_certificate_is_valid(self): ex_record = self._make_ex_record() pop = self._make_pop() + mock_backend = MagicMock() + mock_backend.resolve_signing_material = AsyncMock( + return_value={ + "private_key_pem": private_key_pem, + "certificate_pem": valid_cert_pem, + } + ) + mock_backend.sign_mdoc = AsyncMock( + return_value="oLHC0-T1", # base64url without padding + ) + with ( patch( - "mso_mdoc.cred_processor.isomdl_mdoc_sign", - return_value="oLHC0-T1", # base64url without padding as returned by isomdl-uniffi - ), - patch( - "mso_mdoc.cred_processor.MdocSigningKeyRecord.query", + "mso_mdoc.signing_backend.MdocSigningKeyRecord.query", AsyncMock(return_value=[key_rec]), ), ): + profile.inject_or = MagicMock(return_value=mock_backend) + context = self._make_admin_context(profile) processor = MsoMdocCredProcessor() try: diff --git a/oid4vc/mso_mdoc/tests/test_trust_registry.py b/oid4vc/mso_mdoc/tests/test_trust_registry.py index a99f2f8ae..880975e6c 100644 --- a/oid4vc/mso_mdoc/tests/test_trust_registry.py +++ b/oid4vc/mso_mdoc/tests/test_trust_registry.py @@ -2,7 +2,7 @@ Also covers the processor-layer functions that use these records: - ``_get_trust_anchors()`` — queries TrustAnchorRecord with doctype-aware filtering. -- ``_resolve_signing_key()`` — resolves signing key from registry or legacy fallback. +- ``_resolve_signing_material()`` — resolves signing material via pluggable backend. - ``_assign_status_entry()`` — assigns IETF Token Status List entry at issuance. """ @@ -289,12 +289,12 @@ async def test_reader_auth_records_excluded(self, profile): # --------------------------------------------------------------------------- -# _resolve_signing_key processor method tests +# _resolve_signing_material processor method tests # --------------------------------------------------------------------------- class TestResolveSigningKey: - """Tests for MsoMdocCredProcessor._resolve_signing_key().""" + """Tests for MsoMdocCredProcessor._resolve_signing_material().""" @pytest.fixture def processor(self): @@ -320,7 +320,7 @@ async def test_resolves_from_signing_key_record_by_doctype(self, processor): format="mso_mdoc", format_data={"doctype": "org.iso.18013.5.1.mDL"}, ) - result = await processor._resolve_signing_key(supported, fresh_profile) + result = await processor._resolve_signing_material(supported, fresh_profile) assert result["private_key_pem"] == "PRIV_KEY_FROM_RECORD" assert result["certificate_pem"] == "CERT_FROM_RECORD" From 48b400d56540b9d2a3735953ac7a23bbc8eaef3a Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 09:14:26 -0600 Subject: [PATCH 06/15] fix(integration): update ISOMDL_BRANCH to feat/prepare-complete-signing Signed-off-by: Adam Burdett --- oid4vc/integration/docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oid4vc/integration/docker-compose.yml b/oid4vc/integration/docker-compose.yml index b32af0733..8b62fb279 100644 --- a/oid4vc/integration/docker-compose.yml +++ b/oid4vc/integration/docker-compose.yml @@ -10,7 +10,7 @@ services: target: base args: ACAPY_VERSION: 1.5.1 - ISOMDL_BRANCH: fix/python-build-system + ISOMDL_BRANCH: feat/prepare-complete-signing image: oid4vc-base:latest profiles: - base-only # Only build when explicitly requested @@ -54,7 +54,7 @@ services: context: ../../ args: ACAPY_VERSION: 1.5.1 - ISOMDL_BRANCH: fix/python-build-system + ISOMDL_BRANCH: feat/prepare-complete-signing ports: - "${ACAPY_ISSUER_INBOUND_PORT:-18020}:8020" # inbound transport - "${ACAPY_ISSUER_ADMIN_PORT:-18021}:8021" # admin @@ -92,7 +92,7 @@ services: context: ../../ args: ACAPY_VERSION: 1.5.1 - ISOMDL_BRANCH: fix/python-build-system + ISOMDL_BRANCH: feat/prepare-complete-signing ports: - "${ACAPY_VERIFIER_INBOUND_PORT:-18030}:8030" # inbound transport - "${ACAPY_VERIFIER_ADMIN_PORT:-18031}:8031" # admin From fb1b3297ccccb9974889e034ca75471beda380cb Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 09:50:49 -0600 Subject: [PATCH 07/15] fix(mso_mdoc): use PreparedMdoc.new_mdl() for ISO-compliant mDL CBOR typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PreparedMdoc::new() uses a generic json_to_cbor conversion which stores all string values as plain CBOR text, including date fields like birth_date. ISO 18013-5 requires date fields to be encoded as CBOR full-date values (via OrgIso1801351 typed namespace builder). Add PreparedMdoc::new_mdl() to isomdl-uniffi (v0.1.0-test14) that routes mDL namespace data through OrgIso1801351::from_json() — the same typed parser used by create_and_sign_mdl — so CBOR field types are correct. SoftwareSigningBackend.sign_mdoc() now calls PreparedMdoc.new_mdl() for the org.iso.18013.5.1.mDL doctype, fixing Credo wallet rejection of mDL credentials during presentation. Also bump CI wheel reference from v0.1.0-test13 to v0.1.0-test14. Signed-off-by: Adam Burdett --- .../workflows/pr-linting-and-unit-tests.yaml | 2 +- oid4vc/mso_mdoc/signing_backend.py | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/workflows/pr-linting-and-unit-tests.yaml b/.github/workflows/pr-linting-and-unit-tests.yaml index fdeddc64d..7de3629a5 100644 --- a/.github/workflows/pr-linting-and-unit-tests.yaml +++ b/.github/workflows/pr-linting-and-unit-tests.yaml @@ -82,7 +82,7 @@ jobs: if: contains(steps.changed-plugins.outputs.changed-plugins, 'oid4vc') run: | cd oid4vc - poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test13/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl cd .. #---------------------------------------------- # Lint plugins diff --git a/oid4vc/mso_mdoc/signing_backend.py b/oid4vc/mso_mdoc/signing_backend.py index 452a2ae15..d580ae302 100644 --- a/oid4vc/mso_mdoc/signing_backend.py +++ b/oid4vc/mso_mdoc/signing_backend.py @@ -156,30 +156,37 @@ async def sign_mdoc( json.dumps(holder_jwk) if isinstance(holder_jwk, dict) else str(holder_jwk) ) - # Prepare namespaces — each element value is JSON-encoded. - namespaces: Dict[str, Dict[str, str]] = {} - + # Prepare the mDoc (builds COSE structure, returns payload to sign). + # For mDL we use PreparedMdoc.new_mdl() which routes namespace data + # through OrgIso1801351::from_json() — the same typed parser used by + # create_and_sign_mdl — so fields like birth_date are encoded as proper + # CBOR full-date values rather than plain text strings. if doctype == "org.iso.18013.5.1.mDL": - mdl_ns = "org.iso.18013.5.1" aamva_key = "org.iso.18013.5.1.aamva" - mdl_items: Dict[str, str] = {} + mdl_items: Dict[str, Any] = {} + aamva_payload: Optional[Dict[str, Any]] = None for k, v in payload.items(): if k == aamva_key and isinstance(v, dict): - namespaces[aamva_key] = {ak: json.dumps(av) for ak, av in v.items()} + aamva_payload = v else: - mdl_items[k] = json.dumps(v) - mdl_items.setdefault("driving_privileges", json.dumps([])) - namespaces[mdl_ns] = mdl_items + mdl_items[k] = v + mdl_items.setdefault("driving_privileges", []) + prepared = PreparedMdoc.new_mdl( + mdl_items=json.dumps(mdl_items), + aamva_items=json.dumps(aamva_payload) if aamva_payload is not None else None, + holder_jwk=holder_jwk_str, + signature_algorithm="ES256", + ) else: - namespaces[doctype] = {k: json.dumps(v) for k, v in payload.items()} - - # Prepare the mDoc (builds COSE structure, returns payload to sign) - prepared = PreparedMdoc( - doc_type=doctype, - namespaces=namespaces, - holder_jwk=holder_jwk_str, - signature_algorithm="ES256", - ) + namespaces: Dict[str, Dict[str, str]] = { + doctype: {k: json.dumps(v) for k, v in payload.items()} + } + prepared = PreparedMdoc( + doc_type=doctype, + namespaces=namespaces, + holder_jwk=holder_jwk_str, + signature_algorithm="ES256", + ) sig_payload = prepared.signature_payload() From 78dcfec8cb00aab63f0bce97ef9520fdc1b80d69 Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 09:55:24 -0600 Subject: [PATCH 08/15] style: wrap long line in sign_mdoc aamva_items arg Signed-off-by: Adam Burdett --- oid4vc/mso_mdoc/signing_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oid4vc/mso_mdoc/signing_backend.py b/oid4vc/mso_mdoc/signing_backend.py index d580ae302..67cc2704b 100644 --- a/oid4vc/mso_mdoc/signing_backend.py +++ b/oid4vc/mso_mdoc/signing_backend.py @@ -173,7 +173,9 @@ async def sign_mdoc( mdl_items.setdefault("driving_privileges", []) prepared = PreparedMdoc.new_mdl( mdl_items=json.dumps(mdl_items), - aamva_items=json.dumps(aamva_payload) if aamva_payload is not None else None, + aamva_items=( + json.dumps(aamva_payload) if aamva_payload is not None else None + ), holder_jwk=holder_jwk_str, signature_algorithm="ES256", ) From d1aaa77fb7d909603704e81e13e5225d05aad44f Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 10:06:25 -0600 Subject: [PATCH 09/15] fix(integration): bump isomdl-uniffi test runner wheel from test12 to test14 The integration test runner (river service) was installing isomdl-uniffi v0.1.0-test12 from its pyproject.toml, which predates the PreparedMdoc API and PreparedMdoc::new_mdl(). Updated to v0.1.0-test14 which includes both PreparedMdoc and the new new_mdl() constructor for ISO-compliant mDL CBOR field typing. Signed-off-by: Adam Burdett --- oid4vc/integration/pyproject.toml | 2 +- oid4vc/integration/uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/oid4vc/integration/pyproject.toml b/oid4vc/integration/pyproject.toml index 80970c754..c4693a97a 100644 --- a/oid4vc/integration/pyproject.toml +++ b/oid4vc/integration/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # sd_jwt dependencies "jsonpointer>=3.0.0,<4.0.0", # isomdl-uniffi from GitHub releases (pre-built wheels) - "isomdl-uniffi @ https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test12/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == 'linux'", + "isomdl-uniffi @ https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == 'linux'", "acapy-agent>=1.4.0", "Appium-Python-Client>=4.0.0", "bitarray>=2.9.2", diff --git a/oid4vc/integration/uv.lock b/oid4vc/integration/uv.lock index fb91bbfe2..5120d0da4 100644 --- a/oid4vc/integration/uv.lock +++ b/oid4vc/integration/uv.lock @@ -779,9 +779,9 @@ wheels = [ [[package]] name = "isomdl-uniffi" version = "0.1.0" -source = { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test12/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } +source = { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } wheels = [ - { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test12/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05a6e8f9fa34436981d7a523efd0717271b921ec3485d43a37c10d334df396b0" }, + { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef690f741b7399b14201b43f5ad322f1a6c6f96f172b921ee2d89eb64d9f8e2a" }, ] [package.metadata] @@ -1045,7 +1045,7 @@ requires-dist = [ { name = "cwt", specifier = ">=1.6.0" }, { name = "did-peer-4", specifier = ">=0.1.4" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "isomdl-uniffi", marker = "sys_platform == 'linux'", url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test12/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, + { name = "isomdl-uniffi", marker = "sys_platform == 'linux'", url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { name = "jsonpointer", specifier = ">=3.0.0,<4.0.0" }, { name = "jsonrpc-api-proxy-client", git = "https://github.com/Indicio-tech/json-rpc-api-proxy.git?subdirectory=clients%2Fpython&rev=main" }, { name = "pycose", specifier = ">=1.0.0" }, From 0bdc18c289e364cde1bbb8f309d2809f5755706b Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 11:18:18 -0600 Subject: [PATCH 10/15] fix(docker): pin isomdl-uniffi to commit SHA to bust Docker layer cache The git clone in the isomdl-builder stage uses --branch, which causes Docker to cache the checkout indefinitely as long as the branch name doesn't change. This meant the acapy-issuer/verifier containers were building from commit 1ba8e9b (before new_mdl was added) rather than d018067. Add ISOMDL_COMMIT ARG that, when set, fetches and checks out the exact commit after the shallow clone. Changing ISOMDL_COMMIT in docker-compose.yml forces Docker to invalidate the cached git clone layer and pull fresh source. Pins to d0180670bc3f5ca11b9a49c753f87b48676d624b which includes PreparedMdoc::new_mdl() for ISO-compliant mDL CBOR field encoding. Signed-off-by: Adam Burdett --- oid4vc/docker/Dockerfile | 10 +++++++++- oid4vc/integration/docker-compose.yml | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/oid4vc/docker/Dockerfile b/oid4vc/docker/Dockerfile index 74bfae94b..59c65bf36 100644 --- a/oid4vc/docker/Dockerfile +++ b/oid4vc/docker/Dockerfile @@ -16,9 +16,17 @@ ENV PATH="/root/.cargo/bin:${PATH}" # Accept branch, tag, or commit ref to build from ARG ISOMDL_BRANCH=main +# Optional: pin to an exact commit SHA to bypass Docker layer caching of git clone. +# When this value changes, Docker invalidates the clone layer and fetches fresh source. +ARG ISOMDL_COMMIT="" RUN git clone --depth 1 --branch "${ISOMDL_BRANCH}" \ - https://github.com/Indicio-tech/isomdl-uniffi.git /build/isomdl-uniffi + https://github.com/Indicio-tech/isomdl-uniffi.git /build/isomdl-uniffi \ + && if [ -n "${ISOMDL_COMMIT}" ]; then \ + cd /build/isomdl-uniffi \ + && git fetch --depth=1 origin "${ISOMDL_COMMIT}" \ + && git checkout FETCH_HEAD; \ + fi WORKDIR /build/isomdl-uniffi/python diff --git a/oid4vc/integration/docker-compose.yml b/oid4vc/integration/docker-compose.yml index 8b62fb279..a50f7e3f3 100644 --- a/oid4vc/integration/docker-compose.yml +++ b/oid4vc/integration/docker-compose.yml @@ -11,6 +11,7 @@ services: args: ACAPY_VERSION: 1.5.1 ISOMDL_BRANCH: feat/prepare-complete-signing + ISOMDL_COMMIT: d0180670bc3f5ca11b9a49c753f87b48676d624b image: oid4vc-base:latest profiles: - base-only # Only build when explicitly requested @@ -55,6 +56,7 @@ services: args: ACAPY_VERSION: 1.5.1 ISOMDL_BRANCH: feat/prepare-complete-signing + ISOMDL_COMMIT: d0180670bc3f5ca11b9a49c753f87b48676d624b ports: - "${ACAPY_ISSUER_INBOUND_PORT:-18020}:8020" # inbound transport - "${ACAPY_ISSUER_ADMIN_PORT:-18021}:8021" # admin @@ -93,6 +95,7 @@ services: args: ACAPY_VERSION: 1.5.1 ISOMDL_BRANCH: feat/prepare-complete-signing + ISOMDL_COMMIT: d0180670bc3f5ca11b9a49c753f87b48676d624b ports: - "${ACAPY_VERIFIER_INBOUND_PORT:-18030}:8030" # inbound transport - "${ACAPY_VERIFIER_ADMIN_PORT:-18031}:8031" # admin From 41e6cce61f4dfe68144cb3dd9a200ff1e9aa7b2d Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 11:29:29 -0600 Subject: [PATCH 11/15] fix(mso_mdoc): use Mdoc.create_and_sign_mdl() for mDL instead of new_mdl() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PreparedMdoc::new_mdl() constructor added to the Rust crate in commit d018067 is compiled correctly but the uniffi-generated Python bindings do not expose it as a classmethod (likely a uniffi naming/generation issue). Switch the mDL case in SoftwareSigningBackend.sign_mdoc() to call Mdoc.create_and_sign_mdl() directly — this method is already in the installed wheel, produces correctly typed CBOR field values (dates as full-date, etc.) via the OrgIso1801351 typed namespace builder, and is the exact code path that worked in the original non-pluggable signing. The SoftwareSigningBackend is the software-key backend, so having the private key available for this call is expected. Other backends that hold key material externally continue to use the prepare/complete pattern for non-mDL doctypes. Signed-off-by: Adam Burdett --- oid4vc/mso_mdoc/signing_backend.py | 60 +++++++++++++++++------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/oid4vc/mso_mdoc/signing_backend.py b/oid4vc/mso_mdoc/signing_backend.py index 67cc2704b..caac9e4fa 100644 --- a/oid4vc/mso_mdoc/signing_backend.py +++ b/oid4vc/mso_mdoc/signing_backend.py @@ -20,7 +20,7 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature -from isomdl_uniffi import PreparedMdoc +from isomdl_uniffi import Mdoc, PreparedMdoc from .signing_key import MdocSigningKeyRecord @@ -144,24 +144,29 @@ async def sign_mdoc( headers: Mapping[str, Any], payload: Mapping[str, Any], ) -> str: - """Sign an mDoc using the prepare/complete flow with PEM key material. - - The mDoc structure is prepared in the Rust FFI layer, the signature - payload is signed in Python using the ``cryptography`` library, and - the mDoc is completed with the raw signature and certificate chain. - Private keys never cross the FFI boundary. + """Sign an mDoc using isomdl-uniffi with PEM key material. + + For the ISO 18013-5 mDL doctype, ``Mdoc.create_and_sign_mdl()`` is + used directly so that mDL namespace elements are encoded with the + correct CBOR field types (e.g. ``birth_date`` as a CBOR full-date + rather than a plain text string). + + For all other doctypes, the prepare/complete flow is used — the mDoc + structure is prepared in the Rust FFI layer, the signature payload is + signed in Python, and the mDoc is completed with the raw signature and + certificate chain. Private keys never cross the FFI boundary in that + path. """ doctype = headers.get("doctype", "") holder_jwk_str = ( json.dumps(holder_jwk) if isinstance(holder_jwk, dict) else str(holder_jwk) ) + cert_pem = signing_material["certificate_pem"] + key_pem = signing_material["private_key_pem"] - # Prepare the mDoc (builds COSE structure, returns payload to sign). - # For mDL we use PreparedMdoc.new_mdl() which routes namespace data - # through OrgIso1801351::from_json() — the same typed parser used by - # create_and_sign_mdl — so fields like birth_date are encoded as proper - # CBOR full-date values rather than plain text strings. if doctype == "org.iso.18013.5.1.mDL": + # Use the typed mDL builder (OrgIso1801351::from_json) which + # encodes ISO 18013-5 fields with proper CBOR types. aamva_key = "org.iso.18013.5.1.aamva" mdl_items: Dict[str, Any] = {} aamva_payload: Optional[Dict[str, Any]] = None @@ -171,30 +176,33 @@ async def sign_mdoc( else: mdl_items[k] = v mdl_items.setdefault("driving_privileges", []) - prepared = PreparedMdoc.new_mdl( + mdoc = Mdoc.create_and_sign_mdl( mdl_items=json.dumps(mdl_items), aamva_items=( json.dumps(aamva_payload) if aamva_payload is not None else None ), holder_jwk=holder_jwk_str, - signature_algorithm="ES256", - ) - else: - namespaces: Dict[str, Dict[str, str]] = { - doctype: {k: json.dumps(v) for k, v in payload.items()} - } - prepared = PreparedMdoc( - doc_type=doctype, - namespaces=namespaces, - holder_jwk=holder_jwk_str, - signature_algorithm="ES256", + iaca_cert_pem=cert_pem, + iaca_key_pem=key_pem, ) + return mdoc.issuer_signed_b64() + + # Generic doctype: prepare/complete flow (private key stays in Python) + namespaces: Dict[str, Dict[str, str]] = { + doctype: {k: json.dumps(v) for k, v in payload.items()} + } + prepared = PreparedMdoc( + doc_type=doctype, + namespaces=namespaces, + holder_jwk=holder_jwk_str, + signature_algorithm="ES256", + ) sig_payload = prepared.signature_payload() # Sign with the DS private key via Python's cryptography library private_key = serialization.load_pem_private_key( - signing_material["private_key_pem"].encode("utf-8"), + key_pem.encode("utf-8"), password=None, ) der_sig = private_key.sign(sig_payload, ec.ECDSA(hashes.SHA256())) @@ -208,7 +216,7 @@ async def sign_mdoc( # Complete the mDoc with signature and certificate chain mdoc = prepared.complete( - certificate_chain_pem=signing_material["certificate_pem"], + certificate_chain_pem=cert_pem, signature=raw_sig, ) From 6be8fec3e634eb57f7848db52182f60d65e5e03f Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 11:44:06 -0600 Subject: [PATCH 12/15] fix(mso_mdoc): properly handle namespaced mDL payload in sign_mdoc The credential_subject for mDL can arrive namespaced: {"org.iso.18013.5.1": {"family_name": ..., "given_name": ...}} The previous code iterated payload.items() directly and put the entire "org.iso.18013.5.1" dict as a value in mdl_items, so create_and_sign_mdl received {"org.iso.18013.5.1": {...}} as the top-level JSON object rather than the flat field dict {"family_name": ..., "given_name": ...}. OrgIso1801351::from_json() expects the flat structure and so reported all mandatory fields as Missing. Mirror the old _prepare_mdl_namespaces logic: extract the inner dict with payload.get("org.iso.18013.5.1", payload) so both namespaced and flat credential subjects are handled correctly. Signed-off-by: Adam Burdett --- oid4vc/mso_mdoc/signing_backend.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/oid4vc/mso_mdoc/signing_backend.py b/oid4vc/mso_mdoc/signing_backend.py index caac9e4fa..0eea2ab8a 100644 --- a/oid4vc/mso_mdoc/signing_backend.py +++ b/oid4vc/mso_mdoc/signing_backend.py @@ -167,15 +167,16 @@ async def sign_mdoc( if doctype == "org.iso.18013.5.1.mDL": # Use the typed mDL builder (OrgIso1801351::from_json) which # encodes ISO 18013-5 fields with proper CBOR types. + # The credential subject may be namespaced ({"org.iso.18013.5.1": + # {...}}) or flat; normalise here as the old code did. + mdl_ns = "org.iso.18013.5.1" aamva_key = "org.iso.18013.5.1.aamva" - mdl_items: Dict[str, Any] = {} - aamva_payload: Optional[Dict[str, Any]] = None - for k, v in payload.items(): - if k == aamva_key and isinstance(v, dict): - aamva_payload = v - else: - mdl_items[k] = v + mdl_payload = payload.get(mdl_ns, payload) + mdl_items: Dict[str, Any] = { + k: v for k, v in mdl_payload.items() if k != aamva_key + } mdl_items.setdefault("driving_privileges", []) + aamva_payload: Optional[Dict[str, Any]] = payload.get(aamva_key) mdoc = Mdoc.create_and_sign_mdl( mdl_items=json.dumps(mdl_items), aamva_items=( From 5d8fde3305cff53069b2990e9ea5aa23dfd7371e Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Thu, 2 Apr 2026 12:17:58 -0600 Subject: [PATCH 13/15] fix: bump isomdl-uniffi to test15 (fix subscripted generic isinstance) v0.1.0-test15 fixes TypeError: Subscripted generics cannot be used with class and instance checks in MDocItem.ITEM_MAP and MDocItem.ARRAY Python bindings. This caused Sphereon presentation verification to fail whenever the credential contained array or map typed fields (e.g. driving_privileges). Signed-off-by: Adam Burdett --- .github/workflows/pr-linting-and-unit-tests.yaml | 2 +- oid4vc/integration/docker-compose.yml | 6 +++--- oid4vc/integration/pyproject.toml | 2 +- oid4vc/integration/uv.lock | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-linting-and-unit-tests.yaml b/.github/workflows/pr-linting-and-unit-tests.yaml index 7de3629a5..a915b1a30 100644 --- a/.github/workflows/pr-linting-and-unit-tests.yaml +++ b/.github/workflows/pr-linting-and-unit-tests.yaml @@ -82,7 +82,7 @@ jobs: if: contains(steps.changed-plugins.outputs.changed-plugins, 'oid4vc') run: | cd oid4vc - poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl cd .. #---------------------------------------------- # Lint plugins diff --git a/oid4vc/integration/docker-compose.yml b/oid4vc/integration/docker-compose.yml index a50f7e3f3..29b324272 100644 --- a/oid4vc/integration/docker-compose.yml +++ b/oid4vc/integration/docker-compose.yml @@ -11,7 +11,7 @@ services: args: ACAPY_VERSION: 1.5.1 ISOMDL_BRANCH: feat/prepare-complete-signing - ISOMDL_COMMIT: d0180670bc3f5ca11b9a49c753f87b48676d624b + ISOMDL_COMMIT: 1808d61dca4b55fc15d78cf915cdc600c0eaf9f4 image: oid4vc-base:latest profiles: - base-only # Only build when explicitly requested @@ -56,7 +56,7 @@ services: args: ACAPY_VERSION: 1.5.1 ISOMDL_BRANCH: feat/prepare-complete-signing - ISOMDL_COMMIT: d0180670bc3f5ca11b9a49c753f87b48676d624b + ISOMDL_COMMIT: 1808d61dca4b55fc15d78cf915cdc600c0eaf9f4 ports: - "${ACAPY_ISSUER_INBOUND_PORT:-18020}:8020" # inbound transport - "${ACAPY_ISSUER_ADMIN_PORT:-18021}:8021" # admin @@ -95,7 +95,7 @@ services: args: ACAPY_VERSION: 1.5.1 ISOMDL_BRANCH: feat/prepare-complete-signing - ISOMDL_COMMIT: d0180670bc3f5ca11b9a49c753f87b48676d624b + ISOMDL_COMMIT: 1808d61dca4b55fc15d78cf915cdc600c0eaf9f4 ports: - "${ACAPY_VERIFIER_INBOUND_PORT:-18030}:8030" # inbound transport - "${ACAPY_VERIFIER_ADMIN_PORT:-18031}:8031" # admin diff --git a/oid4vc/integration/pyproject.toml b/oid4vc/integration/pyproject.toml index c4693a97a..8d6a2f956 100644 --- a/oid4vc/integration/pyproject.toml +++ b/oid4vc/integration/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # sd_jwt dependencies "jsonpointer>=3.0.0,<4.0.0", # isomdl-uniffi from GitHub releases (pre-built wheels) - "isomdl-uniffi @ https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == 'linux'", + "isomdl-uniffi @ https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == 'linux'", "acapy-agent>=1.4.0", "Appium-Python-Client>=4.0.0", "bitarray>=2.9.2", diff --git a/oid4vc/integration/uv.lock b/oid4vc/integration/uv.lock index 5120d0da4..81e9de6e0 100644 --- a/oid4vc/integration/uv.lock +++ b/oid4vc/integration/uv.lock @@ -779,9 +779,9 @@ wheels = [ [[package]] name = "isomdl-uniffi" version = "0.1.0" -source = { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } +source = { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } wheels = [ - { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef690f741b7399b14201b43f5ad322f1a6c6f96f172b921ee2d89eb64d9f8e2a" }, + { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318768c512a108f87e79ddeb857fb566c0894d2834f78df18f31c54fdd78e33f" }, ] [package.metadata] @@ -1045,7 +1045,7 @@ requires-dist = [ { name = "cwt", specifier = ">=1.6.0" }, { name = "did-peer-4", specifier = ">=0.1.4" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "isomdl-uniffi", marker = "sys_platform == 'linux'", url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test14/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, + { name = "isomdl-uniffi", marker = "sys_platform == 'linux'", url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { name = "jsonpointer", specifier = ">=3.0.0,<4.0.0" }, { name = "jsonrpc-api-proxy-client", git = "https://github.com/Indicio-tech/json-rpc-api-proxy.git?subdirectory=clients%2Fpython&rev=main" }, { name = "pycose", specifier = ">=1.0.0" }, From ac58693665dc46c2e827d6f54f35c9e0f71c1c8c Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Fri, 3 Apr 2026 10:36:39 -0600 Subject: [PATCH 14/15] chore: bump isomdl-uniffi to v0.1.0-indicio.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all v0.1.0-test15 pins with the first stable indicio-tagged release. Tag v0.1.0-indicio.1 points to main @ 161bd687 (PR #27 merged — PreparedMdoc). Changes: - .github/workflows/pr-linting-and-unit-tests.yaml: update wheel URL - oid4vc/integration/pyproject.toml: update wheel URL - oid4vc/integration/uv.lock: update URL + sha256 (0019dfc4b) - oid4vc/integration/docker-compose.yml: ISOMDL_BRANCH=main, ISOMDL_COMMIT=161bd687313bb52793394f7beb9dbc7b4b6f68e2 - .gitignore: add ISOMDL_TAGGING.md to ignored local docs Signed-off-by: Adam Burdett --- .github/workflows/pr-linting-and-unit-tests.yaml | 2 +- .gitignore | 3 +++ oid4vc/integration/docker-compose.yml | 12 ++++++------ oid4vc/integration/pyproject.toml | 2 +- oid4vc/integration/uv.lock | 6 +++--- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-linting-and-unit-tests.yaml b/.github/workflows/pr-linting-and-unit-tests.yaml index a915b1a30..6c4880e83 100644 --- a/.github/workflows/pr-linting-and-unit-tests.yaml +++ b/.github/workflows/pr-linting-and-unit-tests.yaml @@ -82,7 +82,7 @@ jobs: if: contains(steps.changed-plugins.outputs.changed-plugins, 'oid4vc') run: | cd oid4vc - poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + poetry run pip install --force-reinstall https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-indicio.1/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl cd .. #---------------------------------------------- # Lint plugins diff --git a/.gitignore b/.gitignore index 6f8d45374..8b1562acc 100644 --- a/.gitignore +++ b/.gitignore @@ -197,3 +197,6 @@ settings.json /docs mkdocs.yml *.code-workspace + +# Local process documentation (not for upstream) +ISOMDL_TAGGING.md diff --git a/oid4vc/integration/docker-compose.yml b/oid4vc/integration/docker-compose.yml index 29b324272..4f4237998 100644 --- a/oid4vc/integration/docker-compose.yml +++ b/oid4vc/integration/docker-compose.yml @@ -10,8 +10,8 @@ services: target: base args: ACAPY_VERSION: 1.5.1 - ISOMDL_BRANCH: feat/prepare-complete-signing - ISOMDL_COMMIT: 1808d61dca4b55fc15d78cf915cdc600c0eaf9f4 + ISOMDL_BRANCH: main + ISOMDL_COMMIT: 161bd687313bb52793394f7beb9dbc7b4b6f68e2 image: oid4vc-base:latest profiles: - base-only # Only build when explicitly requested @@ -55,8 +55,8 @@ services: context: ../../ args: ACAPY_VERSION: 1.5.1 - ISOMDL_BRANCH: feat/prepare-complete-signing - ISOMDL_COMMIT: 1808d61dca4b55fc15d78cf915cdc600c0eaf9f4 + ISOMDL_BRANCH: main + ISOMDL_COMMIT: 161bd687313bb52793394f7beb9dbc7b4b6f68e2 ports: - "${ACAPY_ISSUER_INBOUND_PORT:-18020}:8020" # inbound transport - "${ACAPY_ISSUER_ADMIN_PORT:-18021}:8021" # admin @@ -94,8 +94,8 @@ services: context: ../../ args: ACAPY_VERSION: 1.5.1 - ISOMDL_BRANCH: feat/prepare-complete-signing - ISOMDL_COMMIT: 1808d61dca4b55fc15d78cf915cdc600c0eaf9f4 + ISOMDL_BRANCH: main + ISOMDL_COMMIT: 161bd687313bb52793394f7beb9dbc7b4b6f68e2 ports: - "${ACAPY_VERIFIER_INBOUND_PORT:-18030}:8030" # inbound transport - "${ACAPY_VERIFIER_ADMIN_PORT:-18031}:8031" # admin diff --git a/oid4vc/integration/pyproject.toml b/oid4vc/integration/pyproject.toml index 8d6a2f956..b3613dd3a 100644 --- a/oid4vc/integration/pyproject.toml +++ b/oid4vc/integration/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # sd_jwt dependencies "jsonpointer>=3.0.0,<4.0.0", # isomdl-uniffi from GitHub releases (pre-built wheels) - "isomdl-uniffi @ https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == 'linux'", + "isomdl-uniffi @ https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-indicio.1/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl ; sys_platform == 'linux'", "acapy-agent>=1.4.0", "Appium-Python-Client>=4.0.0", "bitarray>=2.9.2", diff --git a/oid4vc/integration/uv.lock b/oid4vc/integration/uv.lock index 81e9de6e0..7ef8215b7 100644 --- a/oid4vc/integration/uv.lock +++ b/oid4vc/integration/uv.lock @@ -779,9 +779,9 @@ wheels = [ [[package]] name = "isomdl-uniffi" version = "0.1.0" -source = { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } +source = { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-indicio.1/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } wheels = [ - { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318768c512a108f87e79ddeb857fb566c0894d2834f78df18f31c54fdd78e33f" }, + { url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-indicio.1/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0019dfc4b32d63c1392aa264aed2253c1e0c2fb09216f8e2cc269bbfb8bb49b5" }, ] [package.metadata] @@ -1045,7 +1045,7 @@ requires-dist = [ { name = "cwt", specifier = ">=1.6.0" }, { name = "did-peer-4", specifier = ">=0.1.4" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "isomdl-uniffi", marker = "sys_platform == 'linux'", url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-test15/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, + { name = "isomdl-uniffi", marker = "sys_platform == 'linux'", url = "https://github.com/Indicio-tech/isomdl-uniffi/releases/download/v0.1.0-indicio.1/isomdl_uniffi-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { name = "jsonpointer", specifier = ">=3.0.0,<4.0.0" }, { name = "jsonrpc-api-proxy-client", git = "https://github.com/Indicio-tech/json-rpc-api-proxy.git?subdirectory=clients%2Fpython&rev=main" }, { name = "pycose", specifier = ">=1.0.0" }, From 63f6b1d684287995fa8ec98558b21bed28d695ea Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Fri, 3 Apr 2026 10:46:18 -0600 Subject: [PATCH 15/15] chore: gitignore ACAPY_TAGGING.md local process doc Signed-off-by: Adam Burdett --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8b1562acc..0e7bb9e39 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,4 @@ mkdocs.yml # Local process documentation (not for upstream) ISOMDL_TAGGING.md +ACAPY_TAGGING.md