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.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.cert.path.CertPath
import org.junit.Test
import java.nio.file.Files
@ -118,7 +117,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
X509Utilities.CORDA_CLIENT_CA,
clientKey.private,
keyPass,
CertPath(arrayOf(clientCACert, intermediateCA.certificate, rootCACert)))
arrayOf(clientCACert, intermediateCA.certificate, rootCACert))
clientCAKeystore.save(nodeKeystore, keyStorePassword)
val tlsKeystore = loadOrCreateKeyStore(sslKeystore, keyStorePassword)
@ -126,7 +125,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
X509Utilities.CORDA_CLIENT_TLS,
tlsKey.private,
keyPass,
CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert)))
arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert))
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 io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.*
import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.flatMap
import net.corda.core.flows.*
import net.corda.core.identity.Party
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.debug
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.api.*
import net.corda.node.services.config.NodeConfiguration
@ -69,15 +72,14 @@ import rx.Observable
import java.io.IOException
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Modifier.*
import java.math.BigInteger
import java.net.JarURLConnection
import java.net.URI
import java.nio.file.Path
import java.nio.file.Paths
import java.security.KeyPair
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.cert.*
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.Clock
import java.util.*
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.
// TODO: Integrate with Key management service?
val certFactory = CertificateFactory.getInstance("X509")
val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
val privateKeyAlias = "$serviceId-private-key"
val privKeyFile = configuration.baseDirectory / privateKeyAlias
val pubIdentityFile = configuration.baseDirectory / "$serviceId-public"
val certificateAndKeyPair = keyStore.certificateAndKeyPair(privateKeyAlias)
val identityCertPathAndKey: Pair<PartyAndCertificate, KeyPair> = if (certificateAndKeyPair != null) {
val clientCertPath = keyStore.keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
val (cert, keyPair) = certificateAndKeyPair
// 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()) {
val compositeKeyAlias = "$serviceId-composite-key"
if (!keyStore.containsAlias(privateKeyAlias)) {
val privKeyFile = configuration.baseDirectory / privateKeyAlias
val pubIdentityFile = configuration.baseDirectory / "$serviceId-public"
// TODO: Remove use of [ServiceIdentityGenerator.generateToDisk].
// 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<Party>()
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>()
if (myIdentity.owningKey !is CompositeKey) { // TODO: Support case where owningKey is a composite key.
keyStore.save(serviceName, privateKeyAlias, keyPair)
// TODO: this is here to smooth out the key storage transition, remove this migration in future release.
if (privKeyFile.exists()) {
migrateKeysFromFile(keyStore, serviceName, pubIdentityFile, privKeyFile, privateKeyAlias, compositeKeyAlias)
} else {
log.info("$privateKeyAlias not found in keystore ${configuration.nodeKeystore}, generating fresh key!")
keyStore.saveNewKeyPair(serviceName, privateKeyAlias, generateKeyPair())
}
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 {
@ -801,10 +802,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override val keyManagementService by lazy { makeKeyManagementService(identityService) }
override val schedulerService by lazy { NodeSchedulerService(this, unfinishedSchedules = busyNodeLatch) }
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(
keyStoreWrapper.keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate,
keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA),
trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA).cert,
caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA),
info.legalIdentityAndCert)
}
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,
clientKey.private,
keyPass,
org.bouncycastle.cert.path.CertPath(arrayOf(clientCACert, intermediateCACert, rootCACert)))
arrayOf(clientCACert, intermediateCACert, rootCACert))
clientCAKeystore.save(clientCAKeystorePath, storePassword)
val tlsKeystore = loadOrCreateKeyStore(sslKeyStorePath, storePassword)
@ -106,6 +106,6 @@ fun createKeystoreForCordaNode(sslKeyStorePath: Path,
X509Utilities.CORDA_CLIENT_TLS,
tlsKey.private,
keyPass,
org.bouncycastle.cert.path.CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert)))
arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))
tlsKeystore.save(sslKeyStorePath, storePassword)
}

View File

@ -1,19 +1,19 @@
package net.corda.node.utilities
import net.corda.core.crypto.CertificateAndKeyPair
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.cert
import net.corda.core.crypto.*
import net.corda.core.internal.exists
import net.corda.core.internal.read
import net.corda.core.internal.write
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.path.CertPath
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.security.*
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
val KEYSTORE_TYPE = "JKS"
@ -77,8 +77,8 @@ fun loadKeyStore(input: InputStream, storePassword: String): KeyStore {
* 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.
*/
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: CertPath) {
addOrReplaceKey(alias, key, password, chain.certificates.map { it.cert }.toTypedArray<Certificate>())
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<out X509CertificateHolder>) {
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.
* @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)) {
this.deleteEntry(alias)
}
@ -168,3 +168,43 @@ fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
val key = getKey(alias, keyPass) as PrivateKey
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)
// Save to the key store.
caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(),
CertPath(arrayOf(selfSignCert)))
arrayOf(selfSignCert))
caKeyStore.save(config.nodeKeystore, keystorePassword)
}
val keyPair = caKeyStore.getKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword)