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:
- An
x5cheader 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. - No
x5c, but theissclaim is adid: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
- Signing certificates: Generate and configure your own pair.
- Issuer trust anchors: Decide which issuers your verifier accepts.
- OID4VP presentation protocol: The wire-level detail of the request signing and validation described above.