Skip to content

Trust Model

A verifier has to answer one question: did this credential really come from an issuer I am willing to believe?

It answers it from the credential itself. Credentials are signed, and the signature carries the issuer's X.509 certificate chain in the JOSE x5c header. The verifier reads the chain out of the credential, so it needs no network lookup and no contact with the issuer at verification time.

How an issuer is resolved

When the verifier receives a presentation, it works out which public key to check the issuer's signature against, in this order:

  1. An x5c header is present. The verifier parses the certificate chain and takes the public key from the leaf certificate. This is the normal path, and it is what our issuer produces.
  2. No x5c, but the iss claim is a did:jwk: identifier. The public key is encoded in the identifier itself, so the verifier decodes it directly. Some third-party issuers use this.

With neither, the verifier cannot resolve the issuer and verification fails.

What gets validated

A signature that checks out only tells you the credential was signed by whoever holds that certificate's private key. It does not tell you that you recognize them. Establishing that second thing is what trust anchors are for, and whether the verifier does it depends on whether you have configured any.

With trust anchors configured, a credential presenting a certificate chain gets PKIX path validation: the verifier builds a path from the presented leaf certificate up to one of the roots in issuerTrustAnchors and rejects the credential if no valid path exists. This applies to SD-JWT and mdoc alike.

With no trust anchors configured, that step is skipped. The signature is still checked, so the credential is verified as internally consistent and untampered — but nothing establishes that its issuer is one you accept.

No anchors means no issuer check

A verifier with an unset or empty issuerTrustAnchors accepts any well-formed credential whose signature matches the certificate it presents — including a self-signed certificate generated by anyone. Configure anchors before facing real holders. The service logs a warning at startup when it is in this mode.

Either configure anchors, or check the issuer identity in the verification result against your own list of accepted issuers before acting on the claims. See Issuer trust anchors and Receive verified data.

One limit applies when validation does run: chains are limited to 10 certificates, and a longer x5c chain is rejected before validation begins.

Validity periods are checked, so an expired certificate anywhere in the path fails it.

A credential presenting no chain at all is a third case: there is nothing to validate a path against, so trust rests entirely on the key resolved above. That is the did:jwk: path, where the key is the identifier. Anchors are not consulted, and deciding whether to trust that identifier is yours.

The verifier's own identity

Trust runs in both directions. Before a wallet hands over a presentation, it may want to know which verifier is asking.

The verifier signs its authorization requests with its own key and certificate, and derives its OID4VP client_id from that certificate:

client_id = "x509_hash:" + base64url( sha256( DER-encoded leaf certificate ) )

Checking this is the wallet's choice, not something the verifier can require: OID4VP leaves it to the wallet whether to establish who the verifier is, and wallets differ. A wallet that does check validates the certificate chain in the request's x5c header against its own trusted roots, verifies the request signature, and confirms that the hash in client_id matches the certificate it just validated. Because the identifier is a hash of the certificate, a verifier cannot claim an identity it does not hold the key for.

So a verifier presenting a self-signed certificate is accepted by wallets that do not check, and by wallets configured to accept that certificate — and refused by the rest. A self-signed pair is what the quickstart generates, and it is fine for local testing against a wallet you control. For a deployment facing wallets you do not operate, your certificate needs to chain to a root they already trust. See Going to production.

Configuring your own keys and certificates

Both services take the same signing block: a private key file and an ordered, leaf-first list of certificate files, which become the x5c header they emit.

"signing": {
  "keyFile": "conf/signing-key/verifier-localhost.key",
  "certificateFiles": ["conf/signing-key/verifier-localhost.pem"]
}

See Signing certificates for generating these and for what changes when you move off self-signed certificates.

What's Next