Add X509 creation and manipulation utilities to core and enable SSL in ArtemisMQ

This commit is contained in:
Matthew Nesbit 2016-07-21 13:56:23 +01:00
parent 38f4711a80
commit 00f897d58d
10 changed files with 888 additions and 13 deletions

View File

@ -27,6 +27,7 @@ buildscript {
ext.jolokia_version = '2.0.0-M1'
ext.slf4j_version = '1.7.21'
ext.assertj_version = '3.5.1'
ext.bouncycastle_version = '1.54'
repositories {
mavenCentral()

View File

@ -60,6 +60,10 @@ dependencies {
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
compile 'net.i2p.crypto:eddsa:0.1.0'
// Bouncy castle support needed for X509 certificate manipulation
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
compile "org.bouncycastle:bcpkix-jdk15on:${bouncycastle_version}"
}
quasarScan.dependsOn('classes')

View File

@ -0,0 +1,542 @@
package com.r3corda.core.crypto
import com.r3corda.core.random63BitValue
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
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.util.IPAddress
import org.bouncycastle.util.io.pem.PemReader
import java.io.*
import java.math.BigInteger
import java.net.InetAddress
import java.nio.file.Files
import java.nio.file.Path
import java.security.*
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
object X509Utilities {
val SIGNATURE_ALGORITHM = "SHA256withECDSA"
val KEY_GENERATION_ALGORITHM = "ECDSA"
val ECDSA_CURVE = "secp256k1" // TLS implementations only support standard SEC2 curves, although internally Corda uses newer EDDSA keys
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"
init {
Security.addProvider(BouncyCastleProvider()) // register Bouncy Castle Crypto Provider required to sign certificates
}
/**
* Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range
* @param daysBefore number of days to roll back returned start date relative to current date
* @param daysAfter number of days to roll forward returned end date relative to current date
* @param parentNotBefore if provided is used to lower bound the date interval returned
* @param parentNotAfter if provided is used to upper bound the date interval returned
*/
private fun GetCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair<Date, Date> {
val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS)
var notBefore = Date.from(startOfDayUTC.minus(daysBefore.toLong(), ChronoUnit.DAYS))
if (parentNotBefore != null) {
if (parentNotBefore.after(notBefore)) {
notBefore = parentNotBefore
}
}
var notAfter = Date.from(startOfDayUTC.plus(daysAfter.toLong(), ChronoUnit.DAYS))
if (parentNotAfter != null) {
if (parentNotAfter.after(notAfter)) {
notAfter = parentNotAfter
}
}
return Pair(notBefore, notAfter)
}
/**
* Encode provided public key in correct format for inclusion in certificate issuer/subject fields
*/
private fun createSubjectKeyIdentifier(key: Key): SubjectKeyIdentifier {
val info = SubjectPublicKeyInfo.getInstance(key.encoded)
return BcX509ExtensionUtils().createSubjectKeyIdentifier(info)
}
/**
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key
*/
private fun signCertificate(certificateBuilder: X509v3CertificateBuilder, signedWithPrivateKey: PrivateKey): X509Certificate {
val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(signedWithPrivateKey)
return JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateBuilder.build(signer))
}
/**
* Helper method to create Subject field contents
*/
fun GetX509Name(domain: String): X500Name {
val nameBuilder = X500NameBuilder(BCStyle.INSTANCE)
nameBuilder.addRDN(BCStyle.CN, domain)
nameBuilder.addRDN(BCStyle.O, "R3")
nameBuilder.addRDN(BCStyle.OU, "corda")
nameBuilder.addRDN(BCStyle.L, "London")
nameBuilder.addRDN(BCStyle.C, "UK")
return nameBuilder.build()
}
/**
* 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.
* @return returns the KeyStore opened/created
*/
fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
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)
}
} else {
keyStore.load(null, pass)
}
return keyStore
}
/**
* Helper method to open an existing keystore for modification/read
* @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException
* @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.
* @return returns the KeyStore opened
*/
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)
}
return keyStore
}
/**
* Helper method to open an existing keystore for modification/read
* @param input stream containing a KeyStore e.g. loaded from a resource 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.
* @return returns the KeyStore opened
*/
fun loadKeyStore(input: InputStream, storePassword: String): KeyStore {
val pass = storePassword.toCharArray()
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
input.use {
keyStore.load(input, pass)
}
return keyStore
}
/**
* Helper method save KeyStore to storage
* @param keyStore the KeyStore to persist
* @param keyStoreFilePath the file location to save to
* @param storePassword password to access the store in future. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
*/
fun saveKeyStore(keyStore: KeyStore, keyStoreFilePath: Path, storePassword: String) {
val pass = storePassword.toCharArray()
val output = FileOutputStream(keyStoreFilePath.toFile())
output.use {
keyStore.store(output, pass)
}
}
/**
* Helper extension method to add, or overwrite any key data in store
* @param alias name to record the private key and certificate chain under
* @param key cryptographic key to store
* @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored,
* 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>) {
try {
this.deleteEntry(alias)
} catch (kse: KeyStoreException) {
// ignore as may not exist in keystore yet
}
this.setKeyEntry(alias, key, password, chain)
}
/**
* Helper extension method to add, or overwrite any public certificate data in store
* @param alias name to record the public certificate under
* @param cert certificate to store
*/
private fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
try {
this.deleteEntry(alias)
} catch (kse: KeyStoreException) {
// ignore as may not exist in keystore yet
}
this.setCertificateEntry(alias, cert)
}
/**
* Generate a standard curve ECDSA KeyPair suitable for TLS, although the rest of Corda uses newer curves.
* @return The generated Public/Private KeyPair
*/
fun generateECDSAKeyPairForSSL(): KeyPair {
val keyGen = KeyPairGenerator.getInstance(KEY_GENERATION_ALGORITHM, BouncyCastleProvider.PROVIDER_NAME)
val ecSpec = ECGenParameterSpec(ECDSA_CURVE) // Force named curve, because TLS implementations don't support many curves
keyGen.initialize(ecSpec, SecureRandom())
return keyGen.generateKeyPair()
}
/**
* Helper data class to pass around public certificate and KeyPair entities when using CA certs
*/
data class CACertAndKey(val certificate: X509Certificate, val keypair: KeyPair)
/**
* Create a de novo root self-signed X509 v3 CA cert and KeyPair.
* @param domain The Common (CN) field of the cert Subject will be populated with the domain string
* @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 {
val keyPair = generateECDSAKeyPairForSSL()
val issuer = GetX509Name(domain)
val serial = BigInteger.valueOf(random63BitValue())
val subject = issuer
val pubKey = keyPair.public
val window = GetCertificateValidityWindow(0, 365 * 10)
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false,
createSubjectKeyIdentifier(pubKey))
builder.addExtension(Extension.basicConstraints, true,
BasicConstraints(2))
val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
purposes.add(KeyPurposeId.anyExtendedKeyUsage)
builder.addExtension(Extension.extendedKeyUsage, false,
DERSequence(purposes))
val cert = signCertificate(builder, keyPair.private)
cert.checkValidity(Date())
cert.verify(pubKey)
return CACertAndKey(cert, keyPair)
}
/**
* Create a de novo root intermediate X509 v3 CA cert and KeyPair.
* @param domain The Common (CN) field of the cert Subject will be populated with the domain string
* @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it
* @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates
*/
fun createIntermediateCert(domain: String,
certificateAuthority: CACertAndKey): CACertAndKey {
val keyPair = generateECDSAKeyPairForSSL()
val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject
val serial = BigInteger.valueOf(random63BitValue())
val subject = GetX509Name(domain)
val pubKey = keyPair.public
// One year certificate validity
val window = GetCertificateValidityWindow(0, 365, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
builder.addExtension(Extension.subjectKeyIdentifier, false,
createSubjectKeyIdentifier(pubKey))
builder.addExtension(Extension.basicConstraints, true,
BasicConstraints(1))
val usage = KeyUsage(KeyUsage.keyCertSign or KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.dataEncipherment or KeyUsage.cRLSign)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
purposes.add(KeyPurposeId.anyExtendedKeyUsage)
builder.addExtension(Extension.extendedKeyUsage, false,
DERSequence(purposes))
val cert = signCertificate(builder, certificateAuthority.keypair.private)
cert.checkValidity(Date())
cert.verify(certificateAuthority.keypair.public)
return CACertAndKey(cert, keyPair)
}
/**
* Create an X509v3 certificate suitable for use in TLS roles.
* @param subject The contents to put in the subject field of the certificate
* @param publicKey The PublicKey to be wrapped in the certificate
* @param certificateAuthority The Public certificate and KeyPair of the parent CA that will sign this certificate
* @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes
* @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes
* @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS.
* This certificate is not marked as a CA cert to be similar in nature to commercial certificates.
*/
fun createServerCert(subject: X500Name,
publicKey: PublicKey,
certificateAuthority: CACertAndKey,
subjectAlternativeNameDomains: List<String>,
subjectAlternativeNameIps: List<String>): X509Certificate {
val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject
val serial = BigInteger.valueOf(random63BitValue())
val window = GetCertificateValidityWindow(0, 365, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val builder = JcaX509v3CertificateBuilder(issuer, serial, window.first, window.second, subject, publicKey)
builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey))
builder.addExtension(Extension.basicConstraints, false, BasicConstraints(false))
val usage = KeyUsage(KeyUsage.digitalSignature)
builder.addExtension(Extension.keyUsage, false, usage)
val purposes = ASN1EncodableVector()
purposes.add(KeyPurposeId.id_kp_serverAuth)
purposes.add(KeyPurposeId.id_kp_clientAuth)
builder.addExtension(Extension.extendedKeyUsage, false,
DERSequence(purposes))
val subjectAlternativeNames = ArrayList<ASN1Encodable>()
subjectAlternativeNames.add(GeneralName(GeneralName.dNSName, subject.getRDNs(BCStyle.CN).first().first.value))
for (subjectAlternativeNameDomain in subjectAlternativeNameDomains) {
subjectAlternativeNames.add(GeneralName(GeneralName.dNSName, subjectAlternativeNameDomain))
}
for (subjectAlternativeNameIp in subjectAlternativeNameIps) {
if (IPAddress.isValidIPv6WithNetmask(subjectAlternativeNameIp)
|| IPAddress.isValidIPv6(subjectAlternativeNameIp)
|| IPAddress.isValidIPv4WithNetmask(subjectAlternativeNameIp)
|| IPAddress.isValidIPv4(subjectAlternativeNameIp)) {
subjectAlternativeNames.add(GeneralName(GeneralName.iPAddress, subjectAlternativeNameIp))
}
}
val subjectAlternativeNamesExtension = DERSequence(subjectAlternativeNames.toTypedArray())
builder.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNamesExtension)
val cert = signCertificate(builder, certificateAuthority.keypair.private)
cert.checkValidity(Date())
cert.verify(certificateAuthority.keypair.public)
return cert
}
/**
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection
* @param x509Certificate certificate to save
* @param filename Target filename
*/
fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) {
val fileWriter = FileWriter(filename.toFile())
var jcaPEMWriter: JcaPEMWriter? = null
try {
jcaPEMWriter = JcaPEMWriter(fileWriter)
jcaPEMWriter.writeObject(x509Certificate)
} finally {
jcaPEMWriter?.close()
fileWriter.close()
}
}
/**
* Helper method to load back a .pem/.cer format file copy of a certificate
* @param filename Source filename
* @return The X509Certificate that was encoded in the file
*/
fun loadCertificateFromPEMFile(filename: Path): X509Certificate {
val reader = PemReader(FileReader(filename.toFile()))
val pemObject = reader.readPemObject()
val certFact = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME)
val inputStream = ByteArrayInputStream(pemObject.content)
try {
val cert = certFact.generateCertificate(inputStream) as X509Certificate
cert.checkValidity()
return cert
} finally {
inputStream.close()
}
}
/**
* Extract public and private keys from a KeyStore file assuming storage alias is know
* @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
* @return The KeyPair found in the KeyStore under the specified alias
*/
fun loadKeyPairFromKeyStore(keyStoreFilePath: Path,
storePassword: String,
keyPassword: String,
alias: String): KeyPair {
val keyStore = loadKeyStore(keyStoreFilePath, storePassword)
val keyEntry = keyStore.getKey(alias, keyPassword.toCharArray()) as PrivateKey
val certificate = keyStore.getCertificate(alias) as X509Certificate
return KeyPair(certificate.publicKey, keyEntry)
}
/**
* Extract public X509 certificate from a KeyStore file assuming storage alias is know
* @param keyStoreFilePath Path to load KeyStore from
* @param storePassword Password to unlock the KeyStore
* @param alias The name to lookup the Key and Certificate chain from
* @return The X509Certificate found in the KeyStore under the specified alias
*/
fun loadCertificateFromKeyStore(keyStoreFilePath: Path,
storePassword: String,
alias: String): X509Certificate {
val keyStore = loadKeyStore(keyStoreFilePath, storePassword)
return keyStore.getCertificate(alias) as X509Certificate
}
/**
* All in one wrapper to manufacture a root CA cert and an Intermediate CA cert.
* Normally this would be run once and then the outputs would be re-used repeatedly to manufacture the server certs
* @param keyStoreFilePath The output KeyStore path to publish the private keys of the CA root and intermediate certs into.
* @param storePassword The storage password to protect access to the generated KeyStore and public certificates
* @param keyPassword The password that protects the CA private keys.
* Unlike the SSL libraries that tend to assume the password is the same as the keystore password.
* These CA private keys should be protected more effectively with a distinct password.
* @param trustStoreFilePath The output KeyStore to place the Root CA public certificate, which can be used as an SSL truststore
* @param trustStorePassword The password to protect the truststore
* @return The KeyStore object that was saved to file
*/
fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path,
storePassword: String,
keyPassword: String,
trustStoreFilePath: Path,
trustStorePassword: String
): KeyStore {
val rootCA = X509Utilities.createSelfSignedCACert("Corda Node Root CA")
val intermediateCA = X509Utilities.createIntermediateCert("Corda Node Intermediate CA", rootCA)
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(INTERMEDIATE_CA_PRIVATE_KEY_ALIAS,
intermediateCA.keypair.private,
keypass,
arrayOf(intermediateCA.certificate, rootCA.certificate))
saveKeyStore(keyStore, keyStoreFilePath, storePassword)
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
trustStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
saveKeyStore(trustStore, trustStoreFilePath, trustStorePassword)
return keyStore
}
/**
* Helper method to load a Certificate and KeyPair from their KeyStore.
* The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys.
* @param keyStore Source KeyStore to look in for the data
* @param keyPassword The password for the PrivateKey (not the store access password)
* @param alias The name to search for the data. Typically if generated with the methods here this will be one of
* CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above
*/
fun loadCertificateAndKey(keyStore: KeyStore,
keyPassword: String,
alias: String): CACertAndKey {
val keypass = keyPassword.toCharArray()
val key = keyStore.getKey(alias, keypass) as PrivateKey
val cert = keyStore.getCertificate(alias) as X509Certificate
return CACertAndKey(cert, KeyPair(cert.publicKey, key))
}
/**
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine
* @param keyStoreFilePath KeyStore path to save output to
* @param storePassword access password for KeyStore
* @param keyPassword PrivateKey access password for the generated keys.
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore
* @param caKeyPassword password to unlock private keys in the CA KeyStore
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications
*/
fun createKeystoreForSSL(keyStoreFilePath: Path,
storePassword: String,
keyPassword: String,
caKeyStore: KeyStore,
caKeyPassword: String): KeyStore {
val rootCA = X509Utilities.loadCertificateAndKey(caKeyStore,
caKeyPassword,
X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS)
val intermediateCA = X509Utilities.loadCertificateAndKey(caKeyStore,
caKeyPassword,
X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
val host = InetAddress.getLocalHost()
val subject = GetX509Name(host.canonicalHostName)
val serverCert = X509Utilities.createServerCert(subject,
serverKey.public,
intermediateCA,
listOf(),
listOf(host.hostAddress))
val keypass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
keyStore.addOrReplaceKey(CERT_PRIVATE_KEY_ALIAS,
serverKey.private,
keypass,
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))
keyStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
saveKeyStore(keyStore, keyStoreFilePath, storePassword)
return keyStore
}
}

View File

@ -0,0 +1,285 @@
package com.r3corda.core.crypto
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.GeneralName
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.file.Paths
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.Signature
import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.*
import kotlin.concurrent.thread
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class X509UtilitiesTest {
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
@Test
fun `create valid self-signed CA certificate`() {
val caCertAndKey = X509Utilities.createSelfSignedCACert("Test Cert")
assertTrue { caCertAndKey.certificate.subjectDN.name.contains("CN=Test Cert") } // using our subject common name
assertEquals(caCertAndKey.certificate.issuerDN, caCertAndKey.certificate.subjectDN) //self-signed
caCertAndKey.certificate.checkValidity(Date()) // throws on verification problems
caCertAndKey.certificate.verify(caCertAndKey.keypair.public) // throws on verification problems
assertTrue { caCertAndKey.certificate.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property)
assertTrue { caCertAndKey.certificate.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate
}
@Test
fun `load and save a PEM file certificate`() {
val tmpCertificateFile = tempFolder.root.toPath().resolve("cacert.pem")
val caCertAndKey = X509Utilities.createSelfSignedCACert("Test Cert")
X509Utilities.saveCertificateAsPEMFile(caCertAndKey.certificate, tmpCertificateFile)
val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile)
assertEquals(caCertAndKey.certificate, readCertificate)
}
@Test
fun `create valid server certificate chain`() {
val caCertAndKey = X509Utilities.createSelfSignedCACert("Test CA Cert")
val subjectDN = X509Utilities.GetX509Name("Server Cert")
val keypair = X509Utilities.generateECDSAKeyPairForSSL()
val serverCert = X509Utilities.createServerCert(subjectDN, keypair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54"))
assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name
assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert
serverCert.checkValidity(Date()) // throws on verification problems
serverCert.verify(caCertAndKey.keypair.public) // throws on verification problems
assertFalse { serverCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property)
assertTrue { serverCert.basicConstraints === -1 } // This returns the signing path length should be -1 for non-CA certificate
assertEquals(3, serverCert.subjectAlternativeNames.size)
var foundMainDnsName = false
var foundAliasDnsName = false
for (entry in serverCert.subjectAlternativeNames) {
val typeId = entry[0] as Int
val value = entry[1] as String
if(typeId == GeneralName.iPAddress) {
assertEquals("10.0.0.54", value)
} else if(typeId == GeneralName.dNSName) {
if(value == "Server Cert") {
foundMainDnsName = true
} else if (value == "alias name") {
foundAliasDnsName = true
}
}
}
assertTrue(foundMainDnsName)
assertTrue(foundAliasDnsName)
}
@Test
fun `create full CA keystore`() {
val tmpKeyStore = tempFolder.root.toPath().resolve("keystore.jks")
val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
X509Utilities.createCAKeyStoreAndTrustStore(tmpKeyStore,
"keystorepass",
"keypass",
tmpTrustStore,
"trustpass")
// 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
assertEquals(rootCaCert, rootCaFromTrustStore)
rootCaCert.checkValidity(Date())
rootCaCert.verify(rootCaCert.publicKey)
// Now sign something with private key and verify against certificate public key
val testData = "12345".toByteArray()
val caSigner = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM)
caSigner.initSign(rootCaPrivateKey)
caSigner.update(testData)
val caSignature = caSigner.sign()
val caVerifier = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM)
caVerifier.initVerify(rootCaCert.publicKey)
caVerifier.update(testData)
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
intermediateCaCert.checkValidity(Date())
intermediateCaCert.verify(rootCaCert.publicKey)
// Now sign something with private key and verify against certificate public key
val intermediateSigner = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM)
intermediateSigner.initSign(intermediateCaCertPrivateKey)
intermediateSigner.update(testData)
val intermediateSignature = intermediateSigner.sign()
val intermediateVerifier = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM)
intermediateVerifier.initVerify(intermediateCaCert.publicKey)
intermediateVerifier.update(testData)
assertTrue { intermediateVerifier.verify(intermediateSignature) }
}
@Test
fun `create server certificate in keystore for SSL`() {
val tmpCAKeyStore = tempFolder.root.toPath().resolve("keystore.jks")
val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks")
val tmpServerKeyStore = tempFolder.root.toPath().resolve("serverkeystore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
X509Utilities.createCAKeyStoreAndTrustStore(tmpCAKeyStore,
"cakeystorepass",
"cakeypass",
tmpTrustStore,
"trustpass")
// Load signing intermediate CA cert
val caKeyStore = X509Utilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass")
val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
// 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)
serverCertAndKey.certificate.checkValidity(Date())
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
val host = InetAddress.getLocalHost()
assertTrue { serverCertAndKey.certificate.subjectDN.name.contains("CN=" + host.hostName) }
// Now sign something with private key and verify against certificate public key
val testData = "123456".toByteArray()
val signer = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM)
signer.initSign(serverCertAndKey.keypair.private)
signer.update(testData)
val signature = signer.sign()
val verifier = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM)
verifier.initVerify(serverCertAndKey.certificate.publicKey)
verifier.update(testData)
assertTrue { verifier.verify(signature) }
}
@Test
fun `create server cert and use in SSL socket`() {
val tmpCAKeyStore = tempFolder.root.toPath().resolve("keystore.jks")
val tmpTrustStore = tempFolder.root.toPath().resolve("truststore.jks")
val tmpServerKeyStore = tempFolder.root.toPath().resolve("serverkeystore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
val caKeyStore = X509Utilities.createCAKeyStoreAndTrustStore(tmpCAKeyStore,
"cakeystorepass",
"cakeypass",
tmpTrustStore,
"trustpass")
// Generate server cert and private key and populate another keystore suitable for SSL
val keyStore = X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass")
val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass")
val context = SSLContext.getInstance("TLS")
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, "serverstorepass".toCharArray())
val keyManagers = keyManagerFactory.getKeyManagers()
val trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustMgrFactory.init(trustStore)
val trustManagers = trustMgrFactory.trustManagers
context.init(keyManagers, trustManagers, SecureRandom())
val serverSocketFactory = context.serverSocketFactory
val clientSocketFactory = context.socketFactory
val serverSocket = serverSocketFactory.createServerSocket(0) as SSLServerSocket // use 0 to get first free socket
val serverParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
arrayOf("TLSv1.2"))
serverParams.wantClientAuth = true
serverParams.needClientAuth = true
serverParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking
serverSocket.sslParameters = serverParams
serverSocket.useClientMode = false
val clientSocket = clientSocketFactory.createSocket() as SSLSocket
val clientParams = SSLParameters(arrayOf("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256"),
arrayOf("TLSv1.2"))
clientParams.endpointIdentificationAlgorithm = "HTTPS" // enable hostname checking
clientSocket.sslParameters = clientParams
clientSocket.useClientMode = true
val lock = Object()
var done = false
var serverError = false
val serverThread = thread() {
try {
val sslServerSocket = serverSocket.accept()
assert(sslServerSocket.isConnected)
val serverInput = DataInputStream(sslServerSocket.inputStream)
val receivedString = serverInput.readUTF()
assertEquals("Hello World", receivedString)
synchronized(lock) {
done = true
lock.notifyAll()
}
sslServerSocket.close()
} catch (ex: Throwable) {
serverError = true
}
}
clientSocket.connect(InetSocketAddress(InetAddress.getLocalHost(), serverSocket.localPort))
assertTrue(clientSocket.isConnected)
// Double check hostname manually
val peerChain = clientSocket.session.peerCertificates
val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal
val x500name = X500Name(peerX500Principal.name)
val cn = x500name.getRDNs(BCStyle.CN).first().first.value.toString()
val hostname = InetAddress.getLocalHost().hostName
assertEquals(hostname, cn)
val output = DataOutputStream(clientSocket.outputStream)
output.writeUTF("Hello World")
var timeout = 0
synchronized(lock) {
while (!done) {
timeout++
if (timeout > 10) throw IOException("Timed out waiting for server to complete")
lock.wait(1000)
}
}
clientSocket.close()
serverThread.join(1000)
assertFalse { serverError }
serverSocket.close()
assertTrue(done)
}
}

View File

@ -41,6 +41,7 @@ dependencies {
// Artemis: for reliable p2p message queues.
compile "org.apache.activemq:artemis-server:${artemis_version}"
compile "org.apache.activemq:artemis-core-client:${artemis_version}"
runtime "org.apache.activemq:artemis-amqp-protocol:${artemis_version}"
// JAnsi: for drawing things to the terminal in nicely coloured ways.
compile "org.fusesource.jansi:jansi:1.13"

View File

@ -71,7 +71,10 @@ class Node(dir: Path, val p2pAddr: HostAndPort, val webServerAddr: HostAndPort,
override fun startMessagingService() {
// Start up the MQ service.
(net as ArtemisMessagingService).start()
(net as ArtemisMessagingService).apply {
configureWithDevSSLCertificate() //Provision a dev certificate and private key if required
start()
}
}
private fun initWebServer(): Server {

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.messaging
import com.google.common.net.HostAndPort
import com.r3corda.core.RunOnCallerThread
import com.r3corda.core.ThreadBox
import com.r3corda.core.crypto.X509Utilities
import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.messaging.*
import com.r3corda.core.serialization.SingletonSerializeAsToken
@ -15,12 +16,9 @@ import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.config.Configuration
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.HOST_PROP_NAME
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.PORT_PROP_NAME
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.*
import org.apache.activemq.artemis.core.security.Role
import org.apache.activemq.artemis.core.server.ActiveMQServer
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
@ -28,6 +26,7 @@ import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
import org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule
import java.math.BigInteger
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import java.util.*
@ -37,13 +36,12 @@ import javax.annotation.concurrent.ThreadSafe
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
// TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later)
// TODO: SSL
/**
* This class implements the [MessagingService] API using Apache Artemis, the successor to their ActiveMQ product.
* Artemis is a message queue broker and here, we embed the entire server inside our own process. Nodes communicate
* with each other using (by default) an Artemis specific protocol, but it supports other protocols like AQMP/1.0
* as well.
* with each other using an Artemis specific protocol, but it supports other protocols like AQMP/1.0
* as well for interop.
*
* The current implementation is skeletal and lacks features like security or firewall tunnelling (that is, you must
* be able to receive TCP connections in order to receive messages). It is good enough for local communication within
@ -98,6 +96,10 @@ class ArtemisMessagingService(val directory: Path,
// TODO: This is not robust and needs to be replaced by more intelligently using the message queue server.
private val undeliveredMessages = CopyOnWriteArrayList<Message>()
private val keyStorePath = directory.resolve("certificates").resolve("sslkeystore.jks")
private val trustStorePath = directory.resolve("certificates").resolve("truststore.jks")
private val KEYSTORE_PASSWORD = "cordacadevpass" // TODO we need a proper way of managing keystores and passwords
init {
require(directory.fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
}
@ -138,9 +140,9 @@ class ArtemisMessagingService(val directory: Path,
activeMQServer.registerActivationFailureListener { exception -> throw exception }
activeMQServer.start()
// Connect to our in-memory server.
// Connect to our server.
clientFactory = ActiveMQClient.createServerLocatorWithoutHA(
TransportConfiguration(InVMConnectorFactory::class.java.name)).createSessionFactory()
tcpTransport(ConnectionDirection.OUTBOUND, myHostPort.hostText, myHostPort.port)).createSessionFactory()
// Create a queue on which to receive messages and set up the handler.
val session = clientFactory.createSession()
@ -303,8 +305,7 @@ class ArtemisMessagingService(val directory: Path,
setConfigDirectories(config, directory)
// We will be talking to our server purely in memory.
config.acceptorConfigurations = setOf(
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port),
TransportConfiguration(InVMAcceptorFactory::class.java.name)
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port)
)
return config
}
@ -316,9 +317,46 @@ class ArtemisMessagingService(val directory: Path,
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
},
mapOf(
// Basic TCP target details
HOST_PROP_NAME to host,
PORT_PROP_NAME to port.toInt()
PORT_PROP_NAME to port.toInt(),
// Turn on AMQP support, which needs the protoclo jar on the classpath.
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop
// It does not use AMQP messages for its own
PROTOCOLS_PROP_NAME to "CORE,AMQP",
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake
// and AES encryption
SSL_ENABLED_PROP_NAME to true,
KEYSTORE_PROVIDER_PROP_NAME to "JKS",
KEYSTORE_PATH_PROP_NAME to keyStorePath,
KEYSTORE_PASSWORD_PROP_NAME to KEYSTORE_PASSWORD, // TODO proper management of keystores and password
TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
TRUSTSTORE_PATH_PROP_NAME to trustStorePath,
TRUSTSTORE_PASSWORD_PROP_NAME to "trustpass",
ENABLED_CIPHER_SUITES_PROP_NAME to "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_DSS_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
NEED_CLIENT_AUTH_PROP_NAME to true
)
)
/**
* Strictly for dev only automatically construct a server certificate\private key signed from
* the CA certs in Node resources.
*/
fun configureWithDevSSLCertificate() {
Files.createDirectories(directory.resolve("certificates"))
if (!Files.exists(trustStorePath)) {
Files.copy(javaClass.classLoader.getResourceAsStream("com/r3corda/node/internal/certificates/cordatruststore.jks"),
trustStorePath)
}
if (!Files.exists(keyStorePath)) {
val caKeyStore = X509Utilities.loadKeyStore(
javaClass.classLoader.getResourceAsStream("com/r3corda/node/internal/certificates/cordadevcakeys.jks"),
"cordacadevpass")
X509Utilities.createKeystoreForSSL(keyStorePath, KEYSTORE_PASSWORD, KEYSTORE_PASSWORD, caKeyStore, "cordacadevkeypass")
}
}
}

View File

@ -56,6 +56,7 @@ class ArtemisMessagingServiceTests {
private fun createMessagingService(): ArtemisMessagingService {
return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply {
configureWithDevSSLCertificate()
messagingNetwork = this
}
}