ISO 18013-7 Annex B presentations

The Verifier Service can request an ISO/IEC 18013-7 Annex B presentation over OpenID4VP draft 18. The request profile is ISO_18013_7_MDOC. This is not an earlier OpenID4VP draft.

This profile belongs to the legacy Verifier Service, which stays off unless verifier-draft-routes is enabled. It is not part of Verifier2.

The supported subset is:

  • Encrypted mdoc responses (direct_post.jwt, ECDH-ES and A256GCM)
  • ES256 reader and issuer signatures
  • P-256 device signatures, or device MAC authentication
  • Reader metadata at the origin root, /.well-known/oauth-client
  • A response_code on the same-device success return. Result reads with that code stay valid until the session expires
  • Several document types, with one document of each type

It does not claim full Annex B algorithm conformance. Confirm the wallet version, the credential algorithms, and the trust anchors with a live presentation before relying on it.

Reader domain and signing key

  1. Set the Verifier Service baseUrl to the public HTTPS origin the wallet will call, including any API path prefix. That host is the client_id. IP addresses are not supported.
  2. Provision an ES256 P-256 signing key in a KMS the verifier depends on. The certificate chain is leaf first, the leaf DNS SAN is that host, and the chain is valid. The verifier publishes the public key only. Signing stays in the KMS. The published kid is the key's public id, and the signed request uses that same id. An ordinary id is kept. An HTTP or HTTPS id, such as a vault URL, is not published: metadata and the request use the public-key thumbprint instead.
  3. Create a host alias as a new child of the verifier, not on the organization, the tenant, or the verifier target itself. The create call rejects a target that already exists, so POST /v1/{verifier}/host-alias-api/host-aliases/create fails and reader metadata is never published. Use a new child, for example POST /v1/{verifier}.reader-domain/host-alias-api/host-aliases/create with {"domain":"verify.example.com"}. This needs ADMIN_HOST_ALIAS_CREATE. The reader-key update in the next step stays on {verifier}.
  4. Approve the signer with PUT /v1/{verifier}/verifier-service-api/configuration/reader-keys (UPDATE_SERVICE_CONFIGURATION):
[
  {
    "jarSigningKeyId": "acme.kms.reader-signing-key",
    "x5Chain": ["<PEM reader leaf certificate>", "<PEM issuing CA certificate>"]
  }
]
  1. Route https://verify.example.com/.well-known/oauth-client to the Enterprise API at the origin root, even when the API otherwise has a path prefix.

That document advertises the reader signing key (use: sig) plus authorization_encrypted_response_alg ECDH-ES, authorization_encrypted_response_enc A256GCM, vp_formats.mso_mdoc.alg ["ES256"], and require_signed_request_object: true. The session encryption key is not here. It is inside each signed request, as client_metadata.jwks with use: enc. An unknown alias, or an alias that exists only on the organization, returns 404.

{
  "jwks": {
    "keys": [
      {
        "kty": "EC",
        "use": "sig",
        "crv": "P-256",
        "kid": "example-reader-signing-key",
        "x": "<signing-public-key-x>",
        "y": "<signing-public-key-y>"
      }
    ]
  },
  "authorization_encrypted_response_alg": "ECDH-ES",
  "authorization_encrypted_response_enc": "A256GCM",
  "vp_formats": { "mso_mdoc": { "alg": ["ES256"] } },
  "require_signed_request_object": true
}

Leave the verifier OpenID metadata name unset for this profile. A set name is published as client_name, and some wallets reject that optional field and stop before consent. Leave logoUri unset as well unless the wallet has been shown to accept logo_uri.

Publish a signing key before a session uses it. A key you remove stays in the metadata for 24 hours, the longest a new session can live, so leave the old KMS key available for that long. An empty signer list stops new sessions of this profile until a signer is approved.

Domain metadata does not replace a reader trust root the wallet was provisioned with. Credential-issuer roots are a separate trust input. At the ingress, set X-Forwarded-Host from the validated public host. The host lookup trusts that header.

Create a request

POST /v1/{target}/verifier-service-api/credentials/verify with Authorization and a successRedirectUri header. The callback is an absolute HTTP(S) URL. It must not contain userinfo or its own response_code. $id is replaced with the session target.

{
  "openid_profile": "ISO_18013_7_MDOC",
  "expires_in_seconds": 300,
  "request_credentials": [
    {
      "input_descriptor": {
        "id": "org.iso.18013.5.1.mDL",
        "format": { "mso_mdoc": { "alg": ["ES256"] } },
        "constraints": {
          "limit_disclosure": "required",
          "fields": [
            {
              "path": ["$['org.iso.18013.5.1']['family_name']"],
              "intent_to_retain": false
            }
          ]
        }
      }
    }
  ],
  "trusted_root_cas": ["<PEM credential issuer trust root>"],
  "x509SanDnsConfig": {
    "jarSigningKeyId": "acme.kms.reader-signing-key",
    "x5Chain": ["<PEM reader leaf certificate>", "<PEM issuing CA certificate>"]
  }
}

The profile defaults to direct_post.jwt and five minutes. A lifetime above 24 hours is rejected. Name each namespace element the verifier needs and set intent_to_retain. A doctype-only request is not accepted for this profile. Other profiles keep their existing defaults.

The returned mdoc-openid4vp:// URL carries client_id, client_id_scheme=x509_san_dns, and a request_uri. Fetching that URI returns application/oauth-authz-req+jwt, signed with the approved key and its x5c chain.

The wallet posts a form field response to the request's response_uri. That URL is the session verify endpoint, under verifier-service-api/openid4vc/verify. The field contains the encrypted JWE. The protected header has to match the request on kid, the encryption algorithms, and apv:

  • kid selects the session encryption key advertised in the request (use: enc).
  • epk is the wallet's own ephemeral public key. It must be a public key on the advertised curve. It is not a copy of the request's encryption key.
  • apv is the request nonce.
  • apu on the wire is the unpadded base64url encoding of the UTF-8 wallet nonce. A value that is not that encoding, or that does not decode as text, is rejected.

The payload carries the same state, one vp_token string, and presentation_submission, or an encrypted wallet error. Mixed encrypted and plain fields are rejected. The body limit is 4 MiB.

Same-device result

On success the response endpoint returns HTTP 200, Cache-Control: no-store, and a JSON redirect_uri. That URI is the configured callback plus a fresh response_code. The code is stored with the session. It is not included in callbacks or in the public session object.

Read the code from the browser redirect and send it as the responseCode header on GET /v1/{session-target}/verifier-service-api/credentials/sessions/view. A successful session of this profile requires the matching code, and the session must not be expired. A missing or wrong code is rejected. The same matching code can be sent again until the session expires. The verifier does not consume it. Finish the browser transaction once, and invalidate the application's own login or session nonce after accepting the result. Do not choose the session from an untrusted query parameter on its own.

Callbacks

Status callbacks run after the session is stored. The JSON adds:

FieldMeaning
eventcredential_policy_results_available
presented_credentialsThe same shape as credentials/sessions/presented-credentials/view, when verification succeeded and the token was kept. If that view cannot be built, the callback still reports the result and this field is null.

The callback does not include response_code. Delivery is best effort. If it does not arrive, read the session endpoint and send the browser's response_code for a successful new session of this profile.

Persistence

This profile stores the session encryption key and the response_code in a private verifier_session_state collection. Include that collection in backup and restore, next to the organization tree. The record expires with the session. Completing the session erases the encryption key. The public session response stays in its existing shape.

What is verified

Each document is checked for issuer trust, the issuer signature, digests, validity, the holder signature or MAC, and the requested fields. Extra fields on the Mobile Security Object can be skipped while decoding. The issuer signature still covers the original payload, including those fields. Signature, holder binding, validity, and request fulfillment are reported as separate policy outcomes.

This documents walt-id/waltid-identity-enterprise#791 (WAL-1566).

Last updated on November 4, 2026