mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Add X509 creation and manipulation utilities to core and enable SSL in ArtemisMQ
This commit is contained in:
parent
38f4711a80
commit
00f897d58d
@ -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()
|
||||
|
@ -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')
|
||||
|
542
core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt
Normal file
542
core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
@ -56,6 +56,7 @@ class ArtemisMessagingServiceTests {
|
||||
|
||||
private fun createMessagingService(): ArtemisMessagingService {
|
||||
return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply {
|
||||
configureWithDevSSLCertificate()
|
||||
messagingNetwork = this
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user