Reputation: 1301
I have certificates for an end entity, some intermediate CAs and some trusted CAs, and I'm trying to use CertPathBuilder
to find the certification path between the end entity and one of the trusted CAs. However, my current implementation is including any intermediate CAs and the end entity, but failing to include the trusted root.
I have tried both the BouncyCastle provider (CertPathBuilder.getInstance("PKIX", "BC")
) and Sun's (CertPathBuilder.getInstance("PKIX")
), but I get the same result.
Here's a self-contained Kotlin snippet, using Bouncy Castle (implementation("org.bouncycastle:bcpkix-jdk15on:1.66")
) to generate the certificates. My path building function is buildCertificationPath
.
package com.example.cert
import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1Primitive
import org.bouncycastle.asn1.DERBMPString
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.PublicKey
import java.security.SecureRandom
import java.security.Security
import java.security.cert.CertPathBuilder
import java.security.cert.CertPathBuilderException
import java.security.cert.CertStore
import java.security.cert.CollectionCertStoreParameters
import java.security.cert.PKIXBuilderParameters
import java.security.cert.PKIXParameters
import java.security.cert.TrustAnchor
import java.security.cert.X509CertSelector
import java.security.cert.X509Certificate
import java.sql.Date
import java.time.ZonedDateTime
val bcToJavaCertificateConverter: JcaX509CertificateConverter =
JcaX509CertificateConverter().setProvider(BouncyCastleProvider())
fun main() {
Security.addProvider(BouncyCastleProvider())
// Issue certificates using BouncyCastle
val rootCAKeyPair = generateRSAKeyPair()
val rootCACert = issueCertificate(
"root",
rootCAKeyPair.public,
rootCAKeyPair.private,
isCA = true,
pathLenConstraint = 2
)
val intermediateCAKeyPair = generateRSAKeyPair()
val intermediateCACert = issueCertificate(
"intermediate",
intermediateCAKeyPair.public,
rootCAKeyPair.private,
rootCACert,
isCA = true,
pathLenConstraint = 1
)
val endEntityKeyPair = generateRSAKeyPair()
val endEntityCert = issueCertificate(
"end",
endEntityKeyPair.public,
intermediateCAKeyPair.private,
intermediateCACert,
isCA = false,
pathLenConstraint = 0
)
// Convert BouncyCastle certificates to Java ones:
val javaRootCert = convertBCCertToJava(rootCACert)
val javaInterCert = convertBCCertToJava(intermediateCACert)
val javaEndCert = convertBCCertToJava(endEntityCert)
val intermediateAndRootPath = buildCertificationPath(
javaInterCert,
emptySet(),
setOf(javaRootCert)
)
if (intermediateAndRootPath.contentEquals(arrayOf("intermediate", "root"))) {
println("Path between intermediate and root CA is OK")
} else {
println(
"Path between intermediate and root CA is wrong: " +
intermediateAndRootPath.joinToString(",")
)
}
val endAndIntermediatePath = buildCertificationPath(
javaEndCert,
emptySet(),
setOf(javaInterCert)
)
if (endAndIntermediatePath.contentEquals(arrayOf("end", "intermediate"))) {
println("Path between end entity and intermediate CA is OK")
} else {
println(
"Path between end entity and intermediate CA is wrong: " +
endAndIntermediatePath.joinToString(",")
)
}
val endAndRootPath = buildCertificationPath(
javaEndCert,
setOf(javaInterCert),
setOf(javaRootCert)
)
if (endAndRootPath.contentEquals(arrayOf("end", "intermediate", "root"))) {
println("Path between end entity and root CA is OK")
} else {
println("Path between end entity and root CA is wrong: " + endAndRootPath.joinToString(","))
}
}
fun buildCertificationPath(
endEntityCert: X509Certificate,
intermediateCACerts: Set<X509Certificate>,
trustedCACerts: Set<X509Certificate>
): Array<String> {
val trustAnchors = trustedCACerts.map { TrustAnchor(it, null) }.toSet()
val intermediateCertStore = CertStore.getInstance(
"Collection",
CollectionCertStoreParameters(intermediateCACerts),
"BC"
)
val endEntitySelector = X509CertSelector()
endEntitySelector.certificate = endEntityCert
val parameters: PKIXParameters = PKIXBuilderParameters(trustAnchors, endEntitySelector)
parameters.isRevocationEnabled = false // TODO: Needed?
parameters.addCertStore(intermediateCertStore)
// val pathBuilder: CertPathBuilder = CertPathBuilder.getInstance("PKIX")
val pathBuilder: CertPathBuilder = CertPathBuilder.getInstance("PKIX", "BC")
val pathBuilderResult = try {
pathBuilder.build(parameters)
} catch (exc: CertPathBuilderException) {
exc.printStackTrace()
return emptyArray()
}
val certificates = pathBuilderResult.certPath.certificates
return certificates.map {
X509CertificateHolder(it.encoded).subject.getRDNs(BCStyle.CN).first().first.value.toString()
}.toTypedArray()
}
fun generateRSAKeyPair(): KeyPair {
val keyGen = KeyPairGenerator.getInstance("RSA")
keyGen.initialize(2048)
return keyGen.generateKeyPair()
}
fun issueCertificate(
subjectCommonName: String,
subjectPublicKey: PublicKey,
issuerPrivateKey: PrivateKey,
issuerCertificate: X509CertificateHolder? = null,
isCA: Boolean = false,
pathLenConstraint: Int = 0
): X509CertificateHolder {
val subjectDistinguishedName = buildDistinguishedName(subjectCommonName)
val issuerDistinguishedName = if (issuerCertificate != null)
issuerCertificate.subject
else
subjectDistinguishedName
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded)
val now = ZonedDateTime.now()
val builder = X509v3CertificateBuilder(
issuerDistinguishedName,
generateRandomBigInteger(),
Date.from(now.toInstant()),
Date.from(now.plusDays(1).toInstant()),
subjectDistinguishedName,
subjectPublicKeyInfo
)
val basicConstraints = BasicConstraintsExtension(
isCA,
pathLenConstraint
)
builder.addExtension(Extension.basicConstraints, true, basicConstraints)
val subjectPublicKeyDigest = getSHA256Digest(subjectPublicKeyInfo.encoded)
val issuerSKI = issuerCertificate?.getExtension(Extension.subjectKeyIdentifier)
if (issuerSKI != null) {
val aki = AuthorityKeyIdentifier((issuerSKI.parsedValue as DEROctetString).octets)
builder.addExtension(Extension.authorityKeyIdentifier, false, aki)
}
val ski = SubjectKeyIdentifier(subjectPublicKeyDigest)
builder.addExtension(Extension.subjectKeyIdentifier, false, ski)
val signerBuilder = JcaContentSignerBuilder("SHA256WITHRSAANDMGF1").build(issuerPrivateKey)
return builder.build(signerBuilder)
}
fun convertBCCertToJava(bcCert: X509CertificateHolder): X509Certificate =
bcToJavaCertificateConverter.getCertificate(bcCert)
fun generateRandomBigInteger(): BigInteger {
val random = SecureRandom()
return BigInteger(64, random)
}
fun buildDistinguishedName(commonName: String): X500Name {
val builder = X500NameBuilder(BCStyle.INSTANCE)
builder.addRDN(BCStyle.CN, DERBMPString(commonName))
return builder.build()
}
fun getSHA256Digest(input: ByteArray): ByteArray {
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(input)
}
class BasicConstraintsExtension(
private val cA: Boolean,
private val pathLenConstraint: Int
) : ASN1Encodable {
init {
if (pathLenConstraint < 0 || 2 < pathLenConstraint) {
throw Exception(
"pathLenConstraint should be between 0 and 2 (got $pathLenConstraint)"
)
}
if (pathLenConstraint != 0 && !cA) {
throw Exception(
"Subject should be a CA if pathLenConstraint=$pathLenConstraint"
)
}
}
override fun toASN1Primitive(): ASN1Primitive {
val sequence = ASN1EncodableVector(2)
sequence.add(ASN1Boolean.getInstance(cA))
sequence.add(ASN1Integer(pathLenConstraint.toLong()))
return DERSequence(sequence)
}
}
Here's the output I get:
Path between intermediate and root CA is wrong: intermediate
Path between end entity and intermediate CA is wrong: end
Path between end entity and root CA is wrong: end,intermediate
As a workaround, I may end up computing the root by iterating over the trusted CAs until I find the one that issued the last certificate in the path, but I'm hoping that won't be necessary.
Upvotes: 2
Views: 1055
Reputation: 4052
Cast pathBuilderResult
to java.security.cert.PKIXCertPathBuilderResult
(implementations of "PKIX" are required to return a result implementing this). You'll then find the getTrustAnchor() method is available, returning the certificate that served as TA for this result.
Upvotes: 4