From 99c420ad272c708c9e191540154159b8c020554e Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Mon, 12 Jan 2026 11:44:54 +0100 Subject: [PATCH 1/9] update critical extensions handling logic --- .../pki/validate/BasicConstraintsValidator.kt | 4 ++-- .../pki/validate/CertValidityValidator.kt | 2 +- .../pki/validate/CertificateValidator.kt | 4 ++-- .../pki/validate/KeyIdentifierValidator.kt | 2 +- .../pki/validate/KeyUsageValidator.kt | 4 ++-- .../pki/validate/NameConstraintsValidator.kt | 4 ++-- .../indispensable/pki/validate/PolicyValidator.kt | 4 ++-- .../pki/validate/TimeValidityValidator.kt | 2 +- .../supreme/validate/CertificateChainValidator.kt | 14 ++++++++------ .../signum/supreme/validate/ChainValidator.kt | 2 +- .../supreme/validate/TrustAnchorValidator.kt | 2 +- .../signum/supreme/validate/ValidationApiTest.kt | 10 ++++++---- 12 files changed, 29 insertions(+), 25 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt index c14a7d7f3..4c65b04e5 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt @@ -17,8 +17,8 @@ class BasicConstraintsValidator( ) : CertificateValidator { @ExperimentalPkiApi - override suspend fun check(currCert: X509Certificate, remainingCriticalExtensions: MutableSet) { - remainingCriticalExtensions.remove(KnownOIDs.basicConstraints_2_5_29_19) + override suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) { + checkedCriticalExtensions.add(KnownOIDs.basicConstraints_2_5_29_19) if (currentCertIndex >= certPathLen - 1) return currentCertIndex++ diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt index 621523079..9c7e3959a 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt @@ -23,7 +23,7 @@ class CertValidityValidator( @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, - remainingCriticalExtensions: MutableSet + checkedCriticalExtensions: MutableSet ) { checkSerialNumber(currCert) isSanCriticalWhenNameIsEmpty(currCert) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt index 53a434405..83bc9dd85 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt @@ -7,8 +7,8 @@ import at.asitplus.signum.indispensable.pki.X509Certificate import kotlin.coroutines.cancellation.CancellationException interface CertificateValidator { - // Every validator removes checked critical extensions + // Every validator adds checked critical extensions @ExperimentalPkiApi @Throws(CertificateException::class, CancellationException::class) - suspend fun check(currCert: X509Certificate, remainingCriticalExtensions: MutableSet) + suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) } \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt index 4654765c2..36028333b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt @@ -15,7 +15,7 @@ class KeyIdentifierValidator( @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, - remainingCriticalExtensions: MutableSet + checkedCriticalExtensions: MutableSet ) { currentCertIndex++ checkSubjectKeyIdentifier(currCert) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt index b4876a6ef..6699c4eda 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt @@ -28,8 +28,8 @@ class KeyUsageValidator ( ) @ExperimentalPkiApi - override suspend fun check(currCert: X509Certificate, remainingCriticalExtensions: MutableSet) { - remainingCriticalExtensions.removeAll(supportedExtensions) + override suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) { + checkedCriticalExtensions.addAll(supportedExtensions) currentCertIndex++ if (currentCertIndex <= certPathLen - 1) verifySignatureKeyUsage(currCert) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt index a21fcd447..016190abe 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt @@ -20,8 +20,8 @@ class NameConstraintsValidator( ) : CertificateValidator { @ExperimentalPkiApi - override suspend fun check(currCert: X509Certificate, remainingCriticalExtensions: MutableSet) { - remainingCriticalExtensions.remove(KnownOIDs.nameConstraints_2_5_29_30) + override suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) { + checkedCriticalExtensions.add(KnownOIDs.nameConstraints_2_5_29_30) currentCertIndex++ if (previousNameConstraints?.isValid == false) { diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/PolicyValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/PolicyValidator.kt index 7cc288013..20edf3813 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/PolicyValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/PolicyValidator.kt @@ -46,8 +46,8 @@ class PolicyValidator( } @ExperimentalPkiApi - override suspend fun check(currCert: X509Certificate, remainingCriticalExtensions: MutableSet) { - remainingCriticalExtensions.removeAll(supportedExtensions) + override suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) { + checkedCriticalExtensions.addAll(supportedExtensions) rootNode = processPolicies( certIndex, diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt index ae720876d..6b273f98b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt @@ -23,7 +23,7 @@ class TimeValidityValidator( @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, - remainingCriticalExtensions: MutableSet + checkedCriticalExtensions: MutableSet ) { if (currCert.isExpired(date)) { throw CertificateValidityException( diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt index 56d8ed6f3..140c11b43 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt @@ -162,13 +162,13 @@ suspend fun CertificateChain.validate( } processingChain.reversed().forEachIndexed { i, cert -> - val remainingCriticalExtensions = cert.criticalExtensionOids.toMutableSet() + val checkedCriticalExtensions = mutableSetOf() val validatorIterator = activeValidators.iterator() while (validatorIterator.hasNext()) { val currValidator = validatorIterator.next() try { - currValidator.check(cert, remainingCriticalExtensions) + currValidator.check(cert, checkedCriticalExtensions) } catch (e: Throwable) { validatorIterator.remove() validatorFailures.add( @@ -182,7 +182,7 @@ suspend fun CertificateChain.validate( ) } } - verifyCriticalExtensions(remainingCriticalExtensions, i, validatorFailures) + verifyCriticalExtensions(checkedCriticalExtensions, cert, i, validatorFailures) } return CertificateValidationResult((validators.find { it is PolicyValidator } as? PolicyValidator)?.rootNode, this.leaf, @@ -268,15 +268,17 @@ private fun defineRFC5280Validators( * which would indicate that the current validators do not support them. */ private fun verifyCriticalExtensions( - remainingCriticalExtensions: Set, + checkedCriticalExtensions: Set, + cert: X509Certificate, certificateIndex: Int, failures: MutableList ) { - if (remainingCriticalExtensions.isNotEmpty() && failures.none { it.validatorName == "CriticalCertificateExtensions" }) { + val unsupportedCriticalExtensions = cert.criticalExtensionOids - checkedCriticalExtensions + if (unsupportedCriticalExtensions.isNotEmpty() && failures.none { it.validatorName == "CriticalCertificateExtensions" }) { failures.add( ValidatorFailure( validatorName = "CriticalCertificateExtensions", - errorMessage = "Unsupported critical extensions: $remainingCriticalExtensions", + errorMessage = "Unsupported critical extensions: $unsupportedCriticalExtensions", certificateIndex = certificateIndex ) ) diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/ChainValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/ChainValidator.kt index e7176a04c..16b246db8 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/ChainValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/ChainValidator.kt @@ -26,7 +26,7 @@ class ChainValidator( @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, - remainingCriticalExtensions: MutableSet + checkedCriticalExtensions: MutableSet ) { if (currentCertIndex < certChain.lastIndex) { val childCert = certChain[currentCertIndex + 1] diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt index e20503cfa..053bf17b6 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt @@ -30,7 +30,7 @@ class TrustAnchorValidator( @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, - remainingCriticalExtensions: MutableSet + checkedCriticalExtensions: MutableSet ) { if (foundTrusted) return val issuingAnchor = trustAnchors.firstOrNull { anchor -> diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt index 6af67dac3..708a7c9f8 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt @@ -128,12 +128,14 @@ val ValidationApiTest by testSuite{ validators } - chain.validate( + val r = chain.validate( customValidatorFactory, CertificateValidationContext( trustAnchors = setOf(TrustAnchor.Certificate(chain.root)) ) - ).isValid shouldBe true + ) + + r.isValid shouldBe true } } @@ -145,10 +147,10 @@ class AttestationTimeValidator( @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, - remainingCriticalExtensions: MutableSet + checkedCriticalExtensions: MutableSet ) { if (currCert != certChain.leaf) { - if (!currCert.isValidAt()) throw CertificateValidityException("Certificate is not valid") + if (!currCert.isValidAt(currCert.tbsCertificate.validUntil.instant)) throw CertificateValidityException("Certificate is not valid") } } From 696e7b4931077852cbe67b7945649f74d6fe403b Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Mon, 12 Jan 2026 13:10:20 +0100 Subject: [PATCH 2/9] minor fixes, introduce lambda for handling leaf key usage extension --- .../pki/validate/CertValidityValidator.kt | 5 ++- .../pki/validate/KeyUsageValidator.kt | 19 ++++------ .../validate/CertificateChainValidator.kt | 36 +++++++++++++------ .../supreme/validate/ValidationApiTest.kt | 2 +- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt index 9c7e3959a..9cf1822af 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt @@ -17,14 +17,13 @@ import kotlin.time.Instant /** * Checks whether the certificate is constructed correctly, since some components are decoded too leniently * */ -class CertValidityValidator( - val date: Instant -) : CertificateValidator { +class CertValidityValidator: CertificateValidator { @ExperimentalPkiApi override suspend fun check( currCert: X509Certificate, checkedCriticalExtensions: MutableSet ) { + checkedCriticalExtensions.add(KnownOIDs.subjectAltName_2_5_29_17) checkSerialNumber(currCert) isSanCriticalWhenNameIsEmpty(currCert) checkTimeValidity(currCert) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt index 6699c4eda..59fd64403 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt @@ -18,30 +18,25 @@ import at.asitplus.signum.indispensable.pki.pkiExtensions.KeyUsageExtension class KeyUsageValidator ( private val certPathLen: Int, private var currentCertIndex: Int = 0, - private val expectedEku: Set = emptySet() + private val expectedEku: Set = emptySet(), + private val leafKeyUsageCheck: suspend (X509Certificate) -> Unit ) : CertificateValidator { - private var supportedExtensions: Set = setOf( + private val supportedExtensions: Set = setOf( KnownOIDs.keyUsage, - KnownOIDs.extKeyUsage, - KnownOIDs.subjectAltName_2_5_29_17, + KnownOIDs.extKeyUsage ) @ExperimentalPkiApi override suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) { checkedCriticalExtensions.addAll(supportedExtensions) currentCertIndex++ - if (currentCertIndex <= certPathLen - 1) + if (currentCertIndex <= certPathLen - 1) { verifySignatureKeyUsage(currCert) + } else { verifyExpectedEKU(currCert) - val basicConstraints = currCert.findExtension() - - if (basicConstraints?.ca == true) verifySignatureKeyUsage(currCert) - - if (basicConstraints?.ca != true && currCert.findExtension()?.keyUsage?.contains(KeyUsage.KEY_CERT_SIGN) == true) { - throw KeyUsageException("Digital signature key usage extension must not be present at leaf cert.") - } + leafKeyUsageCheck(currCert) } } diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt index 140c11b43..f485dcb33 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt @@ -3,6 +3,7 @@ package at.asitplus.signum.supreme.validate import at.asitplus.catchingUnwrapped import at.asitplus.signum.ExperimentalPkiApi import at.asitplus.signum.HazardousMaterials +import at.asitplus.signum.KeyUsageException import at.asitplus.signum.indispensable.asn1.Asn1Exception import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.ObjectIdentifier @@ -10,6 +11,9 @@ import at.asitplus.signum.indispensable.asn1.anyPolicy import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.leaf +import at.asitplus.signum.indispensable.pki.pkiExtensions.BasicConstraintsExtension +import at.asitplus.signum.indispensable.pki.pkiExtensions.KeyUsage +import at.asitplus.signum.indispensable.pki.pkiExtensions.KeyUsageExtension import at.asitplus.signum.indispensable.pki.root import at.asitplus.signum.indispensable.pki.validate.* import kotlin.contracts.ExperimentalContracts @@ -27,7 +31,21 @@ class CertificateValidationContext( val initialPolicies: Set = emptySet(), val allowIncludedTrustAnchor: Boolean = true, val trustAnchors: Set = SystemTrustStore, - val expectedEku: Set = emptySet() + val expectedEku: Set = emptySet(), + /** use this lambda to specify how to handle leaf key usage check */ + val leafKeyUsageCheck: suspend (X509Certificate) -> Unit = { currCert -> + val basicConstraints = currCert.findExtension() + + if (basicConstraints?.ca == true) { + if (currCert.findExtension()?.keyUsage?.contains(KeyUsage.KEY_CERT_SIGN) != true) { + throw KeyUsageException("Digital signature key usage extension not present at leaf cert.") + } + } + + if (basicConstraints?.ca != true && currCert.findExtension()?.keyUsage?.contains(KeyUsage.KEY_CERT_SIGN) == true) { + throw KeyUsageException("Digital signature key usage extension must not be present at leaf cert.") + } + } ) /** @@ -86,7 +104,7 @@ data class ValidatorFailure( fun interface ValidatorFactory { fun CertificateChain.generate( context: CertificateValidationContext - ): MutableList + ): List companion object { val RFC5280: ValidatorFactory = @@ -224,8 +242,7 @@ suspend fun CertificateChain.validate( private fun defineRFC5280Validators( context: CertificateValidationContext, chain: CertificateChain -): MutableList { - val validators = mutableListOf() +): List { val (pathLen, processingChain) = if (context.allowIncludedTrustAnchor && context.trustAnchors.any { it.matchesCertificate(chain.root) }) { chain.size - 1 to chain.dropLast(1) @@ -233,7 +250,7 @@ private fun defineRFC5280Validators( chain.size to chain } - validators.add( + return listOf( PolicyValidator( initialPolicies = context.initialPolicies, expPolicyRequired = context.explicitPolicyRequired, @@ -248,19 +265,16 @@ private fun defineRFC5280Validators( expectedPolicySet = setOf(KnownOIDs.anyPolicy), generatedByPolicyMapping = false ) - ) - ) - validators += listOf( - CertValidityValidator(context.date), + ), + CertValidityValidator(), NameConstraintsValidator(pathLen), - KeyUsageValidator(pathLen, expectedEku = context.expectedEku), + KeyUsageValidator(pathLen, expectedEku = context.expectedEku, leafKeyUsageCheck = context.leafKeyUsageCheck), BasicConstraintsValidator(pathLen), ChainValidator(processingChain.reversed()), TimeValidityValidator(context.date, certChain = processingChain.reversed()), TrustAnchorValidator(context.trustAnchors, processingChain, date = context.date), KeyIdentifierValidator(processingChain) ) - return validators } /** diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt index 708a7c9f8..1c8c809a1 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt @@ -122,7 +122,7 @@ val ValidationApiTest by testSuite{ ).isValid shouldBe false val customValidatorFactory = ValidatorFactory { context -> - val validators = ValidatorFactory.RFC5280.run { chain.generate(context) } + val validators = ValidatorFactory.RFC5280.run { chain.generate(context) }.toMutableList() validators.removeAll { it is CertValidityValidator || it is KeyIdentifierValidator || it is TimeValidityValidator } validators.add(AttestationTimeValidator(this)) validators From b5b3c27ca65eba09971518260452f863fda9446a Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Tue, 13 Jan 2026 10:28:11 +0100 Subject: [PATCH 3/9] introduce supportRevocation flag in context --- .../indispensable/pki/validate/KeyUsageValidator.kt | 9 +++++++-- .../signum/supreme/validate/CertificateChainValidator.kt | 5 +++-- .../asitplus/signum/supreme/validate/NistPkiTestsuite.kt | 5 +++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt index 59fd64403..6b950043b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyUsageValidator.kt @@ -13,13 +13,15 @@ import at.asitplus.signum.indispensable.pki.pkiExtensions.KeyUsageExtension /** * Ensures that intermediate CA certificates have the necessary key usage extensions. - * Key usage to sign certificates (keyCertSign) and sign certificate revocation lists (cRLSign), according to RFC 5280. + * Checks Expected Key Usage in leaf certificate only + * Key usage to sign certificates (keyCertSign) and CRLSign is required according to RFC 5280. */ class KeyUsageValidator ( private val certPathLen: Int, private var currentCertIndex: Int = 0, private val expectedEku: Set = emptySet(), - private val leafKeyUsageCheck: suspend (X509Certificate) -> Unit + private val leafKeyUsageCheck: suspend (X509Certificate) -> Unit, + private val supportRevocationChecking: Boolean ) : CertificateValidator { private val supportedExtensions: Set = setOf( @@ -44,6 +46,9 @@ class KeyUsageValidator ( if (currCert.findExtension()?.keyUsage?.contains(KeyUsage.KEY_CERT_SIGN) != true) { throw KeyUsageException("Digital signature key usage extension not present at cert index $currentCertIndex.") } + if (supportRevocationChecking && currCert.findExtension()?.keyUsage?.contains(KeyUsage.CRL_SIGN) != true) { + throw KeyUsageException("CRL signature key usage extension not present at cert index $currentCertIndex.") + } } private fun verifyExpectedEKU(currCert: X509Certificate) { diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt index f485dcb33..f8faabd3a 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt @@ -45,7 +45,8 @@ class CertificateValidationContext( if (basicConstraints?.ca != true && currCert.findExtension()?.keyUsage?.contains(KeyUsage.KEY_CERT_SIGN) == true) { throw KeyUsageException("Digital signature key usage extension must not be present at leaf cert.") } - } + }, + val supportRevocationChecking: Boolean = false ) /** @@ -268,7 +269,7 @@ private fun defineRFC5280Validators( ), CertValidityValidator(), NameConstraintsValidator(pathLen), - KeyUsageValidator(pathLen, expectedEku = context.expectedEku, leafKeyUsageCheck = context.leafKeyUsageCheck), + KeyUsageValidator(pathLen, expectedEku = context.expectedEku, leafKeyUsageCheck = context.leafKeyUsageCheck, supportRevocationChecking = context.supportRevocationChecking), BasicConstraintsValidator(pathLen), ChainValidator(processingChain.reversed()), TimeValidityValidator(context.date, certChain = processingChain.reversed()), diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt index f067367ff..26e4a32c0 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt @@ -17,7 +17,7 @@ import io.kotest.matchers.shouldNotBe val NistPkiTestSuite by testSuite{ val testSuite = json.decodeFromString>(resourceText("NIST-PKITS.json")).filter { tc -> - !tc.name.contains("cRLSign", ignoreCase = true) + tc.name.contains("cRLSign", ignoreCase = true) } testSuite.forEach { testCase -> @@ -41,7 +41,8 @@ val NistPkiTestSuite by testSuite{ explicitPolicyRequired = testCase.explicitPolicyRequired, initialPolicies = testCase.initialPolicies.map { ObjectIdentifier(it) }.toSet(), anyPolicyInhibited = testCase.anyPolicyInhibited, - policyMappingInhibited = testCase.policyMappingInhibited + policyMappingInhibited = testCase.policyMappingInhibited, + supportRevocationChecking = true ) val result = chain.validate(context) From 17adf821b5ed68c2ec07406bf63b4a4ae2ffaad1 Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Tue, 13 Jan 2026 11:16:53 +0100 Subject: [PATCH 4/9] make CertificateException open, extract checkCaBasicConstraints() from validator to be reusable --- .../kotlin/at/asitplus/signum/Throwables.kt | 2 +- .../pki/validate/BasicConstraintsValidator.kt | 23 ++++++++++--------- .../supreme/validate/TrustAnchorValidator.kt | 4 ++-- .../supreme/validate/NistPkiTestsuite.kt | 4 +--- supreme/src/jvmTest/resources/NIST-PKITS.json | 2 +- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt index 70e07200d..885c653e2 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt @@ -4,7 +4,7 @@ sealed class CryptoException(message: String? = null, cause: Throwable? = null) open class CryptoOperationFailed(message: String) : CryptoException(message) open class UnsupportedCryptoException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause) -sealed class CertificateException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) +open class CertificateException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) class CertificateChainValidatorException(message: String) : CertificateException(message) class KeyUsageException(message: String) : CertificateException(message) class ExtendedKeyUsageException(message: String) : CertificateException(message) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt index 4c65b04e5..5075ae267 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt @@ -26,7 +26,7 @@ class BasicConstraintsValidator( val basicConstraints = currCert.findExtension() ?: throw BasicConstraintsException("Missing basicConstraints extension at cert index $currentCertIndex.") - checkCaBasicConstraints(currCert) + checkCaBasicConstraints(currCert, currentCertIndex) if (remainingPathLength != null && !currCert.isSelfIssued) { if (remainingPathLength?.toInt() == 0) { @@ -41,18 +41,19 @@ class BasicConstraintsValidator( } } } +} - @Throws(BasicConstraintsException::class) - fun checkCaBasicConstraints(cert: X509Certificate) { - val basicConstraints = cert.findExtension() - ?: throw BasicConstraintsException("Missing basicConstraints extension at cert index $currentCertIndex.") +@Throws(BasicConstraintsException::class) +fun checkCaBasicConstraints(cert: X509Certificate, certIndex: Int = 0) { + val location = "at ${if (certIndex == 0) "trust anchor" else "cert index $certIndex."}" + val basicConstraints = cert.findExtension() + ?: throw BasicConstraintsException("Missing basicConstraints extension $location") - if(!basicConstraints.critical) { - throw BasicConstraintsException("basicConstraints extension must be critical (index $currentCertIndex).") - } + if(!basicConstraints.critical) { + throw BasicConstraintsException("basicConstraints extension must be critical $location") + } - if (!basicConstraints.ca) { - throw BasicConstraintsException("Missing CA flag at cert index $currentCertIndex.") - } + if (!basicConstraints.ca) { + throw BasicConstraintsException("Missing CA flag $location") } } \ No newline at end of file diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt index 053bf17b6..271988a9d 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt @@ -7,6 +7,7 @@ import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.validate.BasicConstraintsValidator import at.asitplus.signum.indispensable.pki.validate.CertificateValidator +import at.asitplus.signum.indispensable.pki.validate.checkCaBasicConstraints import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import org.kotlincrypto.error.CertificateException @@ -25,7 +26,6 @@ class TrustAnchorValidator( ) : CertificateValidator { var foundTrusted: Boolean = false - private val basicConstraintsValidator: BasicConstraintsValidator = BasicConstraintsValidator(0) @ExperimentalPkiApi override suspend fun check( @@ -54,7 +54,7 @@ class TrustAnchorValidator( trustAnchor = issuingAnchor - issuingAnchor.cert?.let { basicConstraintsValidator.checkCaBasicConstraints(it) } + issuingAnchor.cert?.let { checkCaBasicConstraints(it) } issuingAnchor.cert?.let { if (it.isExpired(date)) { diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt index 26e4a32c0..bf01dbdbd 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/NistPkiTestsuite.kt @@ -16,9 +16,7 @@ import io.kotest.matchers.shouldNotBe */ val NistPkiTestSuite by testSuite{ - val testSuite = json.decodeFromString>(resourceText("NIST-PKITS.json")).filter { tc -> - tc.name.contains("cRLSign", ignoreCase = true) - } + val testSuite = json.decodeFromString>(resourceText("NIST-PKITS.json")) testSuite.forEach { testCase -> test(testCase.name) { diff --git a/supreme/src/jvmTest/resources/NIST-PKITS.json b/supreme/src/jvmTest/resources/NIST-PKITS.json index b8c96c707..856838575 100644 --- a/supreme/src/jvmTest/resources/NIST-PKITS.json +++ b/supreme/src/jvmTest/resources/NIST-PKITS.json @@ -36,7 +36,7 @@ "leaf": "-----BEGIN CERTIFICATE-----\nMIIDpjCCAo6gAwIBAgIBATANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEf\nMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEyMDAGA1UEAxMpYmFzaWND\nb25zdHJhaW50cyBOb3QgQ3JpdGljYWwgY0EgRmFsc2UgQ0EwHhcNMTAwMTAxMDgz\nMDAwWhcNMzAxMjMxMDgzMDAwWjBeMQswCQYDVQQGEwJVUzEfMB0GA1UEChMWVGVz\ndCBDZXJ0aWZpY2F0ZXMgMjAxMTEuMCwGA1UEAxMlSW52YWxpZCBjQSBGYWxzZSBF\nRSBDZXJ0aWZpY2F0ZSBUZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAOMqfzk11boRFBbpNA1tq4GZJbk1M45bsflY/IBB0d8KUdhotWgSOm065GL3\nsB10F5JZI/sLVpXuH7R9TABBvnZu8XqX5RKKkNB7zMXlkpr4TFXGZnYgSmX09ta+\nfuZ4TQgj6BdztW1l/634f4exa7qZirzCISqddB99CN318WhQgMqxmounqUfkUD12\ne1UM3TjOPKqI7eYOJIfnfw2Ea6BhT55ncTXp5lcAhAajos+iWYhiqlPWc+cd1c8U\n3VrVi7kuKK6uuVBqA68QTiDnuaPEJsxadp9AuYY5WYGT4HT2HpsXzCd6R2fk94hZ\niV/C0hSIAq0mVtq9FJF9qh/XwsECAwEAAaNrMGkwHwYDVR0jBBgwFoAUOdCbt08p\nN77TsIp26mqeze9GvlgwHQYDVR0OBBYEFI/4W2j9iCltyb9KCY6b8q11whJ3MA4G\nA1UdDwEB/wQEAwIE8DAXBgNVHSAEEDAOMAwGCmCGSAFlAwIBMAEwDQYJKoZIhvcN\nAQELBQADggEBAG22QQs5JmmHtqLDRPAiJP+IjtpGyiOCH7IhyGLW6MKk88cjH9xe\nM066O9SE3asTYTrhQBirfy+nOxV1Qg+DdSmtA6ibr3USxTkH5xcz3OR+Sd3fkxEI\nqkjK5nYkXWs6B/xbX8A3iUrRmG2Yp/gLI4Fs3ZEobwh93E7wIamQ8PJ1SbnXTTcx\nxBy/IKY/usd7NdwtobmHP6fH2bWM0GyNxmOscLCxAhESLjmuq30srVG/jHs/Q0vL\nJww3lWZSPbJXcRxKMzlu35IrP92RZWjtByv1Wndugh7q5rjT1ccw0eF95b/U5whx\nX2XPc74bSpqoh8dfNGTWjtq2JeHhGoJd+9k=\n-----END CERTIFICATE-----", "isSuccessful": false, "failedValidator": "BasicConstraintsValidator", - "errorMessage": "basicConstraints extension must be critical (index 1).", + "errorMessage": "basicConstraints extension must be critical at cert index 1.", "explicitPolicyRequired": false, "initialPolicies": [], "anyPolicyInhibited": false, "policyMappingInhibited": false From 061b371a58c5edc569e817066ebc5fbe5f44cc47 Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Tue, 13 Jan 2026 12:48:08 +0100 Subject: [PATCH 5/9] extract validity check in X509Certificate --- .../indispensable/pki/X509Certificate.kt | 23 +++++++++++++++- .../pki/validate/TimeValidityValidator.kt | 12 +-------- .../supreme/validate/TrustAnchorValidator.kt | 27 +++---------------- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt index 16165234c..14f37f7ff 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt @@ -21,6 +21,8 @@ import at.asitplus.signum.indispensable.pki.generalNames.X500Name import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.Transient import kotlinx.serialization.builtins.serializer import kotlin.time.Clock @@ -344,7 +346,26 @@ data class X509Certificate @Throws(IllegalArgumentException::class) constructor( /** * Checks whether this certificate is valid at the specified [date]. */ - fun isValidAt(date: Instant = Clock.System.now()): Boolean = !(isExpired(date) || isNotYetValid(date)) + fun isValidAt(date: Instant = Clock.System.now()): Boolean = runCatching { checkValidityAt(date) }.isSuccess + + @Throws(CertificateValidityException::class) + fun checkValidityAt(date: Instant = Clock.System.now()) { + if (isExpired(date)) { + throw CertificateValidityException( + "certificate expired on " + + tbsCertificate.validUntil.instant + .toLocalDateTime(TimeZone.UTC) + ) + } + + if (isNotYetValid(date)) { + throw CertificateValidityException( + "certificate not valid till " + + tbsCertificate.validFrom.instant + .toLocalDateTime(TimeZone.UTC) + ) + } + } val rawPublicKey get() = tbsCertificate.rawPublicKey val decodedPublicKey get() = tbsCertificate.decodedPublicKey diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt index 6b273f98b..fe3911b2e 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt @@ -25,17 +25,7 @@ class TimeValidityValidator( currCert: X509Certificate, checkedCriticalExtensions: MutableSet ) { - if (currCert.isExpired(date)) { - throw CertificateValidityException( - "certificate expired on " + currCert.tbsCertificate.validUntil.instant.toLocalDateTime(TimeZone.UTC) - ) - } - - if (currCert.isNotYetValid(date)) { - throw CertificateValidityException( - "certificate not valid till " + currCert.tbsCertificate.validFrom.instant.toLocalDateTime(TimeZone.UTC) - ) - } + currCert.checkValidityAt(date) if (currentCertIndex < certChain.lastIndex) { val childCert = certChain[currentCertIndex + 1] diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt index 271988a9d..1a9bf68f9 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt @@ -2,15 +2,14 @@ package at.asitplus.signum.supreme.validate import at.asitplus.signum.CertificateValidityException import at.asitplus.signum.ExperimentalPkiApi +import at.asitplus.signum.TrustAnchorException import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate -import at.asitplus.signum.indispensable.pki.validate.BasicConstraintsValidator import at.asitplus.signum.indispensable.pki.validate.CertificateValidator import at.asitplus.signum.indispensable.pki.validate.checkCaBasicConstraints import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import org.kotlincrypto.error.CertificateException import kotlin.time.Instant /** @@ -47,7 +46,7 @@ class TrustAnchorValidator( val nextIssuerKey = nextCert.decodedPublicKey.getOrThrow() if (anchorKey != nextIssuerKey) { - throw CertificateException("Untrusted certificate: trust anchor key mismatch.") + throw TrustAnchorException("Untrusted certificate: trust anchor key mismatch.") } } @@ -56,29 +55,11 @@ class TrustAnchorValidator( issuingAnchor.cert?.let { checkCaBasicConstraints(it) } - issuingAnchor.cert?.let { - if (it.isExpired(date)) { - throw CertificateValidityException( - "certificate expired on " + currCert.tbsCertificate.validUntil.instant.toLocalDateTime( - TimeZone.currentSystemDefault() - ) - ) - } - } - - issuingAnchor.cert?.let { - if (it.isNotYetValid(date)) { - throw CertificateValidityException( - "certificate not valid till " + currCert.tbsCertificate.validFrom.instant.toLocalDateTime( - TimeZone.currentSystemDefault() - ) - ) - } - } + issuingAnchor.cert?.let { it.checkValidityAt(date) } } if (currentCertIndex == certChain.lastIndex && !foundTrusted) { - throw CertificateException("No trusted issuer found in the chain.") + throw TrustAnchorException("No trusted issuer found in the chain.") } currentCertIndex++ From 87136bcc03670735eebc4bfcf10c020dd73b55d9 Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Tue, 13 Jan 2026 13:14:04 +0100 Subject: [PATCH 6/9] improve exceptions and polish for loop in validate() --- .../kotlin/at/asitplus/signum/Throwables.kt | 24 +++++++++++++++-- .../pki/validate/BasicConstraintsValidator.kt | 14 ++++++---- .../pki/validate/CertValidityValidator.kt | 27 ++++++++----------- .../pki/validate/KeyIdentifierValidator.kt | 23 +++++++++------- .../pki/validate/NameConstraintsValidator.kt | 11 ++++---- .../pki/validate/TimeValidityValidator.kt | 7 ++--- .../validate/CertificateChainValidator.kt | 7 +++-- .../signum/supreme/validate/TrustAnchor.kt | 14 +++++++--- .../supreme/validate/TrustAnchorValidator.kt | 10 +++---- .../supreme/validate/ValidationApiTest.kt | 10 +++---- supreme/src/jvmTest/resources/NIST-PKITS.json | 10 +++---- 11 files changed, 89 insertions(+), 68 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt index 885c653e2..b19dba436 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/Throwables.kt @@ -6,9 +6,29 @@ open class UnsupportedCryptoException(message: String? = null, cause: Throwable? open class CertificateException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) class CertificateChainValidatorException(message: String) : CertificateException(message) +sealed class CertificateValidityException(message: String) : CertificateException(message) +class CertificateSerialNumberException(message: String) : CertificateValidityException(message) +class SanNotCriticalWithEmptySubjectException(message: String) : CertificateValidityException(message) class KeyUsageException(message: String) : CertificateException(message) class ExtendedKeyUsageException(message: String) : CertificateException(message) -class CertificateValidityException(message: String) : CertificateException(message) -class BasicConstraintsException(message: String) : CertificateException(message) +sealed class CertificateTimeValidityException(message: String) : CertificateException(message) +class CertificateNotYetValidException(message: String) : CertificateTimeValidityException(message) +class CertificateExpiredException(message: String) : CertificateTimeValidityException(message) +class InvalidCertificateValidityPeriodException(message: String) : CertificateTimeValidityException(message) +sealed class BasicConstraintsException(message: String) : CertificateException(message) +class MissingBasicConstraintsException(message: String) : BasicConstraintsException(message) +class NonCriticalBasicConstraintsException(message: String) : BasicConstraintsException(message) +class MissingCaFlagException(message: String) : BasicConstraintsException(message) +class PathLenConstraintViolationException(message: String) : BasicConstraintsException(message) + class NameConstraintsException(message: String) : CertificateException(message) +class GeneralNameException(message: String) : CertificateException(message) class CertificatePolicyException(message: String) : CertificateException(message) +sealed class TrustAnchorException(message: String) : CertificateException(message) +class NoTrustedIssuerFoundException(message: String) : TrustAnchorException(message) +class TrustAnchorKeyMismatchException(message: String) : TrustAnchorException(message) +sealed class KeyIdentifierException(message: String) : CertificateException(message) +class MissingSubjectKeyIdentifierException(message: String) : KeyIdentifierException(message) +class CriticalSubjectKeyIdentifierException(message: String) : KeyIdentifierException(message) +class MissingAuthorityKeyIdentifierException(message: String) : KeyIdentifierException(message) +class CriticalAuthorityKeyIdentifierException(message: String) : KeyIdentifierException(message) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt index 5075ae267..db42b089b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt @@ -2,6 +2,10 @@ package at.asitplus.signum.indispensable.pki.validate import at.asitplus.signum.BasicConstraintsException import at.asitplus.signum.ExperimentalPkiApi +import at.asitplus.signum.MissingBasicConstraintsException +import at.asitplus.signum.MissingCaFlagException +import at.asitplus.signum.NonCriticalBasicConstraintsException +import at.asitplus.signum.PathLenConstraintViolationException import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.pki.X509Certificate @@ -24,13 +28,13 @@ class BasicConstraintsValidator( currentCertIndex++ val basicConstraints = currCert.findExtension() - ?: throw BasicConstraintsException("Missing basicConstraints extension at cert index $currentCertIndex.") + ?: throw MissingBasicConstraintsException("Missing basicConstraints extension at cert index $currentCertIndex.") checkCaBasicConstraints(currCert, currentCertIndex) if (remainingPathLength != null && !currCert.isSelfIssued) { if (remainingPathLength?.toInt() == 0) { - throw BasicConstraintsException("pathLenConstraint violated at cert index $currentCertIndex.") + throw PathLenConstraintViolationException("pathLenConstraint violated at cert index $currentCertIndex.") } remainingPathLength = remainingPathLength?.minus(1u) } @@ -47,13 +51,13 @@ class BasicConstraintsValidator( fun checkCaBasicConstraints(cert: X509Certificate, certIndex: Int = 0) { val location = "at ${if (certIndex == 0) "trust anchor" else "cert index $certIndex."}" val basicConstraints = cert.findExtension() - ?: throw BasicConstraintsException("Missing basicConstraints extension $location") + ?: throw MissingBasicConstraintsException("Missing basicConstraints extension $location") if(!basicConstraints.critical) { - throw BasicConstraintsException("basicConstraints extension must be critical $location") + throw NonCriticalBasicConstraintsException("basicConstraints extension must be critical $location") } if (!basicConstraints.ca) { - throw BasicConstraintsException("Missing CA flag $location") + throw MissingCaFlagException("Missing CA flag $location") } } \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt index 9cf1822af..1beb6a592 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertValidityValidator.kt @@ -1,18 +1,13 @@ package at.asitplus.signum.indispensable.pki.validate -import at.asitplus.signum.CertificateChainValidatorException -import at.asitplus.signum.CertificateValidityException +import at.asitplus.signum.CertificateSerialNumberException import at.asitplus.signum.ExperimentalPkiApi -import at.asitplus.signum.indispensable.asn1.Asn1StructuralException +import at.asitplus.signum.InvalidCertificateValidityPeriodException +import at.asitplus.signum.SanNotCriticalWithEmptySubjectException import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.asn1.subjectAltName_2_5_29_17 import at.asitplus.signum.indispensable.pki.X509Certificate -import at.asitplus.signum.indispensable.pki.pkiExtensions.AuthorityKeyIdentifierExtension -import at.asitplus.signum.indispensable.pki.pkiExtensions.SubjectKeyIdentifierExtension -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import kotlin.time.Instant /** * Checks whether the certificate is constructed correctly, since some components are decoded too leniently @@ -29,24 +24,24 @@ class CertValidityValidator: CertificateValidator { checkTimeValidity(currCert) } - @Throws(Asn1StructuralException::class) + @Throws(CertificateSerialNumberException::class) fun checkSerialNumber(cert: X509Certificate) { - if (cert.tbsCertificate.serialNumber.size > 20) throw Asn1StructuralException("Serial number too long") - if (cert.tbsCertificate.serialNumber[0] < 0) throw Asn1StructuralException("Serial number must be positive") - if (cert.tbsCertificate.serialNumber.all { it == 0.toByte() }) throw Asn1StructuralException("Serial number must not be zero") + if (cert.tbsCertificate.serialNumber.size > 20) throw CertificateSerialNumberException("Serial number too long") + if (cert.tbsCertificate.serialNumber[0] < 0) throw CertificateSerialNumberException("Serial number must be positive") + if (cert.tbsCertificate.serialNumber.all { it == 0.toByte() }) throw CertificateSerialNumberException("Serial number must not be zero") } - @Throws(CertificateChainValidatorException::class) + @Throws(SanNotCriticalWithEmptySubjectException::class) private fun isSanCriticalWhenNameIsEmpty(cert: X509Certificate) { val sanExtension = cert.tbsCertificate.extensions?.find { it.oid == KnownOIDs.subjectAltName_2_5_29_17 } if (cert.tbsCertificate.subjectName.relativeDistinguishedNames.isEmpty() && sanExtension?.critical == false) - throw CertificateChainValidatorException("SAN extension is not critical, which is required when subject is empty.") + throw SanNotCriticalWithEmptySubjectException("SAN extension is not critical, which is required when subject is empty.") } - @Throws(Asn1StructuralException::class) + @Throws(InvalidCertificateValidityPeriodException::class) private fun checkTimeValidity(cert: X509Certificate) { if (cert.tbsCertificate.validFrom.instant > cert.tbsCertificate.validUntil.instant) - throw Asn1StructuralException("notBefore is later then notAfter.") + throw InvalidCertificateValidityPeriodException("notBefore is later then notAfter.") } } \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt index 36028333b..0850f5b8b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt @@ -1,15 +1,17 @@ package at.asitplus.signum.indispensable.pki.validate -import at.asitplus.signum.CertificateChainValidatorException +import at.asitplus.signum.CriticalAuthorityKeyIdentifierException +import at.asitplus.signum.CriticalSubjectKeyIdentifierException +import at.asitplus.signum.KeyIdentifierException import at.asitplus.signum.ExperimentalPkiApi +import at.asitplus.signum.MissingAuthorityKeyIdentifierException +import at.asitplus.signum.MissingSubjectKeyIdentifierException import at.asitplus.signum.indispensable.asn1.ObjectIdentifier -import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.pkiExtensions.AuthorityKeyIdentifierExtension import at.asitplus.signum.indispensable.pki.pkiExtensions.SubjectKeyIdentifierExtension class KeyIdentifierValidator( - private val certChain: CertificateChain, private var currentCertIndex: Int = 0 ): CertificateValidator { @ExperimentalPkiApi @@ -20,25 +22,26 @@ class KeyIdentifierValidator( currentCertIndex++ checkSubjectKeyIdentifier(currCert) } - + @Throws(KeyIdentifierException::class) fun checkTrustAnchorAndChild(trustAnchor: X509Certificate?, childCert: X509Certificate) { trustAnchor?.findExtension().let { - if (trustAnchor?.isSelfIssued == false && it == null) throw CertificateChainValidatorException("Missing AuthorityKeyIdentifier extension in Trust Anchor.") - if (it?.critical == true) throw CertificateChainValidatorException("Trust Anchor must mark AuthorityKeyIdentifier as non-critical") + if (trustAnchor?.isSelfIssued == false && it == null) throw MissingAuthorityKeyIdentifierException("Missing AuthorityKeyIdentifier extension in Trust Anchor.") + if (it?.critical == true) throw CriticalAuthorityKeyIdentifierException("Trust Anchor must mark AuthorityKeyIdentifier as non-critical") } trustAnchor?.let{ checkSubjectKeyIdentifier(it) } childCert.findExtension(). let{ - if (it == null) throw CertificateChainValidatorException("Missing AuthorityKeyIdentifier extension in certificate.") - if (it.critical) throw CertificateChainValidatorException("Conforming CAs must mark AuthorityKeyIdentifier as non-critical") + if (it == null) throw MissingAuthorityKeyIdentifierException("Missing AuthorityKeyIdentifier extension in certificate.") + if (it.critical) throw CriticalAuthorityKeyIdentifierException("Conforming CAs must mark AuthorityKeyIdentifier as non-critical") } } + @Throws(KeyIdentifierException::class) private fun checkSubjectKeyIdentifier(cert: X509Certificate) { cert.findExtension().let { - if (it == null) throw CertificateChainValidatorException("Missing SubjectKeyIdentifier extension in certificate at index $currentCertIndex.") - if (it.critical) throw CertificateChainValidatorException("SubjectKeyIdentifier extension must not be critical (index $currentCertIndex).") + if (it == null) throw MissingSubjectKeyIdentifierException("Missing SubjectKeyIdentifier extension in certificate at index $currentCertIndex.") + if (it.critical) throw CriticalSubjectKeyIdentifierException("SubjectKeyIdentifier extension must not be critical (index $currentCertIndex).") } } } \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt index 016190abe..03c6d24fa 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/NameConstraintsValidator.kt @@ -1,11 +1,12 @@ package at.asitplus.signum.indispensable.pki.validate import at.asitplus.signum.CertificateChainValidatorException -import at.asitplus.signum.CertificateValidityException import at.asitplus.signum.ExperimentalPkiApi +import at.asitplus.signum.GeneralNameException import at.asitplus.signum.NameConstraintsException -import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.asn1.nameConstraints_2_5_29_30 import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.pkiExtensions.NameConstraintsExtension @@ -25,11 +26,11 @@ class NameConstraintsValidator( currentCertIndex++ if (previousNameConstraints?.isValid == false) { - throw NameConstraintsException("Invalid GeneralName in NameConstraints extension.") + throw GeneralNameException("Invalid GeneralName in NameConstraints extension.") } // enforcing that all SANs are valid, since our parsing fails softly if (currCert.tbsCertificate.subjectAlternativeNames?.generalNames?.all { it.name.isValid != false } == false) { - throw CertificateValidityException("Invalid GeneralName in Subject Alternative Name at index $currentCertIndex") + throw GeneralNameException("Invalid GeneralName in Subject Alternative Name at index $currentCertIndex") } if (previousNameConstraints != null && (currentCertIndex == certPathLen || !currCert.isSelfIssued)) { @@ -60,7 +61,7 @@ class NameConstraintsValidator( val newNameConstraints = currCert.findExtension() - if (newNameConstraints?.critical == false || previousNameConstraints?.critical == false) throw NameConstraintsException("NameConstraints extension is not critical.") + if (newNameConstraints?.critical == false || previousNameConstraints?.critical == false) throw NameConstraintsException("NameConstraints extension is not critical at cert index $currentCertIndex.") return if (previousNameConstraints == null) { newNameConstraints?.copy() diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt index fe3911b2e..ea7752d6d 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/TimeValidityValidator.kt @@ -1,13 +1,10 @@ package at.asitplus.signum.indispensable.pki.validate import at.asitplus.signum.CertificateChainValidatorException -import at.asitplus.signum.CertificateValidityException import at.asitplus.signum.ExperimentalPkiApi import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import kotlin.time.Instant /** @@ -29,10 +26,10 @@ class TimeValidityValidator( if (currentCertIndex < certChain.lastIndex) { val childCert = certChain[currentCertIndex + 1] + currentCertIndex++ wasCertificateIssuedWithinIssuerValidityPeriod( dateOfIssuance = childCert.tbsCertificate.validFrom.instant, issuer = currCert) - currentCertIndex++ } } @@ -43,7 +40,7 @@ class TimeValidityValidator( val beginValidity = issuer.tbsCertificate.validFrom.instant val endValidity = issuer.tbsCertificate.validUntil.instant if (beginValidity > dateOfIssuance || dateOfIssuance > endValidity) { - throw CertificateChainValidatorException("Certificate issued outside issuer validity period.") + throw CertificateChainValidatorException("Certificate at index $currentCertIndex issued outside issuer validity period.") } } } \ No newline at end of file diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt index f8faabd3a..864e222d6 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt @@ -138,7 +138,7 @@ suspend fun CertificateChain.validate( it.matchesCertificate(this.root) }) this.dropLast(1) else this - val activeValidators = validators.toMutableSet() + val activeValidators = validators.toMutableList() val validatorFailures = mutableListOf() val trustAnchorValidator = activeValidators.filterIsInstance().firstOrNull() val keyIdentifierValidator = activeValidators.filterIsInstance().firstOrNull() @@ -184,8 +184,7 @@ suspend fun CertificateChain.validate( val checkedCriticalExtensions = mutableSetOf() val validatorIterator = activeValidators.iterator() - while (validatorIterator.hasNext()) { - val currValidator = validatorIterator.next() + for (currValidator in validatorIterator) { try { currValidator.check(cert, checkedCriticalExtensions) } catch (e: Throwable) { @@ -274,7 +273,7 @@ private fun defineRFC5280Validators( ChainValidator(processingChain.reversed()), TimeValidityValidator(context.date, certChain = processingChain.reversed()), TrustAnchorValidator(context.trustAnchors, processingChain, date = context.date), - KeyIdentifierValidator(processingChain) + KeyIdentifierValidator() ) } diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchor.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchor.kt index bdc2b93e3..69e2fbb2f 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchor.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchor.kt @@ -1,5 +1,6 @@ package at.asitplus.signum.supreme.validate +import at.asitplus.signum.CertificateChainValidatorException import at.asitplus.signum.HazardousMaterials import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.X509SignatureAlgorithm @@ -50,13 +51,18 @@ sealed class TrustAnchor { } suspend fun isIssuerOf(cert: X509Certificate): Boolean { + if (cert.tbsCertificate.issuerName != principal) return false + + // If trust anchor is certificate based, check issuerUniqueID + this.cert?.let { anchorCert -> + if (cert.tbsCertificate.issuerUniqueID != anchorCert.tbsCertificate.subjectUniqueID) return false + } + val verifier = (cert.signatureAlgorithm as X509SignatureAlgorithm).verifierFor(publicKey).getOrElse { return false } - val signatureValid = verifier.verify( + + return verifier.verify( cert.tbsCertificate.encodeToDer(), cert.decodedSignature.getOrThrow() ).isSuccess - - val issuerName = cert.tbsCertificate.issuerName - return signatureValid && issuerName == principal } } \ No newline at end of file diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt index 1a9bf68f9..dc10044f0 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt @@ -1,15 +1,13 @@ package at.asitplus.signum.supreme.validate -import at.asitplus.signum.CertificateValidityException import at.asitplus.signum.ExperimentalPkiApi -import at.asitplus.signum.TrustAnchorException +import at.asitplus.signum.NoTrustedIssuerFoundException +import at.asitplus.signum.TrustAnchorKeyMismatchException import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.validate.CertificateValidator import at.asitplus.signum.indispensable.pki.validate.checkCaBasicConstraints -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import kotlin.time.Instant /** @@ -46,7 +44,7 @@ class TrustAnchorValidator( val nextIssuerKey = nextCert.decodedPublicKey.getOrThrow() if (anchorKey != nextIssuerKey) { - throw TrustAnchorException("Untrusted certificate: trust anchor key mismatch.") + throw TrustAnchorKeyMismatchException("Public key of certificate at index ${currentCertIndex + 1} does not match the issuing trust anchor.") } } @@ -59,7 +57,7 @@ class TrustAnchorValidator( } if (currentCertIndex == certChain.lastIndex && !foundTrusted) { - throw TrustAnchorException("No trusted issuer found in the chain.") + throw NoTrustedIssuerFoundException("No trusted issuer found in the trust anchor chain.") } currentCertIndex++ diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt index 1c8c809a1..c2aa53e5b 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/ValidationApiTest.kt @@ -1,7 +1,7 @@ package at.asitplus.signum.supreme.validate -import at.asitplus.signum.CertificateValidityException import at.asitplus.signum.ExperimentalPkiApi +import at.asitplus.signum.InvalidCertificateValidityPeriodException import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate @@ -128,14 +128,12 @@ val ValidationApiTest by testSuite{ validators } - val r = chain.validate( + chain.validate( customValidatorFactory, CertificateValidationContext( trustAnchors = setOf(TrustAnchor.Certificate(chain.root)) ) - ) - - r.isValid shouldBe true + ).isValid shouldBe true } } @@ -150,7 +148,7 @@ class AttestationTimeValidator( checkedCriticalExtensions: MutableSet ) { if (currCert != certChain.leaf) { - if (!currCert.isValidAt(currCert.tbsCertificate.validUntil.instant)) throw CertificateValidityException("Certificate is not valid") + if (!currCert.isValidAt(currCert.tbsCertificate.validUntil.instant)) throw InvalidCertificateValidityPeriodException("Certificate is not valid") } } diff --git a/supreme/src/jvmTest/resources/NIST-PKITS.json b/supreme/src/jvmTest/resources/NIST-PKITS.json index 856838575..cb4f82ac7 100644 --- a/supreme/src/jvmTest/resources/NIST-PKITS.json +++ b/supreme/src/jvmTest/resources/NIST-PKITS.json @@ -1185,7 +1185,7 @@ "leaf": "-----BEGIN CERTIFICATE-----\nMIIDfzCCAmegAwIBAgIBATANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQGEwJVUzEf\nMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEWMBQGA1UEAxMNQmFkIFNp\nZ25lZCBDQTAeFw0xMDAxMDEwODMwMDBaFw0zMDEyMzEwODMwMDBaMFMxCzAJBgNV\nBAYTAlVTMR8wHQYDVQQKExZUZXN0IENlcnRpZmljYXRlcyAyMDExMSMwIQYDVQQD\nExpJbnZhbGlkIENBIFNpZ25hdHVyZSBUZXN0MjCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAJpto6V//mi4jC6Q4e/y1J79mi//TInYf0KaP3Aw5rsaMhC8\nIJbEAdkIhfj6Skvg6uPj6gJB0zsmN9Tf8NVArEfcRwjAq7IvvDtVrDz6I5Ie0bFD\n8MGkcdP0otJIpbTTHAcefxoqQQeTCt39X9viZokrLeBHWnB4L6zE4d05f/7Wh8ca\nKlDiKjRQl8aIqj8wPsu4MlVC4jnSYTQ6wEd6cA3gJRygcUWkaLaVleaV+PYyUqD7\n8X7p3+i16q1DzgtL9yXUP1b0rt48UWqfGBiIkTNUB0zN9heA7GyI/ilo1Ga8THpN\npha2SLqNB1rMWl3T9fRTsu0HGKRHZobhzGi7zJsCAwEAAaNrMGkwHwYDVR0jBBgw\nFoAUe90QO0rgyN1EhU6IPFqLzZkik68wHQYDVR0OBBYEFK0h9T1WA6JYJDOopjeq\nQZiYko4FMA4GA1UdDwEB/wQEAwIE8DAXBgNVHSAEEDAOMAwGCmCGSAFlAwIBMAEw\nDQYJKoZIhvcNAQELBQADggEBAJKcaDXNDhjePhSydqe+QrUBUS0YNqamRezwkiQ/\nNoKO8CMTNvta/bbq/tIYarPtac6n4d0rTuHURuCSC1l6VsruAWsNgn6Ja+G3nqeV\nMP8WM5XYIToIPHy8OMzCXfu04IV2dAuaX4igWL3hn0PXrh7JnrTwfnK5ytSbzxbh\nIWcFLpSqxL0XzEAfJz24325SbAyXqvwx+McF5UU34JHgEUQcvTUTXjr2Xp1i5moV\n68a0CjqYABdZ2pDpHAWoFzvVrcv7o93Y+/DbR+5dVpuN2184q7cJcoez/c7aSp8W\nA7R6Ggi+lJEic2RkktRRZTjh0oMM0ndj5c9Ya7CGfEN3gEY=\n-----END CERTIFICATE-----", "isSuccessful": false, "failedValidator": "TrustAnchorValidator", - "errorMessage": "No trusted issuer found in the chain.", + "errorMessage": "No trusted issuer found in the trust anchor chain.", "explicitPolicyRequired": false, "initialPolicies": [], "anyPolicyInhibited": false, "policyMappingInhibited": false @@ -1213,7 +1213,7 @@ "leaf": "-----BEGIN CERTIFICATE-----\nMIIDjTCCAnWgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBAMQswCQYDVQQGEwJVUzEf\nMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEQMA4GA1UEAxMHR29vZCBD\nQTAeFw00NzAxMDExMjAxMDBaFw00OTAxMDExMjAxMDBaMGcxCzAJBgNVBAYTAlVT\nMR8wHQYDVQQKExZUZXN0IENlcnRpZmljYXRlcyAyMDExMTcwNQYDVQQDEy5JbnZh\nbGlkIEVFIG5vdEJlZm9yZSBEYXRlIEVFIENlcnRpZmljYXRlIFRlc3QyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc20YJ/j2rSNlGJ/J9rqsxZKwQvD\n+f2lCf9yV6nowMsJGRYdX5ERKWoqW29xpRaseNUf38roqv9TiBy5bXzOoJ3a6zfN\nSvhWPf//uk4zP5fBAEwq7VL8+UrpZBsKpbTaVIvIeOfTpvWr9qW1N9J1aH0Y5B8D\nVsFsdzrGc7rjbDvb3bz2bymkDKGW2A4XClecaAIGJiJOguEuYMhq4B5tndQ0cAQN\nQcDXS9li//HO3vlYyiRYv40hZwaTt41m5cQ21xTfFOa/ORsCa96sAL2TR64sT/u4\nDWck/9kh3qxsw9gRSkRydr1xOX3HjA6gYUl7nv04OmUSHwf/w590ry4elwIDAQAB\no2swaTAfBgNVHSMEGDAWgBRYAYQkG7wrUpRKPaUQchRR9a86yTAdBgNVHQ4EFgQU\nmXSnPUJsL24xEXiVXHTV4VzB9nIwDgYDVR0PAQH/BAQDAgTwMBcGA1UdIAQQMA4w\nDAYKYIZIAWUDAgEwATANBgkqhkiG9w0BAQsFAAOCAQEAPSuzpUlBOhAZ8lTzHWdq\nZoo5cC8+mX8vg3YOTm02idO47H/HcqUjQMJZaUr/gsnr0dABy2kuiU802/JbLp3i\njLGTygmGtU7Wqj2t9IEgDeZdQflrYfaCm8rPqNeiwrQb0Mw52dXLMz3YFLqG1BAG\nFqrxg2utLDQfstLrpMs2BHXsSxSBq6ad7BS32qRweNr1KXQJ4QNnRGFrOARTMxi6\nMBY3zCiMJWYtgtV6aCt9t/q1kt1NFCkc8CzFdpgV8/rz4poS4FNweXhT+RqyF7UT\nPBzHycgL90/QG2N6LlTHxux9ElHQYy8HwwxlJoE+lPv89DSLNwra1mQ8MJMu7h9+\nqg==\n-----END CERTIFICATE-----", "isSuccessful": false, "failedValidator": "TimeValidityValidator", - "errorMessage": "Certificate issued outside issuer validity period.", + "errorMessage": "Certificate at index 1 issued outside issuer validity period.", "explicitPolicyRequired": false, "initialPolicies": [], "anyPolicyInhibited": false, "policyMappingInhibited": false @@ -1227,7 +1227,7 @@ "leaf": "-----BEGIN CERTIFICATE-----\nMIIDlDCCAnygAwIBAgIBBDANBgkqhkiG9w0BAQsFADBAMQswCQYDVQQGEwJVUzEf\nMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEQMA4GA1UEAxMHR29vZCBD\nQTAeFw01MDAxMDExMjAxMDBaFw0zMDEyMzEwODMwMDBaMG4xCzAJBgNVBAYTAlVT\nMR8wHQYDVQQKExZUZXN0IENlcnRpZmljYXRlcyAyMDExMT4wPAYDVQQDEzVWYWxp\nZCBwcmUyMDAwIFVUQyBub3RCZWZvcmUgRGF0ZSBFRSBDZXJ0aWZpY2F0ZSBUZXN0\nMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALOY1i2dfQm7MR3F7FEB\neUvqhyz0RVOJ5W9ffzQpg8puuROsdevN68qfhN/GTIHMLmeQeEv56Zsl1hRIKMqB\nhDpOvSNoaAeZUGJpAaMBzEdEE0KyVlh70u2IaKk3JYDzsWhmbN4ESXFGpmDccsfZ\n8kFZo+XTapSeFEc5ETbyVIV7HHPE54zPX9Ce67kJI0e41R5hsXUKFBA7ORycbcFh\n8c8sN3mDYhFFX8m7a2qTN6oHQ6I1fReKKOCfYDs671bbiyFCQWNl+7Ok19qUVOgQ\nL/g2HyA1YNvueh8ivNNRRwfv6RTPwhuOWF5FyOXRCTzDaS309Lb3TN9d3a3kAoEf\nHdMCAwEAAaNrMGkwHwYDVR0jBBgwFoAUWAGEJBu8K1KUSj2lEHIUUfWvOskwHQYD\nVR0OBBYEFEBvkKgDjazQ6vxQ2V37VnUlYMSbMA4GA1UdDwEB/wQEAwIE8DAXBgNV\nHSAEEDAOMAwGCmCGSAFlAwIBMAEwDQYJKoZIhvcNAQELBQADggEBABDntMraC1zS\nkjUQmdbhI+l1j8Is7ApNjb03/Hgq8pjDxP2VNPEy9XgXYDOHaAmt7p7jDYMwBxzV\n7TSnFIvR3kYjb5k7YhCMIQXkJJgM2QvZ8m0B5c9YJI6qktAp2sxPfzLuBg7fm4Oe\nBqQ6f2NlmLorDqhXG3QSJmXWRXMxti4rWz4mJfuWuzPdZERE9bJ118ijfksQjGfu\npjoWizxTCt8kRMP9+RSD8Hzipuxfc2JPn16fNrXMkyBtek82L7tNo1raLueyPcEg\nZ5RpwEX/C4nlsQV3JS2viDxhcdtgcmn/A/ho7Ta4QazDqWjQywXExpEpgrp7ExFn\nhiss7AFKddQ=\n-----END CERTIFICATE-----", "isSuccessful": false, "failedValidator": "TimeValidityValidator", - "errorMessage": "Certificate issued outside issuer validity period.", + "errorMessage": "Certificate at index 1 issued outside issuer validity period.", "explicitPolicyRequired": false, "initialPolicies": [], "anyPolicyInhibited": false, "policyMappingInhibited": false @@ -1241,7 +1241,7 @@ "leaf":"-----BEGIN CERTIFICATE-----\nMIIDmjCCAoKgAwIBAgIBBTANBgkqhkiG9w0BAQsFADBAMQswCQYDVQQGEwJVUzEf\nMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEQMA4GA1UEAxMHR29vZCBD\nQTAgGA8yMDAyMDEwMTEyMDEwMFoXDTMwMTIzMTA4MzAwMFowcjELMAkGA1UEBhMC\nVVMxHzAdBgNVBAoTFlRlc3QgQ2VydGlmaWNhdGVzIDIwMTExQjBABgNVBAMTOVZh\nbGlkIEdlbmVyYWxpemVkVGltZSBub3RCZWZvcmUgRGF0ZSBFRSBDZXJ0aWZpY2F0\nZSBUZXN0NDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0TXsIKGzxY\niX5lX1bWQb/MZ9pouA3EQT8bxiWbQe3iWrZE5xIU4jFopRL8RXASP+lQIROJWqjT\nGtZhEaNQyB9CmNSz5w3Q/dME6LPrdiUF63klD3lY/J1cJheLov0Ql9Rshvrcjluq\nxR0aMebihrCVAbGB9fjapEf4D8xtm/l5SMvO7olLyDZKkibHgmzgZ66/ebGPrc3o\nOmxjoqvkyyH+vlRQgfv4oAVFetdVyYB8i2eJp47d2aFGTsWmn6zIoM7fy4iuu0kK\n07KGV+RGIif3sHoWXv2Uahz56Y7MGr135s/S8e80jA4KrfQpzqpN07ncyEtFGEs5\n3WeTcZi5zLsCAwEAAaNrMGkwHwYDVR0jBBgwFoAUWAGEJBu8K1KUSj2lEHIUUfWv\nOskwHQYDVR0OBBYEFM3r3nHYztXgRqo9a88ywemUwz7vMA4GA1UdDwEB/wQEAwIE\n8DAXBgNVHSAEEDAOMAwGCmCGSAFlAwIBMAEwDQYJKoZIhvcNAQELBQADggEBAHWf\nJSZe+DwVSi+fwy98C4I5bZrrkWUX5P3ffOQkO1NrPjVURAvXqyTXbzYcf+PM5W+k\nJ6XD2jJvNCRNQ2R8AIdVbG1fIfAzBR3PhEZPL9qKhi2H1q2IloF1Kw26ghxS5cAF\ncfwkOyQgNUpyFp9kKT2OE+3GM6/zf8SVy0/bFL6Rf/5yrJ273Of0+ymy2CY/irTM\n/4X7WLSkSGyPz9RHiT+LoRSel59eclRDxQKXgyToiTgGTKXbEFcilTBT6+0WCgcJ\n3Lw5qc+s12lQDFF8T903ef6C77dPOYMcxnCD2WlBInOSoqsGhn8NDf6YQYsDFCbl\nOj2T6kRdn8YUigjmrhM=\n-----END CERTIFICATE-----", "isSuccessful": false, "failedValidator": "TimeValidityValidator", - "errorMessage": "Certificate issued outside issuer validity period.", + "errorMessage": "Certificate at index 1 issued outside issuer validity period.", "explicitPolicyRequired": false, "initialPolicies": [], "anyPolicyInhibited": false, "policyMappingInhibited": false @@ -1283,7 +1283,7 @@ "leaf": "-----BEGIN CERTIFICATE-----\nMIIDmjCCAoKgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBAMQswCQYDVQQGEwJVUzEf\nMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEQMA4GA1UEAxMHR29vZCBD\nQTAgGA8xOTk3MDEwMTEyMDEwMFoXDTk5MDEwMTEyMDEwMFowcjELMAkGA1UEBhMC\nVVMxHzAdBgNVBAoTFlRlc3QgQ2VydGlmaWNhdGVzIDIwMTExQjBABgNVBAMTOUlu\ndmFsaWQgcHJlMjAwMCBVVEMgRUUgbm90QWZ0ZXIgRGF0ZSBFRSBDZXJ0aWZpY2F0\nZSBUZXN0NzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ8k5yDE7wMf\nwlOhyZcxb9+PlGIC417Lq+G5CDDQQGbytGuZKYQR3+0AhiEUO7YwxfIeOtE4YOBl\nQsZsPUSOgSCb2l4s2rbH7V5QFfRcf8f3RQVDjMqhFXvIIWW5uys1poKMqXVDLw3x\ng3ysL2kVl/zORmqI3obmehIa2m1EUHR3jY++I43rJnFUsTvTNGKsE7HLTpkLDABH\nwptY+Ztt7J+sMc5w/pXweCkYLSdulazQ0EKwjDdmWS4BE/3CLIeQkTCB/CTkkseg\ndHPaD18xeV+4LLjAB2dsAmIwAnRiI+LouvCaF52pVBedX5cg6xMPTz1XfLzX3SkL\n3hj4LgUtuJMCAwEAAaNrMGkwHwYDVR0jBBgwFoAUWAGEJBu8K1KUSj2lEHIUUfWv\nOskwHQYDVR0OBBYEFNUChZ5TMCZHiJZ+TyOTFmSU8xivMA4GA1UdDwEB/wQEAwIE\n8DAXBgNVHSAEEDAOMAwGCmCGSAFlAwIBMAEwDQYJKoZIhvcNAQELBQADggEBABiL\nKJrH+iYNkDzEmwMr9Iebl4gc2uzCObyDkbaQ/UeuxbCvfUm3x1vYbty3HNEKp+tY\nJBXAEDh2GbHU1cLzhP+tNTYQuqgWzE86ZOKWLYI7wUkezHwL00wtOYI8RQfAScGV\nOySp2qwKgeKjXUAd/BVa6FVOzXkHavXNy903ssmko4d9sD+yb9FFY9UrlmIhXewl\nlR2rcUDtHT0dH5PJf7arPoGTRoePSFWeY6D7IBNuXfuhB3WEj+UQzzoANelmavIL\nVwM88Eszmwmofr2sbWi16b6z7XAKbwQm7n4hrwOd3vYTO/zRM0KBB9Y27p/trFLL\n6CmNjRghrN1erPNL1Z0=\n-----END CERTIFICATE-----", "isSuccessful": false, "failedValidator": "TimeValidityValidator", - "errorMessage": "Certificate issued outside issuer validity period.", + "errorMessage": "Certificate at index 1 issued outside issuer validity period.", "explicitPolicyRequired": false, "initialPolicies": [], "anyPolicyInhibited": false, "policyMappingInhibited": false From 918c0e9178746204fe6f1346a1934df701ce08d5 Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Wed, 14 Jan 2026 11:57:26 +0100 Subject: [PATCH 7/9] improve AKI checks in trustAnchorValidator --- .../pki/validate/BasicConstraintsValidator.kt | 4 +- .../pki/validate/KeyIdentifierValidator.kt | 36 ++-- .../validate/CertificateChainValidator.kt | 16 -- .../supreme/validate/TrustAnchorValidator.kt | 5 +- .../validate/AllowIncludedTrustAnchorTest.kt | 180 +----------------- .../signum/supreme/validate/LimboTests.kt | 7 +- 6 files changed, 36 insertions(+), 212 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt index db42b089b..b1ec2cb59 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/BasicConstraintsValidator.kt @@ -48,8 +48,8 @@ class BasicConstraintsValidator( } @Throws(BasicConstraintsException::class) -fun checkCaBasicConstraints(cert: X509Certificate, certIndex: Int = 0) { - val location = "at ${if (certIndex == 0) "trust anchor" else "cert index $certIndex."}" +fun checkCaBasicConstraints(cert: X509Certificate, certIndex: Int? = null) { + val location = certIndex?.let { "at cert index $it." } ?: "at trust anchor" val basicConstraints = cert.findExtension() ?: throw MissingBasicConstraintsException("Missing basicConstraints extension $location") diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt index 0850f5b8b..96e6e03c4 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/KeyIdentifierValidator.kt @@ -22,26 +22,28 @@ class KeyIdentifierValidator( currentCertIndex++ checkSubjectKeyIdentifier(currCert) } - @Throws(KeyIdentifierException::class) - fun checkTrustAnchorAndChild(trustAnchor: X509Certificate?, childCert: X509Certificate) { - trustAnchor?.findExtension().let { - if (trustAnchor?.isSelfIssued == false && it == null) throw MissingAuthorityKeyIdentifierException("Missing AuthorityKeyIdentifier extension in Trust Anchor.") - if (it?.critical == true) throw CriticalAuthorityKeyIdentifierException("Trust Anchor must mark AuthorityKeyIdentifier as non-critical") - } +} - trustAnchor?.let{ checkSubjectKeyIdentifier(it) } +@Throws(KeyIdentifierException::class) +fun checkTrustAnchorAndChild(trustAnchor: X509Certificate?, childCert: X509Certificate) { + trustAnchor?.findExtension().let { + if (trustAnchor?.isSelfIssued == false && it == null) throw MissingAuthorityKeyIdentifierException("Missing AuthorityKeyIdentifier extension in Trust Anchor.") + if (it?.critical == true) throw CriticalAuthorityKeyIdentifierException("Trust Anchor must mark AuthorityKeyIdentifier as non-critical") + } + + trustAnchor?.let{ checkSubjectKeyIdentifier(it) } - childCert.findExtension(). let{ - if (it == null) throw MissingAuthorityKeyIdentifierException("Missing AuthorityKeyIdentifier extension in certificate.") - if (it.critical) throw CriticalAuthorityKeyIdentifierException("Conforming CAs must mark AuthorityKeyIdentifier as non-critical") - } + childCert.findExtension(). let{ + if (it == null) throw MissingAuthorityKeyIdentifierException("Missing AuthorityKeyIdentifier extension in certificate.") + if (it.critical) throw CriticalAuthorityKeyIdentifierException("Conforming CAs must mark AuthorityKeyIdentifier as non-critical") } +} - @Throws(KeyIdentifierException::class) - private fun checkSubjectKeyIdentifier(cert: X509Certificate) { - cert.findExtension().let { - if (it == null) throw MissingSubjectKeyIdentifierException("Missing SubjectKeyIdentifier extension in certificate at index $currentCertIndex.") - if (it.critical) throw CriticalSubjectKeyIdentifierException("SubjectKeyIdentifier extension must not be critical (index $currentCertIndex).") - } +@Throws(KeyIdentifierException::class) +private fun checkSubjectKeyIdentifier(cert: X509Certificate, currentCertIndex: Int? = null) { + val location = currentCertIndex?.let { " at index $it" }.orEmpty() + cert.findExtension().let { + if (it == null) throw MissingSubjectKeyIdentifierException("Missing SubjectKeyIdentifier extension in certificate$location.") + if (it.critical) throw CriticalSubjectKeyIdentifierException("SubjectKeyIdentifier extension must not be critical$location.") } } \ No newline at end of file diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt index 864e222d6..98da46120 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/CertificateChainValidator.kt @@ -141,28 +141,12 @@ suspend fun CertificateChain.validate( val activeValidators = validators.toMutableList() val validatorFailures = mutableListOf() val trustAnchorValidator = activeValidators.filterIsInstance().firstOrNull() - val keyIdentifierValidator = activeValidators.filterIsInstance().firstOrNull() val nameConstraintsValidator = activeValidators.filterIsInstance().firstOrNull() trustAnchorValidator?.let { trustAnchorValidator -> catchingUnwrapped { processingChain.forEach { trustAnchorValidator.check(it, it.criticalExtensionOids.toMutableSet()) - if (trustAnchorValidator.foundTrusted) { - catchingUnwrapped { - keyIdentifierValidator?.checkTrustAnchorAndChild(trustAnchorValidator.trustAnchor?.cert, it) - }.onFailure { - validatorFailures.add( - ValidatorFailure( - KeyIdentifierValidator::class.simpleName!!, - keyIdentifierValidator, - it.message ?: "Key Identifier validation failed.", - -1, - it - ) - ) - } - } } }.onFailure { validatorFailures.add( diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt index dc10044f0..80678d38c 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/validate/TrustAnchorValidator.kt @@ -8,6 +8,7 @@ import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.validate.CertificateValidator import at.asitplus.signum.indispensable.pki.validate.checkCaBasicConstraints +import at.asitplus.signum.indispensable.pki.validate.checkTrustAnchorAndChild import kotlin.time.Instant /** @@ -53,7 +54,9 @@ class TrustAnchorValidator( issuingAnchor.cert?.let { checkCaBasicConstraints(it) } - issuingAnchor.cert?.let { it.checkValidityAt(date) } + issuingAnchor.cert?.checkValidityAt(date) + + issuingAnchor.cert?.let { checkTrustAnchorAndChild(it, currCert) } } if (currentCertIndex == certChain.lastIndex && !foundTrusted) { diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/validate/AllowIncludedTrustAnchorTest.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/validate/AllowIncludedTrustAnchorTest.kt index d4a64ae7e..f33e6b9f6 100644 --- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/validate/AllowIncludedTrustAnchorTest.kt +++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/validate/AllowIncludedTrustAnchorTest.kt @@ -12,7 +12,7 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @OptIn(ExperimentalPkiApi::class) -val AllowIncludedTrustAnchorTest by testSuite{ +val AllowIncludedTrustAnchorTest by testSuite { val trustAnchorRootCertificate = "-----BEGIN CERTIFICATE-----\n" + "MIIDRzCCAi+gAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJVUzEf\n" + @@ -37,7 +37,10 @@ val AllowIncludedTrustAnchorTest by testSuite{ val trustAnchorRootCert = X509Certificate.decodeFromPem(trustAnchorRootCertificate).getOrThrow() val trustAnchor = TrustAnchor.Certificate(trustAnchorRootCert) val context = CertificateValidationContext(trustAnchors = setOf(trustAnchor) + SystemTrustStore) - val contextNotAllowedRoot = CertificateValidationContext(trustAnchors = setOf(trustAnchor) + SystemTrustStore, allowIncludedTrustAnchor = false) + val contextNotAllowedRoot = CertificateValidationContext( + trustAnchors = setOf(trustAnchor) + SystemTrustStore, + allowIncludedTrustAnchor = false + ) val pathLenConstraint6CACert = "-----BEGIN CERTIFICATE-----\n" + @@ -63,114 +66,6 @@ val AllowIncludedTrustAnchorTest by testSuite{ "CA==\n" + "-----END CERTIFICATE-----\n" - "Valid pathLenConstraint Test13" { - val pathLenConstraint6subCA4Cert = "-----BEGIN CERTIFICATE-----\n" + - "MIIDmjCCAoKgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEf\n" + - "MB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEeMBwGA1UEAxMVcGF0aExl\n" + - "bkNvbnN0cmFpbnQ2IENBMB4XDTEwMDEwMTA4MzAwMFoXDTMwMTIzMTA4MzAwMFow\n" + - "UjELMAkGA1UEBhMCVVMxHzAdBgNVBAoTFlRlc3QgQ2VydGlmaWNhdGVzIDIwMTEx\n" + - "IjAgBgNVBAMTGXBhdGhMZW5Db25zdHJhaW50NiBzdWJDQTQwggEiMA0GCSqGSIb3\n" + - "DQEBAQUAA4IBDwAwggEKAoIBAQDRaciiXPE9CRnZszi4MJScusSJJBpnJRox4Dpn\n" + - "VJKE86q6HuEUtOzhllVlfQtaSGxhyWD8q8HpeGW7F2lqcblT0VbPyTPDF2DiPt6B\n" + - "yGYw2J9IMmkmdJ3RiXxouq336e1FTJoPCsgwHiK1bne2i4d6L5TFen8p6IFL2XR/\n" + - "lQyug93gOPysThsY14A2JJTNAISkJFtfEiHABPlsMtzST5OkazCJiIkGZw7+pId+\n" + - "jkWMoOc1C4nVTUEJJbyUVYTLEetaJVxU+3tVDr1vmTHXdYu4v6rWpX3IcmEMTM9m\n" + - "4HOqO1c92SWna0WvJLsPRm1y2/H+hhaGgvp83VrdXh/fdHrDAgMBAAGjfzB9MB8G\n" + - "A1UdIwQYMBaAFK+8ha7+TK7hjZcjiMilsWALuk7YMB0GA1UdDgQWBBRJhdtL+xFj\n" + - "2ZkCKLQLep4TF1oVdzAOBgNVHQ8BAf8EBAMCAQYwFwYDVR0gBBAwDjAMBgpghkgB\n" + - "ZQMCATABMBIGA1UdEwEB/wQIMAYBAf8CAQQwDQYJKoZIhvcNAQELBQADggEBAJBm\n" + - "SQWFZgiXsoHVxU/Di8Vz/JG2sTHLr58iyp38gcp+7oM/SrRqdtFe3KoaRb1LBHhK\n" + - "Kwssx+5ukA/ZYIrKRTwv7IFUgdeQgsQDbNtAyxMkKwv2QFrtx1zaS0397wqZRGL2\n" + - "c4ph2EI7F0IzOmzuXuj3leZTiAA1z7m+WopfmN7RxPmFT/8ZouNCUnMxryjqEzm0\n" + - "k2vUuGzd7MLEGHlW2UVR4R/hfSOUTUBcUgC/F/aB07nCJ3Gon8ztvzRIAo7IQ1Vt\n" + - "okYWRHbFapAq5NsEn/Z/AxDGh9kJuSIx8/mz4hcdiKvfbQTztUPBVrq1RmF++rHR\n" + - "fwzHkZ48GGyTH5QbV8Y=\n" + - "-----END CERTIFICATE-----\n" - - val pathLenConstraint6subsubCA41Cert = "-----BEGIN CERTIFICATE-----\n" + - "MIIDojCCAoqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBSMQswCQYDVQQGEwJVUzEf\n" + - "MB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEiMCAGA1UEAxMZcGF0aExl\n" + - "bkNvbnN0cmFpbnQ2IHN1YkNBNDAeFw0xMDAxMDEwODMwMDBaFw0zMDEyMzEwODMw\n" + - "MDBaMFYxCzAJBgNVBAYTAlVTMR8wHQYDVQQKExZUZXN0IENlcnRpZmljYXRlcyAy\n" + - "MDExMSYwJAYDVQQDEx1wYXRoTGVuQ29uc3RyYWludDYgc3Vic3ViQ0E0MTCCASIw\n" + - "DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANhwD2w1qEqflNfYtpGLRSNwCAoE\n" + - "N7//EbgaFJKP4OGaerEZ41IHT4G8x0QOsx2RMRd6gXL9zNMmKhjoCL4Nv2mXksGt\n" + - "crt9dSJE2MaUIQFj/cdJv+WNwoQW8amBjeMSNL7BgB8y71fnO8srWabp6vkIgDr9\n" + - "tY0qmHZ1CLxB848d8C14NbaNGMeWJzI8ri2rXD/0sC/0LtWgg624DjmZWgxb7B8J\n" + - "w1ERSKznjFQ4vv9imV7vJ1GZ2r5V6nBPFnmtK2r2yqAeNReHE3U2syb9lpFyOsjh\n" + - "qTjVQdKUk5N+NAZpCipVuymCdvWcJY3C8FVmjxefqv/cJBu3Kxi7Bf6f1iMCAwEA\n" + - "AaN/MH0wHwYDVR0jBBgwFoAUSYXbS/sRY9mZAii0C3qeExdaFXcwHQYDVR0OBBYE\n" + - "FERapgfP9vPIx0bvZKH1W8E/grxXMA4GA1UdDwEB/wQEAwIBBjAXBgNVHSAEEDAO\n" + - "MAwGCmCGSAFlAwIBMAEwEgYDVR0TAQH/BAgwBgEB/wIBATANBgkqhkiG9w0BAQsF\n" + - "AAOCAQEAtz+rkTNDpvnMjCDzmvVltiLfHURT3X/GipGokbedY89ANtS1dRmNFyDS\n" + - "I1Dh17v8HsW2GR40FCIP4ImbxvPrUAQIASOVUR7iLKwSj99RwK8+Tfd9cUBx5cdA\n" + - "nXm+KGqJ04sBKilEM6kGhA+vxZU8OJ7hck3rFVxNiIvGTZmYPlSLLQqv0X3LcWG9\n" + - "XArnZEKfNH6Ph0LCOzlPsLI3iQ5rHluq3liWMD21LBEftUdpJ0DqzEiWEFHL8PuD\n" + - "AtiGA+cOA0DTakpwjLnd2q1wM0S7HLVaYGSv6HErM594IUZQfWAyyyTpl3CERtH+\n" + - "mlou5h4IgFeitqocNlVfoAX5CzZsvQ==\n" + - "-----END CERTIFICATE-----\n" - - val pathLenConstraint6subsubsubCA41XCert = "-----BEGIN CERTIFICATE-----\n" + - "MIIDpzCCAo+gAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJVUzEf\n" + - "MB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEmMCQGA1UEAxMdcGF0aExl\n" + - "bkNvbnN0cmFpbnQ2IHN1YnN1YkNBNDEwHhcNMTAwMTAxMDgzMDAwWhcNMzAxMjMx\n" + - "MDgzMDAwWjBaMQswCQYDVQQGEwJVUzEfMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0\n" + - "ZXMgMjAxMTEqMCgGA1UEAxMhcGF0aExlbkNvbnN0cmFpbnQ2IHN1YnN1YnN1YkNB\n" + - "NDFYMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6hM3qC08TT0qiiB1\n" + - "AXVRGB8La1zcPFASpIZ0T1pRmbvQVay8/l8gAcVytv6KVDgpjXjXXMjjJMuAZQm6\n" + - "ilT+eC3WrLSzBWUQDzAXTARzERVE3u4woJnBdpcyo4ZlTRGijwzYfbVlrdTNRnX1\n" + - "ET0R9BLIK4qxRYdJvlDXoCQQFb1/58RMs9jK7lxHetuVt1ieeWF/fLRPZ2Qbf/qm\n" + - "MpepaATXRf4Nue57jA3FyUAbvgVg2XnhRRdAEnsM90YRZHOD+XbB4Lhz2Pk6hNDM\n" + - "xfl70rGpDXIOh9UmIYZZ2yegRx/rKDI+3wFAtcGYek4trQGg/1HoTbaKszhIgb1s\n" + - "uJ0EUQIDAQABo3wwejAfBgNVHSMEGDAWgBREWqYHz/bzyMdG72Sh9VvBP4K8VzAd\n" + - "BgNVHQ4EFgQUoe2i8zVUpZ+8Y+ZHalMkbEoMciwwDgYDVR0PAQH/BAQDAgEGMBcG\n" + - "A1UdIAQQMA4wDAYKYIZIAWUDAgEwATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\n" + - "DQEBCwUAA4IBAQBM/xztTxc6ooWAmhUfwjeTeHYxZFBCB81yMwM/agD4C3gTf+vl\n" + - "P1CnTaXFV2aoOYDiXCkA3oL4DViFQKHcuIh6ag5pKlxB38KCH5l87W+xb4NuuyhC\n" + - "/sYzP6PsQr43jiWtzbGRgQ8SFwN/+jX4MQnJE660ab09hgm3LmIkWCa3202nbwvR\n" + - "rL0ILpgkzQs+IgbJY9EAE2cGiapoZ8mjKBq4EwZG6xqjqp8LO2Al8Koa1ofFwnoz\n" + - "sYCgl+oyiP2lTkdMpg9p3gmmqWRnv0qWvfjwxnpH470rFWxL1u5nG1MM/Y2GLi5o\n" + - "+poL/laW4KNTZOgEnijxlvBJiLfhb/mymaMZ\n" + - "-----END CERTIFICATE-----\n" - - val leafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIDpjCCAo6gAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzEf\n" + - "MB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEqMCgGA1UEAxMhcGF0aExl\n" + - "bkNvbnN0cmFpbnQ2IHN1YnN1YnN1YkNBNDFYMB4XDTEwMDEwMTA4MzAwMFoXDTMw\n" + - "MTIzMTA4MzAwMFowZjELMAkGA1UEBhMCVVMxHzAdBgNVBAoTFlRlc3QgQ2VydGlm\n" + - "aWNhdGVzIDIwMTExNjA0BgNVBAMTLVZhbGlkIHBhdGhMZW5Db25zdHJhaW50IEVF\n" + - "IENlcnRpZmljYXRlIFRlc3QxMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n" + - "ggEBANGvSnWvqPuuQwnBzwyPeG9cwJSQU/bvAAFK1V8D6Yb5dGmKu++FJuY3+qmG\n" + - "uzN3K1HFo1C/84fQ0UFNp/6H9r+9yUZ3RRSvr8A4JA0CUUv+mXfbd7+HzCqqyj0i\n" + - "RSreaFuiXaM4QAH56nrfg31qQGb1QC14tpEXlBp8YnGh2PRbbdJQf8JbxMOlhWB9\n" + - "A8+Dk0gwSVqUZ0nPDFy2OLQ3cEQTgjnihXBJqHztLtj4rQkwWbQCNIThrZ+XszkW\n" + - "6rcGygZ7ZACnuuHOG5UK6qxakReo8ZuW079tnFJCPSyPk54dHLlTrsz6LyyBQBKv\n" + - "2PzZAQcnzZWHJLoACwhOWUQ9dz8CAwEAAaNrMGkwHwYDVR0jBBgwFoAUoe2i8zVU\n" + - "pZ+8Y+ZHalMkbEoMciwwHQYDVR0OBBYEFLdJcbHtcyweaP/yNyrJR7L7kfqpMA4G\n" + - "A1UdDwEB/wQEAwIE8DAXBgNVHSAEEDAOMAwGCmCGSAFlAwIBMAEwDQYJKoZIhvcN\n" + - "AQELBQADggEBAJTElUFCT4pcCXUHleZsjeg3hPXNJIWYLr3/MACyrDpD7M4kb1J/\n" + - "v03VUo6r9wFgd2LE8Q9kghkRsQ1BvdGX9FijaPDMZ31kPpU1EelGutxkjU21mz6x\n" + - "lXLb4ql8QBCLDZ+N2mEQ8y/KCYpSkKJK2X5XHQ8LRn0b/QKK64UmF4uZFxKA5Ez8\n" + - "q1bescEhdWiAZBrFv5t+OhSHbRkRtLNB2fHkzkovVrhSgrG/qcsGInWTvF1RnV40\n" + - "iP7VcosKbEynP8p5LQMIjt7d4f+PocFcSIVwgcVfZdwx9MS+nAj29Ynu9vQENFHI\n" + - "xwRm/XT3hYzGswzrl5RhHyyjgAvjxwSaxfU=\n" + - "-----END CERTIFICATE-----" - - val ca = X509Certificate.decodeFromPem(pathLenConstraint6CACert).getOrThrow() - val subCa = X509Certificate.decodeFromPem(pathLenConstraint6subCA4Cert).getOrThrow() - val subSubCa = X509Certificate.decodeFromPem(pathLenConstraint6subsubCA41Cert).getOrThrow() - val subSubSubCa = X509Certificate.decodeFromPem(pathLenConstraint6subsubsubCA41XCert).getOrThrow() - val leaf = X509Certificate.decodeFromPem(leafPem).getOrThrow() - val chain: CertificateChain = listOf(leaf, subSubSubCa, subSubCa, subCa, ca) - val chainWithRoot: CertificateChain = listOf(leaf, subSubSubCa, subSubCa, subCa, ca, trustAnchorRootCert) - val result = chain.validate(context) - val resultWithRoot = chainWithRoot.validate(context) - result.isValid shouldBe true - resultWithRoot.isValid shouldBe true - - chainWithRoot.validate(contextNotAllowedRoot).isValid shouldBe false - } - "Invalid pathLenConstraint Test11" { val pathLenConstraintsubCA1Cert = "-----BEGIN CERTIFICATE-----\n" + "MIIDmjCCAoKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEf\n" + @@ -287,69 +182,4 @@ val AllowIncludedTrustAnchorTest by testSuite{ resultNotAllowed.shouldBeInvalid() resultNotAllowed.validatorFailures.firstOrNull {it.validator is BasicConstraintsValidator}!!.errorMessage shouldBe "pathLenConstraint violated at cert index 5." } - - "Valid DN nameConstraints Test1" { - val leafPem = "-----BEGIN CERTIFICATE-----\n" + - "MIIDuDCCAqCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEf\n" + - "MB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEfMB0GA1UEAxMWbmFtZUNv\n" + - "bnN0cmFpbnRzIEROMSBDQTAeFw0xMDAxMDEwODMwMDBaFw0zMDEyMzEwODMwMDBa\n" + - "MIGCMQswCQYDVQQGEwJVUzEfMB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAx\n" + - "MTEaMBgGA1UECxMRcGVybWl0dGVkU3VidHJlZTExNjA0BgNVBAMTLVZhbGlkIERO\n" + - "IG5hbWVDb25zdHJhaW50cyBFRSBDZXJ0aWZpY2F0ZSBUZXN0MTCCASIwDQYJKoZI\n" + - "hvcNAQEBBQADggEPADCCAQoCggEBAMRVH83Hm46JPU28u6W/BR3a95naRI7hTLko\n" + - "SOFjXkkjx8tjModC1M4QxWpoMZj2AF0Fd4Aykw75HS8W37wb8UjFS3SrXQ66qVwY\n" + - "h0CDz8mNOvtGSanquco3tZeMqoDe8/EXhotzwnf5F83cyp7XB7OiEQPzbplkD4jQ\n" + - "GZ5d/SF0lBUdhxilqRlXSemd1kfGop73BOGmgqkQhP+I+4ZZVwxUO5pXGzRpfGK+\n" + - "h8ZiOM/JAxpmhdaU1HC6Zf4YDqA2XpKZKQ232myfX4SObpXZ0DUOLNNDzYisj14W\n" + - "3ygeNgyIUFVp8hLPI6/BXTJqziW0TqoqCb5Joq+32zjYPwJNxcECAwEAAaNrMGkw\n" + - "HwYDVR0jBBgwFoAUQXhCRs1OqILn4Tnf96kWwAr874YwHQYDVR0OBBYEFF3+CfUG\n" + - "qPKNIJRROjk0yUUKmZ2SMA4GA1UdDwEB/wQEAwIE8DAXBgNVHSAEEDAOMAwGCmCG\n" + - "SAFlAwIBMAEwDQYJKoZIhvcNAQELBQADggEBAEzeTUMhQZChACqQef2LLNvj+BfT\n" + - "VkDB0FVyPo3Yti8bRH8ZnItB8CWkS1iBvv4Bwgp7S8MpAGrgmyBCAO93VjySw5a2\n" + - "kEU/55B39tPKMQqWOAvi7DU31mWdWemMwD3u/SuK/8pnPOANjduViBKze8rUj4u0\n" + - "tbaYLor/qh4Lxgux9Xw1K+rwKlV9xDvoRLqNMsSReCLfMjMwXkUV7CL/6XjOGACM\n" + - "XMPQkJx1SnWLerNLLi2dzO1Ly9Ikr4jTEflJ/lxdw8Fa88eLXNte+JH0OcwwwLQM\n" + - "PiSkYK4B8y0A3Psl9309+kocX+YG1ppMoXg64Nt3YQ6kZqw/07gDFODEz3M=\n" + - "-----END CERTIFICATE-----" - - val nameConstraintsDN1CACert = "-----BEGIN CERTIFICATE-----\n" + - "MIID7TCCAtWgAwIBAgIBPjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJVUzEf\n" + - "MB0GA1UEChMWVGVzdCBDZXJ0aWZpY2F0ZXMgMjAxMTEVMBMGA1UEAxMMVHJ1c3Qg\n" + - "QW5jaG9yMB4XDTEwMDEwMTA4MzAwMFoXDTMwMTIzMTA4MzAwMFowTzELMAkGA1UE\n" + - "BhMCVVMxHzAdBgNVBAoTFlRlc3QgQ2VydGlmaWNhdGVzIDIwMTExHzAdBgNVBAMT\n" + - "Fm5hbWVDb25zdHJhaW50cyBETjEgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n" + - "ggEKAoIBAQDIFZChS4W10eHwp3smMrXyTluiMJTrq0f8LEx4D63qlvrjNngGxCHt\n" + - "BOlFbIH3uKwK24yKHywpFK38bHLyDf+2LoaEYox32sfyeqneYurTGJ4sZ1T/fsZk\n" + - "lG/n5fkMlvIU0iu4eOkshyEEtvUGpvdSEg4a7TiadjmsAZkJkgBHV0h9VYaRYvgY\n" + - "BnDsTd8MrzKo0bNnpMgiUGtGJB9lB0DmhO51IelaxiyaJUVIsKUZfpA1NPjSboLi\n" + - "NLgKhP8El/AlBY3BG190xJ3a5xDIhDq5SRTJ16554PIIwzfE7nvY+9TpwfkYvVKL\n" + - "zCOTyrA6VnFwTLKc8sLYKFfmKNEboLafAgMBAAGjgd0wgdowHwYDVR0jBBgwFoAU\n" + - "5H1f0VyVhggsBa6+dbZlp9ldqGYwHQYDVR0OBBYEFEF4QkbNTqiC5+E53/epFsAK\n" + - "/O+GMA4GA1UdDwEB/wQEAwIBBjAXBgNVHSAEEDAOMAwGCmCGSAFlAwIBMAEwDwYD\n" + - "VR0TAQH/BAUwAwEB/zBeBgNVHR4BAf8EVDBSoFAwTqRMMEoxCzAJBgNVBAYTAlVT\n" + - "MR8wHQYDVQQKExZUZXN0IENlcnRpZmljYXRlcyAyMDExMRowGAYDVQQLExFwZXJt\n" + - "aXR0ZWRTdWJ0cmVlMTANBgkqhkiG9w0BAQsFAAOCAQEAaRFMK70B7a7bqMhucX7K\n" + - "3AnChP1D8T/CFQUnOWeC/yKAcHbplQf3uWzL52ZJIoKLJaT7dnCuLxx9St/m5aCI\n" + - "MKZuIda+85I2WisV4brJJWyZlgLauA0WLZuEswqB0viCZG0vgtWTm9uN6O8Lqua3\n" + - "fnM/0WQtcmMMNs3NWN+FTX6SHIu5Z/DuUZWSF0H76jjheSJG2wXn0TJk8RRJ7mn5\n" + - "dnDEoDFUpePO0qaOjl1KGov28zz2QGIr7Nq+S0Z3Gk1Z2O3DlgYMeYtqkiMPKZ4Y\n" + - "sPDZIABuaSYI1o0ZoFnpLgiWVWbBJDO3w5x6eIS/CueS8hKfX0h7+dIcgQhABleo\n" + - "2w==\n" + - "-----END CERTIFICATE-----\n" - - val ca = X509Certificate.decodeFromPem(nameConstraintsDN1CACert).getOrThrow() - val leaf = X509Certificate.decodeFromPem(leafPem).getOrThrow() - val chain = listOf(leaf, ca) - val chainWithRoot = listOf(leaf, ca, trustAnchorRootCert) - - val result = chain.validate(context) - val resultWithRoot = chainWithRoot.validate(context) - result.shouldBeValid() - resultWithRoot.shouldBeValid() - - // Since root is not omitted from the chain, it will expect it to have - val r = chainWithRoot.validate(contextNotAllowedRoot) - r.shouldBeInvalid() - } - } \ No newline at end of file diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/LimboTests.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/LimboTests.kt index 06d02bced..1e2014451 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/LimboTests.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/validate/LimboTests.kt @@ -1,6 +1,9 @@ package at.asitplus.signum.supreme.validate +import at.asitplus.signum.CriticalAuthorityKeyIdentifierException import at.asitplus.signum.ExperimentalPkiApi +import at.asitplus.signum.KeyIdentifierException +import at.asitplus.signum.MissingAuthorityKeyIdentifierException import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.pki.CertificateChain import at.asitplus.signum.indispensable.pki.X509Certificate @@ -12,6 +15,8 @@ import de.infix.testBalloon.framework.core.testSuite import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.matchers.types.shouldBeSameInstanceAs import kotlinx.serialization.json.Json import kotlin.jvm.Throws import kotlin.time.Clock @@ -71,7 +76,7 @@ val LimboTests by testSuite { if (it.expected_result == "FAILURE") { result.shouldBeInvalid() - result.validatorFailures.firstOrNull { it.validator is KeyIdentifierValidator } shouldNotBe null + result.validatorFailures[0].cause.shouldBeInstanceOf() } else { result.shouldBeValid() } From c670120f60e887cc7a0b6fe2128cc887a6c892d0 Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Wed, 14 Jan 2026 12:19:43 +0100 Subject: [PATCH 8/9] add @throws with Throwable in CertificateValidator interface for MP safety --- .../signum/indispensable/pki/X509Certificate.kt | 6 ++++-- .../pki/validate/CertificateValidator.kt | 12 ++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt index 14f37f7ff..4892cb5f8 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt @@ -2,6 +2,8 @@ package at.asitplus.signum.indispensable.pki import at.asitplus.catching import at.asitplus.catchingUnwrapped +import at.asitplus.signum.CertificateExpiredException +import at.asitplus.signum.CertificateNotYetValidException import at.asitplus.signum.CertificateValidityException import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.CryptoSignature @@ -351,7 +353,7 @@ data class X509Certificate @Throws(IllegalArgumentException::class) constructor( @Throws(CertificateValidityException::class) fun checkValidityAt(date: Instant = Clock.System.now()) { if (isExpired(date)) { - throw CertificateValidityException( + throw CertificateExpiredException( "certificate expired on " + tbsCertificate.validUntil.instant .toLocalDateTime(TimeZone.UTC) @@ -359,7 +361,7 @@ data class X509Certificate @Throws(IllegalArgumentException::class) constructor( } if (isNotYetValid(date)) { - throw CertificateValidityException( + throw CertificateNotYetValidException( "certificate not valid till " + tbsCertificate.validFrom.instant .toLocalDateTime(TimeZone.UTC) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt index 83bc9dd85..e91adc882 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/validate/CertificateValidator.kt @@ -7,8 +7,16 @@ import at.asitplus.signum.indispensable.pki.X509Certificate import kotlin.coroutines.cancellation.CancellationException interface CertificateValidator { - // Every validator adds checked critical extensions + + /** + * Performs certificate validation for the given certificate + * Every validator adds checked critical extensions + * + * @throws CertificateException If the certificate fails validation according to the rules implemented by this validator + * @throws CancellationException + * @throws Throwable For multiplatform safety (e.g., Kotlin/Native to Swift), this allows catching all exceptions without crashing the application. + */ @ExperimentalPkiApi - @Throws(CertificateException::class, CancellationException::class) + @Throws(Throwable::class) suspend fun check(currCert: X509Certificate, checkedCriticalExtensions: MutableSet) } \ No newline at end of file From 07194bc354546082c5e7acef03f94b09c399ab0b Mon Sep 17 00:00:00 2001 From: Srdjan Stjepanovic Date: Fri, 27 Feb 2026 12:55:12 +0100 Subject: [PATCH 9/9] fix bug in decoding implicit tags --- .../AuthorityKeyIdentifierExtension.kt | 12 ++-- .../pki/pkiExtensions/GeneralSubtree.kt | 22 ++++--- .../X509CertificateExtensionParsingTest.kt | 64 +++++++++++++++++++ 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/AuthorityKeyIdentifierExtension.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/AuthorityKeyIdentifierExtension.kt index 2dfa720dc..1376bc2dc 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/AuthorityKeyIdentifierExtension.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/AuthorityKeyIdentifierExtension.kt @@ -10,6 +10,7 @@ import at.asitplus.signum.indispensable.asn1.Asn1Sequence import at.asitplus.signum.indispensable.asn1.Asn1StructuralException import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.asn1.TagClass import at.asitplus.signum.indispensable.asn1.authorityKeyIdentifier_2_5_29_35 import at.asitplus.signum.indispensable.asn1.decodeRethrowing import at.asitplus.signum.indispensable.asn1.encoding.decode @@ -42,11 +43,14 @@ class AuthorityKeyIdentifierExtension( val inner = base.value.asEncapsulatingOctetString().single().asSequence() val (keyIdentifier, authorityCertIssuer, pathLenConstraint) = inner.decodeRethrowing { - val keyIdentifier = nextOrNull()?.asPrimitive()?.content - val authorityCertIssuer: List = nextOrNull()?.asSequence()?.children - ?.map { GeneralName.decodeFromTlv(it.asSequence()) } + val contents = listOfNotNull(nextOrNull(), nextOrNull(), nextOrNull()) + val keyIdentifier = contents.firstOrNull { it.tag.tagValue == 0uL }?.asPrimitive() + ?.decode(Asn1Element.Tag(0UL, constructed = false, tagClass = TagClass.CONTEXT_SPECIFIC)) { it } + val authorityCertIssuer = contents.firstOrNull { it.tag.tagValue == 1uL }?.asExplicitlyTagged()?.children + ?.map { GeneralName.decodeFromTlv(it) } ?: emptyList() - val authorityCertSerialNumber = nextOrNull()?.asPrimitive()?.decode(Asn1Element.Tag.INT) { it } + val authorityCertSerialNumber = contents.firstOrNull { it.tag.tagValue == 2uL }?.asPrimitive() + ?.decode(Asn1Element.Tag(2UL, constructed = false, tagClass = TagClass.CONTEXT_SPECIFIC)) { it } Triple(keyIdentifier, authorityCertIssuer, authorityCertSerialNumber) } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/GeneralSubtree.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/GeneralSubtree.kt index b15843e87..294f175c3 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/GeneralSubtree.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/pkiExtensions/GeneralSubtree.kt @@ -9,6 +9,7 @@ import at.asitplus.signum.indispensable.asn1.Asn1ExplicitlyTagged import at.asitplus.signum.indispensable.asn1.Asn1Integer import at.asitplus.signum.indispensable.asn1.Asn1Sequence import at.asitplus.signum.indispensable.asn1.Asn1String +import at.asitplus.signum.indispensable.asn1.Asn1StructuralException import at.asitplus.signum.indispensable.asn1.decodeRethrowing import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import at.asitplus.signum.indispensable.asn1.encoding.parse @@ -39,17 +40,20 @@ data class GeneralSubtree( override fun doDecode(src: Asn1Sequence): GeneralSubtree = src.decodeRethrowing { val base = GeneralName.decodeFromTlv(next()) var minimum = Asn1Integer(0) - if (hasNext()) { - minimum = Asn1Integer.decodeFromTlv(next().asPrimitive()) + var maximum: Asn1Integer? = null + + while (hasNext()) { + val child = next() + when (child.tag.tagValue) { + 0uL -> minimum = Asn1Integer.decodeFromTlv(child.asPrimitive()) + 1uL -> maximum = Asn1Integer.decodeFromTlv(child.asPrimitive()) + else -> throw Asn1StructuralException("Unexpected tag in GeneralSubtree: ${child.tag}") + } } - - return if (!hasNext()) GeneralSubtree( + GeneralSubtree( base = base, - minimum = minimum - ) else GeneralSubtree( - base, - minimum, - Asn1Integer.decodeFromTlv(next().asPrimitive()) + minimum = minimum, + maximum = maximum ) } } diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtensionParsingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtensionParsingTest.kt index f4c5730ae..07389179d 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtensionParsingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtensionParsingTest.kt @@ -8,6 +8,7 @@ import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Bool import at.asitplus.signum.indispensable.asn1.encoding.parse import at.asitplus.signum.indispensable.asn1.keyUsage +import at.asitplus.signum.indispensable.pki.pkiExtensions.AuthorityKeyIdentifierExtension import at.asitplus.signum.indispensable.pki.pkiExtensions.KeyUsageExtension import at.asitplus.testballoon.invoke import at.asitplus.testballoon.withDataSuites @@ -17,6 +18,9 @@ import org.bouncycastle.asn1.x509.KeyUsage import de.infix.testBalloon.framework.core.TestConfig import kotlin.time.Duration.Companion.minutes import de.infix.testBalloon.framework.core.testScope +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.collections.shouldNotHaveSize +import io.kotest.matchers.shouldNotBe val X509CertificateExtensionParsingTest by testSuite { @@ -52,4 +56,64 @@ val X509CertificateExtensionParsingTest by testSuite { val ext = runCatching { X509CertificateExtension.decodeFromTlv(seq) }.getOrNull() ext!!::class shouldBe X509CertificateExtension::class } + + "valid authority key identifier" { + val pem = "-----BEGIN CERTIFICATE-----\n" + + "MIIDIjCCAgqgAwIBAgIBAzANBgkqhkiG9w0BAQUFADApMQ0wCwYDVQQKDARQeUNB\n" + + "MRgwFgYDVQQDDA9jcnlwdG9ncmFwaHkuaW8wHhcNMTUwNTAzMDk0OTU2WhcNMTYw\n" + + "NTAyMDk0OTU2WjApMQ0wCwYDVQQKDARQeUNBMRgwFgYDVQQDDA9jcnlwdG9ncmFw\n" + + "aHkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCadi1UZioxdnP\n" + + "ajqlRZHeKsSxvXXhgrWvlt91P3gV0dBThRFhJsLOhjNLz6PO6KeRbjz9GhTA2hdk\n" + + "xtIpXrjvTv9dEJ1/k0xebsHWgFC43aTlgekw0U4cMwMe5NGeeg1tfzbJwldIN+cK\n" + + "vabc08ADlkmM6DMnUArkzA2yii0DErRFMSIGrkDr6E9puord3h6Mh8Jfnc3TDAq8\n" + + "Qo1DI2XM7oFSWNfecQ9KbIC5wzzT+7Shoyz7QmCk/XhRzt8Xcfc3yAXIwazvLf8b\n" + + "YP1auaSG11a5E+w6onj91h8UHKKOXu+rdq5YYPZ+qUYpxA7ZJ/VAGadMulYbXaO8\n" + + "Syi39HTpAgMBAAGjVTBTMFEGA1UdIwRKMEiAFDlFPso9Yh3qhkn2WqtAt6RwmPHs\n" + + "oS2kKzApMQ0wCwYDVQQKDARQeUNBMRgwFgYDVQQDDA9jcnlwdG9ncmFwaHkuaW+C\n" + + "AQMwDQYJKoZIhvcNAQEFBQADggEBAFbZYy6aZJUK/f7nJx2Rs/ht6hMbM32/RoXZ\n" + + "JGbYapNVqVu/vymcfc/se3FHS5OVmPsnRlo/FIKDn/r5DGl73Sn/FvDJiLJZFucT\n" + + "msyYuHZ+ZRYWzWmN2fcB3cfxj0s3qps6f5OoCOqoINOSe4HRGlw4X9keZSD+3xAt\n" + + "vHNwQdlPC7zWbPdrzLT+FqR0e/O81vFJJS6drHJWqPcR3NQVtZw+UF7A/HKwbfeL\n" + + "Nu2zj6165hzOi9HUxa2/mPr/eLUUV1sTzXp2+TFjt3rVCjW1XnpMLdwNBHzjpyAB\n" + + "dTOX3iw0+BPy3s2jtnCW1PLpc74kvSTaBwhg74sq39EXfIKax00=\n" + + "-----END CERTIFICATE-----" + + val cert = X509Certificate.decodeFromPem(pem).getOrThrow() + + val aki = cert.findExtension() + aki shouldNotBe null + aki?.keyIdentifier shouldNotBe null + aki?.authorityCertIssuer?.size shouldNotBe 0 + aki?.authorityCertSerialNumber shouldNotBe null + } + + "valid authority key identifier without keyIdentifier field" { + val pem = "-----BEGIN CERTIFICATE-----\n" + + "MIIDDDCCAfSgAwIBAgIBAzANBgkqhkiG9w0BAQUFADApMQ0wCwYDVQQKDARQeUNB\n" + + "MRgwFgYDVQQDDA9jcnlwdG9ncmFwaHkuaW8wHhcNMTUwNTAzMTAxNTU2WhcNMTYw\n" + + "NTAyMTAxNTU2WjApMQ0wCwYDVQQKDARQeUNBMRgwFgYDVQQDDA9jcnlwdG9ncmFw\n" + + "aHkuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCadi1UZioxdnP\n" + + "ajqlRZHeKsSxvXXhgrWvlt91P3gV0dBThRFhJsLOhjNLz6PO6KeRbjz9GhTA2hdk\n" + + "xtIpXrjvTv9dEJ1/k0xebsHWgFC43aTlgekw0U4cMwMe5NGeeg1tfzbJwldIN+cK\n" + + "vabc08ADlkmM6DMnUArkzA2yii0DErRFMSIGrkDr6E9puord3h6Mh8Jfnc3TDAq8\n" + + "Qo1DI2XM7oFSWNfecQ9KbIC5wzzT+7Shoyz7QmCk/XhRzt8Xcfc3yAXIwazvLf8b\n" + + "YP1auaSG11a5E+w6onj91h8UHKKOXu+rdq5YYPZ+qUYpxA7ZJ/VAGadMulYbXaO8\n" + + "Syi39HTpAgMBAAGjPzA9MDsGA1UdIwQ0MDKhLaQrMCkxDTALBgNVBAoMBFB5Q0Ex\n" + + "GDAWBgNVBAMMD2NyeXB0b2dyYXBoeS5pb4IBAzANBgkqhkiG9w0BAQUFAAOCAQEA\n" + + "AViX0VIVQW2xyf0lfLiuFhrpdgX9i49StZvs+n/qH5yvWxfqRJAyVT1pk2Xs0Goj\n" + + "ul7vYMfIGU0nIr8eLMlAH9j6lkllAd/oO1BDONZ1kH6PMdkOdvgz5gmhMQx6MFr6\n" + + "zMzzQ+JOAnXKFFUEycOiRJyh3VXiTY1M1IG1kWY+LoqB72S7y9c25yFoHqUNi2Xf\n" + + "rbuaR7gNS/4z7XvLJkbNbVS2+y69gQGL+8vk5AG7MiZ1mzUQ44r/zy6HNDBb55kK\n" + + "H+YTYavijRApH5hccJBXyoIM0x9ZtKdcrV0h+J2KOFGEyHp3FXViFEB2IZUpJNA/\n" + + "aduVbH8gZy5Y+cHzenwzBg==\n" + + "-----END CERTIFICATE-----" + val cert = X509Certificate.decodeFromPem(pem).getOrThrow() + + val aki = cert.findExtension() + aki shouldNotBe null + aki?.keyIdentifier shouldBe null + aki?.authorityCertIssuer?.size shouldNotBe 0 + aki?.authorityCertSerialNumber shouldNotBe null + + } } \ No newline at end of file