Store notaries's identity composite key in keystore (#1036)

* * Store composite key in keystore from file for notaries's identity.
* Some refactoring.

* * Addressed PR issues

* * Remove unintended format changes

* * Fixed failing test due to getting keys from wrong keystore
This commit is contained in:
Patrick Kuo 2017-07-19 11:14:48 +01:00 committed by GitHub
parent 8a2074eeee
commit 264b9316e3
5 changed files with 114 additions and 88 deletions

View File

@ -22,7 +22,6 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.cert.path.CertPath
import org.junit.Test import org.junit.Test
import java.nio.file.Files import java.nio.file.Files
@ -118,7 +117,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
X509Utilities.CORDA_CLIENT_CA, X509Utilities.CORDA_CLIENT_CA,
clientKey.private, clientKey.private,
keyPass, keyPass,
CertPath(arrayOf(clientCACert, intermediateCA.certificate, rootCACert))) arrayOf(clientCACert, intermediateCA.certificate, rootCACert))
clientCAKeystore.save(nodeKeystore, keyStorePassword) clientCAKeystore.save(nodeKeystore, keyStorePassword)
val tlsKeystore = loadOrCreateKeyStore(sslKeystore, keyStorePassword) val tlsKeystore = loadOrCreateKeyStore(sslKeystore, keyStorePassword)
@ -126,7 +125,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
X509Utilities.CORDA_CLIENT_TLS, X509Utilities.CORDA_CLIENT_TLS,
tlsKey.private, tlsKey.private,
keyPass, keyPass,
CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert))) arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert))
tlsKeystore.save(sslKeystore, keyStorePassword) tlsKeystore.save(sslKeystore, keyStorePassword)
} }
} }

View File

@ -8,9 +8,9 @@ import com.google.common.util.concurrent.MoreExecutors
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.flatMap
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.identity.PartyAndCertificate
@ -28,7 +28,10 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.toNonEmptySet
import net.corda.flows.* import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow
import net.corda.node.services.* import net.corda.node.services.*
import net.corda.node.services.api.* import net.corda.node.services.api.*
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
@ -69,15 +72,14 @@ import rx.Observable
import java.io.IOException import java.io.IOException
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier.* import java.lang.reflect.Modifier.*
import java.math.BigInteger
import java.net.JarURLConnection import java.net.JarURLConnection
import java.net.URI import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyStore
import java.security.KeyStoreException import java.security.KeyStoreException
import java.security.cert.* import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.Clock import java.time.Clock
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -709,63 +711,62 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
// the legal name is actually validated in some way. // the legal name is actually validated in some way.
// TODO: Integrate with Key management service? // TODO: Integrate with Key management service?
val certFactory = CertificateFactory.getInstance("X509")
val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
val privateKeyAlias = "$serviceId-private-key" val privateKeyAlias = "$serviceId-private-key"
val privKeyFile = configuration.baseDirectory / privateKeyAlias val compositeKeyAlias = "$serviceId-composite-key"
val pubIdentityFile = configuration.baseDirectory / "$serviceId-public"
val certificateAndKeyPair = keyStore.certificateAndKeyPair(privateKeyAlias) if (!keyStore.containsAlias(privateKeyAlias)) {
val identityCertPathAndKey: Pair<PartyAndCertificate, KeyPair> = if (certificateAndKeyPair != null) { val privKeyFile = configuration.baseDirectory / privateKeyAlias
val clientCertPath = keyStore.keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) val pubIdentityFile = configuration.baseDirectory / "$serviceId-public"
val (cert, keyPair) = certificateAndKeyPair // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk].
// Get keys from keystore.
val loadedServiceName = cert.subject
if (loadedServiceName != serviceName) {
throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:" +
"$serviceName vs $loadedServiceName")
}
val certPath = certFactory.generateCertPath(listOf(cert.cert) + clientCertPath)
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 migration in future release.
// Check that the identity in the config file matches the identity file we have stored to disk. if (privKeyFile.exists()) {
// This is just a sanity check. It shouldn't fail unless the admin has fiddled with the files and messed migrateKeysFromFile(keyStore, serviceName, pubIdentityFile, privKeyFile, privateKeyAlias, compositeKeyAlias)
// things up for us. } else {
val myIdentity = pubIdentityFile.readAll().deserialize<Party>() log.info("$privateKeyAlias not found in keystore ${configuration.nodeKeystore}, generating fresh key!")
if (myIdentity.name != serviceName) keyStore.saveNewKeyPair(serviceName, privateKeyAlias, generateKeyPair())
throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:" +
"$serviceName vs ${myIdentity.name}")
// Load the private key.
val keyPair = privKeyFile.readAll().deserialize<KeyPair>()
if (myIdentity.owningKey !is CompositeKey) { // TODO: Support case where owningKey is a composite key.
keyStore.save(serviceName, privateKeyAlias, keyPair)
} }
val dummyCaKey = entropyToKeyPair(BigInteger.valueOf(111))
val dummyCa = CertificateAndKeyPair(
X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), dummyCaKey),
dummyCaKey)
val partyAndCertificate = getTestPartyAndCertificate(myIdentity, dummyCa)
// Sanity check the certificate and path
val validatorParameters = PKIXParameters(setOf(TrustAnchor(dummyCa.certificate.cert, null)))
val validator = CertPathValidator.getInstance("PKIX")
validatorParameters.isRevocationEnabled = false
validator.validate(partyAndCertificate.certPath, validatorParameters) as PKIXCertPathValidatorResult
Pair(partyAndCertificate, keyPair)
} else {
val clientCertPath = keyStore.keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
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 = certFactory.generateCertPath(listOf(cert.cert) + clientCertPath)
keyStore.save(serviceName, privateKeyAlias, keyPair)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
Pair(PartyAndCertificate(serviceName, keyPair.public, cert, certPath), keyPair)
} }
partyKeys += identityCertPathAndKey.second
return identityCertPathAndKey val (cert, keyPair) = keyStore.certificateAndKeyPair(privateKeyAlias)
// Get keys from keystore.
val loadedServiceName = cert.subject
if (loadedServiceName != serviceName)
throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:$serviceName vs $loadedServiceName")
val certPath = CertificateFactory.getInstance("X509").generateCertPath(keyStore.getCertificateChain(privateKeyAlias).toList())
// Use composite key instead if exists
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
val publicKey = if (keyStore.containsAlias(compositeKeyAlias)) {
Crypto.toSupportedPublicKey(keyStore.getCertificate(compositeKeyAlias).publicKey)
} else {
keyPair.public
}
partyKeys += keyPair
return Pair(PartyAndCertificate(loadedServiceName, publicKey, cert, certPath), keyPair)
}
private fun migrateKeysFromFile(keyStore: KeyStoreWrapper, serviceName: X500Name,
pubIdentityFile: Path, privKeyFile: Path,
privateKeyAlias: String, compositeKeyAlias: String) {
log.info("Migrating $privateKeyAlias from file to keystore...")
val myIdentity = pubIdentityFile.readAll().deserialize<Party>()
// 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.
if (myIdentity.name != serviceName)
throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:$serviceName vs ${myIdentity.name}")
// Load the private key.
val keyPair = privKeyFile.readAll().deserialize<KeyPair>()
keyStore.saveNewKeyPair(serviceName, privateKeyAlias, keyPair)
// Store composite key separately.
if (myIdentity.owningKey is CompositeKey) {
keyStore.savePublicKey(serviceName, compositeKeyAlias, myIdentity.owningKey)
}
log.info("Finish migrating $privateKeyAlias from file to keystore.")
} }
private fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair): PartyAndCertificate { private fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair): PartyAndCertificate {
@ -801,10 +802,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override val keyManagementService by lazy { makeKeyManagementService(identityService) } override val keyManagementService by lazy { makeKeyManagementService(identityService) }
override val schedulerService by lazy { NodeSchedulerService(this, unfinishedSchedules = busyNodeLatch) } override val schedulerService by lazy { NodeSchedulerService(this, unfinishedSchedules = busyNodeLatch) }
override val identityService by lazy { override val identityService by lazy {
val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
makeIdentityService( makeIdentityService(
keyStoreWrapper.keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate, trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).cert,
keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA), caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA),
info.legalIdentityAndCert) info.legalIdentityAndCert)
} }
override val attachments: AttachmentStorage get() = this@AbstractNode.attachments override val attachments: AttachmentStorage get() = this@AbstractNode.attachments
@ -836,18 +838,3 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
} }
private class KeyStoreWrapper(val keyStore: KeyStore, val storePath: Path, private val storePassword: String) {
constructor(storePath: Path, storePassword: String) : this(loadKeyStore(storePath, storePassword), storePath, storePassword)
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair? {
return if (keyStore.containsAlias(alias)) keyStore.getCertificateAndKeyPair(alias, storePassword) else null
}
fun save(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) {
val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, storePassword)
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public).cert
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), arrayOf(cert, *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)))
keyStore.save(storePath, storePassword)
}
}

View File

@ -98,7 +98,7 @@ fun createKeystoreForCordaNode(sslKeyStorePath: Path,
X509Utilities.CORDA_CLIENT_CA, X509Utilities.CORDA_CLIENT_CA,
clientKey.private, clientKey.private,
keyPass, keyPass,
org.bouncycastle.cert.path.CertPath(arrayOf(clientCACert, intermediateCACert, rootCACert))) arrayOf(clientCACert, intermediateCACert, rootCACert))
clientCAKeystore.save(clientCAKeystorePath, storePassword) clientCAKeystore.save(clientCAKeystorePath, storePassword)
val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword) val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword)
@ -106,6 +106,6 @@ fun createKeystoreForCordaNode(sslKeyStorePath: Path,
X509Utilities.CORDA_CLIENT_TLS, X509Utilities.CORDA_CLIENT_TLS,
tlsKey.private, tlsKey.private,
keyPass, keyPass,
org.bouncycastle.cert.path.CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))) arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))
tlsKeystore.save(sslKeyStorePath, storePassword) tlsKeystore.save(sslKeyStorePath, storePassword)
} }

View File

@ -1,19 +1,19 @@
package net.corda.node.utilities package net.corda.node.utilities
import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.*
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.cert
import net.corda.core.internal.exists import net.corda.core.internal.exists
import net.corda.core.internal.read import net.corda.core.internal.read
import net.corda.core.internal.write import net.corda.core.internal.write
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.path.CertPath
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Path import java.nio.file.Path
import java.security.* import java.security.*
import java.security.cert.CertPath
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory
val KEYSTORE_TYPE = "JKS" val KEYSTORE_TYPE = "JKS"
@ -77,8 +77,8 @@ fun loadKeyStore(input: InputStream, storePassword: String): KeyStore {
* but for SSL purposes this is recommended. * but for SSL purposes this is recommended.
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert. * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert.
*/ */
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: CertPath) { fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<out X509CertificateHolder>) {
addOrReplaceKey(alias, key, password, chain.certificates.map { it.cert }.toTypedArray<Certificate>()) addOrReplaceKey(alias, key, password, chain.map { it.cert }.toTypedArray<Certificate>())
} }
/** /**
@ -89,7 +89,7 @@ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain
* but for SSL purposes this is recommended. * but for SSL purposes this is recommended.
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert. * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert.
*/ */
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) { fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<out Certificate>) {
if (containsAlias(alias)) { if (containsAlias(alias)) {
this.deleteEntry(alias) this.deleteEntry(alias)
} }
@ -168,3 +168,43 @@ fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
val key = getKey(alias, keyPass) as PrivateKey val key = getKey(alias, keyPass) as PrivateKey
return Crypto.toSupportedPrivateKey(key) return Crypto.toSupportedPrivateKey(key)
} }
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
private fun createCertificate(serviceName: X500Name, pubKey: PublicKey): CertPath {
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// Assume key password = store password.
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
val certPath = CertificateFactory.getInstance("X509").generateCertPath(listOf(cert.cert) + clientCertPath)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
return certPath
}
fun saveNewKeyPair(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) {
val certPath = createCertificate(serviceName, keyPair.public)
// Assume key password = store password.
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
keyStore.save(storePath, storePassword)
}
fun savePublicKey(serviceName: X500Name, pubKeyAlias: String, pubKey: PublicKey) {
val certPath = createCertificate(serviceName, pubKey)
// Assume key password = store password.
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
keyStore.save(storePath, storePassword)
}
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
fun containsAlias(alias: String) = keyStore.containsAlias(alias)
fun getX509Certificate(alias: String) = keyStore.getX509Certificate(alias)
fun getCertificateChain(alias: String): Array<out Certificate> = keyStore.getCertificateChain(alias)
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
fun certificateAndKeyPair(alias: String) = keyStore.getCertificateAndKeyPair(alias, storePassword)
}

View File

@ -49,7 +49,7 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService:
val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName, keyPair) val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName, keyPair)
// Save to the key store. // Save to the key store.
caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(), caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(),
CertPath(arrayOf(selfSignCert))) arrayOf(selfSignCert))
caKeyStore.save(config.nodeKeystore, keystorePassword) caKeyStore.save(config.nodeKeystore, keystorePassword)
} }
val keyPair = caKeyStore.getKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) val keyPair = caKeyStore.getKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword)