diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt index 3c7164800c..0bad95ea41 100644 --- a/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt @@ -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) { + fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array) { 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) diff --git a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt index b95ffe0d60..e046f3743a 100644 --- a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt @@ -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) diff --git a/node/build.gradle b/node/build.gradle index bba3a47451..9de44530bf 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -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') diff --git a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt new file mode 100644 index 0000000000..b73c6011c1 --- /dev/null +++ b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigner.kt @@ -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() + } + + 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 { + // 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) { + 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() +} + diff --git a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigningService.kt b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigningService.kt new file mode 100644 index 0000000000..e4131cabf4 --- /dev/null +++ b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/CertificateSigningService.kt @@ -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? +} \ No newline at end of file diff --git a/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt new file mode 100644 index 0000000000..1ca9323699 --- /dev/null +++ b/node/src/main/kotlin/com/r3corda/node/utilities/certsigning/HTTPCertificateSigningService.kt @@ -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? { + // 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() + 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)}") + } + + } +} \ No newline at end of file diff --git a/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks b/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks index c7aee7e249..77a6bb5db5 100644 Binary files a/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks and b/node/src/main/resources/com/r3corda/node/internal/certificates/cordadevcakeys.jks differ diff --git a/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks b/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks index af011804e3..f21f91eb2f 100644 Binary files a/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks and b/node/src/main/resources/com/r3corda/node/internal/certificates/cordatruststore.jks differ diff --git a/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt b/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt new file mode 100644 index 0000000000..1b23defabc --- /dev/null +++ b/node/src/test/kotlin/com/r3corda/node/utilities/certsigning/CertificateSignerTest.kt @@ -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()) + } + +} \ No newline at end of file