Skip to content

Commit 3a98721

Browse files
authored
Merge pull request #28 from Indicio-tech/fix/mdoc-issuance-error-handling
fix: validate mandatory mDL fields early with clear error messages
2 parents 174cc9d + f834dd5 commit 3a98721

6 files changed

Lines changed: 83 additions & 10 deletions

File tree

oid4vc/integration/conformance/setup_acapy.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,19 @@ async def create_credential_offer(
237237
credential_config_id: str,
238238
issuer_did: str,
239239
pin: str | None = None,
240+
credential_subject: dict | None = None,
240241
) -> dict:
241242
"""Create a pre-authorized credential offer and return offer details."""
242-
exchange_body: dict[str, Any] = {
243-
"supported_cred_id": credential_config_id,
244-
"credential_subject": {
243+
if credential_subject is None:
244+
credential_subject = {
245245
"given_name": "Alice",
246246
"family_name": "Smith",
247247
"email": "alice@example.com",
248248
"birthdate": "1990-01-15",
249-
},
249+
}
250+
exchange_body: dict[str, Any] = {
251+
"supported_cred_id": credential_config_id,
252+
"credential_subject": credential_subject,
250253
# verification_method format: {did}#0 (selects the first key on the DID)
251254
"verification_method": f"{issuer_did}#0",
252255
}
@@ -773,6 +776,19 @@ async def main() -> None:
773776
ISSUER_ADMIN_URL,
774777
mdoc_config["supported_cred_id"],
775778
p256_did,
779+
credential_subject={
780+
"family_name": "Smith",
781+
"given_name": "Alice",
782+
"birth_date": "1990-01-15",
783+
"issue_date": "2024-01-01",
784+
"expiry_date": "2029-01-01",
785+
"issuing_country": "US",
786+
"issuing_authority": "US DMV",
787+
"document_number": "DL-12345678",
788+
"portrait": "bXVzdGFjaGlv",
789+
"driving_privileges": [],
790+
"un_distinguishing_sign": "USA",
791+
},
776792
)
777793

778794
setup_output["issuer"] = {

oid4vc/integration/tests/helpers/constants.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,16 @@ class ENDPOINTS:
107107

108108
# ISO 18013-5 mDL mandatory fields required by create_and_sign_mdl
109109
# All mandatory fields per ISO 18013-5 OrgIso1801351 struct:
110-
# family_name, given_name, birth_date are provided by each test individually.
111-
# The remaining 8 mandatory fields are collected here.
110+
# family_name, given_name are provided by each test individually.
111+
# All remaining mandatory fields are collected here (including birth_date).
112112
MDL_PORTRAIT: Final[str] = "SGVsbG8gV29ybGQ=" # base64("Hello World") placeholder
113113
MDL_DRIVING_PRIVILEGES: Final[list] = []
114114
MDL_UN_DISTINGUISHING_SIGN: Final[str] = "USA"
115115

116-
# Merge these into any mDL credential_subject["org.iso.18013.5.1"] dict
116+
# Merge these into any mDL credential_subject["org.iso.18013.5.1"] dict.
117+
# Tests that need a dynamic birth_date should override it after spreading.
117118
MDL_MANDATORY_FIELDS: Final[dict] = {
119+
"birth_date": "1990-01-15",
118120
"issue_date": "2024-01-01",
119121
"expiry_date": "2029-01-01",
120122
"issuing_country": "US",

oid4vc/integration/tests/mdoc/test_mdoc_age_predicates.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import pytest
2121

22+
from tests.helpers.constants import MDL_MANDATORY_FIELDS
23+
2224
LOGGER = logging.getLogger(__name__)
2325

2426

@@ -97,7 +99,8 @@ async def test_age_over_18_with_birth_date(
9799
"org.iso.18013.5.1": {
98100
"given_name": "Alice",
99101
"family_name": "Smith",
100-
"birth_date": birth_date,
102+
**MDL_MANDATORY_FIELDS,
103+
"birth_date": birth_date, # override with computed age-based date
101104
"age_over_18": True,
102105
"age_over_21": True,
103106
}

oid4vc/mso_mdoc/cred_processor.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from oid4vc.pop_result import PopResult
2828

2929
from .key_generation import generate_self_signed_certificate, pem_from_jwk, pem_to_jwk # noqa: F401
30-
from .mdoc.issuer import isomdl_mdoc_sign
30+
from .mdoc.issuer import MDL_MANDATORY_FIELDS, isomdl_mdoc_sign
3131
from .mdoc.cred_verifier import MsoMdocCredVerifier
3232
from .mdoc.pres_verifier import MsoMdocPresVerifier
3333
from .mdoc.trust_store import WalletTrustStore
@@ -524,6 +524,26 @@ def validate_credential_subject(self, supported: SupportedCredential, subject: d
524524
if not isinstance(subject, dict):
525525
raise CredProcessorError("Credential subject must be a dictionary")
526526

527+
# For mDL doctypes, validate mandatory ISO 18013-5 fields early so
528+
# that the API caller gets an actionable error at exchange-creation
529+
# time rather than an opaque FFI error at issuance time.
530+
doctype = (supported.format_data or {}).get("doctype", "")
531+
if doctype == "org.iso.18013.5.1.mDL":
532+
# The subject may be namespace-wrapped or flat.
533+
claims = subject.get("org.iso.18013.5.1", subject)
534+
# driving_privileges defaults to [] at issuance time, so
535+
# exclude it from the early check.
536+
missing = [
537+
f
538+
for f in MDL_MANDATORY_FIELDS
539+
if f != "driving_privileges" and f not in claims
540+
]
541+
if missing:
542+
raise CredProcessorError(
543+
f"mDL credential_subject is missing mandatory ISO 18013-5 "
544+
f"data element(s): {', '.join(missing)}"
545+
)
546+
527547
return True
528548

529549
def validate_supported_credential(self, supported: SupportedCredential):

oid4vc/mso_mdoc/mdoc/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""MDoc module."""
22

3-
from .issuer import isomdl_mdoc_sign, parse_mdoc
3+
from .issuer import MDL_MANDATORY_FIELDS, isomdl_mdoc_sign, parse_mdoc
44
from .mdoc_verify import MdocVerifyResult, mdoc_verify
55
from .utils import extract_signing_cert, flatten_trust_anchors, split_pem_chain
66

77
__all__ = [
8+
"MDL_MANDATORY_FIELDS",
89
"isomdl_mdoc_sign",
910
"parse_mdoc",
1011
"mdoc_verify",

oid4vc/mso_mdoc/mdoc/issuer.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@
3939
LOGGER = logging.getLogger(__name__)
4040

4141

42+
# ISO 18013-5 mandatory data elements for the org.iso.18013.5.1 namespace.
43+
# These are non-Option fields in the upstream isomdl OrgIso1801351 struct;
44+
# omitting any of them causes a GeneralConstructionError from the Rust FFI.
45+
MDL_MANDATORY_FIELDS = (
46+
"family_name",
47+
"given_name",
48+
"birth_date",
49+
"issue_date",
50+
"expiry_date",
51+
"issuing_country",
52+
"issuing_authority",
53+
"document_number",
54+
"portrait",
55+
"driving_privileges",
56+
"un_distinguishing_sign",
57+
)
58+
59+
4260
def _prepare_mdl_namespaces(
4361
payload: Mapping[str, Any],
4462
) -> tuple[str, Optional[str]]:
@@ -51,6 +69,9 @@ def _prepare_mdl_namespaces(
5169
Tuple of (mdl_items_json, aamva_items_json) where aamva_items_json
5270
may be None. Both are JSON-serialized dicts; isomdl-uniffi handles
5371
CBOR encoding internally.
72+
73+
Raises:
74+
ValueError: If any ISO 18013-5 mandatory data element is missing.
5475
"""
5576
mdl_payload = payload.get("org.iso.18013.5.1", payload)
5677
mdl_items = {k: v for k, v in mdl_payload.items() if k != "org.iso.18013.5.1.aamva"}
@@ -60,6 +81,16 @@ def _prepare_mdl_namespaces(
6081
# the field don't hit a GeneralConstructionError.
6182
mdl_items.setdefault("driving_privileges", [])
6283

84+
# Validate mandatory fields before calling into the Rust FFI so that
85+
# callers get a clear error message instead of an opaque
86+
# GeneralConstructionError.
87+
missing = [f for f in MDL_MANDATORY_FIELDS if f not in mdl_items]
88+
if missing:
89+
raise ValueError(
90+
f"mDL credential_subject is missing mandatory ISO 18013-5 "
91+
f"data element(s): {', '.join(missing)}"
92+
)
93+
6394
aamva_payload = payload.get("org.iso.18013.5.1.aamva")
6495
aamva_items_json = json.dumps(aamva_payload) if aamva_payload else None
6596

0 commit comments

Comments
 (0)