3.20.0-PREVIEW / 0.20.0-PREVIEW
This is a preview release to gather feedback for the newly introduced PKIX cert path validation API. It introduces some breaking changes, so please refer to the full changelog below!
Using it in your projects
Add the following maven repo: https://raw.githubusercontent.com/a-sit-plus/signum/refs/heads/mvn/repo and use versions 3.20.0-PREVIEW/ 0.20.0-PREVIEW respectively.
This gets you the following:
X.509 Certificate validation
Provides a flexible framework for X.509 certificate chain validation, supporting both:
- RFC 5280-compliant validation
- Custom validator pipelines for application-specific needs.
- No revocation checks
- Only linear certificate chains, no complex graphs yet
Remember: this is a preview, o use with care!
Validation never throws; instead it returns a structured CertificateValidationResult describing all encountered issues.
The validation system is built around the CertificateValidator interface. Each validator:
- Receives a certificate being processed
- Removes supported critical extensions
- Throws an exception when it cannot validate (caught and recorded by the framework)
Unsupported / Not Yet Implemented:
- Revocation checking: OCSP and CRL checks are not yet supported
- Partial cross-validation support. Our validation logic assumes a linear certificate chain and does not perform graph-based path discovery, meaning it cannot search for or create alternative certification paths.:
- Multiple trust anchors can be supplied and checked (implemented)
- Validation succeeds if any certificate in the chain is issued by a trust anchor (implemented)
- If the trust anchor matches an intermediate certificate, the TA’s public key is verified against the certificate before it (implemented)
- Full cross-validation (exploring alternative chains through intermediates) (not implemented)
Use CertificateValidationContext to configure RFC 5280 validation:
val context = CertificateValidationContext(
date = Clock.System.now(),
explicitPolicyRequired = false,
policyMappingInhibited = false,
anyPolicyInhibited = false,
policyQualifiersRejected = false,
initialPolicies = emptySet(),
trustAnchors = setOf(TrustAnchor.Certificate(myRootCert)),
expectedEku = setOf(KnownOIDs.id_kp_serverAuth)
)This lets you specify:
- Validation time
- Whether explicit policy is required
- Whether anyPolicy or policy mapping is allowed
policyQualifiersRejectedflag, which indicates should certificates that include policy qualifiers in a certificate policies extension that is marked critical be rejected- Expected EKUs
- Which trust anchors are acceptable
Trust anchors
A TrustAnchor represents the starting point of trust for certificate path validation.
It defines the cryptographic identity against which the certificate chain is ultimately verified.
A trust anchor can be represented in two ways:
Certificate(full X.509 certificate)PublicKey(bare public key with an optional subject name and name constraints)
- Certificate based Trust anchor:
TrustAnchor.Certificate(cert: X509Certificate)
This is the standard RFC 5280 trust model:- Trust is a self-signed or intermediate CA certificate.
- The certificate’s subject name becomes the trust anchor’s principal.
- Name constraints (if present) propagate into path validation.
- The public key is extracted from the certificate.
- Public Key based Trust anchor:
-
TrustAnchor.PublicKey(publicKey: CryptoPublicKey, principal: X500Name?, nameConstraints: NameConstraintsExtension? = null) -
There is also a convenience constructor (An unnamed trust anchor has no distinguishing subject name. Use only when a raw key truly makes sense):
@HazardousMaterials
constructor(publicKey: CryptoPublicKey)RFC 5280 validation:
val root: X509Certificate = TODO("Trusted root")
val intermediate: X509Certificate = TODO()
val leaf: X509Certificate = TODO("Certificate to validate")
val context = CertificateValidationContext(
trustAnchors = setOf(TrustAnchor.Certificate(root)),
)
// Build chain leaf → intermediates
val chain: CertificateChain = listOf(leaf, intermediate, root)
val result = chain.validate(context)
println("Chain valid? ${result.isValid}")
if (!result.isValid) {
println("Failures:")
result.validatorFailures.forEach {
println(" - [${it.validatorName}] ${it.errorMessage}")
}
}If no trust anchors are explicitly specified, the validation will fall back to using the system trust store.
If allowIncludedTrustAnchor is set to true in the validation context, validation will try to match root with one of the trust anchors and the if match is found root will be omitted from the chain during the validation.
Custom validation:
val intermediate: X509Certificate = TODO()
val leaf: X509Certificate = TODO("Certificate to validate")
val chain: CertificateChain = listOf(leaf, intermediate)
// It is optional, falls back to the default if missing
val context = CertificateValidationContext(
date = Clock.System.now()
)
// TODO: Replace with your own custom validator factory
val myValidatorFactory = ValidatorFactory { context ->
// Here you can define your own validators for this chain
// e.g. use rfc5280 factory to create mutable list and remove unnecessary validators
val validators = ValidatorFactory.RFC5280.run { chain.generate(context) }
validators.removeAll { it is CertValidityValidator || it is KeyIdentifierValidator || it is TimeValidityValidator }
validators
}
val result = chain.validate(
validatorFactory = myValidatorFactory,
context = context
)
println("Chain valid? ${result.isValid}")Changelog
- Introduce full X.509 certificate validation support
- RFC Compliance:
- Implements RFC 5280 path validation rules, including policy processing, name constraints, key usage, and basic constraints
- Unsupported / Not Yet Implemented:
- Revocation checking: OCSP and CRL checks are not yet supported
- Partial cross-validation support:
- Multiple trust anchors can be supplied and checked (implemented)
- Validation succeeds if any certificate in the chain is issued by a trust anchor (implemented)
- If the trust anchor matches an intermediate certificate, the TA’s public key is verified against the certificate before it (implemented)
- Full cross-validation (exploring alternative chains through intermediates) (not implemented)
- Added core
CertificateChainValidatorcoordinating the full validation pipelinevalidate()method returnsCertificateValidationResultwhich contains root policy node, leaf certificate and list ofValidatorFailure- Validation fails softly, by returning
ValidatorFailurefor every exception thrown during validation
- Modular validator design with pluggable components:
PolicyValidator– enforces certificate policies and policy constraintsBasicConstraintsValidator– validates basicConstraintsNameConstraintsValidator– enforces permitted/excluded name constraints across the chainChainValidator- validates signatures and name chainingKeyUsageValidator- validates KeyUsage extensionsTimeValidityValidator- checks certificate time validity and that each certificate was issued within the validity period of its issuerTrustAnchorValidator- checks if any certificate from the chain is trustedKeyIdentifierValidator- validates Subject and Authority key identifiersCertValidityValidator- checks whether the certificate is constructed correctly, since some components are decoded too leniently
- RFC Compliance:
- Make more provider functions suspending
- digest calculation
- MAC calculation
- private key export
- signature verification
- ephemeral key creation
- signer creation
- verifier creation
- hash to curve
- Introduced dedicated X509 extension classes:
X509CertificateExtensionis now a base class- Enables polymorphic decoding/encoding of extension types
BasicConstraintsExtensionCertificatePoliciesExtensionInhibitAnyPolicyExtensionKeyUsageExtensionNameConstraintsExtensionandGeneralSubtreePolicyConstraintsExtensionPolicyMappingsExtensionAuthorityKeyIdentifierExtensionExtendedKeyUsageExtensionSubjectKeyIdentifier
- If decoding a dedicated extension fails, an
InvalidCertificateExtensionis returned, containing the original extension’s properties and the cause of the decoding failure.
- Refactored
AlternativeNamesfor SAN/IAN extraction- Removed detailed parsing of individual name types; now delegates decoding to
GeneralName - Introduced dedicated
GeneralNameclasses:DNSNameEDIPartyNameIPAddressNameOtherNameRegisteredIDNameRFC822NameUriNameX400AddressNameX500Name
- Removed detailed parsing of individual name types; now delegates decoding to
- Add
constrains()method inGeneralNameOptioninterface to perform constraint checking between General Names, this method is intended for use in NameConstraints check during certificate chain validation - Add full RFC 2253 support for Distinguished Names (
X500Name,RDN,AttributeTypeAndValue):X500Name.fromString()/.toRfc2253String()- parse and serialize complete DNs with escaping and normalizationRelativeDistinguishedName.fromString()- parse multi-attribute RDNsAttributeTypeAndValue.fromString()/.toRfc2253String- handle known attributes and canonicalize values
- Fix a glaring JWS bug that caused an error whenever trying to get the digest of a JWS signature algorithm
- Add
EnumerableandEnumerationinterfaces to support the pattern in sealed types where the companion object providesentriescontaining all possible instances- Classes and interfaces refactored to use
Setasentriesto follow the standardized pattern:Asn1Element.TagCoseAlgorithmand its nested implementationsJsonWebAlgorithmJwsAlgorithmand its nested implementationsDataIntegrityAlgorithmMessageAuthenticationCodeSignatureAlgorithmRSAPaddingSymmetricEncryptionAlgorithmand its nested implementations
- Classes and interfaces refactored to use