Skip to content

Typed elements: bstr, full-date and tdate from the claims schema (value_type) - #41

Draft
scott410md wants to merge 2 commits into
mainfrom
feat/typed-element-values
Draft

scott410md wants to merge 2 commits into
mainfrom
feat/typed-element-values

Conversation

@scott410md

@scott410md scott410md commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

What it does

For generic (non-mDL) mso_mdoc doctypes, every element value travels to isomdl-uniffi as JSON. As a result, an element that ISO 18013-5 defines as a byte string or a tagged date was written as CBOR text.

With this change, a credential configuration can name the ISO 18013-5 data type of an element in its claims descriptor:

"claims": {"<namespace>": {"<element>": {"value_type": "bstr"}}}

The OID4VCI 1.0 list form, [{"path": ["<namespace>", "<element>"], "value_type": "..."}], is also read.

At issuance the issuer checks the input and passes the element to isomdl-uniffi as a typed element envelope. isomdl-uniffi then writes the CBOR type. The envelope is:

{"$isomdl": "<type>", "value": "<text>"}

Elements without a value_type are unchanged, and so are mDL (org.iso.18013.5.1.mDL) credentials, whose element types come from the namespace definition.

Only bstr, full-date and tdate select a typed element. Any other value_type (configurations already use it as claim metadata, for example boolean or string) is left alone, and the element is converted as before.

value_type values

value_type input expected CBOR written
bstr base64 or base64url text, padded or not byte string
full-date YYYY-MM-DD, a calendar-valid date tag 1004 over the text
tdate YYYY-MM-DDTHH:MM:SSZ, UTC, no fractional seconds tag 0 over the text

Errors

Each check happens as early as it can:

  • Exchange creation refuses a subject whose typed element input does not match its type, so no offer is made for a credential that could not be issued.
  • Issuance checks the input again before signing.

Errors are raised as CredProcessorError and name the element and the type, never the value, for example: claim '<element>': tdate value is not a valid YYYY-MM-DDTHH:MM:SSZ date-time.

value_type is stored with the configuration but is not added to the issuer metadata served to wallets.

Dependency on the library change

  • This needs the isomdl-uniffi change "Typed elements: bstr, full-date and tdate from the claims schema (value_type)".
  • No FFI signature changes in either repository. The plugin calls the same Mdoc.create_and_sign as before.
  • Guard against an older library: one without typed-element support would write the envelope as an ordinary CBOR map. After signing, the issuer reads the element values back. If a typed element came back as a map holding $isomdl, the issue fails with installed isomdl-uniffi does not support typed elements (element '<element>' was written as a map), so no mdoc with a wrong element type is issued.
  • Configurations without value_type do not depend on the library change.

Files

  • oid4vc/mso_mdoc/mdoc/typed_values.py (new): reads value_type from the claims, checks inputs and builds the envelope.
  • oid4vc/mso_mdoc/mdoc/issuer.py: wraps typed elements for generic doctypes and adds the guard above.
  • oid4vc/mso_mdoc/cred_processor.py: the checks at exchange creation and issuance.
  • oid4vc/mso_mdoc/tests/test_typed_values.py (new): 44 tests.

Tests

The new tests cover:

  • reading value_type from both claims forms;
  • other value_type values (for example boolean, string) left alone at registration, exchange creation and issuance, including an mDL configuration with value_type: boolean;
  • accepting and refusing inputs for each type, with error messages checked not to contain the value;
  • envelope wrapping, and untyped elements left unchanged;
  • the exchange-creation hook;
  • value_type kept out of issuer metadata;
  • real signing with a typed-capable isomdl-uniffi, where the elements read back as a byte string, tag 1004 and tag 0;
  • the guard failing an issue when the library writes the envelope as a map.

Results on this branch, with isomdl-uniffi built from the library change: pytest mso_mdoc -q gives 149 passed (44 of them in test_typed_values.py), 0 failed.

Depends on: Indicio-tech/isomdl-uniffi#37

…ue_type)

A claims descriptor in a credential configuration can name the ISO 18013-5
data type of an element with value_type: bstr, full-date or tdate. The
issuer checks the input and passes it to isomdl-uniffi as a typed element
envelope, which writes a byte string, tag 1004 or tag 0. Elements without
a value_type are unchanged. Unknown types are refused at registration;
bad inputs at exchange creation and at issuance, naming the element only.

Needs isomdl-uniffi with typed-element support. With an older library the
issue fails (the envelope would otherwise be written as a CBOR map).

Signed-off-by: Scott Harris <82886769+scott410md@users.noreply.github.com>
Claim descriptors already use value_type as claim metadata with other
values, for example "boolean" or "string". Only bstr, full-date and tdate
now select a typed element; any other value is ignored and the element is
converted as before, so those configurations register and issue as they
did. A malformed input for one of the three types is still refused,
naming the element only.

Signed-off-by: Scott Harris <82886769+scott410md@users.noreply.github.com>
@scott410md
scott410md force-pushed the feat/typed-element-values branch from e1d83ec to 36e3d6c Compare October 7, 2026 21:07
@KimEbert42

Copy link
Copy Markdown

Indicio Team Review: PR #41 — Typed elements (bstr, full-date, tdate) for mso_mdoc

Reviewed by a virtual team assembled from Indicio's engineering playbooks: Backend Engineer, Architect, Security Engineer, Tech Lead, and QA Test Engineer, each reviewing the full diff independently.

Summary

This PR fixes a real ISO 18013-5 spec-compliance gap: generic mso_mdoc doctypes currently write every element as CBOR text, even elements the spec defines as byte strings or tagged dates (e.g., portrait images, issue dates). The fix lets a credential configuration tag an element with value_type: bstr|full-date|tdate, validates the input twice (at exchange-creation and again at issuance), wraps it in an envelope the companion Rust library (isomdl-uniffi, a separate, as-yet-unmerged PR) recognizes, and adds a post-signing guard against a stale library silently mis-encoding the data. It's unusually well tested (44 new tests) and disciplined about never leaking claim values into error messages or logs.

All five reviewers converged on the same verdict: good design, not yet mergeable as-is. Two independent reviewers (Security Engineer and QA) found the same functional bug by actually running the code, and Backend Engineer found a separate, more serious crash risk in the one code path the test suite doesn't really exercise yet (because the companion library isn't merged). None of this requires rethinking the design — it's tightening before ship.

Findings by role

Backend Engineer

Blocking: _check_typed_support in issuer.py checks TYPED_ELEMENT_KEY in element.value. If a working isomdl-uniffi returns a bstr element's value as Python bytes (the natural PyO3 mapping for a CBOR byte string), "$isomdl" in b"..." raises TypeError, which gets swallowed by a broad except Exception and reported as a generic signing failure — on the PR's own primary happy path. The mocked guard test only exercises element.value as a JSON string, never as bytes, so this is untested today and will likely only surface once the companion library actually ships. Fix: isinstance(element.value, Mapping) and TYPED_ELEMENT_KEY in element.value.

Also flagged: no version pin on the unmerged companion dependency; silent no-op on near-miss value_type typos (e.g. "Bstr") with no warning logged; status-list slot allocated before the typed-value check that can still fail at issuance; _is_base64 reimplements what base64.b64decode(validate=True) already gives for free; no README update for the new value_type config key.

Architect

Confirmed via pyproject.toml that isomdl-uniffi is installed from GitHub wheels, not a poetry-managed dependency — there is no packaging-level way to express "requires isomdl-uniffi ≥ X" today, which is the structural reason the post-signing guard exists at all. The guard is a sound stopgap but only detects one failure mode (envelope mistaken for a map); it wouldn't catch a library that wires the envelope correctly but writes the wrong tag. Flagged the value_type field overload (ISO-type selector vs. pre-existing free-form claim metadata) as a collision risk worth documenting as reserved values. Recommended, as a backlog item rather than for this PR, moving type-awareness into the FFI contract directly rather than smuggling it through a JSON envelope.

Security Engineer

Traced the full call graph and confirmed defense-in-depth actually holds: issue() (the public, wallet-facing path) doesn't call check_typed_payload directly, but every typed element is still unconditionally checked inside wrap_typed_value before signing — no bypass path found. Confirmed no ReDoS in the base64/date regexes. Found no value leakage in logs or error paths from the new code itself, but flagged a pre-existing adjacent risk this PR newly activates: unrelated LOGGER.debug lines elsewhere already log the full signed mdoc and full exchange record — now routinely containing real portrait bytes and DOB once bstr support is in use. Medium finding: no per-field size cap on bstr values before they're pushed through decode → CBOR encode → signing; a ~1MB portrait is already feasible under the existing global HTTP body cap and burns a real signing operation.

Tech Lead

Verified (by grepping every credential_subject= construction site) that issue() never runs today without validate_credential_subject having already run on the same payload — so the double validation is real defense-in-depth, not redundancy, but nothing in the code says so, and a future engineer is likely to "simplify" it away once a deferred-issuance or admin-edit path exists. Asked for a one-line comment making that intent explicit. Also flagged that _check_typed_support does its work after a real signing operation has already run, wasting crypto work on every stale-library issuance, and that VALUE_TYPES should carry an inline comment on why it's deliberately non-exhaustive (the second commit fixed exactly this once already).

QA Test Engineer

Ran the code directly rather than just reading it, and confirmed a functional bug independently of Security Engineer: _is_base64('') returns True, so an empty string passes bstr validation and would be signed into the mdoc as a zero-length byte string — e.g. an empty portrait. full-date/tdate correctly reject empty string via their regexes; only bstr is affected, and no test catches it. Also found check_typed_payload has no direct unit test (only indirect coverage through the processor), and that defensive non-Mapping branches in value_types_for's list-form parser are never exercised by a test. Recommended an end-to-end test against a real ACA-Py instance plus a real wallet/mdoc-reader before shipping, and running explicitly against the currently-released (non-typed-element) isomdl-uniffi to confirm the guard fires in practice, not just in the mock.

Consensus

  • The design is sound and appropriately scoped (generic doctypes only, mDL untouched, backward-compatible with existing value_type reuse).
  • Error-message discipline (never leak the value) is genuinely good and consistently tested.
  • The real risk in this PR is the unmerged, unpinned cross-repo dependency on isomdl-uniffi — every reviewer independently flagged this as the structural root cause behind the version-skew guard's necessity and limitations.
  • Test suite is above-average for this kind of change, but has real, specific blind spots rather than general thinness.

Action items

# Action Priority
1 Fix _check_typed_support's in check to handle bytes/non-str element.value (currently crashes on the happy path for a working library); add a direct unit test with real bytes High
2 Fix or explicitly document the empty-string bstr bypass (_is_base64('') → True); add a regression test High
3 Do not merge/deploy until the companion isomdl-uniffi PR has landed; document the minimum required library version and verify it at runtime, not only reactively post-signing High
4 Add a per-element size cap on bstr values before decode/sign Medium
5 Add a one-line comment documenting the double-validation (exchange-creation + sign-time) as intentional defense-in-depth, not redundancy Medium
6 Add direct unit tests for check_typed_payload and for value_types_for's defensive non-Mapping branches Medium
7 Log a warning when value_type is present but unrecognized (typo protection) Medium
8 File a follow-up ticket on pre-existing LOGGER.debug lines that now routinely log portrait/DOB data once bstr is in active use Low
9 Document bstr/full-date/tdate as reserved value_type values in claim-descriptor docs/README Low

🤖 Generated with Claude Code

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