Skip to content

Issuance Flow

After registration, the user has a did:jwk DID bound to a FIDO authenticator. Issuance is the process of receiving a credential whose subject is that DID — proving control of the DID at the moment of issuance by signing a Verifiable Presentation with the bound FIDO key.

Credential Issuance Flow with FIDO-signed proof of possession

For FIDO4VC issuance to work, the credential issuer must support two things:

  1. ldp_vp proof type. The OpenID4VCI spec lets the issuer advertise which proof type the wallet should use to prove DID control. ldp_vp (Linked Data Proof, Verifiable Presentation) is the JSON-LD proof format — needed because the FIDO assertion is wrapped as a W3C Data Integrity proof.
  2. The fido4vc-jcs-2026 cryptosuite. The issuer must recognize the cryptosuite name and have an implementation that can verify a signature in this format. In our reference deployment, that’s done by walt.id’s signature_ld-vp policy which delegates to the verifier sidecar over HTTP.

The issuer advertises this in its credential issuer metadata:

proof_types_supported = {
ldp_vp = {
proof_signing_alg_values_supported = ["fido4vc-jcs-2026"]
}
}

The flow has three logical phases, mapping to the regions in the sequence diagram.

Phase 1 — Credential Offer (steps 1–4)

Section titled “Phase 1 — Credential Offer (steps 1–4)”

Standard OpenID4VCI Pre-Authorized Code flow.

  1. The user provides whatever the issuer requires (identity verification, etc.) and the issuer creates a Credential Offer.
  2. The offer is sent to the wallet (typically via a URL pasted into the Wallet UI, or a QR scan).
  3. The user selects which DID to use as the credential subject.
  4. The wallet fetches the issuer’s credential metadata, confirms ldp_vp + fido4vc-jcs-2026 are supported, and exchanges the pre-authorized code for an access token.

Phase 2 — Proof Preparation and Signing (steps 4.1–4.13)

Section titled “Phase 2 — Proof Preparation and Signing (steps 4.1–4.13)”

This is the FIDO4VC-specific part. The wallet must produce a signed VP that proves control of the DID, but the private key lives on the user’s device — so signing must be externalized.

  1. Prepare unsigned VP. The wallet backend constructs a ldp_vp proof structure (type, cryptosuite, verificationMethod, proofPurpose, challenge, domain, created) but leaves proofValue empty.
  2. Canonicalize. The unsigned VP is JCS-canonicalized (RFC 8785), producing a deterministic byte sequence. SHA-256 is computed over JCS(VP_without_proof) ‖ JCS(proof_options). This hash becomes the WebAuthn challenge.
  3. Request FIDO assertion. The FIDO Middleware calls navigator.credentials.get() via WebAuthn options containing this challenge and the user’s registered credential ID.
  4. User authenticates. The user is prompted (biometric or PIN). The authenticator signs authenticatorData ‖ SHA-256(clientDataJSON), where clientDataJSON embeds the challenge.
  5. Assemble proof. The middleware folds the WebAuthn outputs into the proof’s proofValue:
    "proofValue": {
    "signature": "<base64url DER ECDSA signature>",
    "authenticatorData": "<base64url>",
    "clientData": "<base64url clientDataJSON>"
    }
  6. Return signed VP to wallet backend. The wallet now has a complete, verifiable VP demonstrating DID control.

Standard OpenID4VCI again. The wallet submits the credential request with the signed VP as the proof of possession. The issuer:

  1. Verifies the VP using the fido4vc-jcs-2026 cryptosuite — recomputes the JCS hash, parses clientDataJSON, asserts the embedded challenge matches the hash, resolves the DID to its JWK, and verifies the FIDO signature.
  2. On success, issues the Verifiable Credential with the user’s DID as the credential subject.
  3. The wallet stores the credential.

Signing is externalized; everything else is standard. The wallet doesn’t gain new responsibilities. The issuer doesn’t change its issuance protocol. The OpenID4VCI flow is unmodified. The only new piece is the cryptosuite definition for how a FIDO assertion becomes a Data Integrity proofValue.

The challenge is the document hash. Standard WebAuthn challenges are random nonces. In FIDO4VC, the challenge equals SHA-256 of the canonicalized VP. This binds the FIDO assertion to this specific document — a verifier doesn’t need to trust the relying party’s challenge generator; the challenge is structurally derived from what the user signed over.

The proof value contains three artifacts, not one. Standard ECDSA Data Integrity proofs put a single signature byte string in proofValue. FIDO4VC’s proofValue is a structured object containing signature, authenticatorData, and clientData. Verifiers parse it as WebAuthn output, not raw ECDSA. See the spec.

A few specific failure cases worth understanding:

  • Issuer doesn’t advertise ldp_vp + fido4vc-jcs-2026. Wallet rejects the offer before involving the FIDO authenticator. The user sees a clear error: “this issuer doesn’t support FIDO-based proofs.”
  • User cancels FIDO prompt. The flow aborts cleanly; no partial state on the issuer side because the credential request hasn’t been submitted yet.
  • Canonicalization mismatch between signer and verifier. Catastrophic — the issuer rejects the VP because the recomputed challenge doesn’t match. Almost always caused by non-RFC-8785-compliant JCS implementations. Both reference implementations use Erdtman’s library, which is the de-facto reference.
  • Time skew between issuance and verification. The proof embeds a created timestamp; if the issuer enforces a freshness window, retries might fail. Wallet implementations should regenerate the timestamp on retry, not reuse the original.

The credential is now in the wallet. To use it, the holder must present it to a verifier.