X.509 Certificate Validation
The IDK provides X.509 certificate chain validation through the X509TrustValidationService. This service handles certificate chain building, CA bundle matching, fingerprint comparison, revocation checking, and integration with the platform trust store. It is a core building block for verifying the PKI trust chains behind mDoc issuer certificates, TLS connections, and other X.509-based trust relationships.
Architecture
X509TrustValidationService extends AbstractTrustValidationService("x509", setOf(TYPE_X509, TYPE_CA_BUNDLE)) and is contributed into the session scope via @ContributesIntoSet(SessionScope::class). It participates in the IDK trust framework alongside other trust validation services (such as the ETSI trust list validator) and can be used independently or as part of a composite trust evaluation.
The service supports four validation methods:
| Method | Description |
|---|---|
| Certificate chain building | Constructs a chain from the end-entity certificate up to a trusted root CA |
| CA bundle matching | Compares certificates against PEM-format CA bundles loaded from files or URLs |
| Fingerprint comparison | Matches certificates by SHA-256 fingerprint or JWK thumbprint |
| Platform trust store | Delegates to the operating system's built-in trust store |
Certificate Chain Validation
The ValidateX509TrustCommand (command ID: trust.x509.validate) validates a certificate by building a chain to a configured trust anchor and optionally checking revocation status.
- Android/Kotlin
- iOS/Swift
val x509Validator = session.graph.x509TrustValidationService
// Validate a certificate
val result = x509Validator.validate(
identifierJson = certificateIdentifierJson,
checkRevocation = true
)
if (result.trusted) {
println("Certificate chain is valid")
println("Trust anchor: ${result.trustAnchorSubject}")
println("Chain depth: ${result.chainDepth}")
} else {
println("Validation failed: ${result.reason}")
}
let x509Validator = session.graph.x509TrustValidationService
// Validate a certificate
let result = try await x509Validator.validate(
identifierJson: certificateIdentifierJson,
checkRevocation: true
)
if result.trusted {
print("Certificate chain is valid")
print("Trust anchor: \(result.trustAnchorSubject)")
print("Chain depth: \(result.chainDepth)")
} else {
print("Validation failed: \(result.reason)")
}
Chain building works by starting from the end-entity certificate and iterating through intermediate CA certificates until it finds one issued by a configured trust anchor (root CA). Each link in the chain is verified: the issuer's public key must validate the subject's signature, and each certificate must be within its validity period.
CA Bundle Configuration
CA bundles are collections of trusted root certificates in PEM format. The IDK can load them from local file paths or remote URLs, giving you flexibility in how trust anchors are distributed.
From Local Files
- Android/Kotlin
- iOS/Swift
// Configure CA bundles from local PEM files
// Set in properties:
// trust.anchors.x509.ca-bundle-paths=/etc/ssl/certs/ca-bundle.pem,/app/config/custom-cas.pem
// On iOS, CA bundles are typically bundled as app resources
// Configure via properties:
// trust.anchors.x509.ca-bundle-paths=/path/to/ca-bundle.pem
From Remote URLs
# Load CA bundles from HTTPS endpoints
trust.anchors.x509.ca-bundle-urls=https://example.com/ca-bundle.pem
Remote CA bundles are fetched over HTTPS and cached locally. This is useful for scenarios where the CA bundle is maintained centrally and distributed to multiple application instances.
Fingerprint Matching
For cases where you want to trust specific certificates without maintaining a full CA bundle, you can configure trusted fingerprints. The IDK supports SHA-256 certificate fingerprints.
# Trust specific certificates by SHA-256 fingerprint
trust.anchors.x509.trusted-fingerprints=SHA256:a1b2c3d4e5f6...,SHA256:f6e5d4c3b2a1...
Fingerprint matching compares the SHA-256 hash of the DER-encoded certificate against the configured values. JWK thumbprints (RFC 7638) are also supported when validating keys presented in JWK format.
Revocation Checking
Certificate revocation checking verifies that a certificate has not been revoked by its issuing CA after issuance. The IDK implements revocation checking through the RevocationChecker interface, with CompositeRevocationChecker as the default implementation.
Revocation Strategy
The CompositeRevocationChecker tries multiple revocation checking methods in order:
- OCSP first (when
preferOcsp = true): sends a real-time query to the certificate's OCSP responder - CRL fallback: downloads and checks the Certificate Revocation List if OCSP is unavailable or fails
- If both methods are disabled or unavailable, the result depends on the
failOnUnknownsetting
- Android/Kotlin
- iOS/Swift
val revocationChecker = session.graph.revocationChecker
// Check revocation status for a certificate
val revocationResult = revocationChecker.check(
certificate = endEntityCertificate,
issuerCertificate = issuerCertificate,
options = RevocationCheckOptions(
checkOCSP = true,
checkCRL = true,
preferOCSP = true,
timeoutMs = 10000,
useCache = true,
maxCacheAgeMs = 3600000, // 1 hour
failOnUnknown = false
)
)
when (revocationResult.status) {
RevocationStatus.GOOD -> {
println("Certificate is not revoked")
println("Checked via: ${revocationResult.method}")
}
RevocationStatus.REVOKED -> {
println("Certificate has been revoked")
println("Reason: ${revocationResult.revocationReason}")
}
RevocationStatus.UNKNOWN -> {
println("Revocation status could not be determined")
}
RevocationStatus.UNAVAILABLE -> {
println("Revocation checking service is unavailable")
}
}
let revocationChecker = session.graph.revocationChecker
// Check revocation status for a certificate
let revocationResult = try await revocationChecker.check(
certificate: endEntityCertificate,
issuerCertificate: issuerCertificate,
options: RevocationCheckOptions(
checkOCSP: true,
checkCRL: true,
preferOCSP: true,
timeoutMs: 10000,
useCache: true,
maxCacheAgeMs: 3600000, // 1 hour
failOnUnknown: false
)
)
switch revocationResult.status {
case .good:
print("Certificate is not revoked")
print("Checked via: \(revocationResult.method)")
case .revoked:
print("Certificate has been revoked")
print("Reason: \(revocationResult.revocationReason)")
case .unknown:
print("Revocation status could not be determined")
case .unavailable:
print("Revocation checking service is unavailable")
}
OCSP Checking
The OCSPChecker sends an Online Certificate Status Protocol request to the OCSP responder URL embedded in the certificate's Authority Information Access (AIA) extension. You can also provide an explicit responder URL via the ocspResponderUrl option, which overrides the AIA value.
OCSP provides real-time revocation status and is the preferred method for most use cases because it does not require downloading a potentially large CRL file.
CRL Checking
The CRLChecker downloads the Certificate Revocation List from the distribution point specified in the certificate's CRL Distribution Points extension. An explicit crlDistributionPoint URL can be provided in the options to override the extension value.
CRLs are cached locally to avoid repeated downloads. The cache respects the maxCacheAgeMs option and the CRL's own nextUpdate field.
RevocationCheckResult
The result of a revocation check contains the following fields:
| Field | Type | Description |
|---|---|---|
status | RevocationStatus | GOOD, REVOKED, UNKNOWN, or UNAVAILABLE |
method | RevocationCheckMethod | How the status was determined: OCSP, CRL, OCSP_STAPLING, NONE, or MULTIPLE |
revocationReason | RevocationReason? | If revoked, the reason code |
The RevocationReason enum includes standard RFC 5280 reason codes:
| Reason | Description |
|---|---|
UNSPECIFIED | No specific reason given |
KEY_COMPROMISE | The certificate's private key was compromised |
CA_COMPROMISE | The issuing CA's private key was compromised |
AFFILIATION_CHANGED | The certificate subject's affiliation changed |
SUPERSEDED | The certificate has been replaced by a newer one |
CESSATION_OF_OPERATION | The entity has ceased operations |
CERTIFICATE_HOLD | The certificate is temporarily on hold |
PRIVILEGE_WITHDRAWN | Privileges granted by the certificate were withdrawn |
AA_COMPROMISE | The attribute authority was compromised |
RevocationCheckOptions Reference
The full set of options available for revocation checking:
| Option | Type | Default | Description |
|---|---|---|---|
checkOCSP | Boolean | true | Enable OCSP checking |
checkCRL | Boolean | true | Enable CRL checking |
preferOCSP | Boolean | true | Try OCSP before CRL |
timeoutMs | Long | 10000 | Network timeout in milliseconds |
useCache | Boolean | true | Cache OCSP responses and CRLs |
maxCacheAgeMs | Long | 3600000 | Maximum cache age in milliseconds |
ocspResponderUrl | String? | null | Override OCSP responder URL |
crlDistributionPoint | String? | null | Override CRL distribution point URL |
failOnUnknown | Boolean | false | Treat unknown status as a validation failure |
Configuration
Configure X.509 certificate validation and revocation checking through properties:
# Enable X.509 trust validation (disabled by default)
trust.anchors.x509.enabled=false
# CA bundle paths (comma-separated, PEM format)
trust.anchors.x509.ca-bundle-paths=/path/to/ca-bundle.pem
# CA bundle URLs (comma-separated, fetched over HTTPS)
trust.anchors.x509.ca-bundle-urls=https://example.com/ca-bundle.pem
# Trusted certificate fingerprints (comma-separated, SHA-256)
trust.anchors.x509.trusted-fingerprints=SHA256:abc123...
Revocation Configuration
# Enable revocation checking
trust.revocation.enabled=true
# OCSP settings
trust.revocation.check-ocsp=true
# CRL settings
trust.revocation.check-crl=true
# Prefer OCSP over CRL when both are available
trust.revocation.prefer-ocsp=true
# Network timeout for revocation checks (milliseconds)
trust.revocation.timeout-ms=10000
Module Dependencies
To use X.509 certificate validation, include the trust module in your project:
- Android/Kotlin
- iOS/Swift
// build.gradle.kts
dependencies {
implementation("com.sphereon.idk:lib-trust-x509:<version>")
}
The X.509 trust module is included in the main IDK Swift package. No additional dependency is needed.
The X.509 trust module depends on:
lib-trust-api: core trust validation interfaces (AbstractTrustValidationService,TrustContext)lib-crypto-core-public/lib-crypto-core-impl: cryptographic primitives for signature and fingerprint verificationlib-http-client: HTTP client for downloading CA bundles and revocation data (OCSP, CRL)