ENT-8898: Replaced JDK cert revocation with custom plugable implementation (#7322)

This commit is contained in:
Shams Asari
2023-04-03 10:26:01 +01:00
committed by GitHub
parent 0213861d22
commit 1e6ccfdb60
16 changed files with 361 additions and 181 deletions

View File

@ -29,7 +29,7 @@ dependencies {
// SQL connection pooling library
compile "com.zaxxer:HikariCP:$hikari_version"
// ClassGraph: classpath scanning
compile "io.github.classgraph:classgraph:$class_graph_version"
@ -54,6 +54,9 @@ dependencies {
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
testCompile project(':node-driver')
// Unit testing helpers.
testCompile "org.assertj:assertj-core:$assertj_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"

View File

@ -438,6 +438,8 @@ class X509CertificateFactory {
fun generateCertPath(vararg certificates: X509Certificate): CertPath = generateCertPath(certificates.asList())
fun generateCertPath(certificates: List<X509Certificate>): CertPath = delegate.generateCertPath(certificates)
fun generateCRL(input: InputStream): X509CRL = delegate.generateCRL(input) as X509CRL
}
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean, val role: CertRole?) {

View File

@ -28,7 +28,6 @@ import org.apache.qpid.proton.framing.TransportFrame
import org.slf4j.MDC
import java.net.InetSocketAddress
import java.nio.channels.ClosedChannelException
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509Certificate
import javax.net.ssl.ExtendedSSLSession
import javax.net.ssl.SNIHostName
@ -47,7 +46,6 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
private val password: String?,
private val trace: Boolean,
private val suppressLogs: Boolean,
private val revocationChecker: PKIXRevocationChecker,
private val onOpen: (SocketChannel, ConnectionChange) -> Unit,
private val onClose: (SocketChannel, ConnectionChange) -> Unit,
private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() {
@ -170,13 +168,6 @@ internal class AMQPChannelHandler(private val serverMode: Boolean,
} else {
handleFailedHandshake(ctx, evt)
}
if (log.isDebugEnabled) {
withMDC {
revocationChecker.softFailExceptions.forEachIndexed { index, e ->
log.debug("Revocation soft fail exception (${index + 1}/${revocationChecker.softFailExceptions.size})", e)
}
}
}
}
}
}

View File

@ -26,7 +26,7 @@ import rx.Observable
import rx.subjects.PublishSubject
import java.lang.Long.min
import java.net.InetSocketAddress
import java.security.cert.PKIXRevocationChecker
import java.security.cert.CertPathValidatorException
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.KeyManagerFactory
@ -83,6 +83,7 @@ class AMQPClient(private val targets: List<NetworkHostAndPort>,
private var targetIndex = 0
private var currentTarget: NetworkHostAndPort = targets.first()
private var retryInterval = MIN_RETRY_INTERVAL
private val revocationChecker = configuration.revocationConfig.createPKIXRevocationChecker()
private val badCertTargets = mutableSetOf<NetworkHostAndPort>()
@Volatile
private var amqpActive = false
@ -145,15 +146,13 @@ class AMQPClient(private val targets: List<NetworkHostAndPort>,
private class ClientChannelInitializer(val parent: AMQPClient) : ChannelInitializer<SocketChannel>() {
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
private val revocationChecker: PKIXRevocationChecker
private val conf = parent.configuration
@Volatile
private lateinit var amqpChannelHandler: AMQPChannelHandler
init {
keyManagerFactory.init(conf.keyStore)
revocationChecker = createPKIXRevocationChecker(conf.revocationConfig)
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, revocationChecker))
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, parent.revocationChecker))
}
@Suppress("ComplexMethod")
@ -211,7 +210,6 @@ class AMQPClient(private val targets: List<NetworkHostAndPort>,
conf.password,
conf.trace,
false,
revocationChecker,
onOpen = { _, change -> onChannelOpen(change) },
onClose = { _, change -> onChannelClose(change, target) },
onReceive = parent._onReceive::onNext
@ -330,4 +328,6 @@ class AMQPClient(private val targets: List<NetworkHostAndPort>,
private val _onConnection = PublishSubject.create<ConnectionChange>().toSerialized()
val onConnection: Observable<ConnectionChange>
get() = _onConnection
val softFailExceptions: List<CertPathValidatorException> get() = revocationChecker.softFailExceptions
}

View File

@ -25,7 +25,7 @@ import rx.Observable
import rx.subjects.PublishSubject
import java.net.BindException
import java.net.InetSocketAddress
import java.security.cert.PKIXRevocationChecker
import java.security.cert.CertPathValidatorException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import javax.net.ssl.KeyManagerFactory
@ -56,18 +56,17 @@ class AMQPServer(val hostName: String,
private var bossGroup: EventLoopGroup? = null
private var workerGroup: EventLoopGroup? = null
private var serverChannel: Channel? = null
private val revocationChecker = configuration.revocationConfig.createPKIXRevocationChecker()
private val clientChannels = ConcurrentHashMap<InetSocketAddress, SocketChannel>()
private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer<SocketChannel>() {
private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
private val revocationChecker: PKIXRevocationChecker
private val conf = parent.configuration
init {
keyManagerFactory.init(conf.keyStore.value.internal, conf.keyStore.entryPassword.toCharArray())
revocationChecker = createPKIXRevocationChecker(conf.revocationConfig)
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, revocationChecker))
trustManagerFactory.init(initialiseTrustStoreAndEnableCrlChecking(conf.trustStore, parent.revocationChecker))
}
override fun initChannel(ch: SocketChannel) {
@ -88,7 +87,6 @@ class AMQPServer(val hostName: String,
conf.password,
conf.trace,
suppressLogs,
revocationChecker,
onOpen = ::onChannelOpen,
onClose = ::onChannelClose,
onReceive = parent._onReceive::onNext
@ -227,4 +225,6 @@ class AMQPServer(val hostName: String,
private val _onConnection = PublishSubject.create<ConnectionChange>().toSerialized()
val onConnection: Observable<ConnectionChange>
get() = _onConnection
val softFailExceptions: List<CertPathValidatorException> get() = revocationChecker.softFailExceptions
}

View File

@ -3,10 +3,11 @@ package net.corda.nodeapi.internal.protonwrapper.netty
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
interface ExternalCrlSource {
@FunctionalInterface
interface CrlSource {
/**
* Given certificate provides a set of CRLs, potentially performing remote communication.
*/
fun fetch(certificate: X509Certificate) : Set<X509CRL>
}
fun fetch(certificate: X509Certificate): Set<X509CRL>
}

View File

@ -3,11 +3,15 @@ package net.corda.nodeapi.internal.protonwrapper.netty
import com.typesafe.config.Config
import net.corda.nodeapi.internal.config.ConfigParser
import net.corda.nodeapi.internal.config.CustomConfigParser
import net.corda.nodeapi.internal.revocation.CertDistPointCrlSource
import net.corda.nodeapi.internal.revocation.CordaRevocationChecker
import java.security.cert.PKIXRevocationChecker
/**
* Data structure for controlling the way how Certificate Revocation Lists are handled.
*/
@CustomConfigParser(parser = RevocationConfigParser::class)
// TODO This and RevocationConfigImpl should really be a single sealed data type
interface RevocationConfig {
enum class Mode {
@ -26,7 +30,7 @@ interface RevocationConfig {
/**
* CRLs are obtained from external source
* @see ExternalCrlSource
* @see CrlSource
*/
EXTERNAL_SOURCE,
@ -39,14 +43,23 @@ interface RevocationConfig {
val mode: Mode
/**
* Optional `ExternalCrlSource` which only makes sense with `mode` = `EXTERNAL_SOURCE`
* Optional [CrlSource] which only makes sense with `mode` = `EXTERNAL_SOURCE`
*/
val externalCrlSource: ExternalCrlSource?
val externalCrlSource: CrlSource?
/**
* Creates a copy of `RevocationConfig` with ExternalCrlSource enriched
* Creates a copy of [RevocationConfig] enriched by a [CrlSource].
*/
fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig
fun enrichExternalCrlSource(sourceFunc: (() -> CrlSource)?): RevocationConfig
fun createPKIXRevocationChecker(): PKIXRevocationChecker {
return when (mode) {
Mode.OFF -> AllowAllRevocationChecker
Mode.EXTERNAL_SOURCE -> CordaRevocationChecker(externalCrlSource!!, softFail = true)
Mode.SOFT_FAIL -> CordaRevocationChecker(CertDistPointCrlSource(), softFail = true)
Mode.HARD_FAIL -> CordaRevocationChecker(CertDistPointCrlSource(), softFail = false)
}
}
}
/**
@ -54,13 +67,21 @@ interface RevocationConfig {
*/
fun Boolean.toRevocationConfig() = if(this) RevocationConfigImpl(RevocationConfig.Mode.SOFT_FAIL) else RevocationConfigImpl(RevocationConfig.Mode.HARD_FAIL)
data class RevocationConfigImpl(override val mode: RevocationConfig.Mode, override val externalCrlSource: ExternalCrlSource? = null) : RevocationConfig {
override fun enrichExternalCrlSource(sourceFunc: (() -> ExternalCrlSource)?): RevocationConfig {
return if(mode != RevocationConfig.Mode.EXTERNAL_SOURCE) {
data class RevocationConfigImpl(override val mode: RevocationConfig.Mode, override val externalCrlSource: CrlSource? = null) : RevocationConfig {
init {
if (mode == RevocationConfig.Mode.EXTERNAL_SOURCE) {
requireNotNull(externalCrlSource) { "externalCrlSource must not be null" }
}
}
// TODO This doesn't really need to be a member method. All it does is change externalCrlSource if applicable, which is the same as
// just creating a new RevocationConfigImpl with that CrlSource.
override fun enrichExternalCrlSource(sourceFunc: (() -> CrlSource)?): RevocationConfig {
return if (mode != RevocationConfig.Mode.EXTERNAL_SOURCE) {
this
} else {
assert(sourceFunc != null) { "There should be a way to obtain ExternalCrlSource" }
copy(externalCrlSource = sourceFunc!!())
val func = requireNotNull(sourceFunc) { "There should be a way to obtain CrlSource" }
copy(externalCrlSource = func())
}
}
}
@ -80,4 +101,4 @@ class RevocationConfigParser : ConfigParser<RevocationConfig> {
else -> throw IllegalArgumentException("Unsupported mode : '$mode'")
}
}
}
}

View File

@ -13,15 +13,17 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.VisibleForTesting
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.toHex
import net.corda.nodeapi.internal.ArtemisTcpTransport
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.toBc
import net.corda.nodeapi.internal.crypto.x509
import net.corda.nodeapi.internal.protonwrapper.netty.revocation.ExternalSourceRevocationChecker
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.ASN1Primitive
import org.bouncycastle.asn1.DERIA5String
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier
import org.bouncycastle.asn1.x509.CRLDistPoint
import org.bouncycastle.asn1.x509.DistributionPointName
@ -30,13 +32,15 @@ import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.net.Socket
import java.net.URI
import java.security.KeyStore
import java.security.cert.*
import java.util.*
import java.util.concurrent.Executor
import javax.net.ssl.*
import javax.security.auth.x500.X500Principal
import kotlin.collections.HashMap
import kotlin.system.measureTimeMillis
private const val HOSTNAME_FORMAT = "%s.corda.net"
@ -46,40 +50,61 @@ internal const val DP_DEFAULT_ANSWER = "NO CRLDP ext"
internal val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.protonwrapper.netty.SSLHelper")
fun X509Certificate.distributionPoints(): Set<String> {
logger.debug("Checking CRLDPs for $subjectX500Principal")
/**
* Returns all the CRL distribution points in the certificate as [URI]s along with the CRL issuer names, if any.
*/
@Suppress("ComplexMethod")
fun X509Certificate.distributionPoints(): Map<URI, List<X500Principal>?> {
logger.debug { "Checking CRLDPs for $subjectX500Principal" }
val crldpExtBytes = getExtensionValue(Extension.cRLDistributionPoints.id)
if (crldpExtBytes == null) {
logger.debug(DP_DEFAULT_ANSWER)
return emptySet()
return emptyMap()
}
val derObjCrlDP = ASN1InputStream(ByteArrayInputStream(crldpExtBytes)).readObject()
val derObjCrlDP = crldpExtBytes.toAsn1Object()
val dosCrlDP = derObjCrlDP as? DEROctetString
if (dosCrlDP == null) {
logger.error("Expected to have DEROctetString, actual type: ${derObjCrlDP.javaClass}")
return emptySet()
return emptyMap()
}
val crldpExtOctetsBytes = dosCrlDP.octets
val dpObj = ASN1InputStream(ByteArrayInputStream(crldpExtOctetsBytes)).readObject()
val distPoint = CRLDistPoint.getInstance(dpObj)
if (distPoint == null) {
val dpObj = dosCrlDP.octets.toAsn1Object()
val crlDistPoint = CRLDistPoint.getInstance(dpObj)
if (crlDistPoint == null) {
logger.error("Could not instantiate CRLDistPoint, from: $dpObj")
return emptySet()
return emptyMap()
}
val dpNames = distPoint.distributionPoints.mapNotNull { it.distributionPoint }.filter { it.type == DistributionPointName.FULL_NAME }
val generalNames = dpNames.flatMap { GeneralNames.getInstance(it.name).names.asList() }
return generalNames.filter { it.tagNo == GeneralName.uniformResourceIdentifier}.map { DERIA5String.getInstance(it.name).string }.toSet()
val dpMap = HashMap<URI, List<X500Principal>?>()
for (distributionPoint in crlDistPoint.distributionPoints) {
val distributionPointName = distributionPoint.distributionPoint
if (distributionPointName?.type != DistributionPointName.FULL_NAME) continue
val issuerNames = distributionPoint.crlIssuer?.names?.mapNotNull {
if (it.tagNo == GeneralName.directoryName) {
X500Principal(X500Name.getInstance(it.name).encoded)
} else {
null
}
}
for (generalName in GeneralNames.getInstance(distributionPointName.name).names) {
if (generalName.tagNo == GeneralName.uniformResourceIdentifier) {
val uri = URI(DERIA5String.getInstance(generalName.name).string)
dpMap[uri] = issuerNames
}
}
}
return dpMap
}
fun X509Certificate.distributionPointsToString(): String {
return with(distributionPoints()) {
return with(distributionPoints().keys) {
if (isEmpty()) DP_DEFAULT_ANSWER else sorted().joinToString()
}
}
fun ByteArray.toAsn1Object(): ASN1Primitive = ASN1InputStream(this).readObject()
fun certPathToString(certPath: Array<out X509Certificate>?): String {
if (certPath == null) {
return "<empty certpath>"
@ -256,10 +281,9 @@ fun createAndInitSslContext(keyManagerFactory: KeyManagerFactory, trustManagerFa
return sslContext
}
@VisibleForTesting
fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore,
revocationConfig: RevocationConfig): CertPathTrustManagerParameters {
return initialiseTrustStoreAndEnableCrlChecking(trustStore, createPKIXRevocationChecker(revocationConfig))
return initialiseTrustStoreAndEnableCrlChecking(trustStore, revocationConfig.createPKIXRevocationChecker())
}
fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore,
@ -269,33 +293,6 @@ fun initialiseTrustStoreAndEnableCrlChecking(trustStore: CertificateStore,
return CertPathTrustManagerParameters(pkixParams)
}
fun createPKIXRevocationChecker(revocationConfig: RevocationConfig): PKIXRevocationChecker {
return when (revocationConfig.mode) {
RevocationConfig.Mode.OFF -> AllowAllRevocationChecker // Custom PKIXRevocationChecker skipping CRL check
RevocationConfig.Mode.EXTERNAL_SOURCE -> {
require(revocationConfig.externalCrlSource != null) { "externalCrlSource must not be null" }
ExternalSourceRevocationChecker(revocationConfig.externalCrlSource!!) { Date() } // Custom PKIXRevocationChecker which uses `externalCrlSource`
}
else -> {
val certPathBuilder = CertPathBuilder.getInstance("PKIX")
val pkixRevocationChecker = certPathBuilder.revocationChecker as PKIXRevocationChecker
val options = EnumSet.of(
// Prefer CRL over OCSP
PKIXRevocationChecker.Option.PREFER_CRLS,
// Don't fall back to OCSP checking
PKIXRevocationChecker.Option.NO_FALLBACK
)
if (revocationConfig.mode == RevocationConfig.Mode.SOFT_FAIL) {
// Allow revocation check to succeed if the revocation status cannot be determined for one of
// the following reasons: The CRL or OCSP response cannot be obtained because of a network error.
options += PKIXRevocationChecker.Option.SOFT_FAIL
}
pkixRevocationChecker.options = options
pkixRevocationChecker
}
}
}
/**
* Creates a special SNI handler used only when openSSL is used for AMQPServer
*/

View File

@ -0,0 +1,84 @@
package net.corda.nodeapi.internal.revocation
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import net.corda.core.internal.readFully
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.protonwrapper.netty.CrlSource
import net.corda.nodeapi.internal.protonwrapper.netty.distributionPoints
import java.net.URI
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.security.auth.x500.X500Principal
/**
* [CrlSource] which downloads CRLs from the distribution points in the X509 certificate.
*/
class CertDistPointCrlSource : CrlSource {
companion object {
// The default SSL handshake timeout is 60s (DEFAULT_SSL_HANDSHAKE_TIMEOUT). Considering there are 3 CRLs endpoints to check in a
// node handshake, we want to keep the total timeout within that.
private const val DEFAULT_CONNECT_TIMEOUT = 9_000
private const val DEFAULT_READ_TIMEOUT = 9_000
private const val DEFAULT_CACHE_SIZE = 185L // Same default as the JDK (URICertStore)
private const val DEFAULT_CACHE_EXPIRY = 5 * 60 * 1000L
private val cache: LoadingCache<URI, X509CRL> = Caffeine.newBuilder()
.maximumSize(java.lang.Long.getLong("net.corda.dpcrl.cache.size", DEFAULT_CACHE_SIZE))
.expireAfterWrite(java.lang.Long.getLong("net.corda.dpcrl.cache.expiry", DEFAULT_CACHE_EXPIRY), TimeUnit.MILLISECONDS)
.build(::retrieveCRL)
private val connectTimeout = Integer.getInteger("net.corda.dpcrl.connect.timeout", DEFAULT_CONNECT_TIMEOUT)
private val readTimeout = Integer.getInteger("net.corda.dpcrl.read.timeout", DEFAULT_READ_TIMEOUT)
private fun retrieveCRL(uri: URI): X509CRL {
val bytes = run {
val conn = uri.toURL().openConnection()
conn.connectTimeout = connectTimeout
conn.readTimeout = readTimeout
// Read all bytes first and then pass them into the CertificateFactory. This may seem unnecessary when generateCRL already takes
// in an InputStream, but the JDK implementation (sun.security.provider.X509Factory.engineGenerateCRL) converts any IOException
// into CRLException and drops the cause chain.
conn.getInputStream().readFully()
}
return X509CertificateFactory().generateCRL(bytes.inputStream())
}
}
@Suppress("TooGenericExceptionCaught")
override fun fetch(certificate: X509Certificate): Set<X509CRL> {
val approvedCRLs = HashSet<X509CRL>()
var exception: Exception? = null
for ((distPointUri, issuerNames) in certificate.distributionPoints()) {
try {
val possibleCRL = getPossibleCRL(distPointUri)
if (verifyCRL(possibleCRL, certificate, issuerNames)) {
approvedCRLs += possibleCRL
}
} catch (e: Exception) {
if (exception == null) {
exception = e
} else {
exception.addSuppressed(e)
}
}
}
// Only throw if no CRLs are retrieved
if (exception != null && approvedCRLs.isEmpty()) {
throw exception
} else {
return approvedCRLs
}
}
private fun getPossibleCRL(uri: URI): X509CRL {
return cache[uri]!!
}
// DistributionPointFetcher.verifyCRL
private fun verifyCRL(crl: X509CRL, certificate: X509Certificate, distPointIssuerNames: List<X500Principal>?): Boolean {
val crlIssuer = crl.issuerX500Principal
return distPointIssuerNames?.any { it == crlIssuer } ?: (certificate.issuerX500Principal == crlIssuer)
}
}

View File

@ -1,30 +1,53 @@
package net.corda.nodeapi.internal.protonwrapper.netty.revocation
package net.corda.nodeapi.internal.revocation
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource
import net.corda.nodeapi.internal.protonwrapper.netty.CrlSource
import org.bouncycastle.asn1.x509.Extension
import java.security.cert.CRLReason
import java.security.cert.CertPathValidatorException
import java.security.cert.CertPathValidatorException.BasicReason
import java.security.cert.Certificate
import java.security.cert.CertificateRevokedException
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.util.*
import kotlin.collections.ArrayList
/**
* Implementation of [PKIXRevocationChecker] which determines whether certificate is revoked using [externalCrlSource] which knows how to
* obtain a set of CRLs for a given certificate from an external source
* Custom [PKIXRevocationChecker] which delegates to a plugable [CrlSource] to retrieve the CRLs for certificate revocation checks.
*/
class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrlSource, private val dateSource: () -> Date) : PKIXRevocationChecker() {
class CordaRevocationChecker(private val crlSource: CrlSource,
private val softFail: Boolean,
private val dateSource: () -> Date = ::Date) : PKIXRevocationChecker() {
companion object {
private val logger = contextLogger()
}
private val softFailExceptions = ArrayList<CertPathValidatorException>()
override fun check(cert: Certificate, unresolvedCritExts: MutableCollection<String>?) {
val x509Certificate = cert as X509Certificate
checkApprovedCRLs(x509Certificate, externalCrlSource.fetch(x509Certificate))
checkApprovedCRLs(x509Certificate, getCRLs(x509Certificate))
}
@Suppress("TooGenericExceptionCaught")
private fun getCRLs(cert: X509Certificate): Set<X509CRL> {
val crls = try {
crlSource.fetch(cert)
} catch (e: Exception) {
if (softFail) {
addSoftFailException(e)
return emptySet()
} else {
throw undeterminedRevocationException("Unable to retrieve CRLs", e)
}
}
if (crls.isNotEmpty() || softFail) {
return crls
}
// Note, the JDK tries to find a valid CRL from a different signing key before giving up (RevocationChecker.verifyWithSeparateSigningKey)
throw undeterminedRevocationException("Could not find any valid CRLs", null)
}
/**
@ -47,11 +70,11 @@ class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrl
* 5.3 of RFC 5280).
*/
val unresCritExts = entry.criticalExtensionOIDs
if (unresCritExts != null && !unresCritExts.isEmpty()) {
if (unresCritExts != null && unresCritExts.isNotEmpty()) {
/* remove any that we will process */
unresCritExts.remove(Extension.cRLDistributionPoints.id)
unresCritExts.remove(Extension.certificateIssuer.id)
if (!unresCritExts.isEmpty()) {
if (unresCritExts.isNotEmpty()) {
throw CertPathValidatorException(
"Unrecognized critical extension(s) in revoked CRL entry: $unresCritExts")
}
@ -64,14 +87,22 @@ class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrl
revocationDate, reasonCode,
crl.issuerX500Principal, mutableMapOf())
throw CertPathValidatorException(
t.message, t, null, -1, CertPathValidatorException.BasicReason.REVOKED)
t.message, t, null, -1, BasicReason.REVOKED)
}
}
}
}
/**
* This is set to false intentionally for security reasons.
* It ensures that certificates are provided in reverse direction (from most-trusted CA to target certificate)
* after the necessary validation checks have already been performed.
*
* If that wasn't the case, we could be reaching out to CRL endpoints for invalid certificates, which would open security holes
* e.g. systems that are not part of a Corda network could force a Corda firewall to initiate outbound requests to systems under their control.
*/
override fun isForwardCheckingSupported(): Boolean {
return true
return false
}
override fun getSupportedExtensions(): MutableSet<String>? {
@ -79,10 +110,19 @@ class ExternalSourceRevocationChecker(private val externalCrlSource: ExternalCrl
}
override fun init(forward: Boolean) {
// Nothing to do
softFailExceptions.clear()
}
override fun getSoftFailExceptions(): MutableList<CertPathValidatorException> {
return LinkedList()
return Collections.unmodifiableList(softFailExceptions)
}
private fun addSoftFailException(e: Exception) {
logger.debug("Soft fail exception", e)
softFailExceptions += undeterminedRevocationException(e.message, e)
}
private fun undeterminedRevocationException(message: String?, cause: Throwable?): CertPathValidatorException {
return CertPathValidatorException(message, cause, null, -1, BasicReason.UNDETERMINED_REVOCATION_STATUS)
}
}

View File

@ -0,0 +1,51 @@
package net.corda.nodeapi.internal.revocation
import net.corda.core.crypto.Crypto
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.node.internal.network.CrlServer
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.math.BigInteger
class CertDistPointCrlSourceTest {
private lateinit var crlServer: CrlServer
@Before
fun setUp() {
// Do not use Security.addProvider(BouncyCastleProvider()) to avoid EdDSA signature disruption in other tests.
Crypto.findProvider(BouncyCastleProvider.PROVIDER_NAME)
crlServer = CrlServer(NetworkHostAndPort("localhost", 0))
crlServer.start()
}
@After
fun tearDown() {
if (::crlServer.isInitialized) {
crlServer.close()
}
}
@Test(timeout=300_000)
fun `happy path`() {
val crlSource = CertDistPointCrlSource()
with(crlSource.fetch(crlServer.intermediateCa.certificate)) {
assertThat(size).isEqualTo(1)
assertThat(single().revokedCertificates).isNull()
}
val nodeCaCert = crlServer.replaceNodeCertDistPoint(createDevNodeCa(crlServer.intermediateCa, ALICE_NAME).certificate)
crlServer.revokedNodeCerts += listOf(BigInteger.ONE, BigInteger.TEN)
with(crlSource.fetch(nodeCaCert)) { // Use a different cert to avoid the cache
assertThat(size).isEqualTo(1)
val revokedCertificates = single().revokedCertificates
assertThat(revokedCertificates.map { it.serialNumber }).containsExactlyInAnyOrder(BigInteger.ONE, BigInteger.TEN)
}
}
}

View File

@ -1,26 +1,27 @@
package net.corda.nodeapi.internal.protonwrapper.netty.revocation
package net.corda.nodeapi.internal.revocation
import net.corda.core.utilities.Try
import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS
import net.corda.nodeapi.internal.DEV_CA_PRIVATE_KEY_PASS
import net.corda.nodeapi.internal.config.CertificateStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.protonwrapper.netty.ExternalCrlSource
import net.corda.nodeapi.internal.protonwrapper.netty.CrlSource
import org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory
import org.junit.Test
import java.math.BigInteger
import java.security.cert.X509CRL
import java.security.cert.X509Certificate
import java.sql.Date
import java.time.LocalDate
import java.time.ZoneOffset
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ExternalSourceRevocationCheckerTest {
class CordaRevocationCheckerTest {
@Test(timeout=300_000)
fun checkRevoked() {
val checkResult = performCheckOnDate(Date.valueOf("2019-09-27"))
val checkResult = performCheckOnDate(LocalDate.of(2019, 9, 27))
val failedChecks = checkResult.filterNot { it.second.isSuccess }
assertEquals(1, failedChecks.size)
assertEquals(BigInteger.valueOf(8310484079152632582), failedChecks.first().first.serialNumber)
@ -28,11 +29,11 @@ class ExternalSourceRevocationCheckerTest {
@Test(timeout=300_000)
fun checkTooEarly() {
val checkResult = performCheckOnDate(Date.valueOf("2019-08-27"))
val checkResult = performCheckOnDate(LocalDate.of(2019, 8, 27))
assertTrue(checkResult.all { it.second.isSuccess })
}
private fun performCheckOnDate(date: Date): List<Pair<X509Certificate, Try<Unit>>> {
private fun performCheckOnDate(date: LocalDate): List<Pair<X509Certificate, Try<Unit>>> {
val certStore = CertificateStore.fromResource(
"net/corda/nodeapi/internal/protonwrapper/netty/sslkeystore_Revoked.jks",
DEV_CA_KEY_STORE_PASS, DEV_CA_PRIVATE_KEY_PASS)
@ -40,16 +41,17 @@ class ExternalSourceRevocationCheckerTest {
val resourceAsStream = javaClass.getResourceAsStream("/net/corda/nodeapi/internal/protonwrapper/netty/doorman.crl")
val crl = CertificateFactory().engineGenerateCRL(resourceAsStream) as X509CRL
//val crlHolder = X509CRLHolder(resourceAsStream)
//crlHolder.revokedCertificates as X509CRLEntryHolder
val instance = ExternalSourceRevocationChecker(object : ExternalCrlSource {
val crlSource = object : CrlSource {
override fun fetch(certificate: X509Certificate): Set<X509CRL> = setOf(crl)
}) { date }
}
val checker = CordaRevocationChecker(crlSource,
softFail = true,
dateSource = { Date.from(date.atStartOfDay().toInstant(ZoneOffset.UTC)) }
)
return certStore.query {
getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).map {
Pair(it, Try.on { instance.check(it, mutableListOf()) })
Pair(it, Try.on { checker.check(it, mutableListOf()) })
}
}
}