From 08c91bd61180fe2efb1473b7430c4162852b119c Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Wed, 31 May 2017 11:52:50 +0100 Subject: [PATCH] Add certificate to node identity (#769) Add certificate and path to node identity via the `NodeInfo` class. --- .../node/services/BFTNotaryServiceTests.kt | 2 ++ .../services/messaging/P2PMessagingTest.kt | 1 + .../services/messaging/P2PSecurityTest.kt | 4 +-- .../kotlin/net/corda/node/driver/Driver.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 33 +++++++++++-------- .../utilities/ServiceIdentityGenerator.kt | 19 ++++++++--- .../net/corda/notarydemo/BFTNotaryCordform.kt | 3 +- .../corda/notarydemo/RaftNotaryCordform.kt | 3 +- .../net/corda/testing/node/MockServices.kt | 3 +- .../net/corda/testing/node/NodeBasedTest.kt | 2 ++ 10 files changed, 49 insertions(+), 23 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 155fe9c43c..57c0330e39 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.ALICE +import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException @@ -66,6 +67,7 @@ class BFTNotaryServiceTests : NodeBasedTest() { val replicaNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } ServiceIdentityGenerator.generateToDisk( replicaNames.map { baseDirectory(it) }, + DUMMY_CA, serviceType.id, clusterName, minCorrectReplicas(clusterSize)) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index b4332d56b9..04b69f6c86 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -64,6 +64,7 @@ class P2PMessagingTest : NodeBasedTest() { fun `communicating with a distributed service which the network map node is part of`() { ServiceIdentityGenerator.generateToDisk( listOf(DUMMY_MAP.name, SERVICE_2_NAME).map { baseDirectory(it) }, + DUMMY_CA, RaftValidatingNotaryService.type.id, DISTRIBUTED_SERVICE_NAME) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index 95fb7c9b23..79d042304b 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -3,13 +3,13 @@ package net.corda.services.messaging import com.google.common.util.concurrent.ListenableFuture import net.corda.core.crypto.X509Utilities import net.corda.core.getOrThrow -import net.corda.core.identity.Party 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.node.internal.NetworkMapInfo import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest @@ -66,7 +66,7 @@ class P2PSecurityTest : NodeBasedTest() { } private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture { - val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public), MOCK_VERSION_INFO.platformVersion) + val nodeInfo = NodeInfo(net.myAddress, getTestPartyAndCertificate(registrationName, identity.public), MOCK_VERSION_INFO.platformVersion) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX) val request = RegistrationRequest(registration.toWire(keyService, identity.public), net.myAddress) return net.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.net.myAddress) diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 0d9bfe3273..58ecd6c17d 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -556,7 +556,7 @@ class DriverDSL( ): ListenableFuture>> { val nodeNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } val paths = nodeNames.map { baseDirectory(it) } - ServiceIdentityGenerator.generateToDisk(paths, type.id, notaryName) + ServiceIdentityGenerator.generateToDisk(paths, DUMMY_CA, type.id, notaryName) val advertisedServices = setOf(ServiceInfo(type, notaryName)) val notaryClusterAddress = portAllocation.nextHostAndPort() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index c87b0361b0..cd23b29918 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -12,6 +12,7 @@ import net.corda.core.* import net.corda.core.crypto.* import net.corda.core.flows.* import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient @@ -708,11 +709,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) = StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage) - protected fun obtainLegalIdentity(): Party = identityKeyPair.first + protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } - private fun obtainKeyPair(serviceId: String, serviceName: X500Name): Pair { + private fun obtainKeyPair(serviceId: String, serviceName: X500Name): Pair { // Load the private identity key, creating it if necessary. The identity key is a long term well known key that // is distributed to other peers and we use it (or a key signed by it) when we need to do something // "permissioned". The identity file is what gets distributed and contains the node's legal name along with @@ -724,21 +725,24 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val privateKeyAlias = "$serviceId-private-key" val privKeyFile = configuration.baseDirectory / privateKeyAlias val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" - val identityAndKey = keyStore.certificateAndKeyPair(privateKeyAlias)?.let { (cert, keyPair) -> + val certificateAndKeyPair = keyStore.certificateAndKeyPair(privateKeyAlias) + val identityCertPathAndKey: Pair = if (certificateAndKeyPair != null) { + val (cert, keyPair) = certificateAndKeyPair // Get keys from keystore. val loadedServiceName = X509CertificateHolder(cert.encoded).subject if (loadedServiceName != serviceName) { throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:" + "$serviceName vs $loadedServiceName") } - Pair(Party(loadedServiceName, keyPair.public), keyPair) - } ?: if (privKeyFile.exists()) { + val certPath = X509Utilities.createCertificatePath(cert, cert, revocationEnabled = false) + Pair(PartyAndCertificate(loadedServiceName, keyPair.public, cert, certPath), keyPair) + } else if (privKeyFile.exists()) { // Get keys from key file. // TODO: this is here to smooth out the key storage transition, remove this in future release. // Check that the identity in the config file matches the identity file we have stored to disk. // This is just a sanity check. It shouldn't fail unless the admin has fiddled with the files and messed // things up for us. - val myIdentity = pubIdentityFile.readAll().deserialize() + val myIdentity = pubIdentityFile.readAll().deserialize() if (myIdentity.name != serviceName) throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:" + "$serviceName vs ${myIdentity.name}") @@ -749,14 +753,18 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } Pair(myIdentity, keyPair) } else { + val clientCA = keyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)!! // Create new keys and store in keystore. log.info("Identity key not found, generating fresh key!") val keyPair: KeyPair = generateKeyPair() + val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) + val certPath = X509Utilities.createCertificatePath(cert, cert, revocationEnabled = false) keyStore.save(serviceName, privateKeyAlias, keyPair) - Pair(Party(serviceName, keyPair.public), keyPair) + require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } + Pair(PartyAndCertificate(serviceName, keyPair.public, cert, certPath), keyPair) } - partyKeys += identityAndKey.second - return identityAndKey + partyKeys += identityCertPathAndKey.second + return identityCertPathAndKey } protected open fun generateKeyPair() = cryptoGenerateKeyPair() @@ -783,11 +791,10 @@ private class KeyStoreWrapper(private val storePath: Path, private val storePass } fun save(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) { - val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, storePassword) val converter = JcaX509CertificateConverter() - val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) - keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), - arrayOf(converter.getCertificate(cert), *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, storePassword) + val cert = converter.getCertificate(X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public)) + keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), arrayOf(cert, *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) keyStore.save(storePath, storePassword) } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt index 31d0bce621..21e535e51e 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -1,8 +1,8 @@ package net.corda.node.utilities -import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.* import net.corda.core.identity.Party -import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.PartyAndCertificate import net.corda.core.serialization.serialize import net.corda.core.serialization.storageKryo import net.corda.core.utilities.loggerFor @@ -20,16 +20,27 @@ object ServiceIdentityGenerator { * This method should be called *before* any of the nodes are started. * * @param dirs List of node directories to place the generated identity and key pairs in. + * @param serviceCa Certificate authority to use when signing identity certificates. * @param serviceId The service id of the distributed service. * @param serviceName The legal name of the distributed service. * @param threshold The threshold for the generated group [CompositeKey]. */ - fun generateToDisk(dirs: List, serviceId: String, serviceName: X500Name, threshold: Int = 1) { + // TODO: This needs to write out to the key store, not just files on disk + fun generateToDisk(dirs: List, + serviceCa: CertificateAndKeyPair, + serviceId: String, + serviceName: X500Name, + threshold: Int = 1) { log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } val keyPairs = (1..dirs.size).map { generateKeyPair() } val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) - val notaryParty = Party(serviceName, notaryKey).serialize() + // TODO: This doesn't work until we have composite keys in X.509 certificates, so we make up a certificate that nothing checks + // val notaryCert = X509Utilities.createCertificate(CertificateType.IDENTITY, serviceCa.certificate, + // serviceCa.keyPair, serviceName, notaryKey) + val notaryCert = X509Utilities.createSelfSignedCACertificate(serviceName, generateKeyPair()) + val notaryCertPath = X509Utilities.createCertificatePath(serviceCa.certificate, notaryCert, revocationEnabled = false) + val notaryParty = PartyAndCertificate(serviceName, notaryKey, notaryCert, notaryCertPath).serialize() keyPairs.zip(dirs) { keyPair, dir -> Files.createDirectories(dir) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 06a198c41d..a1decb054c 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -5,6 +5,7 @@ import net.corda.core.div import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_CA import net.corda.demorun.util.* import net.corda.demorun.runNodes import net.corda.node.services.transactions.BFTNonValidatingNotaryService @@ -64,6 +65,6 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not } override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, advertisedService.type.id, clusterName, minCorrectReplicas(clusterSize)) + ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, DUMMY_CA, advertisedService.type.id, clusterName, minCorrectReplicas(clusterSize)) } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 7c2dd027bf..fd818bb9a3 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -6,6 +6,7 @@ import net.corda.core.div import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.DUMMY_NOTARY import net.corda.demorun.util.* import net.corda.node.services.transactions.RaftValidatingNotaryService @@ -65,6 +66,6 @@ object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", no } override fun setup(context: CordformContext) { - ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, advertisedService.type.id, clusterName) + ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, DUMMY_CA, advertisedService.type.id, clusterName) } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index b9bf2ae80b..a41d27720e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -12,6 +12,7 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshKeyAndCert import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransactionMappingStorage @@ -68,7 +69,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override val vaultService: VaultService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() - override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, Party(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) + override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, getTestPartyAndCertificate(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) fun makeVaultService(dataSourceProps: Properties): VaultService { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 7b44b39910..47afafc3dc 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType +import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.DUMMY_MAP import net.corda.core.utilities.WHITESPACE import net.corda.node.driver.addressMustNotBeBound @@ -110,6 +111,7 @@ abstract class NodeBasedTest { serviceType: ServiceType = RaftValidatingNotaryService.type): ListenableFuture> { ServiceIdentityGenerator.generateToDisk( (0 until clusterSize).map { baseDirectory(notaryName.appendToCommonName("-$it")) }, + DUMMY_CA, serviceType.id, notaryName)