Merged in mnesbit-cor-261-artemis-over-ssl (pull request #238)

Add X509 creation and manipulation utilities to core and enable SSL in ArtemisMQ
This commit is contained in:
Matthew Nesbit 2016-07-27 15:59:52 +01:00
commit 510fa34053
17 changed files with 1330 additions and 16 deletions

View File

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

View File

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

View File

@ -0,0 +1,178 @@
package com.r3corda.core.crypto
import sun.security.util.HostnameChecker
import java.net.InetAddress
import java.net.Socket
import java.net.UnknownHostException
import java.security.KeyStore
import java.security.Provider
import java.security.Security
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.ConcurrentHashMap
import javax.net.ssl.*
/**
* Call this to change the default verification algorithm and this use the WhitelistTrustManager
* implementation. This is a work around to the fact that ArtemisMQ and probably many other libraries
* don't correctly configure the SSLParameters with setEndpointIdentificationAlgorithm and thus don't check
* that the certificate matches with the DNS entry requested. This exposes us to man in the middle attacks.
* The issue has been raised with ArtemisMQ: https://issues.apache.org/jira/browse/ARTEMIS-656
*/
fun registerWhitelistTrustManager() {
if (Security.getProvider("WhitelistTrustManager") == null) {
Security.addProvider(WhitelistTrustManagerProvider)
}
}
/**
* Custom Security Provider that forces the TrustManagerFactory to be our custom one.
* Also holds the identity of the original TrustManager algorithm so
* that we can delegate most of the checking to the proper Java code. We simply add some more checks.
*
* The whitelist automatically includes the local server DNS name and IP address
*
*/
object WhitelistTrustManagerProvider : Provider("WhitelistTrustManager",
1.0,
"Provider for custom trust manager that always validates certificate names") {
val originalTrustProviderAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm")
private val _whitelist = ConcurrentHashMap.newKeySet<String>()
val whitelist: Set<String> get() = _whitelist.toSet() // The acceptable IP and DNS names for clients and servers.
init {
// Add ourselves to whitelist as currently we have to connect to a local ArtemisMQ broker
val host = InetAddress.getLocalHost()
addWhitelistEntry(host.hostName)
// Register our custom TrustManagerFactorySpi
put("TrustManagerFactory.whitelistTrustManager", "com.r3corda.core.crypto.WhitelistTrustManagerSpi")
// Forcibly change the TrustManagerFactory defaultAlgorithm to be us
// This will apply to all code using TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
// Which includes the standard HTTPS implementation and most other SSL code
// TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)) will
// allow access to the original implementation which is normally "PKIX"
Security.setProperty("ssl.TrustManagerFactory.algorithm", "whitelistTrustManager")
}
/**
* Adds an extra name to the whitelist if not already present
* If this is a new entry it will internally request a DNS lookup which may block the calling thread.
*/
fun addWhitelistEntry(serverName: String) {
if (!_whitelist.contains(serverName)) { // Safe as we never delete from the set
addWhitelistEntries(listOf(serverName))
}
}
/**
* Adds a list of servers to the whitelist and also adds their fully resolved name/ip address after DNS lookup
* If the server name is not an actual DNS name this is silently ignored.
* The DNS request may block the calling thread.
*/
fun addWhitelistEntries(serverNames: List<String>) {
_whitelist.addAll(serverNames)
for (name in serverNames) {
try {
val addresses = InetAddress.getAllByName(name).toList()
_whitelist.addAll(addresses.map { y -> y.canonicalHostName })
_whitelist.addAll(addresses.map { y -> y.hostAddress })
} catch (ex: UnknownHostException) {
// Ignore if the server name is not resolvable e.g. for wildcard addresses, or addresses that can only be resolved externally
}
}
}
}
/**
* Registered TrustManagerFactorySpi
*/
class WhitelistTrustManagerSpi : TrustManagerFactorySpi() {
// Get the original implementation to delegate to (can't use Kotlin delegation on abstract classes unfortunately).
val originalProvider = TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)
override fun engineInit(keyStore: KeyStore?) {
originalProvider.init(keyStore)
}
override fun engineInit(managerFactoryParameters: ManagerFactoryParameters?) {
originalProvider.init(managerFactoryParameters)
}
override fun engineGetTrustManagers(): Array<out TrustManager> {
val parent = originalProvider.trustManagers.first() as X509ExtendedTrustManager
// Wrap original provider in ours and return
return arrayOf(WhitelistTrustManager(parent))
}
}
/**
* Our TrustManager extension takes the standard certificate checker and first delegates all the
* chain checking to that. If everything is well formed we then simply add a check against our whitelist
*/
class WhitelistTrustManager(val originalProvider: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
// Use same Helper class as standard HTTPS library validator
val checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS)
private fun checkIdentity(hostname: String?, cert: X509Certificate) {
// Based on standard code in sun.security.ssl.X509TrustManagerImpl.checkIdentity
// if IPv6 strip off the "[]"
if ((hostname != null) && hostname.startsWith("[") && hostname.endsWith("]")) {
checker.match(hostname.substring(1, hostname.length - 1), cert)
} else {
checker.match(hostname, cert)
}
}
/**
* scan whitelist and confirm the certificate matches at least one entry
*/
private fun checkWhitelist(cert: X509Certificate) {
for (whiteListEntry in WhitelistTrustManagerProvider.whitelist) {
try {
checkIdentity(whiteListEntry, cert)
return // if we get here without throwing we had a match
} catch(ex: CertificateException) {
// Ignore and check the next entry until we find a match, or exhaust the whitelist
}
}
throw CertificateException("Certificate not on whitelist ${cert.subjectDN}")
}
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String, socket: Socket?) {
originalProvider.checkClientTrusted(chain, authType, socket)
checkWhitelist(chain[0])
}
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String, engine: SSLEngine?) {
originalProvider.checkClientTrusted(chain, authType, engine)
checkWhitelist(chain[0])
}
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
originalProvider.checkClientTrusted(chain, authType)
checkWhitelist(chain[0])
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String, socket: Socket?) {
originalProvider.checkServerTrusted(chain, authType, socket)
checkWhitelist(chain[0])
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String, engine: SSLEngine?) {
originalProvider.checkServerTrusted(chain, authType, engine)
checkWhitelist(chain[0])
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
originalProvider.checkServerTrusted(chain, authType)
checkWhitelist(chain[0])
}
override fun getAcceptedIssuers(): Array<out X509Certificate> {
return originalProvider.acceptedIssuers
}
}

View File

@ -0,0 +1,556 @@
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
* Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date
* Thus we avoid too many round trip conversions.
*/
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 getDevX509Name(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)
val output = FileOutputStream(keyStoreFilePath.toFile())
output.use {
keyStore.store(output, 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 = getDevX509Name(domain)
val serial = BigInteger.valueOf(random63BitValue())
val subject = issuer
val pubKey = keyPair.public
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
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 = getDevX509Name(domain)
val pubKey = keyPair.public
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
val window = getCertificateValidityWindow(0, 365*10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val 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())
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
val window = getCertificateValidityWindow(0, 365*10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val 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 = getDevX509Name(host.canonicalHostName)
val serverCert = X509Utilities.createServerCert(subject,
serverKey.public,
intermediateCA,
if(host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
listOf(host.hostAddress))
val keypass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
keyStore.addOrReplaceKey(CERT_PRIVATE_KEY_ALIAS,
serverKey.private,
keypass,
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))
keyStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
saveKeyStore(keyStore, keyStoreFilePath, storePassword)
return keyStore
}
}

View File

@ -0,0 +1,203 @@
package com.r3corda.core.crypto
import org.junit.BeforeClass
import org.junit.Test
import java.net.Socket
import java.security.KeyStore
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.SSLEngine
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedTrustManager
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class WhitelistTrustManagerTest {
companion object {
@BeforeClass
@JvmStatic
fun registerTrustManager() {
// Validate original factory
assertEquals("PKIX", TrustManagerFactory.getDefaultAlgorithm())
//register for all tests
registerWhitelistTrustManager()
}
}
private fun getTrustmanagerAndCert(whitelist: String, certificateName: String): Pair<X509ExtendedTrustManager, X509Certificate> {
WhitelistTrustManagerProvider.addWhitelistEntry(whitelist)
val caCertAndKey = X509Utilities.createSelfSignedCACert(certificateName)
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
keyStore.setCertificateEntry("cacert", caCertAndKey.certificate)
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, caCertAndKey.certificate)
}
private fun getTrustmanagerAndUntrustedChainCert(): Pair<X509ExtendedTrustManager, X509Certificate> {
WhitelistTrustManagerProvider.addWhitelistEntry("test.r3corda.com")
val otherCaCertAndKey = X509Utilities.createSelfSignedCACert("bad root")
val caCertAndKey = X509Utilities.createSelfSignedCACert("good root")
val subject = X509Utilities.getDevX509Name("test.r3corda.com")
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
val serverCert = X509Utilities.createServerCert(subject,
serverKey.public,
otherCaCertAndKey,
listOf(),
listOf())
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
keyStore.setCertificateEntry("cacert", caCertAndKey.certificate)
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, serverCert)
}
@Test
fun `getDefaultAlgorithm TrustManager is WhitelistTrustManager`() {
registerWhitelistTrustManager() // Check double register is safe
assertEquals("whitelistTrustManager", TrustManagerFactory.getDefaultAlgorithm())
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
assertTrue { trustManagers.all { it is WhitelistTrustManager } }
}
@Test
fun `check certificate works for whitelisted certificate and specific domain`() {
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.r3corda.com")
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
}
@Test
fun `check certificate works for specific certificate and wildcard permitted domain`() {
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com")
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
}
@Test
fun `check certificate works for wildcard certificate and non wildcard domain`() {
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com")
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
}
@Test
fun `check unknown certificate rejected`() {
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.notr3.com")
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
@Test
fun `check unknown wildcard certificate rejected`() {
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "*.notr3.com")
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
@Test
fun `check unknown certificate rejected against mismatched wildcard`() {
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.notr3.com")
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
@Test
fun `check certificate signed by untrusted root is still rejected, despite matched name`() {
val (trustManager, cert) = getTrustmanagerAndUntrustedChainCert()
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
assertFailsWith<java.security.cert.CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
}
}

View File

@ -0,0 +1,285 @@
package com.r3corda.core.crypto
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.GeneralName
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.file.Paths
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.Signature
import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.*
import kotlin.concurrent.thread
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class X509UtilitiesTest {
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
@Test
fun `create valid self-signed CA certificate`() {
val caCertAndKey = X509Utilities.createSelfSignedCACert("Test Cert")
assertTrue { caCertAndKey.certificate.subjectDN.name.contains("CN=Test Cert") } // using our subject common name
assertEquals(caCertAndKey.certificate.issuerDN, caCertAndKey.certificate.subjectDN) //self-signed
caCertAndKey.certificate.checkValidity(Date()) // throws on verification problems
caCertAndKey.certificate.verify(caCertAndKey.keypair.public) // throws on verification problems
assertTrue { caCertAndKey.certificate.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property)
assertTrue { caCertAndKey.certificate.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate
}
@Test
fun `load and save a PEM file certificate`() {
val tmpCertificateFile = tempFolder.root.toPath().resolve("cacert.pem")
val caCertAndKey = X509Utilities.createSelfSignedCACert("Test Cert")
X509Utilities.saveCertificateAsPEMFile(caCertAndKey.certificate, tmpCertificateFile)
val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile)
assertEquals(caCertAndKey.certificate, readCertificate)
}
@Test
fun `create valid server certificate chain`() {
val caCertAndKey = X509Utilities.createSelfSignedCACert("Test CA Cert")
val subjectDN = X509Utilities.getDevX509Name("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.canonicalHostName) }
// 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().canonicalHostName
assertEquals(hostname, cn)
val output = DataOutputStream(clientSocket.outputStream)
output.writeUTF("Hello World")
var timeout = 0
synchronized(lock) {
while (!done) {
timeout++
if (timeout > 10) throw IOException("Timed out waiting for server to complete")
lock.wait(1000)
}
}
clientSocket.close()
serverThread.join(1000)
assertFalse { serverError }
serverSocket.close()
assertTrue(done)
}
}

View File

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

View File

@ -67,11 +67,14 @@ class Node(dir: Path, val p2pAddr: HostAndPort, val webServerAddr: HostAndPort,
// when our process shuts down, but we try in stop() anyway just to be nice.
private var nodeFileLock: FileLock? = null
override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr, serverThread)
override fun makeMessagingService(): MessagingService = ArtemisMessagingService(dir, p2pAddr, configuration, serverThread)
override fun startMessagingService() {
// Start up the MQ service.
(net as ArtemisMessagingService).start()
(net as ArtemisMessagingService).apply {
configureWithDevSSLCertificate() // TODO Create proper certificate provisioning process
start()
}
}
private fun initWebServer(): Server {

View File

@ -119,6 +119,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
override val myLegalName: String = legalName ?: "Mock Company $id"
override val exportJMXto: String = ""
override val nearestCity: String = "Atlantis"
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
}
val node = nodeFactory.create(path, config, this, networkMapAddress, advertisedServices.toSet(), id, keyPair)
if (start) {

View File

@ -59,6 +59,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override val myLegalName: String = "Bank $letter"
override val exportJMXto: String = ""
override val nearestCity: String = city
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
}
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair)
}
@ -77,6 +79,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override val myLegalName: String = "Network coordination center"
override val exportJMXto: String = ""
override val nearestCity: String = "Amsterdam"
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
}
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {}
@ -91,6 +95,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override val myLegalName: String = "Notary Service"
override val exportJMXto: String = ""
override val nearestCity: String = "Zurich"
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
}
return SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair)
}
@ -104,6 +110,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override val myLegalName: String = "Rates Service Provider"
override val exportJMXto: String = ""
override val nearestCity: String = "Madrid"
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
}
return object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
@ -122,6 +130,8 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
override val myLegalName: String = "Regulator A"
override val exportJMXto: String = ""
override val nearestCity: String = "Paris"
override val keyStorePassword: String = "dummy"
override val trustStorePassword: String = "trustpass"
}
val n = object : SimulatedNode(dir, cfg, network, networkMapAddr, advertisedServices, id, keyPair) {

View File

@ -8,6 +8,8 @@ interface NodeConfiguration {
val myLegalName: String
val exportJMXto: String
val nearestCity: String
val keyStorePassword: String
val trustStorePassword: String
}
// Allow the use of "String by config" syntax. TODO: Make it more flexible.
@ -17,4 +19,6 @@ class NodeConfigurationFromConfig(val config: Config = ConfigFactory.load()) : N
override val myLegalName: String by config
override val exportJMXto: String by config
override val nearestCity: String by config
override val keyStorePassword: String by config
override val trustStorePassword: String by config
}

View File

@ -3,11 +3,13 @@ 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
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.internal.Node
import com.r3corda.node.services.config.NodeConfiguration
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.api.core.client.*
@ -15,12 +17,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 +27,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 +37,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 AMQP/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
@ -51,11 +50,13 @@ import javax.annotation.concurrent.ThreadSafe
*
* @param directory A place where Artemis can stash its message journal and other files.
* @param myHostPort What host and port to bind to for receiving inbound connections.
* @param config The config object is used to pass in the passwords for the certificate KeyStore and TrustStore
* @param defaultExecutor This will be used as the default executor to run message handlers on, if no other is specified.
*/
@ThreadSafe
class ArtemisMessagingService(val directory: Path,
val myHostPort: HostAndPort,
val config: NodeConfiguration,
val defaultExecutor: Executor = RunOnCallerThread) : SingletonSerializeAsToken(), MessagingService {
// In future: can contain onion routing info, etc.
@ -98,6 +99,21 @@ 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")
// Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher.
// Our self-generated certificates all use ECDSA for handshakes, but we allow classical RSA certificates to work
// in case we need to use keytool certificates in some demos
private val CIPHER_SUITES = listOf(
"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")
init {
require(directory.fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
}
@ -138,9 +154,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 +319,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 +331,47 @@ 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 protocol 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 messages e.g. topology and heartbeats
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications
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 config.keyStorePassword, // TODO proper management of keystores and password
TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
TRUSTSTORE_PATH_PROP_NAME to trustStorePath,
TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
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. Then provision KeyStores into certificates folder under node path.
*/
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, config.keyStorePassword, config.keyStorePassword, caKeyStore, "cordacadevkeypass")
}
}
}

View File

@ -2,6 +2,7 @@ package com.r3corda.node.services
import com.r3corda.core.messaging.Message
import com.r3corda.core.testing.freeLocalHostAndPort
import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.messaging.ArtemisMessagingService
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
@ -55,7 +56,16 @@ class ArtemisMessagingServiceTests {
}
private fun createMessagingService(): ArtemisMessagingService {
return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort).apply {
val config = object: NodeConfiguration {
override val myLegalName: String = "me"
override val exportJMXto: String = ""
override val nearestCity: String = "London"
override val keyStorePassword: String = "testpass"
override val trustStorePassword: String = "trustpass"
}
return ArtemisMessagingService(temporaryFolder.newFolder().toPath(), hostAndPort, config).apply {
configureWithDevSSLCertificate()
messagingNetwork = this
}
}

View File

@ -76,6 +76,8 @@ fun main(args: Array<String>) {
override val myLegalName: String = "Rate fix demo node"
override val exportJMXto: String = "http"
override val nearestCity: String = "Atlantis"
override val keyStorePassword: String = "cordacadevpass"
override val trustStorePassword: String = "trustpass"
}
val apiAddr = HostAndPort.fromParts(myNetAddr.hostText, myNetAddr.port + 1)

View File

@ -1,3 +1,5 @@
myLegalName = "Vast Global MegaCorp, Ltd"
exportJMXto = "http"
nearestCity = "The Moon"
nearestCity = "The Moon"
keyStorePassword = "cordacadevpass"
trustStorePassword = "trustpass"