Add certificate to node identity (#769)

Add certificate and path to node identity via the `NodeInfo` class.
This commit is contained in:
Ross Nicoll 2017-05-31 11:52:50 +01:00 committed by GitHub
parent f148765c57
commit 08c91bd611
10 changed files with 49 additions and 23 deletions

View File

@ -10,6 +10,7 @@ import net.corda.core.identity.Party
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.DUMMY_CA
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.NotaryError import net.corda.flows.NotaryError
import net.corda.flows.NotaryException import net.corda.flows.NotaryException
@ -66,6 +67,7 @@ class BFTNotaryServiceTests : NodeBasedTest() {
val replicaNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } val replicaNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") }
ServiceIdentityGenerator.generateToDisk( ServiceIdentityGenerator.generateToDisk(
replicaNames.map { baseDirectory(it) }, replicaNames.map { baseDirectory(it) },
DUMMY_CA,
serviceType.id, serviceType.id,
clusterName, clusterName,
minCorrectReplicas(clusterSize)) minCorrectReplicas(clusterSize))

View File

@ -64,6 +64,7 @@ class P2PMessagingTest : NodeBasedTest() {
fun `communicating with a distributed service which the network map node is part of`() { fun `communicating with a distributed service which the network map node is part of`() {
ServiceIdentityGenerator.generateToDisk( ServiceIdentityGenerator.generateToDisk(
listOf(DUMMY_MAP.name, SERVICE_2_NAME).map { baseDirectory(it) }, listOf(DUMMY_MAP.name, SERVICE_2_NAME).map { baseDirectory(it) },
DUMMY_CA,
RaftValidatingNotaryService.type.id, RaftValidatingNotaryService.type.id,
DISTRIBUTED_SERVICE_NAME) DISTRIBUTED_SERVICE_NAME)

View File

@ -3,13 +3,13 @@ package net.corda.services.messaging
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.random63BitValue import net.corda.core.random63BitValue
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_BANK_B
import net.corda.core.utilities.getTestPartyAndCertificate
import net.corda.node.internal.NetworkMapInfo import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.sendRequest import net.corda.node.services.messaging.sendRequest
@ -66,7 +66,7 @@ class P2PSecurityTest : NodeBasedTest() {
} }
private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture<NetworkMapService.RegistrationResponse> { private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture<NetworkMapService.RegistrationResponse> {
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 registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX)
val request = RegistrationRequest(registration.toWire(keyService, identity.public), net.myAddress) val request = RegistrationRequest(registration.toWire(keyService, identity.public), net.myAddress)
return net.sendRequest<NetworkMapService.RegistrationResponse>(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.net.myAddress) return net.sendRequest<NetworkMapService.RegistrationResponse>(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.net.myAddress)

View File

@ -556,7 +556,7 @@ class DriverDSL(
): ListenableFuture<Pair<Party, List<NodeHandle>>> { ): ListenableFuture<Pair<Party, List<NodeHandle>>> {
val nodeNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } val nodeNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") }
val paths = nodeNames.map { baseDirectory(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 advertisedServices = setOf(ServiceInfo(type, notaryName))
val notaryClusterAddress = portAllocation.nextHostAndPort() val notaryClusterAddress = portAllocation.nextHostAndPort()

View File

@ -12,6 +12,7 @@ import net.corda.core.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
@ -708,11 +709,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) = stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) =
StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage) StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage)
protected fun obtainLegalIdentity(): Party = identityKeyPair.first protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first
protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second
private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) }
private fun obtainKeyPair(serviceId: String, serviceName: X500Name): Pair<Party, KeyPair> { private fun obtainKeyPair(serviceId: String, serviceName: X500Name): Pair<PartyAndCertificate, KeyPair> {
// Load the private identity key, creating it if necessary. The identity key is a long term well known key that // 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 // 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 // "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 privateKeyAlias = "$serviceId-private-key"
val privKeyFile = configuration.baseDirectory / privateKeyAlias val privKeyFile = configuration.baseDirectory / privateKeyAlias
val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" val pubIdentityFile = configuration.baseDirectory / "$serviceId-public"
val identityAndKey = keyStore.certificateAndKeyPair(privateKeyAlias)?.let { (cert, keyPair) -> val certificateAndKeyPair = keyStore.certificateAndKeyPair(privateKeyAlias)
val identityCertPathAndKey: Pair<PartyAndCertificate, KeyPair> = if (certificateAndKeyPair != null) {
val (cert, keyPair) = certificateAndKeyPair
// Get keys from keystore. // Get keys from keystore.
val loadedServiceName = X509CertificateHolder(cert.encoded).subject val loadedServiceName = X509CertificateHolder(cert.encoded).subject
if (loadedServiceName != serviceName) { if (loadedServiceName != serviceName) {
throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:" + throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:" +
"$serviceName vs $loadedServiceName") "$serviceName vs $loadedServiceName")
} }
Pair(Party(loadedServiceName, keyPair.public), keyPair) val certPath = X509Utilities.createCertificatePath(cert, cert, revocationEnabled = false)
} ?: if (privKeyFile.exists()) { Pair(PartyAndCertificate(loadedServiceName, keyPair.public, cert, certPath), keyPair)
} else if (privKeyFile.exists()) {
// Get keys from key file. // Get keys from key file.
// TODO: this is here to smooth out the key storage transition, remove this in future release. // 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. // 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 // This is just a sanity check. It shouldn't fail unless the admin has fiddled with the files and messed
// things up for us. // things up for us.
val myIdentity = pubIdentityFile.readAll().deserialize<Party>() val myIdentity = pubIdentityFile.readAll().deserialize<PartyAndCertificate>()
if (myIdentity.name != serviceName) if (myIdentity.name != serviceName)
throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:" + throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:" +
"$serviceName vs ${myIdentity.name}") "$serviceName vs ${myIdentity.name}")
@ -749,14 +753,18 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
Pair(myIdentity, keyPair) Pair(myIdentity, keyPair)
} else { } else {
val clientCA = keyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)!!
// Create new keys and store in keystore. // Create new keys and store in keystore.
log.info("Identity key not found, generating fresh key!") log.info("Identity key not found, generating fresh key!")
val keyPair: KeyPair = generateKeyPair() 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) 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 partyKeys += identityCertPathAndKey.second
return identityAndKey return identityCertPathAndKey
} }
protected open fun generateKeyPair() = cryptoGenerateKeyPair() 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) { fun save(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) {
val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, storePassword)
val converter = JcaX509CertificateConverter() val converter = JcaX509CertificateConverter()
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, storePassword)
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), val cert = converter.getCertificate(X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public))
arrayOf(converter.getCertificate(cert), *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), arrayOf(cert, *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)))
keyStore.save(storePath, storePassword) keyStore.save(storePath, storePassword)
} }
} }

View File

@ -1,8 +1,8 @@
package net.corda.node.utilities 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.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.serialize
import net.corda.core.serialization.storageKryo import net.corda.core.serialization.storageKryo
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
@ -20,16 +20,27 @@ object ServiceIdentityGenerator {
* This method should be called *before* any of the nodes are started. * 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 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 serviceId The service id of the distributed service.
* @param serviceName The legal name of the distributed service. * @param serviceName The legal name of the distributed service.
* @param threshold The threshold for the generated group [CompositeKey]. * @param threshold The threshold for the generated group [CompositeKey].
*/ */
fun generateToDisk(dirs: List<Path>, 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<Path>,
serviceCa: CertificateAndKeyPair,
serviceId: String,
serviceName: X500Name,
threshold: Int = 1) {
log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" }
val keyPairs = (1..dirs.size).map { generateKeyPair() } val keyPairs = (1..dirs.size).map { generateKeyPair() }
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) 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 -> keyPairs.zip(dirs) { keyPair, dir ->
Files.createDirectories(dir) Files.createDirectories(dir)

View File

@ -5,6 +5,7 @@ import net.corda.core.div
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_CA
import net.corda.demorun.util.* import net.corda.demorun.util.*
import net.corda.demorun.runNodes import net.corda.demorun.runNodes
import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.BFTNonValidatingNotaryService
@ -64,6 +65,6 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not
} }
override fun setup(context: CordformContext) { 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))
} }
} }

View File

@ -6,6 +6,7 @@ import net.corda.core.div
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_CA
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.demorun.util.* import net.corda.demorun.util.*
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
@ -65,6 +66,6 @@ object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", no
} }
override fun setup(context: CordformContext) { 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)
} }
} }

View File

@ -12,6 +12,7 @@ import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.DUMMY_NOTARY 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.identity.InMemoryIdentityService
import net.corda.node.services.keys.freshKeyAndCert import net.corda.node.services.keys.freshKeyAndCert
import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransactionMappingStorage 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 vaultService: VaultService get() = throw UnsupportedOperationException()
override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException()
override val clock: Clock get() = Clock.systemUTC() 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) override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2)
fun makeVaultService(dataSourceProps: Properties): VaultService { fun makeVaultService(dataSourceProps: Properties): VaultService {

View File

@ -8,6 +8,7 @@ import net.corda.core.crypto.appendToCommonName
import net.corda.core.crypto.commonName import net.corda.core.crypto.commonName
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType 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.DUMMY_MAP
import net.corda.core.utilities.WHITESPACE import net.corda.core.utilities.WHITESPACE
import net.corda.node.driver.addressMustNotBeBound import net.corda.node.driver.addressMustNotBeBound
@ -110,6 +111,7 @@ abstract class NodeBasedTest {
serviceType: ServiceType = RaftValidatingNotaryService.type): ListenableFuture<List<Node>> { serviceType: ServiceType = RaftValidatingNotaryService.type): ListenableFuture<List<Node>> {
ServiceIdentityGenerator.generateToDisk( ServiceIdentityGenerator.generateToDisk(
(0 until clusterSize).map { baseDirectory(notaryName.appendToCommonName("-$it")) }, (0 until clusterSize).map { baseDirectory(notaryName.appendToCommonName("-$it")) },
DUMMY_CA,
serviceType.id, serviceType.id,
notaryName) notaryName)