mirror of
https://github.com/corda/corda.git
synced 2025-01-20 03:36:29 +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
|
||||
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.use
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
@ -16,9 +17,14 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
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.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.net.InetAddress
|
||||
import java.nio.file.Files
|
||||
@ -41,10 +47,14 @@ object X509Utilities {
|
||||
val ECDSA_CURVE = "secp256r1"
|
||||
|
||||
val KEYSTORE_TYPE = "JKS"
|
||||
val CA_CERT_ALIAS = "CA Cert"
|
||||
val CERT_PRIVATE_KEY_ALIAS = "Server Private Key"
|
||||
val ROOT_CA_CERT_PRIVATE_KEY_ALIAS = "Root CA Private Key"
|
||||
val INTERMEDIATE_CA_PRIVATE_KEY_ALIAS = "Intermediate CA Private Key"
|
||||
|
||||
// Aliases for private keys and certificates.
|
||||
val CORDA_ROOT_CA_PRIVATE_KEY = "cordarootcaprivatekey"
|
||||
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 {
|
||||
Security.addProvider(BouncyCastleProvider()) // register Bouncy Castle Crypto Provider required to sign certificates
|
||||
@ -108,8 +118,15 @@ object X509Utilities {
|
||||
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 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.
|
||||
@ -119,13 +136,10 @@ object X509Utilities {
|
||||
val pass = storePassword.toCharArray()
|
||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||
if (Files.exists(keyStoreFilePath)) {
|
||||
val input = FileInputStream(keyStoreFilePath.toFile())
|
||||
input.use {
|
||||
keyStore.load(input, pass)
|
||||
}
|
||||
keyStoreFilePath.use { keyStore.load(it, pass) }
|
||||
} else {
|
||||
keyStore.load(null, pass)
|
||||
val output = FileOutputStream(keyStoreFilePath.toFile())
|
||||
val output = Files.newOutputStream(keyStoreFilePath)
|
||||
output.use {
|
||||
keyStore.store(output, pass)
|
||||
}
|
||||
@ -143,10 +157,7 @@ object X509Utilities {
|
||||
fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
|
||||
val pass = storePassword.toCharArray()
|
||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||
val input = FileInputStream(keyStoreFilePath.toFile())
|
||||
input.use {
|
||||
keyStore.load(input, pass)
|
||||
}
|
||||
keyStoreFilePath.use { keyStore.load(it, pass) }
|
||||
return keyStore
|
||||
}
|
||||
|
||||
@ -175,7 +186,7 @@ object X509Utilities {
|
||||
*/
|
||||
fun saveKeyStore(keyStore: KeyStore, keyStoreFilePath: Path, storePassword: String) {
|
||||
val pass = storePassword.toCharArray()
|
||||
val output = FileOutputStream(keyStoreFilePath.toFile())
|
||||
val output = Files.newOutputStream(keyStoreFilePath)
|
||||
output.use {
|
||||
keyStore.store(output, pass)
|
||||
}
|
||||
@ -189,7 +200,7 @@ object X509Utilities {
|
||||
* 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
|
||||
*/
|
||||
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 {
|
||||
this.deleteEntry(alias)
|
||||
} catch (kse: KeyStoreException) {
|
||||
@ -203,7 +214,7 @@ object X509Utilities {
|
||||
* @param alias name to record the public certificate under
|
||||
* @param cert certificate to store
|
||||
*/
|
||||
private fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
|
||||
fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
|
||||
try {
|
||||
this.deleteEntry(alias)
|
||||
} catch (kse: KeyStoreException) {
|
||||
@ -224,6 +235,24 @@ object X509Utilities {
|
||||
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
|
||||
*/
|
||||
@ -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.
|
||||
* 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 issuer = getDevX509Name(domain)
|
||||
val issuer = getDevX509Name(myLegalName)
|
||||
val serial = BigInteger.valueOf(random63BitValue())
|
||||
val subject = issuer
|
||||
val pubKey = keyPair.public
|
||||
@ -292,7 +321,7 @@ object X509Utilities {
|
||||
|
||||
// Ten year certificate validity
|
||||
// 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, pubKey)
|
||||
@ -341,7 +370,7 @@ object X509Utilities {
|
||||
|
||||
// Ten year certificate validity
|
||||
// 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)
|
||||
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 storePassword Password to unlock the KeyStore
|
||||
* @param keyPassword Password to unlock the private key entries
|
||||
@ -437,6 +466,32 @@ object X509Utilities {
|
||||
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
|
||||
* @param keyStoreFilePath Path to load KeyStore from
|
||||
@ -475,9 +530,9 @@ object X509Utilities {
|
||||
val keypass = keyPassword.toCharArray()
|
||||
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,
|
||||
keypass,
|
||||
arrayOf(intermediateCA.certificate, rootCA.certificate))
|
||||
@ -486,7 +541,8 @@ object X509Utilities {
|
||||
|
||||
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)
|
||||
|
||||
@ -527,10 +583,10 @@ object X509Utilities {
|
||||
caKeyPassword: String): KeyStore {
|
||||
val rootCA = X509Utilities.loadCertificateAndKey(caKeyStore,
|
||||
caKeyPassword,
|
||||
X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS)
|
||||
X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)
|
||||
val intermediateCA = X509Utilities.loadCertificateAndKey(caKeyStore,
|
||||
caKeyPassword,
|
||||
X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
|
||||
X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
|
||||
|
||||
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
|
||||
val host = InetAddress.getLocalHost()
|
||||
@ -538,18 +594,18 @@ object X509Utilities {
|
||||
val serverCert = X509Utilities.createServerCert(subject,
|
||||
serverKey.public,
|
||||
intermediateCA,
|
||||
if(host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
|
||||
if (host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
|
||||
listOf(host.hostAddress))
|
||||
|
||||
val keypass = keyPassword.toCharArray()
|
||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||
|
||||
keyStore.addOrReplaceKey(CERT_PRIVATE_KEY_ALIAS,
|
||||
keyStore.addOrReplaceKey(CORDA_CLIENT_CA_PRIVATE_KEY,
|
||||
serverKey.private,
|
||||
keypass,
|
||||
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))
|
||||
|
||||
keyStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
|
||||
keyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, serverCert)
|
||||
|
||||
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
|
||||
val keyStore = X509Utilities.loadKeyStore(tmpKeyStore, "keystorepass")
|
||||
val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass")
|
||||
val rootCaCert = keyStore.getCertificate(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS) as X509Certificate
|
||||
val rootCaPrivateKey = keyStore.getKey(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey
|
||||
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CA_CERT_ALIAS) as X509Certificate
|
||||
val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY) as X509Certificate
|
||||
val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey
|
||||
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
|
||||
assertEquals(rootCaCert, rootCaFromTrustStore)
|
||||
rootCaCert.checkValidity(Date())
|
||||
rootCaCert.verify(rootCaCert.publicKey)
|
||||
@ -116,8 +116,8 @@ class X509UtilitiesTest {
|
||||
assertTrue { caVerifier.verify(caSignature) }
|
||||
|
||||
// Load back generated intermediate CA Cert and private key
|
||||
val intermediateCaCert = keyStore.getCertificate(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS) as X509Certificate
|
||||
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey
|
||||
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) as X509Certificate
|
||||
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey
|
||||
intermediateCaCert.checkValidity(Date())
|
||||
intermediateCaCert.verify(rootCaCert.publicKey)
|
||||
|
||||
@ -148,14 +148,14 @@ class X509UtilitiesTest {
|
||||
|
||||
// Load signing intermediate CA cert
|
||||
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
|
||||
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass")
|
||||
|
||||
// Load back server certificate
|
||||
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.verify(caCertAndKey.certificate.publicKey)
|
||||
|
@ -132,6 +132,8 @@ dependencies {
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
|
||||
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
||||
}
|
||||
|
||||
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…
Reference in New Issue
Block a user