mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
Add X509 creation and manipulation utilities to core and enable SSL in ArtemisMQ
This commit is contained in:
parent
119813a36d
commit
129eeca7de
@ -27,6 +27,7 @@ buildscript {
|
|||||||
ext.jolokia_version = '2.0.0-M1'
|
ext.jolokia_version = '2.0.0-M1'
|
||||||
ext.slf4j_version = '1.7.21'
|
ext.slf4j_version = '1.7.21'
|
||||||
ext.assertj_version = '3.5.1'
|
ext.assertj_version = '3.5.1'
|
||||||
|
ext.bouncycastle_version = '1.54'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -60,6 +60,10 @@ dependencies {
|
|||||||
|
|
||||||
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
|
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
|
||||||
compile 'net.i2p.crypto:eddsa:0.1.0'
|
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')
|
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.
|
// Artemis: for reliable p2p message queues.
|
||||||
compile "org.apache.activemq:artemis-server:${artemis_version}"
|
compile "org.apache.activemq:artemis-server:${artemis_version}"
|
||||||
compile "org.apache.activemq:artemis-core-client:${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.
|
// JAnsi: for drawing things to the terminal in nicely coloured ways.
|
||||||
compile "org.fusesource.jansi:jansi:1.13"
|
compile "org.fusesource.jansi:jansi:1.13"
|
||||||
|
@ -71,7 +71,10 @@ class Node(dir: Path, val p2pAddr: HostAndPort, val webServerAddr: HostAndPort,
|
|||||||
|
|
||||||
override fun startMessagingService() {
|
override fun startMessagingService() {
|
||||||
// Start up the MQ service.
|
// 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 {
|
private fun initWebServer(): Server {
|
||||||
|
@ -3,6 +3,7 @@ package com.r3corda.node.services.messaging
|
|||||||
import com.google.common.net.HostAndPort
|
import com.google.common.net.HostAndPort
|
||||||
import com.r3corda.core.RunOnCallerThread
|
import com.r3corda.core.RunOnCallerThread
|
||||||
import com.r3corda.core.ThreadBox
|
import com.r3corda.core.ThreadBox
|
||||||
|
import com.r3corda.core.crypto.X509Utilities
|
||||||
import com.r3corda.core.crypto.newSecureRandom
|
import com.r3corda.core.crypto.newSecureRandom
|
||||||
import com.r3corda.core.messaging.*
|
import com.r3corda.core.messaging.*
|
||||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
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.Configuration
|
||||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||||
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
|
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.NettyAcceptorFactory
|
||||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
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.*
|
||||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.PORT_PROP_NAME
|
|
||||||
import org.apache.activemq.artemis.core.security.Role
|
import org.apache.activemq.artemis.core.security.Role
|
||||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||||
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
|
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 org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
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: 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: 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.
|
* 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
|
* 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
|
* with each other using an Artemis specific protocol, but it supports other protocols like AQMP/1.0
|
||||||
* as well.
|
* as well for interop.
|
||||||
*
|
*
|
||||||
* The current implementation is skeletal and lacks features like security or firewall tunnelling (that is, you must
|
* 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
|
* 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.
|
// 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 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 {
|
init {
|
||||||
require(directory.fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
|
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.registerActivationFailureListener { exception -> throw exception }
|
||||||
activeMQServer.start()
|
activeMQServer.start()
|
||||||
|
|
||||||
// Connect to our in-memory server.
|
// Connect to our server.
|
||||||
clientFactory = ActiveMQClient.createServerLocatorWithoutHA(
|
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.
|
// Create a queue on which to receive messages and set up the handler.
|
||||||
val session = clientFactory.createSession()
|
val session = clientFactory.createSession()
|
||||||
@ -303,8 +305,7 @@ class ArtemisMessagingService(val directory: Path,
|
|||||||
setConfigDirectories(config, directory)
|
setConfigDirectories(config, directory)
|
||||||
// We will be talking to our server purely in memory.
|
// We will be talking to our server purely in memory.
|
||||||
config.acceptorConfigurations = setOf(
|
config.acceptorConfigurations = setOf(
|
||||||
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port),
|
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port)
|
||||||
TransportConfiguration(InVMAcceptorFactory::class.java.name)
|
|
||||||
)
|
)
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@ -316,9 +317,46 @@ class ArtemisMessagingService(val directory: Path,
|
|||||||
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
|
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
|
||||||
},
|
},
|
||||||
mapOf(
|
mapOf(
|
||||||
|
// Basic TCP target details
|
||||||
HOST_PROP_NAME to host,
|
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 {
|
private fun createMessagingService(): ArtemisMessagingService {
|
||||||
return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply {
|
return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply {
|
||||||
|
configureWithDevSSLCertificate()
|
||||||
messagingNetwork = this
|
messagingNetwork = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user