Enforce certificate constraints on all identities

* Enforce that the identity service must always have a root CA specified, which all identities have
certificates signed by (or intermediaries of). Also adds a certificate store to the identity service
for help building/verifying certificate paths.
* Add a certificate store for the CA certificate and intermediaries
* Use the certificate factory directly to build paths rather than assembling them via an interim
API call. After reducing the complexity of the utility API, it's replacing two lines of code,
at which point it seems better to make the behaviour clearer rather than having a function
hide what's actually going on.
This commit is contained in:
Ross Nicoll 2017-06-23 15:18:13 +01:00 committed by GitHub
parent 1866f6ff7f
commit 14068f1b96
9 changed files with 57 additions and 39 deletions

View File

@ -2,13 +2,11 @@ package net.corda.core.node.services
import net.corda.core.contracts.PartyAndReference
import net.corda.core.identity.*
import net.corda.core.node.NodeInfo
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.InvalidAlgorithmParameterException
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.CertificateExpiredException
import java.security.cert.CertificateNotYetValidException
import java.security.cert.*
/**
* An identity service maintains a directory of parties by their associated distinguished name/public keys and thus
@ -16,6 +14,10 @@ import java.security.cert.CertificateNotYetValidException
* identities back to the well known identity (i.e. the identity in the network map) of a party.
*/
interface IdentityService {
val trustRoot: X509Certificate
val trustRootHolder: X509CertificateHolder
val caCertStore: CertStore
/**
* Verify and then store a well known identity.
*

View File

@ -73,6 +73,7 @@ object DefaultKryoCustomizer {
noReferencesWithin<WireTransaction>()
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)

View File

@ -2,14 +2,12 @@ package net.corda.services.messaging
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.cert
import net.corda.core.getOrThrow
import net.corda.core.node.NodeInfo
import net.corda.core.random63BitValue
import net.corda.core.seconds
import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_BANK_B
import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.core.utilities.*
import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.sendRequest
@ -24,8 +22,8 @@ import net.corda.testing.node.SimpleNode
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import org.junit.Test
import java.security.cert.X509Certificate
import java.time.Instant
import java.util.concurrent.TimeoutException
@ -46,7 +44,7 @@ class P2PSecurityTest : NodeBasedTest() {
@Test
fun `register with the network map service using a legal name different from the TLS CN`() {
startSimpleNode(DUMMY_BANK_A.name).use {
startSimpleNode(DUMMY_BANK_A.name, DUMMY_CA.certificate.cert).use {
// Register with the network map using a different legal name
val response = it.registerWithNetworkMap(DUMMY_BANK_B.name)
// We don't expect a response because the network map's host verification will prevent a connection back
@ -58,7 +56,7 @@ class P2PSecurityTest : NodeBasedTest() {
}
private fun startSimpleNode(legalName: X500Name,
trustRoot: X509CertificateHolder? = null): SimpleNode {
trustRoot: X509Certificate): SimpleNode {
val config = TestNodeConfiguration(
baseDirectory = baseDirectory(legalName),
myLegalName = legalName,

View File

@ -221,7 +221,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
// Do all of this in a database transaction so anything that might need a connection has one.
initialiseDatabasePersistence {
val tokenizableServices = makeServices()
val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
val tokenizableServices = makeServices(keyStoreWrapper)
smm = StateMachineManager(services,
checkpointStorage,
@ -452,7 +453,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
* Builds node internal, advertised, and plugin services.
* Returns a list of tokenizable services to be added to the serialisation context.
*/
private fun makeServices(): MutableList<Any> {
private fun makeServices(keyStoreWrapper: KeyStoreWrapper): MutableList<Any> {
val keyStore = keyStoreWrapper.keyStore
val storageServices = initialiseStorageService(configuration.baseDirectory)
storage = storageServices.first
checkpointStorage = storageServices.second
@ -465,7 +467,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
auditService = DummyAuditService()
info = makeInfo()
identity = makeIdentityService()
identity = makeIdentityService(keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate,
keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA),
info.legalIdentityAndCert)
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
// the identity key. But the infrastructure to make that easy isn't here yet.
@ -701,10 +705,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
protected abstract fun makeUniquenessProvider(type: ServiceType): UniquenessProvider
protected open fun makeIdentityService(): IdentityService {
val keyStore = KeyStoreUtilities.loadKeyStore(configuration.trustStoreFile, configuration.trustStorePassword)
val trustRoot = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as? X509Certificate
val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot)
protected open fun makeIdentityService(trustRoot: X509Certificate,
clientCa: CertificateAndKeyPair?,
legalIdentity: PartyAndCertificate): IdentityService {
val caCertificates: Array<X509Certificate> = listOf(legalIdentity.certificate.cert, clientCa?.certificate?.cert)
.filterNotNull()
.toTypedArray()
val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates)
services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.legalIdentityAndCert) }
netMapCache.changed.subscribe { mapChange ->
// TODO how should we handle network map removal

View File

@ -31,22 +31,30 @@ import kotlin.collections.ArrayList
* @param certPaths initial set of certificate paths for the service, typically only used for unit tests.
*/
@ThreadSafe
class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
class InMemoryIdentityService(identities: Iterable<PartyAndCertificate> = emptySet(),
certPaths: Map<AnonymousParty, CertPath> = emptyMap(),
val trustRoot: X509Certificate?) : SingletonSerializeAsToken(), IdentityService {
override val trustRoot: X509Certificate,
vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityService {
constructor(identities: Iterable<PartyAndCertificate> = emptySet(),
certPaths: Map<AnonymousParty, CertPath> = emptyMap(),
trustRoot: X509CertificateHolder?) : this(identities, certPaths, trustRoot?.cert)
trustRoot: X509CertificateHolder) : this(identities, certPaths, trustRoot.cert)
companion object {
private val log = loggerFor<InMemoryIdentityService>()
}
private val trustAnchor: TrustAnchor? = trustRoot?.let { cert -> TrustAnchor(cert, null) }
/**
* Certificate store for certificate authority and intermediary certificates.
*/
override val caCertStore: CertStore
override val trustRootHolder = X509CertificateHolder(trustRoot.encoded)
private val trustAnchor: TrustAnchor = TrustAnchor(trustRoot, null)
private val keyToParties = ConcurrentHashMap<PublicKey, PartyAndCertificate>()
private val principalToParties = ConcurrentHashMap<X500Name, PartyAndCertificate>()
private val partyToPath = ConcurrentHashMap<AbstractParty, CertPath>()
init {
val caCertificatesWithRoot: Set<X509Certificate> = caCertificates.toSet() + trustRoot
caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificatesWithRoot))
keyToParties.putAll(identities.associateBy { it.owningKey } )
principalToParties.putAll(identities.associateBy { it.name })
partyToPath.putAll(certPaths)
@ -57,7 +65,7 @@ class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
override fun registerIdentity(party: PartyAndCertificate) {
require(party.certPath.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
// Validate the chain first, before we do anything clever with it
if (trustRoot != null) validateCertificatePath(party.party, party.certPath)
validateCertificatePath(party.party, party.certPath)
log.trace { "Registering identity $party" }
require(Arrays.equals(party.certificate.subjectPublicKeyInfo.encoded, party.owningKey.encoded)) { "Party certificate must end with party's public key" }
@ -122,7 +130,7 @@ class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
val fullParty = certificateFromParty(party) ?: throw IllegalArgumentException("Unknown identity ${party.name}")
require(path.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
// Validate the chain first, before we do anything clever with it
if (trustRoot != null) validateCertificatePath(anonymousParty, path)
validateCertificatePath(anonymousParty, path)
val subjectCertificate = path.certificates.first()
require(subjectCertificate is X509Certificate && subjectCertificate.subject == fullParty.name) { "Subject of the transaction certificate must match the well known identity" }

View File

@ -1,6 +1,6 @@
{
"fixedLeg": {
"fixedRatePayer": "8Kqd4oWdx4KQAVcA8RDJXNvzFMvBkqWTZPhRtg9dSpNs6T6eZ4cGJWA7FWK",
"fixedRatePayer": "8Kqd4oWdx4KQGHGR7xcgpFf9JmP6HiXqTf85NpSgdSu431EGEhujA6ePaFD",
"notional": "$25000000",
"paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-11",
@ -22,7 +22,7 @@
"interestPeriodAdjustment": "Adjusted"
},
"floatingLeg": {
"floatingRatePayer": "8Kqd4oWdx4KQAVc3Si48msuQrMJPGpA3TnGGuWTqXyoshjL25wzzdxtGyjq",
"floatingRatePayer": "8Kqd4oWdx4KQGHGJSFTX4kdZukmHohBRN3gvPekticL4eHTdmbJTVZNZJUj",
"notional": {
"quantity": 2500000000,
"token": "USD"

View File

@ -5,11 +5,7 @@ package net.corda.testing
import com.google.common.net.HostAndPort
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.commonName
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AnonymousParty
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.ServiceHub
@ -33,7 +29,6 @@ import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.CertPath
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
@ -89,7 +84,7 @@ val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference
val ALL_TEST_KEYS: List<KeyPair> get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY)
val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY)
val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate)
val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate.cert)
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor")

View File

@ -5,8 +5,9 @@ import com.google.common.jimfs.Jimfs
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.*
import net.corda.core.crypto.CertificateAndKeyPair
import net.corda.core.crypto.cert
import net.corda.core.crypto.entropyToKeyPair
import net.corda.flows.TxKeyFlow
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.RPCOps
@ -18,6 +19,7 @@ import net.corda.core.node.services.*
import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.core.utilities.loggerFor
import net.corda.flows.TxKeyFlow
import net.corda.node.internal.AbstractNode
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.identity.InMemoryIdentityService
@ -166,9 +168,14 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
.getOrThrow()
}
// TODO: Specify a CA to validate registration against
override fun makeIdentityService(): IdentityService {
return InMemoryIdentityService((mockNet.identities + info.legalIdentityAndCert).toSet(), trustRoot = null as X509Certificate?)
override fun makeIdentityService(trustRoot: X509Certificate,
clientCa: CertificateAndKeyPair?,
legalIdentity: PartyAndCertificate): IdentityService {
val caCertificates: Array<X509Certificate> = listOf(legalIdentity.certificate.cert, clientCa?.certificate?.cert)
.filterNotNull()
.toTypedArray()
return InMemoryIdentityService((mockNet.identities + info.legalIdentityAndCert).toSet(),
trustRoot = trustRoot, caCertificates = *caCertificates)
}
override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties)

View File

@ -21,10 +21,10 @@ import net.corda.node.utilities.configureDatabase
import net.corda.node.utilities.transaction
import net.corda.testing.MOCK_VERSION_INFO
import net.corda.testing.freeLocalHostAndPort
import org.bouncycastle.cert.X509CertificateHolder
import org.jetbrains.exposed.sql.Database
import java.io.Closeable
import java.security.KeyPair
import java.security.cert.X509Certificate
import kotlin.concurrent.thread
/**
@ -33,7 +33,7 @@ import kotlin.concurrent.thread
*/
class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(),
rpcAddress: HostAndPort = freeLocalHostAndPort(),
trustRoot: X509CertificateHolder? = null) : AutoCloseable {
trustRoot: X509Certificate) : AutoCloseable {
private val databaseWithCloseable: Pair<Closeable, Database> = configureDatabase(config.dataSourceProperties)
val database: Database get() = databaseWithCloseable.second