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.
Anchors are deployment-wide: they decide which issuers this verifier accepts at all. A single
verification query can narrow that further
with a trusted_authorities entry, accepting only credentials issued under a named certificate authority. That is
a filter on top of the anchors, not a substitute for them.
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.
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 ) )
Deriving the identifier this way is what makes the check possible at all: because the client_id is a hash of
the certificate, a verifier cannot claim an identity it does not hold the key for. A wallet that wants to limit
which verifiers it will answer therefore can — it 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.
Whether a wallet does so is its own decision. OID4VP leaves it to the wallet, so this is a capability wallets may use rather than a step a verifier can require, and wallets differ. The Partisia ID Wallet accepts any verifier; a wallet built for a closed ecosystem may instead refuse every verifier outside it.
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. 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.