Skip to content

Signing Certificates

Both the issuer and the verifier sign what they send, and both carry their certificate chain in the signature so the receiving party can check it. This is how trust is established, so signing is a required setting: neither service will start without it.

The signing block

The two services take the same block:

"signing": {
  "keyFile": "conf/signing-key/verifier-localhost.key", // (1)!
  "certificateFiles": [ // (2)!
    "conf/signing-key/verifier-localhost.pem"
  ]
}
  1. Path to the PEM-encoded EC private key used to sign. Required.
  2. The certificate chain, ordered leaf first. Required. These are emitted verbatim as the JOSE x5c header of everything the service signs.

Paths are resolved relative to the container's working directory, /app. In the quickstart layout the host's ./verifier/conf is mounted at /app/conf, so conf/signing-key/… refers to a file you put in verifier/conf/signing-key/ on the host.

Order matters in certificateFiles

The list is a chain, not a set. The leaf — the certificate matching keyFile — comes first, then each issuing certificate in turn. A chain in the wrong order will be rejected by the party validating it, and nothing validates the order at startup.

Generating a pair for local use

For local testing, a self-signed EC P-256 pair per service is enough:

for b in issuer verifier; do
  mkdir -p "$b/conf/signing-key"
  openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
    -keyout "$b/conf/signing-key/$b-localhost.key" \
    -out    "$b/conf/signing-key/$b-localhost.pem" \
    -days 365 -subj "/CN=localhost" -sha256
done

Give each service its own pair. Sharing one between the issuer and the verifier makes their identities indistinguishable to a wallet, since both derive their identity from the certificate.

What a self-signed certificate can and cannot do

A self-signed certificate proves possession of a key. It does not connect that key to anything a third party already trusts, which has different consequences on each side:

  • The verifier. Its OID4VP client_id is x509_hash: followed by a hash of its leaf certificate, and a wallet validates the request's chain against its own trusted roots before showing the consent screen. A wallet you control can be told to accept your self-signed certificate; wallets you do not control will refuse the request. See Trust model.
  • The issuer. Credentials it signs carry its chain. A verifier that performs path validation — which is the case for mdoc credentials — accepts them only if the chain reaches one of that verifier's configured trust anchors.

So a self-signed pair is right for local development and for a closed setup where you configure both ends. Facing wallets or verifiers you do not operate, you need a certificate issued by an authority they already recognize. See Going to production.

Rotation

Certificates expire, and the pair generated above is valid for 365 days.

Rotating the verifier's certificate changes its client_id, because the identifier is derived from the certificate. Any wallet or configuration that pinned the old value has to be updated.

Rotating the issuer's certificate does not invalidate credentials already issued: each credential carries the chain that was current when it was signed, and that chain remains verifiable until its own certificates expire. To keep older credentials verifiable across a rotation, keep the retiring certificate available to whoever validates paths rather than removing it the moment the new one is in place.

Plan rotation before the expiry date. Neither service warns you that a certificate is close to expiring.

What's Next