mirror of
https://github.com/corda/corda.git
synced 2025-06-13 04:38:19 +00:00
ENT-8898: Replaced JDK cert revocation with custom plugable implementation (#7322)
This commit is contained in:
@ -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"
|
||||
|
@ -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?) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
||||
}
|
@ -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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()) })
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user