mirror of
https://github.com/corda/corda.git
synced 2025-06-18 15:18:16 +00:00
Add X509 creation and manipulation utilities to core and enable SSL in ArtemisMQ
This commit is contained in:
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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user