# X.509 certificates

> Create self-signed and CA-issued X.509 certificates and certificate signing requests with agent.x509, parse certificates, validate chains and check revocation.

mdoc issuers, SD-JWT VC issuers and OpenID4VP verifiers often sign with a key that's bound to an X.509 certificate instead of a DID. `agent.x509` creates certificates and certificate signing requests (CSRs), validates certificate chains and creates certificate revocation lists (CRLs). How a verifier decides which certificates to trust is on [Trusted issuers](https://credo.js.org/guides/next/dids-and-trust/trust.md).

In the examples, one agent plays the certificate authority (CA) and a second agent is a verifier. They use these imports:

```ts
import {
  Agent,
  CredoWebCrypto,
  X509Certificate,
  X509CertificateRevocationListEntryReason,
  X509ExtendedKeyUsage,
  X509KeyUsage,
  X509Module,
  X509RevocationCheckMode,
  X509ValidationError,
} from '@credo-ts/core'
import { PublicJwk } from '@credo-ts/core/kms'
import { agentDependencies } from '@credo-ts/node'
```

## Create a CA certificate

A CA signs the certificates of issuers or verifiers, and whoever checks those certificates trusts the root certificate of the CA. For mdoc issuers, this root is the issuing authority CA (IACA). Create a key in the [KMS](https://credo.js.org/guides/next/agent-setup/key-management.md) and a self-signed certificate for it:

```ts
const rootKey = await agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })
const rootPublicJwk = PublicJwk.fromPublicJwk(rootKey.publicJwk)

// Without a subjectPublicKey the certificate is self-signed
const rootCertificate = await agent.x509.createCertificate({
  authorityKey: rootPublicJwk,
  issuer: { commonName: 'Example Root CA', countryName: 'NL' },
  validity: { notBefore: new Date(), notAfter: new Date(Date.now() + 5 * oneYear) },
  extensions: {
    subjectKeyIdentifier: { include: true },
    keyUsage: { usages: [X509KeyUsage.KeyCertSign, X509KeyUsage.CrlSign], markAsCritical: true },
    basicConstraints: { ca: true, pathLenConstraint: 0, markAsCritical: true },
  },
})

const rootCertificatePem = rootCertificate.toString('pem')
```

Without `subjectPublicKey`, the `authorityKey` signs its own certificate and `issuer` is also the subject. Passing `subject` for a self-signed certificate throws. `pathLenConstraint: 0` lets this CA sign end-entity certificates, but no intermediate CAs.

`issuer` and `subject` take `commonName`, `countryName`, `stateOrProvinceName` and `organizationalUnit`. For other name attributes, pass a distinguished name string such as `'CN=Example Root CA, O=Example, C=NL'`.

## Issue a certificate

Sign a certificate for another key with the CA key. This one is an issuer certificate:

```ts
const issuerKey = await agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })

const issuerCertificate = await agent.x509.createCertificate({
  authorityKey: rootPublicJwk,
  subjectPublicKey: PublicJwk.fromPublicJwk(issuerKey.publicJwk),
  issuer: rootCertificate.subject,
  subject: { commonName: 'Example Issuer', countryName: 'NL' },
  validity: { notBefore: new Date(), notAfter: new Date(Date.now() + oneYear) },
  extensions: {
    authorityKeyIdentifier: { include: true },
    subjectKeyIdentifier: { include: true },
    keyUsage: { usages: [X509KeyUsage.DigitalSignature], markAsCritical: true },
    // Document signer usage, required for mdoc issuer certificates
    extendedKeyUsage: { usages: [X509ExtendedKeyUsage.MdlDs], markAsCritical: true },
    // An SD-JWT VC `iss` must match a SAN URI, or its host a SAN DNS name
    subjectAlternativeName: { name: [{ type: 'dns', value: 'issuer.example.com' }] },
  },
})

// The certificate keeps the KMS key id of the subject key, so Credo can sign with it
console.log(issuerCertificate.keyId === issuerKey.keyId)
```

Credo signs with `authorityKey`, so it must be a key in the agent's KMS. The certificate gets the key id of `subjectPublicKey`, which lets the agent that holds that key sign with the certificate.

The extensions and subject alternative names (SANs) depend on what the certificate is for:

| Certificate | What it needs |
| --- | --- |
| CA, or mdoc IACA | `basicConstraints` with `ca: true`, and a key usage with `KeyCertSign`, plus `CrlSign` if the CA signs CRLs. Credo rejects a CA certificate without `basicConstraints`, or with a key usage that lacks these. |
| mdoc issuer (document signer) | A country (`countryName`) in the subject, `DigitalSignature` key usage, and the `MdlDs` extended key usage. Credo checks the country but not the extended key usage, which ISO 18013-5 requires. See [mdoc](https://credo.js.org/guides/next/credential-formats/mdoc.md). |
| SD-JWT VC issuer | When the credential has an `iss`, a SAN URI equal to it, or a SAN DNS name equal to its host. Without `iss`, no SAN is needed. See [SD-JWT VC](https://credo.js.org/guides/next/credential-formats/sd-jwt-vc.md). |
| OpenID4VP verifier (access certificate) | With the `x509_san_dns` client id prefix (the default), the host of the verifier's `baseUrl` as a SAN DNS name. With `x509_hash`, no SAN is needed. See [Verifying credentials](https://credo.js.org/guides/next/protocols/openid4vc/verifier.md). |
| mdoc reader (DC API reader authentication) | No specific extensions: the wallet checks that the chain leads to a reader certificate it trusts. See [Digital Credentials API](https://credo.js.org/guides/next/protocols/dc-api/verifier.md#sign-the-requests). |

## Configure the X.509 module

`X509Module` is registered by default under the key `x509`. Register it yourself to pass options:

```ts
const verifierAgent = new Agent({
  dependencies: agentDependencies,
  modules: {
    ...storage, // Storage and key management, see Agent setup
    x509: new X509Module({
      trustedCertificates: [rootCertificatePem],
      revocationCheck: { mode: X509RevocationCheckMode.SoftFail },
    }),
  },
})
```

| Option | Description |
| --- | --- |
| `trustedCertificates` | PEM or base64 DER strings, or `X509Certificate` instances. Credo uses them as trust anchors when nothing else decides trust, see [Trusted issuers](https://credo.js.org/guides/next/dids-and-trust/trust.md). |
| `getTrustedCertificatesForVerification` | Deprecated. Use the agent's `getTrustedIssuersForVerification` callback instead. |
| `revocationCheck` | Enables CRL checking during chain validation. See [Revocation](#revocation). |
| `parseOptions` | Limits for parsing certificates. See [Parse limits](#parse-limits). |

To change the trusted certificates at runtime, use `agent.x509.config.addTrustedCertificate(certificate)` or `agent.x509.config.setTrustedCertificates(certificates)`.

## Certificate signing requests

When the key holder and the CA are different parties, the key holder sends a CSR. Here the verifier requests an access certificate:

```ts
// Verifier: create a key and a certificate signing request (CSR) for it
const verifierKey = await verifierAgent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })

const csr = await verifierAgent.x509.createCertificateSigningRequest({
  subjectPublicKey: PublicJwk.fromPublicJwk(verifierKey.publicJwk),
  subject: { commonName: 'Example Verifier', countryName: 'NL' },
  extensions: {
    // With the x509_san_dns client id prefix, the verifier's host must be a SAN DNS name
    subjectAlternativeName: { name: [{ type: 'dns', value: 'verifier.example.com' }] },
  },
})
const encodedCsr = csr.toString('pem')

// CA: parse the CSR and check its signature, which proves possession of the key
const receivedCsr = agent.x509.parseCertificateSigningRequest({ encodedCertificateSigningRequest: encodedCsr })
await receivedCsr.verify(new CredoWebCrypto(agent.context))

const accessCertificate = await agent.x509.createCertificate({
  authorityKey: rootPublicJwk,
  subjectPublicKey: receivedCsr.publicJwk,
  issuer: rootCertificate.subject,
  subject: receivedCsr.subject,
  validity: { notBefore: new Date(), notAfter: new Date(Date.now() + oneYear) },
  extensions: {
    authorityKeyIdentifier: { include: true },
    keyUsage: { usages: [X509KeyUsage.DigitalSignature], markAsCritical: true },
    subjectAlternativeName: { name: receivedCsr.sanDnsNames.map((value) => ({ type: 'dns', value })) },
  },
})
const accessCertificatePem = accessCertificate.toString('pem')
```

`parseCertificateSigningRequest` doesn't check the CSR signature. Call `verify` to make sure the requester holds the private key. The CA decides which extensions go into the certificate: copy only the values from the CSR that you've checked.

## Parse a certificate

`X509Certificate.fromEncodedCertificate` parses a PEM or base64 DER certificate. A parsed certificate has no key id, so set `keyId` to the KMS key id before you sign with it:

```ts
// Verifier: parse the certificate returned by the CA and link it to the KMS key
const verifierCertificate = X509Certificate.fromEncodedCertificate(accessCertificatePem)
verifierCertificate.keyId = verifierKey.keyId

console.log(verifierCertificate.subject, verifierCertificate.sanDnsNames)
```

Getters include `subject`, `issuer`, `sanDnsNames`, `sanUriNames`, `keyUsage`, `extendedKeyUsage`, `isCertificateAuthority`, `crlDistributionPoints` and `publicJwk`. Encode a certificate with `toString('pem')` or `toString('base64')`, and compare two with `equal`.

## Validate a certificate chain

```ts
const validatedChain = await verifierAgent.x509.validateCertificateChain({
  // Leaf first, as in an x5c header. The root may be left out when it is a trusted certificate.
  certificateChain: [issuerCertificate.toString('base64')],
  trustedCertificates: [rootCertificatePem],
})

// The chain starts at the trust anchor and ends with the leaf
console.log(
  validatedChain[0].equal(rootCertificate),
  validatedChain[validatedChain.length - 1].equal(issuerCertificate)
)
```

Pass the chain leaf first, as in an `x5c` header. The chain may leave out the root if it's in `trustedCertificates`. On success Credo returns the chain from the trust anchor to the leaf. On failure it throws an `X509ValidationError`, whose `validationResult.validations` shows which check failed.

Credo checks the signatures and validity periods, basic constraints, the `keyCertSign` key usage of CA certificates, path length constraints, critical extensions, and revocation when enabled. It doesn't check OCSP, name constraints or certificate policies, and it doesn't enforce extended key usage.

:::caution
Without `trustedCertificates`, Credo validates the chain but doesn't check that it ends at a certificate you trust. The module's `trustedCertificates` aren't used here.
:::

`allowNonRootTrustedCertificate` defaults to `true`: a trusted intermediate or leaf certificate acts as the trust anchor, and Credo doesn't verify its signature. ISO 18013-5 needs this, because mdoc issuers don't include the root in the chain. Set it to `false` to require that trusted certificates are verified up to a self-signed root. The default may change in a future version.

## Revocation

Credo checks revocation with CRLs, from the URLs in a certificate's `crlDistributionPoints`. Revocation checking is off unless you set `revocationCheck` on the module:

| Mode | Behaviour |
| --- | --- |
| `X509RevocationCheckMode.SoftFail` | Default when `revocationCheck` is set. Rejects revoked certificates and invalid CRLs, and skips the check when the CRL can't be downloaded. |
| `X509RevocationCheckMode.Require` | Also fails when the CRL can't be downloaded. |
| `X509RevocationCheckMode.Disabled` | No revocation checks. |

Certificates without a CRL distribution point pass the check. Credo checks every certificate in the chain except the root (`checkFullChain`), and caches a CRL for an hour (`crlCacheExpirySeconds`), but never past its `nextUpdate`.

To revoke certificates, create a CRL signed by the CA key and publish it at the distribution point URL:

```ts
const revokedKey = await agent.kms.createKey({ type: { kty: 'EC', crv: 'P-256' } })
const revokedCertificate = await agent.x509.createCertificate({
  authorityKey: rootPublicJwk,
  subjectPublicKey: PublicJwk.fromPublicJwk(revokedKey.publicJwk),
  issuer: rootCertificate.subject,
  subject: { commonName: 'Revoked Issuer', countryName: 'NL' },
  validity: { notBefore: new Date(), notAfter: new Date(Date.now() + oneYear) },
  extensions: {
    authorityKeyIdentifier: { include: true },
    keyUsage: { usages: [X509KeyUsage.DigitalSignature], markAsCritical: true },
    crlDistributionPoints: { urls: [crlUrl] },
  },
})

const crl = await agent.x509.createCertificateRevocationList({
  authorityKey: rootPublicJwk,
  issuer: rootCertificate.subject,
  validity: { nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) },
  entries: [{ certificate: revokedCertificate, reason: X509CertificateRevocationListEntryReason.KeyCompromise }],
  extensions: { crlNumber: { value: 1 }, authorityKeyIdentifier: { include: true } },
})

// Serve the DER encoded CRL at the URL in the certificate's crlDistributionPoints
const crlBytes = crl.rawCertificateRevocationList
```

Validating the revoked certificate now fails on the revocation check:

```ts
try {
  await verifierAgent.x509.validateCertificateChain({
    certificateChain: [revokedCertificate.toString('base64')],
    trustedCertificates: [rootCertificatePem],
  })
} catch (error) {
  if (!(error instanceof X509ValidationError)) throw error
  // The validations show which check failed, here revocationStatus
  console.log('Rejected:', error.message, error.validationResult.validations.revocationStatus?.isValid)
}
```

## Parse limits

Credo limits how much work parsing a certificate or CRL may take, as these often come from untrusted input. `X509ParseOptions` has three limits:

| Option | Default | Description |
| --- | --- | --- |
| `maxDepth` | 100 | Maximum ASN.1 nesting depth. |
| `maxNodes` | 1,500,000 | Maximum number of ASN.1 nodes. The default fits a CRL of 10 MB, the default download limit. |
| `maxContentLength` | 16 MB | Maximum size of a single ASN.1 value. |

Set them on the module with `parseOptions`, per call on `validateCertificateChain`, `parseCertificateSigningRequest`, `checkCertificateRevocation` and `fetchCertificateRevocationList`, or as the second argument of `X509Certificate.fromEncodedCertificate`. Options replace the defaults as a whole: a limit you leave out falls back to the parser's own default, not to Credo's. For `maxNodes` that's 10,000, which rejects CRLs with more than about 2,000 entries. Spread `defaultX509ParseOptions` to change one limit:

```ts
import { defaultX509ParseOptions, X509Module } from '@credo-ts/core'

new X509Module({ parseOptions: { ...defaultX509ParseOptions, maxDepth: 50 } })
```

The module's `parseOptions` only apply to parsing done by the X.509 module. Certificates in the `x5c` of an SD-JWT VC or the issuer data of an mdoc are parsed with `defaultX509ParseOptions`.
