mirror of
https://github.com/corda/corda.git
synced 2025-02-01 08:48:09 +00:00
Merged in pat-certificate-signing-utility (pull request #364)
Client certificate signing utility
This commit is contained in:
commit
4ec33f549f
@ -1,6 +1,7 @@
|
|||||||
package com.r3corda.core.crypto
|
package com.r3corda.core.crypto
|
||||||
|
|
||||||
import com.r3corda.core.random63BitValue
|
import com.r3corda.core.random63BitValue
|
||||||
|
import com.r3corda.core.use
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||||
import org.bouncycastle.asn1.DERSequence
|
import org.bouncycastle.asn1.DERSequence
|
||||||
@ -16,9 +17,14 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||||
import org.bouncycastle.util.IPAddress
|
import org.bouncycastle.util.IPAddress
|
||||||
import org.bouncycastle.util.io.pem.PemReader
|
import org.bouncycastle.util.io.pem.PemReader
|
||||||
import java.io.*
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.io.InputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@ -41,10 +47,14 @@ object X509Utilities {
|
|||||||
val ECDSA_CURVE = "secp256r1"
|
val ECDSA_CURVE = "secp256r1"
|
||||||
|
|
||||||
val KEYSTORE_TYPE = "JKS"
|
val KEYSTORE_TYPE = "JKS"
|
||||||
val CA_CERT_ALIAS = "CA Cert"
|
|
||||||
val CERT_PRIVATE_KEY_ALIAS = "Server Private Key"
|
// Aliases for private keys and certificates.
|
||||||
val ROOT_CA_CERT_PRIVATE_KEY_ALIAS = "Root CA Private Key"
|
val CORDA_ROOT_CA_PRIVATE_KEY = "cordarootcaprivatekey"
|
||||||
val INTERMEDIATE_CA_PRIVATE_KEY_ALIAS = "Intermediate CA Private Key"
|
val CORDA_ROOT_CA = "cordarootca"
|
||||||
|
val CORDA_INTERMEDIATE_CA_PRIVATE_KEY = "cordaintermediatecaprivatekey"
|
||||||
|
val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
|
||||||
|
val CORDA_CLIENT_CA_PRIVATE_KEY = "cordaclientcaprivatekey"
|
||||||
|
val CORDA_CLIENT_CA = "cordaclientca"
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Security.addProvider(BouncyCastleProvider()) // register Bouncy Castle Crypto Provider required to sign certificates
|
Security.addProvider(BouncyCastleProvider()) // register Bouncy Castle Crypto Provider required to sign certificates
|
||||||
@ -108,8 +118,15 @@ object X509Utilities {
|
|||||||
return nameBuilder.build()
|
return nameBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getX509Name(myLegalName: String, nearestCity: String, email: String): X500Name {
|
||||||
|
return X500NameBuilder(BCStyle.INSTANCE)
|
||||||
|
.addRDN(BCStyle.CN, myLegalName)
|
||||||
|
.addRDN(BCStyle.L, nearestCity)
|
||||||
|
.addRDN(BCStyle.E, email).build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to either open an existing keystore for modification, or create a new blank keystore
|
* Helper method to either open an existing keystore for modification, or create a new blank keystore.
|
||||||
* @param keyStoreFilePath location of KeyStore file
|
* @param keyStoreFilePath location of KeyStore file
|
||||||
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
|
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
|
||||||
* but for SSL purposes this is recommended.
|
* but for SSL purposes this is recommended.
|
||||||
@ -119,13 +136,10 @@ object X509Utilities {
|
|||||||
val pass = storePassword.toCharArray()
|
val pass = storePassword.toCharArray()
|
||||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
if (Files.exists(keyStoreFilePath)) {
|
if (Files.exists(keyStoreFilePath)) {
|
||||||
val input = FileInputStream(keyStoreFilePath.toFile())
|
keyStoreFilePath.use { keyStore.load(it, pass) }
|
||||||
input.use {
|
|
||||||
keyStore.load(input, pass)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
keyStore.load(null, pass)
|
keyStore.load(null, pass)
|
||||||
val output = FileOutputStream(keyStoreFilePath.toFile())
|
val output = Files.newOutputStream(keyStoreFilePath)
|
||||||
output.use {
|
output.use {
|
||||||
keyStore.store(output, pass)
|
keyStore.store(output, pass)
|
||||||
}
|
}
|
||||||
@ -143,10 +157,7 @@ object X509Utilities {
|
|||||||
fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
|
fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
|
||||||
val pass = storePassword.toCharArray()
|
val pass = storePassword.toCharArray()
|
||||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
val input = FileInputStream(keyStoreFilePath.toFile())
|
keyStoreFilePath.use { keyStore.load(it, pass) }
|
||||||
input.use {
|
|
||||||
keyStore.load(input, pass)
|
|
||||||
}
|
|
||||||
return keyStore
|
return keyStore
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +186,7 @@ object X509Utilities {
|
|||||||
*/
|
*/
|
||||||
fun saveKeyStore(keyStore: KeyStore, keyStoreFilePath: Path, storePassword: String) {
|
fun saveKeyStore(keyStore: KeyStore, keyStoreFilePath: Path, storePassword: String) {
|
||||||
val pass = storePassword.toCharArray()
|
val pass = storePassword.toCharArray()
|
||||||
val output = FileOutputStream(keyStoreFilePath.toFile())
|
val output = Files.newOutputStream(keyStoreFilePath)
|
||||||
output.use {
|
output.use {
|
||||||
keyStore.store(output, pass)
|
keyStore.store(output, pass)
|
||||||
}
|
}
|
||||||
@ -189,7 +200,7 @@ object X509Utilities {
|
|||||||
* 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
|
||||||
*/
|
*/
|
||||||
private fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) {
|
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) {
|
||||||
try {
|
try {
|
||||||
this.deleteEntry(alias)
|
this.deleteEntry(alias)
|
||||||
} catch (kse: KeyStoreException) {
|
} catch (kse: KeyStoreException) {
|
||||||
@ -203,7 +214,7 @@ object X509Utilities {
|
|||||||
* @param alias name to record the public certificate under
|
* @param alias name to record the public certificate under
|
||||||
* @param cert certificate to store
|
* @param cert certificate to store
|
||||||
*/
|
*/
|
||||||
private fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
|
fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
|
||||||
try {
|
try {
|
||||||
this.deleteEntry(alias)
|
this.deleteEntry(alias)
|
||||||
} catch (kse: KeyStoreException) {
|
} catch (kse: KeyStoreException) {
|
||||||
@ -224,6 +235,24 @@ object X509Utilities {
|
|||||||
return keyGen.generateKeyPair()
|
return keyGen.generateKeyPair()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create certificate signing request using provided information.
|
||||||
|
*
|
||||||
|
* @param myLegalName The legal name of your organization. This should not be abbreviated and should include suffixes such as Inc, Corp, or LLC.
|
||||||
|
* @param nearestCity The city where your organization is located.
|
||||||
|
* @param email An email address used to contact your organization.
|
||||||
|
* @param keyPair Standard curve ECDSA KeyPair generated for TLS.
|
||||||
|
* @return The generated Certificate signing request.
|
||||||
|
*/
|
||||||
|
fun createCertificateSigningRequest(myLegalName: String, nearestCity: String, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
|
||||||
|
val subject = getX509Name(myLegalName, nearestCity, email)
|
||||||
|
val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keyPair.private)
|
||||||
|
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper data class to pass around public certificate and KeyPair entities when using CA certs
|
* Helper data class to pass around public certificate and KeyPair entities when using CA certs
|
||||||
*/
|
*/
|
||||||
@ -236,10 +265,10 @@ object X509Utilities {
|
|||||||
* @return A data class is returned containing the new root CA Cert and its KeyPair for signing downstream certificates.
|
* @return A data class is returned containing the new root CA Cert and its KeyPair for signing downstream certificates.
|
||||||
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
|
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
|
||||||
*/
|
*/
|
||||||
fun createSelfSignedCACert(domain: String): CACertAndKey {
|
fun createSelfSignedCACert(myLegalName: String): CACertAndKey {
|
||||||
val keyPair = generateECDSAKeyPairForSSL()
|
val keyPair = generateECDSAKeyPairForSSL()
|
||||||
|
|
||||||
val issuer = getDevX509Name(domain)
|
val issuer = getDevX509Name(myLegalName)
|
||||||
val serial = BigInteger.valueOf(random63BitValue())
|
val serial = BigInteger.valueOf(random63BitValue())
|
||||||
val subject = issuer
|
val subject = issuer
|
||||||
val pubKey = keyPair.public
|
val pubKey = keyPair.public
|
||||||
@ -292,7 +321,7 @@ object X509Utilities {
|
|||||||
|
|
||||||
// Ten year certificate validity
|
// Ten year certificate validity
|
||||||
// TODO how do we manage certificate expiry, revocation and loss
|
// TODO how do we manage certificate expiry, revocation and loss
|
||||||
val window = getCertificateValidityWindow(0, 365*10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
|
val window = getCertificateValidityWindow(0, 365 * 10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
|
||||||
|
|
||||||
val builder = JcaX509v3CertificateBuilder(
|
val builder = JcaX509v3CertificateBuilder(
|
||||||
issuer, serial, window.first, window.second, subject, pubKey)
|
issuer, serial, window.first, window.second, subject, pubKey)
|
||||||
@ -341,7 +370,7 @@ object X509Utilities {
|
|||||||
|
|
||||||
// Ten year certificate validity
|
// Ten year certificate validity
|
||||||
// TODO how do we manage certificate expiry, revocation and loss
|
// TODO how do we manage certificate expiry, revocation and loss
|
||||||
val window = getCertificateValidityWindow(0, 365*10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
|
val window = getCertificateValidityWindow(0, 365 * 10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
|
||||||
|
|
||||||
val builder = JcaX509v3CertificateBuilder(issuer, serial, window.first, window.second, subject, publicKey)
|
val builder = JcaX509v3CertificateBuilder(issuer, serial, window.first, window.second, subject, publicKey)
|
||||||
builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey))
|
builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey))
|
||||||
@ -420,7 +449,7 @@ object X509Utilities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract public and private keys from a KeyStore file assuming storage alias is know
|
* Extract public and private keys from a KeyStore file assuming storage alias is known.
|
||||||
* @param keyStoreFilePath Path to load KeyStore from
|
* @param keyStoreFilePath Path to load KeyStore from
|
||||||
* @param storePassword Password to unlock the KeyStore
|
* @param storePassword Password to unlock the KeyStore
|
||||||
* @param keyPassword Password to unlock the private key entries
|
* @param keyPassword Password to unlock the private key entries
|
||||||
@ -437,6 +466,32 @@ object X509Utilities {
|
|||||||
return KeyPair(certificate.publicKey, keyEntry)
|
return KeyPair(certificate.publicKey, keyEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract public and private keys from a KeyStore file assuming storage alias is known, or
|
||||||
|
* create a new pair of keys using the provided function if the keys not exist.
|
||||||
|
* @param keyStoreFilePath Path to load KeyStore from
|
||||||
|
* @param storePassword Password to unlock the KeyStore
|
||||||
|
* @param keyPassword Password to unlock the private key entries
|
||||||
|
* @param alias The name to lookup the Key and Certificate chain from
|
||||||
|
* @param keyGenerator Function for generating new keys
|
||||||
|
* @return The KeyPair found in the KeyStore under the specified alias
|
||||||
|
*/
|
||||||
|
fun loadOrCreateKeyPairFromKeyStore(keyStoreFilePath: Path, storePassword: String, keyPassword: String,
|
||||||
|
alias: String, keyGenerator: () -> CACertAndKey): KeyPair {
|
||||||
|
val keyStore = X509Utilities.loadKeyStore(keyStoreFilePath, storePassword)
|
||||||
|
if (!keyStore.containsAlias(alias)) {
|
||||||
|
val selfSignCert = keyGenerator()
|
||||||
|
// Save to the key store.
|
||||||
|
keyStore.addOrReplaceKey(alias, selfSignCert.keypair.private, keyPassword.toCharArray(), arrayOf(selfSignCert.certificate))
|
||||||
|
X509Utilities.saveKeyStore(keyStore, keyStoreFilePath, storePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
val certificate = keyStore.getCertificate(alias)
|
||||||
|
val keyEntry = keyStore.getKey(alias, keyPassword.toCharArray())
|
||||||
|
|
||||||
|
return KeyPair(certificate.publicKey, keyEntry as PrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract public X509 certificate from a KeyStore file assuming storage alias is know
|
* Extract public X509 certificate from a KeyStore file assuming storage alias is know
|
||||||
* @param keyStoreFilePath Path to load KeyStore from
|
* @param keyStoreFilePath Path to load KeyStore from
|
||||||
@ -475,9 +530,9 @@ object X509Utilities {
|
|||||||
val keypass = keyPassword.toCharArray()
|
val keypass = keyPassword.toCharArray()
|
||||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||||
|
|
||||||
keyStore.addOrReplaceKey(ROOT_CA_CERT_PRIVATE_KEY_ALIAS, rootCA.keypair.private, keypass, arrayOf(rootCA.certificate))
|
keyStore.addOrReplaceKey(CORDA_ROOT_CA_PRIVATE_KEY, rootCA.keypair.private, keypass, arrayOf(rootCA.certificate))
|
||||||
|
|
||||||
keyStore.addOrReplaceKey(INTERMEDIATE_CA_PRIVATE_KEY_ALIAS,
|
keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY,
|
||||||
intermediateCA.keypair.private,
|
intermediateCA.keypair.private,
|
||||||
keypass,
|
keypass,
|
||||||
arrayOf(intermediateCA.certificate, rootCA.certificate))
|
arrayOf(intermediateCA.certificate, rootCA.certificate))
|
||||||
@ -486,7 +541,8 @@ object X509Utilities {
|
|||||||
|
|
||||||
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
|
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
|
||||||
|
|
||||||
trustStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
|
trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, rootCA.certificate)
|
||||||
|
trustStore.addOrReplaceCertificate(CORDA_INTERMEDIATE_CA, intermediateCA.certificate)
|
||||||
|
|
||||||
saveKeyStore(trustStore, trustStoreFilePath, trustStorePassword)
|
saveKeyStore(trustStore, trustStoreFilePath, trustStorePassword)
|
||||||
|
|
||||||
@ -527,10 +583,10 @@ object X509Utilities {
|
|||||||
caKeyPassword: String): KeyStore {
|
caKeyPassword: String): KeyStore {
|
||||||
val rootCA = X509Utilities.loadCertificateAndKey(caKeyStore,
|
val rootCA = X509Utilities.loadCertificateAndKey(caKeyStore,
|
||||||
caKeyPassword,
|
caKeyPassword,
|
||||||
X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS)
|
X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)
|
||||||
val intermediateCA = X509Utilities.loadCertificateAndKey(caKeyStore,
|
val intermediateCA = X509Utilities.loadCertificateAndKey(caKeyStore,
|
||||||
caKeyPassword,
|
caKeyPassword,
|
||||||
X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
|
X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
|
||||||
|
|
||||||
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
|
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
|
||||||
val host = InetAddress.getLocalHost()
|
val host = InetAddress.getLocalHost()
|
||||||
@ -538,18 +594,18 @@ object X509Utilities {
|
|||||||
val serverCert = X509Utilities.createServerCert(subject,
|
val serverCert = X509Utilities.createServerCert(subject,
|
||||||
serverKey.public,
|
serverKey.public,
|
||||||
intermediateCA,
|
intermediateCA,
|
||||||
if(host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
|
if (host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
|
||||||
listOf(host.hostAddress))
|
listOf(host.hostAddress))
|
||||||
|
|
||||||
val keypass = keyPassword.toCharArray()
|
val keypass = keyPassword.toCharArray()
|
||||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||||
|
|
||||||
keyStore.addOrReplaceKey(CERT_PRIVATE_KEY_ALIAS,
|
keyStore.addOrReplaceKey(CORDA_CLIENT_CA_PRIVATE_KEY,
|
||||||
serverKey.private,
|
serverKey.private,
|
||||||
keypass,
|
keypass,
|
||||||
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))
|
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))
|
||||||
|
|
||||||
keyStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
|
keyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, serverCert)
|
||||||
|
|
||||||
saveKeyStore(keyStore, keyStoreFilePath, storePassword)
|
saveKeyStore(keyStore, keyStoreFilePath, storePassword)
|
||||||
|
|
||||||
|
@ -97,9 +97,9 @@ class X509UtilitiesTest {
|
|||||||
// Load back generated root CA Cert and private key from keystore and check against copy in truststore
|
// Load back generated root CA Cert and private key from keystore and check against copy in truststore
|
||||||
val keyStore = X509Utilities.loadKeyStore(tmpKeyStore, "keystorepass")
|
val keyStore = X509Utilities.loadKeyStore(tmpKeyStore, "keystorepass")
|
||||||
val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass")
|
val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass")
|
||||||
val rootCaCert = keyStore.getCertificate(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS) as X509Certificate
|
val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY) as X509Certificate
|
||||||
val rootCaPrivateKey = keyStore.getKey(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey
|
val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey
|
||||||
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CA_CERT_ALIAS) as X509Certificate
|
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
|
||||||
assertEquals(rootCaCert, rootCaFromTrustStore)
|
assertEquals(rootCaCert, rootCaFromTrustStore)
|
||||||
rootCaCert.checkValidity(Date())
|
rootCaCert.checkValidity(Date())
|
||||||
rootCaCert.verify(rootCaCert.publicKey)
|
rootCaCert.verify(rootCaCert.publicKey)
|
||||||
@ -116,8 +116,8 @@ class X509UtilitiesTest {
|
|||||||
assertTrue { caVerifier.verify(caSignature) }
|
assertTrue { caVerifier.verify(caSignature) }
|
||||||
|
|
||||||
// Load back generated intermediate CA Cert and private key
|
// Load back generated intermediate CA Cert and private key
|
||||||
val intermediateCaCert = keyStore.getCertificate(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS) as X509Certificate
|
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) as X509Certificate
|
||||||
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey
|
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey
|
||||||
intermediateCaCert.checkValidity(Date())
|
intermediateCaCert.checkValidity(Date())
|
||||||
intermediateCaCert.verify(rootCaCert.publicKey)
|
intermediateCaCert.verify(rootCaCert.publicKey)
|
||||||
|
|
||||||
@ -148,14 +148,14 @@ class X509UtilitiesTest {
|
|||||||
|
|
||||||
// Load signing intermediate CA cert
|
// Load signing intermediate CA cert
|
||||||
val caKeyStore = X509Utilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass")
|
val caKeyStore = X509Utilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass")
|
||||||
val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
|
val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
|
||||||
|
|
||||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||||
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass")
|
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass")
|
||||||
|
|
||||||
// Load back server certificate
|
// Load back server certificate
|
||||||
val serverKeyStore = X509Utilities.loadKeyStore(tmpServerKeyStore, "serverstorepass")
|
val serverKeyStore = X509Utilities.loadKeyStore(tmpServerKeyStore, "serverstorepass")
|
||||||
val serverCertAndKey = X509Utilities.loadCertificateAndKey(serverKeyStore, "serverkeypass", X509Utilities.CERT_PRIVATE_KEY_ALIAS)
|
val serverCertAndKey = X509Utilities.loadCertificateAndKey(serverKeyStore, "serverkeypass", X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY)
|
||||||
|
|
||||||
serverCertAndKey.certificate.checkValidity(Date())
|
serverCertAndKey.certificate.checkValidity(Date())
|
||||||
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
|
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
|
||||||
|
@ -132,6 +132,8 @@ dependencies {
|
|||||||
|
|
||||||
// Integration test helpers
|
// Integration test helpers
|
||||||
integrationTestCompile 'junit:junit:4.12'
|
integrationTestCompile 'junit:junit:4.12'
|
||||||
|
|
||||||
|
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')
|
quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
package com.r3corda.node.utilities.certsigning
|
||||||
|
|
||||||
|
import com.r3corda.core.crypto.X509Utilities
|
||||||
|
import com.r3corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||||
|
import com.r3corda.core.crypto.X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY
|
||||||
|
import com.r3corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||||
|
import com.r3corda.core.crypto.X509Utilities.addOrReplaceCertificate
|
||||||
|
import com.r3corda.core.crypto.X509Utilities.addOrReplaceKey
|
||||||
|
import com.r3corda.core.div
|
||||||
|
import com.r3corda.core.minutes
|
||||||
|
import com.r3corda.core.utilities.loggerFor
|
||||||
|
import com.r3corda.node.services.config.FullNodeConfiguration
|
||||||
|
import com.r3corda.node.services.config.NodeConfiguration
|
||||||
|
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
|
||||||
|
import joptsimple.OptionParser
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This check the [certificatePath] for certificates required to connect to the Corda network.
|
||||||
|
* If the certificates are not found, a [PKCS10CertificationRequest] will be submitted to Corda network permissioning server using [CertificateSigningService].
|
||||||
|
* This process will enter a slow polling loop until the request has been approved, and then
|
||||||
|
* the certificate chain will be downloaded and stored in [KeyStore] reside in [certificatePath].
|
||||||
|
*/
|
||||||
|
class CertificateSigner(certificatePath: Path, val nodeConfig: NodeConfiguration, val certService: CertificateSigningService) : ArtemisMessagingComponent(certificatePath, nodeConfig) {
|
||||||
|
companion object {
|
||||||
|
val pollInterval = 1.minutes
|
||||||
|
val log = loggerFor<CertificateSigner>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildKeyStore() {
|
||||||
|
val caKeyStore = X509Utilities.loadOrCreateKeyStore(keyStorePath, config.keyStorePassword)
|
||||||
|
|
||||||
|
if (!caKeyStore.containsAlias(CORDA_CLIENT_CA)) {
|
||||||
|
// No certificate found in key store, create certificate signing request and post request to signing server.
|
||||||
|
log.info("No certificate found in key store, creating certificate signing request...")
|
||||||
|
|
||||||
|
// Create or load key pair from the key store.
|
||||||
|
val keyPair = X509Utilities.loadOrCreateKeyPairFromKeyStore(keyStorePath, config.keyStorePassword,
|
||||||
|
config.keyStorePassword, CORDA_CLIENT_CA_PRIVATE_KEY) {
|
||||||
|
X509Utilities.createSelfSignedCACert(nodeConfig.myLegalName)
|
||||||
|
}
|
||||||
|
log.info("Submitting certificate signing request to Corda certificate signing server.")
|
||||||
|
val requestId = submitCertificateSigningRequest(keyPair)
|
||||||
|
log.info("Successfully submitted request to Corda certificate signing server, request ID : $requestId")
|
||||||
|
log.info("Start polling server for certificate signing approval.")
|
||||||
|
val certificates = pollServerForCertificates(requestId)
|
||||||
|
log.info("Certificate signing request approved, installing new certificates.")
|
||||||
|
|
||||||
|
// Save private key and certificate chain to the key store.
|
||||||
|
caKeyStore.addOrReplaceKey(CORDA_CLIENT_CA_PRIVATE_KEY, keyPair.private,
|
||||||
|
config.keyStorePassword.toCharArray(), certificates)
|
||||||
|
|
||||||
|
// Assumes certificate chain always starts with client certificate and end with root certificate.
|
||||||
|
caKeyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, certificates.first())
|
||||||
|
|
||||||
|
X509Utilities.saveKeyStore(caKeyStore, keyStorePath, config.keyStorePassword)
|
||||||
|
|
||||||
|
// Save certificates to trust store.
|
||||||
|
val trustStore = X509Utilities.loadOrCreateKeyStore(trustStorePath, config.trustStorePassword)
|
||||||
|
|
||||||
|
// Assumes certificate chain always starts with client certificate and end with root certificate.
|
||||||
|
trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last())
|
||||||
|
|
||||||
|
X509Utilities.saveKeyStore(trustStore, trustStorePath, config.trustStorePassword)
|
||||||
|
} else {
|
||||||
|
log.trace("Certificate already exists, exiting certificate signer...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll Certificate Signing Server for approved certificate,
|
||||||
|
* enter a slow polling loop if server return null.
|
||||||
|
* @param requestId Certificate signing request ID.
|
||||||
|
* @return Map of certificate chain.
|
||||||
|
*/
|
||||||
|
private fun pollServerForCertificates(requestId: String): Array<Certificate> {
|
||||||
|
// Poll server to download the signed certificate once request has been approved.
|
||||||
|
var certificates = certService.retrieveCertificates(requestId)
|
||||||
|
|
||||||
|
while (certificates == null) {
|
||||||
|
Thread.sleep(pollInterval.toMillis())
|
||||||
|
certificates = certService.retrieveCertificates(requestId)
|
||||||
|
}
|
||||||
|
return certificates
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit Certificate Signing Request to Certificate signing service if request ID not found in file system
|
||||||
|
* New request ID will be stored in requestId.txt
|
||||||
|
* @param keyPair Public Private key pair generated for SSL certification.
|
||||||
|
* @return Request ID return from the server.
|
||||||
|
*/
|
||||||
|
private fun submitCertificateSigningRequest(keyPair: KeyPair): String {
|
||||||
|
val requestIdStore = certificatePath / "certificate-request-id.txt"
|
||||||
|
// Retrieve request id from file if exists, else post a request to server.
|
||||||
|
return if (!Files.exists(requestIdStore)) {
|
||||||
|
val request = X509Utilities.createCertificateSigningRequest(nodeConfig.myLegalName, nodeConfig.nearestCity, nodeConfig.emailAddress, keyPair)
|
||||||
|
// Post request to signing server via http.
|
||||||
|
val requestId = certService.submitRequest(request)
|
||||||
|
// Persists request ID to file in case of node shutdown.
|
||||||
|
Files.write(requestIdStore, listOf(requestId), Charsets.UTF_8)
|
||||||
|
requestId
|
||||||
|
} else {
|
||||||
|
Files.readAllLines(requestIdStore).first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ParamsSpec {
|
||||||
|
val parser = OptionParser()
|
||||||
|
val baseDirectoryArg = parser.accepts("base-dir", "The directory to put all key stores under").withRequiredArg()
|
||||||
|
val configFileArg = parser.accepts("config-file", "The path to the config file").withRequiredArg()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val cmdlineOptions = try {
|
||||||
|
ParamsSpec.parser.parse(*args)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
CertificateSigner.log.error("Unable to parse args", ex)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
val baseDirectoryPath = Paths.get(cmdlineOptions.valueOf(ParamsSpec.baseDirectoryArg) ?: throw IllegalArgumentException("Please provide Corda node base directory path"))
|
||||||
|
val configFile = if (cmdlineOptions.has(ParamsSpec.configFileArg)) Paths.get(cmdlineOptions.valueOf(ParamsSpec.configFileArg)) else null
|
||||||
|
val conf = FullNodeConfiguration(NodeConfiguration.loadConfig(baseDirectoryPath, configFile, allowMissingConfig = true))
|
||||||
|
// TODO: Use HTTPS instead
|
||||||
|
CertificateSigner(baseDirectoryPath / "certificate", conf, HTTPCertificateSigningService(conf.certificateSigningService)).buildKeyStore()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.r3corda.node.utilities.certsigning
|
||||||
|
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
|
||||||
|
interface CertificateSigningService {
|
||||||
|
/** Submits a CSR to the signing service and returns an opaque request ID. */
|
||||||
|
fun submitRequest(request: PKCS10CertificationRequest): String
|
||||||
|
/** Poll Certificate Signing Server for the request and returns a chain of certificates if request has been approved, null otherwise. */
|
||||||
|
fun retrieveCertificates(requestId: String): Array<Certificate>?
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package com.r3corda.node.utilities.certsigning
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.util.*
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
class HTTPCertificateSigningService(val server: HostAndPort) : CertificateSigningService {
|
||||||
|
companion object {
|
||||||
|
// TODO: Propagate version information from gradle
|
||||||
|
val clientVersion = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retrieveCertificates(requestId: String): Array<Certificate>? {
|
||||||
|
// Poll server to download the signed certificate once request has been approved.
|
||||||
|
val url = URL("http://$server/api/certificate/$requestId")
|
||||||
|
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
|
||||||
|
return when (conn.responseCode) {
|
||||||
|
HttpURLConnection.HTTP_OK -> conn.inputStream.use {
|
||||||
|
ZipInputStream(it).use {
|
||||||
|
val certificates = ArrayList<Certificate>()
|
||||||
|
while (it.nextEntry != null) {
|
||||||
|
certificates.add(CertificateFactory.getInstance("X.509").generateCertificate(it))
|
||||||
|
}
|
||||||
|
certificates.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HttpURLConnection.HTTP_NO_CONTENT -> null
|
||||||
|
HttpURLConnection.HTTP_UNAUTHORIZED -> throw IOException("Certificate signing request has been rejected, please contact Corda network administrator for more information.")
|
||||||
|
else -> throw IOException("Unexpected response code ${conn.responseCode} - ${IOUtils.toString(conn.errorStream)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||||
|
// Post request to certificate signing server via http.
|
||||||
|
val conn = URL("http://$server/api/certificate").openConnection() as HttpURLConnection
|
||||||
|
conn.doOutput = true
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/octet-stream")
|
||||||
|
conn.setRequestProperty("Client-Version", clientVersion)
|
||||||
|
conn.outputStream.write(request.encoded)
|
||||||
|
|
||||||
|
return when (conn.responseCode) {
|
||||||
|
HttpURLConnection.HTTP_OK -> IOUtils.toString(conn.inputStream)
|
||||||
|
HttpURLConnection.HTTP_FORBIDDEN -> throw IOException("Client version $clientVersion is forbidden from accessing permissioning server, please upgrade to newer version.")
|
||||||
|
else -> throw IOException("Unexpected response code ${conn.responseCode} - ${IOUtils.toString(conn.errorStream)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,81 @@
|
|||||||
|
package com.r3corda.node.utilities.certsigning
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import com.nhaarman.mockito_kotlin.any
|
||||||
|
import com.nhaarman.mockito_kotlin.eq
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.crypto.X509Utilities
|
||||||
|
import com.r3corda.core.div
|
||||||
|
import com.r3corda.node.services.config.NodeConfiguration
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class CertificateSignerTest {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val tempFolder: TemporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun buildKeyStore() {
|
||||||
|
|
||||||
|
val id = SecureHash.randomSHA256().toString()
|
||||||
|
|
||||||
|
val certs = arrayOf(X509Utilities.createSelfSignedCACert("CORDA_CLIENT_CA").certificate,
|
||||||
|
X509Utilities.createSelfSignedCACert("CORDA_INTERMEDIATE_CA").certificate,
|
||||||
|
X509Utilities.createSelfSignedCACert("CORDA_ROOT_CA").certificate)
|
||||||
|
|
||||||
|
val certService: CertificateSigningService = mock {
|
||||||
|
on { submitRequest(any()) }.then { id }
|
||||||
|
on { retrieveCertificates(eq(id)) }.then { certs }
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyStore = tempFolder.root.toPath().resolve("sslkeystore.jks")
|
||||||
|
val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks")
|
||||||
|
|
||||||
|
assertFalse(Files.exists(keyStore))
|
||||||
|
assertFalse(Files.exists(tmpTrustStore))
|
||||||
|
|
||||||
|
val config = object : NodeConfiguration {
|
||||||
|
override val myLegalName: String = "me"
|
||||||
|
override val nearestCity: String = "London"
|
||||||
|
override val emailAddress: String = ""
|
||||||
|
override val devMode: Boolean = true
|
||||||
|
override val exportJMXto: String = ""
|
||||||
|
override val keyStorePassword: String = "testpass"
|
||||||
|
override val trustStorePassword: String = "trustpass"
|
||||||
|
override val certificateSigningService: HostAndPort = HostAndPort.fromParts("localhost", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateSigner(tempFolder.root.toPath(), config, certService).buildKeyStore()
|
||||||
|
|
||||||
|
assertTrue(Files.exists(keyStore))
|
||||||
|
assertTrue(Files.exists(tmpTrustStore))
|
||||||
|
|
||||||
|
X509Utilities.loadKeyStore(keyStore, config.keyStorePassword).run {
|
||||||
|
assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY))
|
||||||
|
assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
X509Utilities.loadKeyStore(tmpTrustStore, config.trustStorePassword).run {
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY))
|
||||||
|
assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA))
|
||||||
|
assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(id, Files.readAllLines(tempFolder.root.toPath() / "certificate-request-id.txt").first())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user