Skip to content

v1.24.0

Choose a tag to compare

@luisgf luisgf released this 29 Jul 13:21
· 15 commits to main since this release

One additive feature on 1.23.1, and one behaviour tightening that ships with it:
OpenID4VCI key resolution now sees the proof it is resolving for, and key attestations
are parsed and bound — never trusted.

The attested-key form

ResolveProofKey saw only the kid, which is blind exactly where the EU ecosystem
lives. A wallet sends {typ, alg, kid, key_attestation} and the key that signed the
proof is inside the header — one of the attestation's attested_keys. A callback
holding only the kid cannot reach it, so a consumer had to base64url-decode the proof
header itself before calling in: openvc decoding a header once and the caller decoding it
again, two implementations of "what is a header", one of them free to drift.

from openvc.openid4vci import peek_key_attestation, verify_credential_request_proofs

proofs = verify_credential_request_proofs(
    body, credential_issuer=CREDENTIAL_ISSUER, check_nonce=store.consume,
    # this ecosystem reads `kid` as a position in attested_keys; yours may differ
    resolve_proof_key_in_context=lambda ctx: ctx.key_attestation.attested_keys[int(ctx.kid)])

attested = peek_key_attestation(proofs[0].key_attestation)   # UNVERIFIED
if "iso_18045_high" not in attested.key_storage:             # your policy, your call
    raise PermissionError("this credential needs high-assurance key storage")

New public surface: resolve_proof_key_in_context= taking the frozen ProofKeyContext
(kid, alg, a read-only header, the parsed key_attestation, credential_issuer,
index); peek_key_attestationUnverifiedKeyAttestation; peek_proof_header;
KEY_ATTESTATION_TYP; MAX_KEY_ATTESTATION_BYTES.

ResolveProofKey is unchanged and not deprecated. Existing resolvers keep working
untouched — the new resolver is a separate keyword taking a context object, which grows
without ever breaking them again. Pass one or the other, never both: a precedence between
two key resolvers is the same silent-preference defect the exactly-one-key-parameter rule
exists to prevent.

Which key a kid names stays yours. OpenID4VCI fixes no rule for it: the spec's own
example uses an index, wallets also use each JWK's kid member or an RFC 7638 thumbprint.
openvc hands over the material rather than guessing between three conventions and picking
the wrong key out of a list the attacker supplied.

Behaviour change: the App. D binding

A proof carrying key_attestation must now be signed by one of its attested_keys
(App. D's MUST, compared by RFC 7638 thumbprint, on every key source), and a malformed
attestation rejects the proof before any crypto rather than being read at the end.
Previously the header was captured verbatim and never looked at.

This check stops no attacker. Whoever forges a proof also chooses its
key_attestation, whose signature nothing here verifies, so they attest their own key.
It catches an honest wallet — or your own resolver — handing over a key the wallet never
claimed, which would otherwise mint a credential bound to the wrong key and verify
cleanly. It can only reject, never accept, which is the only reason an unverified blob may
drive it at all. Believing an attestation — its signature, its wallet-provider anchor, its
assurance levels — remains yours (ADR-0007 D9, amended by a 1.24.0 addendum).

Upgrading: if your wallets send key_attestation today, check that the key your
deployment resolves is actually listed in it. Previously accepted, now rejected: an
attestation that is not a parseable JWS or whose attested_keys is missing, empty, or not
an array of JWK objects; and x5c / kid deployments whose attestation does not carry
the key the chain or the registry produced. One case is worth knowing about because it
will not show up in testing: jwk_thumbprint digests the coordinate strings as given, so
a JWK with non-fixed-width x/y — non-conformant per RFC 7518 §6.2.1.2, roughly 1 in
256 per coordinate — thumbprints differently from the same key encoded correctly. The
rejection message names that cause.

No key proof that used to be rejected is now accepted.

Also

  • Vectors for App. D: the spec's key attestation and the proof that indexes it. Both are
    printed decoded in the spec and the proof's attestation is a placeholder, so what is
    third-party is the shape; the fixture's provenance says exactly that. Still missing, and
    recorded as such: an attestation from a real wallet provider.
  • examples/13_oid4vci_key_attestation.py, the whole flow end to end, executed by CI.
  • Guide, ADR-0007 addendum, threat-model I19 and Security-Model updated — each worded to
    what the binding actually buys.

Full changelog: https://github.com/luisgf/openvc/blob/main/CHANGELOG.md