Merged in pat-certificate-signing-utility (pull request #364)

Client certificate signing utility
This commit is contained in:
Patrick Kuo 2016-09-20 11:42:38 +01:00
commit 4ec33f549f
9 changed files with 380 additions and 38 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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()
}

View File

@ -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>?
}

View File

@ -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)}")
}
}
}

View File

@ -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())
}
}