Merge OS to Enterprise.

Main differences preserved in Enterprise version:
* constants.properties: platform version 1 (OS has 2)
* gradle-wrapper.properties: higher Gradle version gradle-4.3.1 (OS has gradle-4.3)
* Driver.kt - setting system property "user.dir"
This commit is contained in:
szymonsztuka
2017-11-30 23:54:12 +00:00
217 changed files with 3465 additions and 3010 deletions

View File

@ -0,0 +1,40 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.SignatureScheme
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.operator.ContentSigner
import java.io.OutputStream
import java.security.PrivateKey
import java.security.Provider
import java.security.SecureRandom
import java.security.Signature
/**
* Provide extra OID look up for signature algorithm not supported by bouncy castle.
* This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm.
*/
object ContentSignerBuilder {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
val sigAlgId = signatureScheme.signatureOID
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
if (random != null) {
initSign(privateKey, random)
} else {
initSign(privateKey)
}
}
return object : ContentSigner {
private val stream = SignatureOutputStream(sig)
override fun getAlgorithmIdentifier(): AlgorithmIdentifier = sigAlgId
override fun getOutputStream(): OutputStream = stream
override fun getSignature(): ByteArray = stream.signature
}
}
private class SignatureOutputStream(private val sig: Signature) : OutputStream() {
internal val signature: ByteArray get() = sig.sign()
override fun write(bytes: ByteArray, off: Int, len: Int) = sig.update(bytes, off, len)
override fun write(bytes: ByteArray) = sig.update(bytes)
override fun write(b: Int) = sig.update(b.toByte())
}
}

View File

@ -0,0 +1,168 @@
@file:JvmName("KeyStoreUtilities")
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
import org.bouncycastle.cert.X509CertificateHolder
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.security.*
import java.security.cert.Certificate
import java.security.cert.X509Certificate
const val KEYSTORE_TYPE = "JKS"
/**
* 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 (keyStoreFilePath.exists()) {
keyStoreFilePath.read { keyStore.load(it, pass) }
} else {
keyStore.load(null, pass)
keyStoreFilePath.write { keyStore.store(it, 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.
* @throws IOException if there was an error reading the key store from the file.
* @throws KeyStoreException if the password is incorrect or the key store is damaged.
*/
@Throws(KeyStoreException::class, IOException::class)
fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
return keyStoreFilePath.read { loadKeyStore(it, storePassword) }
}
/**
* 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.
* @throws IOException if there was an error reading the key store from the stream.
* @throws KeyStoreException if the password is incorrect or the key store is damaged.
*/
@Throws(KeyStoreException::class, IOException::class)
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 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.
*/
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<out X509CertificateHolder>) {
addOrReplaceKey(alias, key, password, chain.map { it.cert }.toTypedArray<Certificate>())
}
/**
* 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.
*/
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<out Certificate>) {
if (containsAlias(alias)) {
this.deleteEntry(alias)
}
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.
*/
fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
if (containsAlias(alias)) {
this.deleteEntry(alias)
}
this.setCertificateEntry(alias, cert)
}
/**
* Helper method save KeyStore to storage.
* @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 KeyStore.save(keyStoreFilePath: Path, storePassword: String) = keyStoreFilePath.write { store(it, storePassword) }
fun KeyStore.store(out: OutputStream, password: String) = store(out, password.toCharArray())
/**
* Extract public and private keys from a KeyStore file assuming storage alias is known.
* @param alias The name to lookup the Key and Certificate chain from.
* @param keyPassword Password to unlock the private key entries.
* @return The KeyPair found in the KeyStore under the specified alias.
*/
fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKeyPair(alias, keyPassword).keyPair
/**
* 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 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.
* @param keyPassword The password for the PrivateKey (not the store access password).
*/
fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair {
val cert = getX509Certificate(alias).toX509CertHolder()
val publicKey = Crypto.toSupportedPublicKey(cert.subjectPublicKeyInfo)
return CertificateAndKeyPair(cert, KeyPair(publicKey, getSupportedKey(alias, keyPassword)))
}
/**
* Extract public X509 certificate from a KeyStore file assuming storage alias is known.
* @param alias The name to lookup the Key and Certificate chain from.
* @return The X509Certificate found in the KeyStore under the specified alias.
*/
fun KeyStore.getX509Certificate(alias: String): X509Certificate {
val certificate = getCertificate(alias) ?: throw IllegalArgumentException("No certificate under alias \"$alias\".")
return certificate as? X509Certificate ?: throw IllegalArgumentException("Certificate under alias \"$alias\" is not an X.509 certificate.")
}
/**
* Extract a private key from a KeyStore file assuming storage alias is known.
* By default, a JKS keystore returns PrivateKey implementations supported by the SUN provider.
* For instance, if one imports a BouncyCastle ECC key, JKS will return a SUN ECC key implementation on getKey.
* To convert to a supported implementation, an encode->decode method is applied to the keystore's returned object.
* @param alias The name to lookup the Key.
* @param keyPassword Password to unlock the private key entries.
* @return the requested private key in supported type.
* @throws KeyStoreException if the keystore has not been initialized.
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found (not supported from the Keystore provider).
* @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong).
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
val keyPass = keyPassword.toCharArray()
val key = getKey(alias, keyPass) as PrivateKey
return Crypto.toSupportedPrivateKey(key)
}

View File

@ -0,0 +1,52 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.read
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
private fun createCertificate(serviceName: CordaX500Name, pubKey: PublicKey): CertPath {
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
// Assume key password = store password.
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore.
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(cert.cert) + clientCertPath)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
// TODO: X509Utilities.validateCertificateChain()
return certPath
}
fun signAndSaveNewKeyPair(serviceName: CordaX500Name, privateKeyAlias: String, keyPair: KeyPair) {
val certPath = createCertificate(serviceName, keyPair.public)
// Assume key password = store password.
keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray())
keyStore.save(storePath, storePassword)
}
fun savePublicKey(serviceName: CordaX500Name, pubKeyAlias: String, pubKey: PublicKey) {
val certPath = createCertificate(serviceName, pubKey)
// Assume key password = store password.
keyStore.addOrReplaceCertificate(pubKeyAlias, certPath.certificates.first())
keyStore.save(storePath, storePassword)
}
// Delegate methods to keystore. Sadly keystore doesn't have an interface.
fun containsAlias(alias: String) = keyStore.containsAlias(alias)
fun getX509Certificate(alias: String) = keyStore.getX509Certificate(alias)
fun getCertificateChain(alias: String): Array<out Certificate> = keyStore.getCertificateChain(alias)
fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias)
fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword)
}

View File

@ -0,0 +1,361 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.read
import net.corda.core.internal.write
import net.corda.core.internal.x500Name
import net.corda.core.utilities.days
import net.corda.core.utilities.millis
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERUTF8String
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
import org.bouncycastle.util.io.pem.PemReader
import java.io.FileWriter
import java.io.InputStream
import java.math.BigInteger
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.*
import java.security.cert.Certificate
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
object X509Utilities {
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
// Aliases for private keys and certificates.
const val CORDA_ROOT_CA = "cordarootca"
const val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
const val CORDA_CLIENT_TLS = "cordaclienttls"
const val CORDA_CLIENT_CA = "cordaclientca"
const val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate"
private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days)
/**
* Helper function to return the latest out of an instant and an optional date.
*/
private fun max(first: Instant, second: Date?): Date {
return if (second != null && second.time > first.toEpochMilli())
second
else
Date(first.toEpochMilli())
}
/**
* Helper function to return the earliest out of an instant and an optional date.
*/
private fun min(first: Instant, second: Date?): Date {
return if (second != null && second.time < first.toEpochMilli())
second
else
Date(first.toEpochMilli())
}
/**
* Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range.
* @param before duration to roll back returned start date relative to current date.
* @param after duration to roll forward returned end date relative to current date.
* @param parent if provided certificate whose validity should bound the date interval returned.
*/
fun getCertificateValidityWindow(before: Duration, after: Duration, parent: X509CertificateHolder? = null): Pair<Date, Date> {
val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS)
val notBefore = max(startOfDayUTC - before, parent?.notBefore)
val notAfter = min(startOfDayUTC + after, parent?.notAfter)
return Pair(notBefore, notAfter)
}
/*
* Create a de novo root self-signed X509 v3 CA cert.
*/
@JvmStatic
fun createSelfSignedCACertificate(subject: CordaX500Name,
keyPair: KeyPair,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
return createCertificate(CertificateType.ROOT_CA, subject.x500Name, keyPair, subject.x500Name, keyPair.public, window)
}
/**
* Create a X509 v3 certificate for use as a CA or for TLS. This does not require a [CordaX500Name] because the
* constraints are inappropriate for TLS/CA usage, however as a result this is unsuitable for Corda identity
* certificate generation.
*
* @param issuerCertificate The Public certificate of the root CA above this used to sign it.
* @param issuerKeyPair The KeyPair of the root CA above this used to sign it.
* @param subject subject of the generated certificate.
* @param subjectPublicKey subject's public key.
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
* @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.
*/
@JvmStatic
fun createCertificate(certificateType: CertificateType,
issuerCertificate: X509CertificateHolder,
issuerKeyPair: KeyPair,
subject: CordaX500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
return createCertificate(certificateType, issuerCertificate, issuerKeyPair, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
}
/**
* Create a X509 v3 certificate for use as a CA or for TLS. This does not require a [CordaX500Name] because the
* constraints are inappropriate for TLS/CA usage, however as a result this is unsuitable for Corda identity
* certificate generation.
*
* @param issuerCertificate The Public certificate of the root CA above this used to sign it.
* @param issuerKeyPair The KeyPair of the root CA above this used to sign it.
* @param subject subject of the generated certificate.
* @param subjectPublicKey subject's public key.
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
* @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.
*/
@JvmStatic
fun createCertificate(certificateType: CertificateType,
issuerCertificate: X509CertificateHolder,
issuerKeyPair: KeyPair,
subject: X500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate)
return createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints)
}
@Throws(CertPathValidatorException::class)
fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) {
require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
params.isRevocationEnabled = false
val certPath = X509CertificateFactory().delegate.generateCertPath(certificates.toList())
val pathValidator = CertPathValidator.getInstance("PKIX")
pathValidator.validate(certPath, params)
}
/**
* 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 file Target file.
*/
@JvmStatic
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, file: Path) {
JcaPEMWriter(file.toFile().writer()).use {
it.writeObject(x509Certificate)
}
}
/**
* Helper method to load back a .pem/.cer format file copy of a certificate.
* @param file Source file.
* @return The X509Certificate that was encoded in the file.
*/
@JvmStatic
fun loadCertificateFromPEMFile(file: Path): X509CertificateHolder {
val cert = file.read {
val reader = PemReader(it.reader())
val pemObject = reader.readPemObject()
X509CertificateHolder(pemObject.content)
}
cert.isValidOn(Date())
return cert
}
/**
* Build a partial X.509 certificate ready for signing.
*
* @param issuer name of the issuing entity.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
fun createCertificate(certificateType: CertificateType,
issuer: CordaX500Name,
subject: CordaX500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509v3CertificateBuilder {
return createCertificate(certificateType, issuer.x500Name, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
}
/**
* Build a partial X.509 certificate ready for signing.
*
* @param issuer name of the issuing entity.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
internal fun createCertificate(certificateType: CertificateType,
issuer: X500Name,
subject: X500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509v3CertificateBuilder {
val serial = BigInteger.valueOf(random63BitValue())
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded))
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second,
subject, subjectPublicKey)
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo))
.addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA))
.addExtension(Extension.keyUsage, false, certificateType.keyUsage)
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
if (nameConstraints != null) {
builder.addExtension(Extension.nameConstraints, true, nameConstraints)
}
return builder
}
/**
* Build and sign an X.509 certificate with the given signer.
*
* @param issuer name of the issuing entity.
* @param issuerSigner content signer to sign the certificate with.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
fun createCertificate(certificateType: CertificateType,
issuer: X500Name,
issuerSigner: ContentSigner,
subject: CordaX500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val builder = createCertificate(certificateType, issuer, subject.x500Name, subjectPublicKey, validityWindow, nameConstraints)
return builder.build(issuerSigner).apply {
require(isValidOn(Date()))
}
}
/**
* Build and sign an X.509 certificate with CA cert private key.
*
* @param issuer name of the issuing entity.
* @param issuerKeyPair the public & private key to sign the certificate with.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
fun createCertificate(certificateType: CertificateType,
issuer: X500Name,
issuerKeyPair: KeyPair,
subject: X500Name,
subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private)
val provider = Crypto.findProvider(signatureScheme.providerName)
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider)
return builder.build(signer).apply {
require(isValidOn(Date()))
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public)))
}
}
/**
* Create certificate signing request using provided information.
*/
private fun createCertificateSigningRequest(subject: CordaX500Name,
email: String,
keyPair: KeyPair,
signatureScheme: SignatureScheme): PKCS10CertificationRequest {
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName))
return JcaPKCS10CertificationRequestBuilder(subject.x500Name, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer)
}
fun createCertificateSigningRequest(subject: CordaX500Name, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
}
}
/**
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
* so assume this class is not.
*/
class X509CertificateFactory {
val delegate: CertificateFactory = CertificateFactory.getInstance("X.509")
fun generateCertificate(input: InputStream): X509Certificate {
return delegate.generateCertificate(input) as X509Certificate
}
}
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) {
ROOT_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
),
INTERMEDIATE_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
),
CLIENT_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
),
TLS(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = false
),
// TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints
IDENTITY(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = true
)
}
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)

View File

@ -0,0 +1,216 @@
package net.corda.nodeapi.internal.persistence
import net.corda.core.schemas.MappedSchema
import rx.Observable
import rx.Subscriber
import rx.subjects.UnicastSubject
import java.io.Closeable
import java.sql.Connection
import java.sql.SQLException
import java.util.concurrent.CopyOnWriteArrayList
import javax.persistence.AttributeConverter
import javax.sql.DataSource
/**
* Table prefix for all tables owned by the node module.
*/
const val NODE_DATABASE_PREFIX = "node_"
// This class forms part of the node config and so any changes to it must be handled with care
data class DatabaseConfig(
val initialiseSchema: Boolean = true,
val serverNameTablePrefix: String = "",
val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ
)
// This class forms part of the node config and so any changes to it must be handled with care
enum class TransactionIsolationLevel {
NONE,
READ_UNCOMMITTED,
READ_COMMITTED,
REPEATABLE_READ,
SERIALIZABLE;
/**
* The JDBC constant value of the same name but prefixed with TRANSACTION_ defined in [java.sql.Connection].
*/
val jdbcValue: Int = java.sql.Connection::class.java.getField("TRANSACTION_$name").get(null) as Int
}
class CordaPersistence(
val dataSource: DataSource,
databaseConfig: DatabaseConfig,
schemas: Set<MappedSchema>,
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet()
) : Closeable {
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
val hibernateConfig: HibernateConfiguration by lazy {
transaction {
HibernateConfiguration(schemas, databaseConfig, attributeConverters)
}
}
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
init {
DatabaseTransactionManager(this)
// Check not in read-only mode.
transaction {
dataSource.connection.use {
check(!it.metaData.isReadOnly) { "Database should not be readonly." }
}
}
}
/**
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
*/
fun createTransaction(isolationLevel: TransactionIsolationLevel): DatabaseTransaction {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
DatabaseTransactionManager.dataSource = this
return DatabaseTransactionManager.currentOrNew(isolationLevel)
}
/**
* Creates an instance of [DatabaseTransaction], with the default transaction isolation level.
*/
fun createTransaction(): DatabaseTransaction = createTransaction(defaultIsolationLevel)
fun createSession(): Connection {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
DatabaseTransactionManager.dataSource = this
val ctx = DatabaseTransactionManager.currentOrNull()
return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.")
}
/**
* Executes given statement in the scope of transaction, with the given isolation level.
* @param isolationLevel isolation level for the transaction.
* @param statement to be executed in the scope of this transaction.
*/
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
DatabaseTransactionManager.dataSource = this
return transaction(isolationLevel, 3, statement)
}
/**
* Executes given statement in the scope of transaction with the transaction level specified at the creation time.
* @param statement to be executed in the scope of this transaction.
*/
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
val outer = DatabaseTransactionManager.currentOrNull()
return if (outer != null) {
outer.statement()
} else {
inTopLevelTransaction(isolationLevel, repetitionAttempts, statement)
}
}
private fun <T> inTopLevelTransaction(isolationLevel: TransactionIsolationLevel, repetitionAttempts: Int, statement: DatabaseTransaction.() -> T): T {
var repetitions = 0
while (true) {
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
try {
val answer = transaction.statement()
transaction.commit()
return answer
} catch (e: SQLException) {
transaction.rollback()
repetitions++
if (repetitions >= repetitionAttempts) {
throw e
}
} catch (e: Throwable) {
transaction.rollback()
throw e
} finally {
transaction.close()
}
}
}
override fun close() {
// DataSource doesn't implement AutoCloseable so we just have to hope that the implementation does so that we can close it
(dataSource as? AutoCloseable)?.close()
}
}
/**
* Buffer observations until after the current database transaction has been closed. Observations are never
* dropped, simply delayed.
*
* Primarily for use by component authors to publish observations during database transactions without racing against
* closing the database transaction.
*
* For examples, see the call hierarchy of this function.
*/
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
val currentTxId = DatabaseTransactionManager.transactionId
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId }
val subject = UnicastSubject.create<T>()
subject.delaySubscription(databaseTxBoundary).subscribe(this)
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
return subject
}
// A subscriber that delegates to multiple others, wrapping a database transaction around the combination.
private class DatabaseTransactionWrappingSubscriber<U>(val db: CordaPersistence?) : Subscriber<U>() {
// Some unsubscribes happen inside onNext() so need something that supports concurrent modification.
val delegates = CopyOnWriteArrayList<Subscriber<in U>>()
fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) {
(db ?: DatabaseTransactionManager.dataSource).transaction {
delegates.filter { !it.isUnsubscribed }.forEach {
it.block()
}
}
}
override fun onCompleted() = forEachSubscriberWithDbTx { onCompleted() }
override fun onError(e: Throwable?) = forEachSubscriberWithDbTx { onError(e) }
override fun onNext(s: U) = forEachSubscriberWithDbTx { onNext(s) }
override fun onStart() = forEachSubscriberWithDbTx { onStart() }
fun cleanUp() {
if (delegates.removeIf { it.isUnsubscribed }) {
if (delegates.isEmpty()) {
unsubscribe()
}
}
}
}
// A subscriber that wraps another but does not pass on observations to it.
private class NoOpSubscriber<U>(t: Subscriber<in U>) : Subscriber<U>(t) {
override fun onCompleted() {}
override fun onError(e: Throwable?) {}
override fun onNext(s: U) {}
}
/**
* Wrap delivery of observations in a database transaction. Multiple subscribers will receive the observations inside
* the same database transaction. This also lazily subscribes to the source [rx.Observable] to preserve any buffering
* that might be in place.
*/
fun <T : Any> rx.Observable<T>.wrapWithDatabaseTransaction(db: CordaPersistence? = null): rx.Observable<T> {
var wrappingSubscriber = DatabaseTransactionWrappingSubscriber<T>(db)
// Use lift to add subscribers to a special subscriber that wraps a database transaction around observations.
// Each subscriber will be passed to this lambda when they subscribe, at which point we add them to wrapping subscriber.
return this.lift { toBeWrappedInDbTx: Subscriber<in T> ->
// Add the subscriber to the wrapping subscriber, which will invoke the original subscribers together inside a database transaction.
wrappingSubscriber.delegates.add(toBeWrappedInDbTx)
// If we are the first subscriber, return the shared subscriber, otherwise return a subscriber that does nothing.
if (wrappingSubscriber.delegates.size == 1) wrappingSubscriber else NoOpSubscriber(toBeWrappedInDbTx)
// Clean up the shared list of subscribers when they unsubscribe.
}.doOnUnsubscribe {
wrappingSubscriber.cleanUp()
// If cleanup removed the last subscriber reset the system, as future subscribers might need the stream again
if (wrappingSubscriber.delegates.isEmpty()) {
wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db)
}
}
}

View File

@ -0,0 +1,61 @@
package net.corda.nodeapi.internal.persistence
import org.hibernate.Session
import org.hibernate.Transaction
import rx.subjects.Subject
import java.sql.Connection
import java.util.*
class DatabaseTransaction(
isolation: Int,
private val threadLocal: ThreadLocal<DatabaseTransaction>,
private val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>,
val cordaPersistence: CordaPersistence
) {
val id: UUID = UUID.randomUUID()
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
cordaPersistence.dataSource.connection.apply {
autoCommit = false
transactionIsolation = isolation
}
}
private val sessionDelegate = lazy {
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
hibernateTransaction = session.beginTransaction()
session
}
val session: Session by sessionDelegate
private lateinit var hibernateTransaction: Transaction
private val outerTransaction: DatabaseTransaction? = threadLocal.get()
fun commit() {
if (sessionDelegate.isInitialized()) {
hibernateTransaction.commit()
}
connection.commit()
}
fun rollback() {
if (sessionDelegate.isInitialized() && session.isOpen) {
session.clear()
}
if (!connection.isClosed) {
connection.rollback()
}
}
fun close() {
if (sessionDelegate.isInitialized() && session.isOpen) {
session.close()
}
connection.close()
threadLocal.set(outerTransaction)
if (outerTransaction == null) {
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
}
}
}

View File

@ -0,0 +1,77 @@
package net.corda.nodeapi.internal.persistence
import co.paralleluniverse.strands.Strand
import org.hibernate.Session
import rx.subjects.PublishSubject
import rx.subjects.Subject
import java.util.*
import java.util.concurrent.ConcurrentHashMap
fun currentDBSession(): Session = DatabaseTransactionManager.current().session
class DatabaseTransactionManager(initDataSource: CordaPersistence) {
companion object {
private val threadLocalDb = ThreadLocal<CordaPersistence>()
private val threadLocalTx = ThreadLocal<DatabaseTransaction>()
private val databaseToInstance = ConcurrentHashMap<CordaPersistence, DatabaseTransactionManager>()
fun setThreadLocalTx(tx: DatabaseTransaction?): DatabaseTransaction? {
val oldTx = threadLocalTx.get()
threadLocalTx.set(tx)
return oldTx
}
fun restoreThreadLocalTx(context: DatabaseTransaction?) {
if (context != null) {
threadLocalDb.set(context.cordaPersistence)
}
threadLocalTx.set(context)
}
var dataSource: CordaPersistence
get() = threadLocalDb.get() ?: throw IllegalStateException("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}")
set(value) = threadLocalDb.set(value)
val transactionId: UUID
get() = threadLocalTx.get()?.id ?: throw IllegalStateException("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}")
val manager: DatabaseTransactionManager get() = databaseToInstance[dataSource]!!
val transactionBoundaries: Subject<Boundary, Boundary> get() = manager._transactionBoundaries
fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull()
fun currentOrNew(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
return currentOrNull() ?: manager.newTransaction(isolation.jdbcValue)
}
fun current(): DatabaseTransaction = currentOrNull() ?: error("No transaction in context.")
fun newTransaction(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
return manager.newTransaction(isolation.jdbcValue)
}
}
data class Boundary(val txId: UUID)
private val _transactionBoundaries = PublishSubject.create<Boundary>().toSerialized()
init {
// Found a unit test that was forgetting to close the database transactions. When you close() on the top level
// database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
// database transaction open. The [transaction] helper above handles this in a finally clause for you
// but any manual database transaction management is liable to have this problem.
if (threadLocalTx.get() != null) {
throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}")
}
dataSource = initDataSource
databaseToInstance[dataSource] = this
}
private fun newTransaction(isolation: Int) =
DatabaseTransaction(isolation, threadLocalTx, transactionBoundaries, dataSource).apply {
threadLocalTx.set(this)
}
private fun currentOrNull(): DatabaseTransaction? = threadLocalTx.get()
}

View File

@ -0,0 +1,155 @@
package net.corda.nodeapi.internal.persistence
import net.corda.core.internal.castIfPossible
import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.toHexString
import org.hibernate.SessionFactory
import org.hibernate.boot.MetadataSources
import org.hibernate.boot.model.naming.Identifier
import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
import org.hibernate.cfg.Configuration
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
import org.hibernate.service.UnknownUnwrapTypeException
import org.hibernate.type.AbstractSingleColumnStandardBasicType
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
import java.sql.Connection
import java.util.concurrent.ConcurrentHashMap
import javax.persistence.AttributeConverter
class HibernateConfiguration(
schemas: Set<MappedSchema>,
private val databaseConfig: DatabaseConfig,
private val attributeConverters: Collection<AttributeConverter<*, *>>
) {
companion object {
private val logger = contextLogger()
}
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
private val sessionFactories = ConcurrentHashMap<Set<MappedSchema>, SessionFactory>()
val sessionFactoryForRegisteredSchemas = schemas.let {
logger.info("Init HibernateConfiguration for schemas: $it")
sessionFactoryForSchemas(it)
}
/** @param key must be immutable, not just read-only. */
fun sessionFactoryForSchemas(key: Set<MappedSchema>) = sessionFactories.computeIfAbsent(key, { makeSessionFactoryForSchemas(key) })
private fun makeSessionFactoryForSchemas(schemas: Set<MappedSchema>): SessionFactory {
logger.info("Creating session factory for schemas: $schemas")
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
val metadataSources = MetadataSources(serviceRegistry)
// We set a connection provider as the auto schema generation requires it. The auto schema generation will not
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
// TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs.
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name)
.setProperty("hibernate.hbm2ddl.auto", if (databaseConfig.initialiseSchema) "update" else "validate")
.setProperty("hibernate.format_sql", "true")
.setProperty("hibernate.connection.isolation", databaseConfig.transactionIsolationLevel.jdbcValue.toString())
if (databaseConfig.schema != null) {
// This property helps 'hibernate.hbm2ddl.auto' to work properly when many schemas have similar table names.
config.setProperty("hibernate.default_schema", databaseConfig.schema)
}
schemas.forEach { schema ->
// TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
}
val sessionFactory = buildSessionFactory(config, metadataSources, databaseConfig.serverNameTablePrefix)
logger.info("Created session factory for schemas: $schemas")
return sessionFactory
}
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory {
config.standardServiceRegistryBuilder.applySettings(config.properties)
val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run {
applyPhysicalNamingStrategy(object : PhysicalNamingStrategyStandardImpl() {
override fun toPhysicalTableName(name: Identifier?, context: JdbcEnvironment?): Identifier {
val default = super.toPhysicalTableName(name, context)
return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted)
}
})
// register custom converters
attributeConverters.forEach { applyAttributeConverter(it) }
// Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages.
// to avoid OOM when large blobs might get logged.
applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name)
applyBasicType(CordaWrapperBinaryType, CordaWrapperBinaryType.name)
build()
}
return metadata.sessionFactoryBuilder.run {
allowOutOfTransactionUpdateOperations(true)
applySecondLevelCacheSupport(false)
applyQueryCacheSupport(false)
enableReleaseResourcesOnCloseEnabled(true)
build()
}
}
// Supply Hibernate with connections from our underlying Exposed database integration. Only used
// during schema creation / update.
class NodeDatabaseConnectionProvider : ConnectionProvider {
override fun closeConnection(conn: Connection) {
conn.autoCommit = false
val tx = DatabaseTransactionManager.current()
tx.commit()
tx.close()
}
override fun supportsAggressiveRelease(): Boolean = true
override fun getConnection(): Connection {
return DatabaseTransactionManager.newTransaction().connection
}
override fun <T : Any?> unwrap(unwrapType: Class<T>): T {
return unwrapType.castIfPossible(this) ?: throw UnknownUnwrapTypeException(unwrapType)
}
override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = unwrapType == NodeDatabaseConnectionProvider::class.java
}
// A tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. Also logs in hex.
private object CordaMaterializedBlobType : AbstractSingleColumnStandardBasicType<ByteArray>(BlobTypeDescriptor.DEFAULT, CordaPrimitiveByteArrayTypeDescriptor) {
override fun getName(): String {
return "materialized_blob"
}
}
// A tweaked version of `org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor` that truncates logged messages.
private object CordaPrimitiveByteArrayTypeDescriptor : PrimitiveByteArrayTypeDescriptor() {
private val LOG_SIZE_LIMIT = 1024
override fun extractLoggableRepresentation(value: ByteArray?): String {
return if (value == null) {
super.extractLoggableRepresentation(value)
} else {
if (value.size <= LOG_SIZE_LIMIT) {
"[size=${value.size}, value=${value.toHexString()}]"
} else {
"[size=${value.size}, value=${value.copyOfRange(0, LOG_SIZE_LIMIT).toHexString()}...truncated...]"
}
}
}
}
// A tweaked version of `org.hibernate.type.WrapperBinaryType` that deals with ByteArray (java primitive byte[] type).
private object CordaWrapperBinaryType : AbstractSingleColumnStandardBasicType<ByteArray>(VarbinaryTypeDescriptor.INSTANCE, PrimitiveByteArrayTypeDescriptor.INSTANCE) {
override fun getRegistrationKeys(): Array<String> {
return arrayOf(name, "ByteArray", ByteArray::class.java.name)
}
override fun getName(): String {
return "corda-wrapper-binary"
}
}
}

View File

@ -26,5 +26,8 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = (obj as? Binary)?.array ?: obj
override fun readObject(
obj: Any,
schemas: SerializationSchemas,
input: DeserializationInput): Any = (obj as? Binary)?.array ?: obj
}

View File

@ -35,5 +35,5 @@ interface AMQPSerializer<out T> {
/**
* Read the given object from the input. The envelope is provided in case the schema is required.
*/
fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T
fun readObject(obj: Any, schema: SerializationSchemas, input: DeserializationInput): T
}

View File

@ -56,9 +56,9 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
if (obj is List<*>) {
return obj.map { input.readObjectOrNull(it, schema, elementType) }.toArrayOfType(elementType)
return obj.map { input.readObjectOrNull(it, schemas, elementType) }.toArrayOfType(elementType)
} else throw NotSerializableException("Expected a List but found $obj")
}

View File

@ -77,8 +77,8 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
// TODO: Can we verify the entries in the list?
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) })
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0]) })
}
}

View File

@ -67,8 +67,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
superClassSerializer.writeDescribedObject(obj, data, type, output)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
return superClassSerializer.readObject(obj, schema, input)
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
return superClassSerializer.readObject(obj, schemas, input)
}
}
@ -133,8 +133,8 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input))
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schemas, input))
return fromProxy(proxy)
}
}
@ -166,7 +166,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
data.putString(unmaker(obj))
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T {
val proxy = obj as String
return maker(proxy)
}

View File

@ -97,21 +97,21 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
@Throws(NotSerializableException::class)
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
val envelope = getEnvelope(bytes)
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz))
}
@Throws(NotSerializableException::class)
fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
val envelope = getEnvelope(bytes)
// Now pick out the obj and schema from the envelope.
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz)), envelope)
}
internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type): Any? {
return if (obj == null) null else readObject(obj, schema, type)
}
internal fun readObject(obj: Any, schema: Schema, type: Type): Any =
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type): Any =
if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
// It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
val objectIndex = (obj.described as UnsignedInteger).toInt()
@ -127,11 +127,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
val objectRead = when (obj) {
is DescribedType -> {
// Look up serializer in factory by descriptor
val serializer = serializerFactory.get(obj.descriptor, schema)
val serializer = serializerFactory.get(obj.descriptor, schemas)
if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
"expected to be of type $type but was ${serializer.type}")
serializer.readObject(obj.described, schema, this)
serializer.readObject(obj.described, schemas, this)
}
is Binary -> obj.array
else -> obj // this will be the case for primitive types like [boolean] et al.

View File

@ -27,7 +27,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
output.writeTypeNotations(typeNotation)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
val enumName = (obj as List<*>)[0] as String
val enumOrd = obj[1] as Int
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?

View File

@ -32,8 +32,8 @@ class EvolutionSerializer(
* @param property object to read the actual property value
*/
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) =
property.readProperty(paramValues[idx], schema, input)
fun readProperty(paramValues: List<*>, schemas: SerializationSchemas, input: DeserializationInput) =
property.readProperty(paramValues[idx], schemas, input)
}
companion object {
@ -121,10 +121,10 @@ class EvolutionSerializer(
*
* TODO: Object references
*/
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
return construct(readers.map { it?.readProperty(obj, schema, input) })
return construct(readers.map { it?.readProperty(obj, schemas, input) })
}
}

View File

@ -88,15 +88,15 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schema, input, it) }
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schemas, input, it) }
concreteBuilder(entries.toMap())
}
private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
private fun readEntry(schemas: SerializationSchemas, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
input.readObjectOrNull(entry.key, schemas, declaredType.actualTypeArguments[0]) to
input.readObjectOrNull(entry.value, schemas, declaredType.actualTypeArguments[1])
// Cannot use * as a bound for EnumMap and EnumSet since * is not an enum. So, we use a sample enum instead.
// We don't actually care about the type, we just need to make the compiler happier.

View File

@ -55,10 +55,15 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
override fun readObject(
obj: Any,
schemas: SerializationSchemas,
input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
if (obj is List<*>) {
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
if (obj.size > propertySerializers.size) {
throw NotSerializableException("Too many properties in described type $typeName")
}
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schemas, input) }
construct(params)
} else throw NotSerializableException("Body of described type is unexpected $obj")
}

View File

@ -14,7 +14,7 @@ import kotlin.reflect.jvm.javaGetter
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any?
val type: String = generateType()
val requires: List<String> = generateRequires()
@ -91,8 +91,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
}
}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
input.readObjectOrNull(obj, schema, resolvedType)
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
input.readObjectOrNull(obj, schemas, resolvedType)
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
@ -108,7 +108,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
return if (obj is Binary) obj.array else obj
}
@ -131,7 +131,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
PropertySerializer(name, readMethod, Character::class.java) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
override fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any? {
return if (obj == null) null else (obj as Short).toChar()
}

View File

@ -4,7 +4,9 @@ import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeResolver
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.ClassWhitelist
import net.corda.nodeapi.internal.serialization.carpenter.*
import net.corda.nodeapi.internal.serialization.carpenter.CarpenterMetaSchema
import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter
import net.corda.nodeapi.internal.serialization.carpenter.MetaCarpenter
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.reflect.*
@ -13,7 +15,8 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
/**
* Factory of serializers designed to be shared across threads and invocations.
@ -40,7 +43,10 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
val classloader: ClassLoader
get() = classCarpenter.classloader
private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>): AMQPSerializer<Any> {
private fun getEvolutionSerializer(
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
transforms: TransformsSchema): AMQPSerializer<Any> {
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
@ -168,7 +174,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
* contained in the [Schema].
*/
@Throws(NotSerializableException::class)
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
return serializersByDescriptor[typeDescriptor] ?: {
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
@ -194,9 +200,9 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
* if not use the [ClassCarpenter] to generate a class to use in it's place
*/
private fun processSchema(schema: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
val metaSchema = CarpenterMetaSchema.newInstance()
for (typeNotation in schema.schema.types) {
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
try {
val serialiser = processSchemaEntry(typeNotation)
@ -204,7 +210,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
// doesn't match that of the serialised object then we are dealing with different
// instance of the class, as such we need to build an EvolutionSerialiser
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
getEvolutionSerializer(typeNotation, serialiser)
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas.transforms)
}
} catch (e: ClassNotFoundException) {
if (sentinel) throw e
@ -215,7 +221,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
if (metaSchema.isNotEmpty()) {
val mc = MetaCarpenter(metaSchema, classCarpenter)
mc.build()
processSchema(schema, true)
processSchema(schemaAndDescriptor, true)
}
}

View File

@ -28,7 +28,7 @@ class SingletonSerializer(override val type: Class<*>, val singleton: Any, facto
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
return singleton
}
}

View File

@ -85,6 +85,9 @@ class UnknownTransform : Transform() {
override val name: String get() = typeName
}
/**
* Used by the unit testing framework
*/
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
companion object : DescribedTypeConstructor<UnknownTestTransform> {
val typeName = "UnknownTest"

View File

@ -34,8 +34,8 @@ object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStr
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): InputStream {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): InputStream {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return ByteArrayInputStream(bits)
}
}

View File

@ -20,8 +20,8 @@ object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PrivateKey {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return Crypto.decodePrivateKey(bits)
}
}

View File

@ -17,8 +17,8 @@ object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::c
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PublicKey {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return Crypto.decodePublicKey(bits)
}
}

View File

@ -1,9 +1,9 @@
package net.corda.nodeapi.internal.serialization.amqp.custom
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.serialization.amqp.*
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(X509Certificate::class.java) {
@ -20,8 +20,8 @@ object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X509Certificate {
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
return CertificateFactory.getInstance("X.509").generateCertificate(bits.inputStream()) as X509Certificate
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): X509Certificate {
val bits = input.readObject(obj, schemas, ByteArray::class.java) as ByteArray
return X509CertificateFactory().generateCertificate(bits.inputStream())
}
}

View File

@ -22,6 +22,7 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.toFuture
import net.corda.core.toObservable
import net.corda.core.transactions.*
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.core.utilities.SgxSupport
import net.corda.nodeapi.internal.serialization.CordaClassResolver
import net.corda.nodeapi.internal.serialization.serializationContextKey
@ -490,8 +491,7 @@ object CertPathSerializer : Serializer<CertPath>() {
@ThreadSafe
object X509CertificateSerializer : Serializer<X509Certificate>() {
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
val factory = CertificateFactory.getInstance("X.509")
return factory.generateCertificate(input.readBytesWithLength().inputStream()) as X509Certificate
return X509CertificateFactory().generateCertificate(input.readBytesWithLength().inputStream())
}
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {

View File

@ -0,0 +1,441 @@
package net.corda.nodeapi.internal.crypto
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.div
import net.corda.core.internal.toTypedArray
import net.corda.core.internal.x500Name
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.config.createKeystoreForCordaNode
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.BOB_PUBKEY
import net.corda.testing.MEGA_CORP
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.BasicConstraints
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.KeyUsage
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
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.Path
import java.security.KeyStore
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.util.*
import java.util.stream.Stream
import javax.net.ssl.*
import kotlin.concurrent.thread
import kotlin.test.*
class X509UtilitiesTest {
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
@Test
fun `create valid self-signed CA certificate`() {
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val caCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Test Cert", organisation = "R3 Ltd", locality = "London", country = "GB"), caKey)
assertEquals(X500Name("CN=Test Cert,O=R3 Ltd,L=London,C=GB"), caCert.subject) // using our subject common name
assertEquals(caCert.issuer, caCert.subject) //self-signed
caCert.isValidOn(Date()) // throws on verification problems
caCert.isSignatureValid(JcaContentVerifierProviderBuilder().build(caKey.public)) // throws on verification problems
val basicConstraints = BasicConstraints.getInstance(caCert.getExtension(Extension.basicConstraints).parsedValue)
val keyUsage = KeyUsage.getInstance(caCert.getExtension(Extension.keyUsage).parsedValue)
assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property)
assertNull(basicConstraints.pathLenConstraint) // No length constraint specified on this CA certificate
}
@Test
fun `load and save a PEM file certificate`() {
val tmpCertificateFile = tempFile("cacert.pem")
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val caCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Test Cert", organisation = "R3 Ltd", locality = "London", country = "GB"), caKey)
X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile)
val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile)
assertEquals(caCert, readCertificate)
}
@Test
fun `create valid server certificate chain`() {
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val caCert = X509Utilities.createSelfSignedCACertificate(CordaX500Name(commonName = "Test CA Cert", organisation = "R3 Ltd", locality = "London", country = "GB"), caKey)
val subject = CordaX500Name(commonName = "Server Cert", organisation = "R3 Ltd", locality = "London", country = "GB")
val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public)
assertEquals(X500Name("C=GB,L=London,O=R3 Ltd,CN=Server Cert"), serverCert.subject) // using our subject common name
assertEquals(caCert.issuer, serverCert.issuer) // Issued by our CA cert
serverCert.isValidOn(Date()) // throws on verification problems
serverCert.isSignatureValid(JcaContentVerifierProviderBuilder().build(caKey.public)) // throws on verification problems
val basicConstraints = BasicConstraints.getInstance(serverCert.getExtension(Extension.basicConstraints).parsedValue)
val keyUsage = KeyUsage.getInstance(serverCert.getExtension(Extension.keyUsage).parsedValue)
assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property)
assertNull(basicConstraints.pathLenConstraint) // Non-CA certificate
}
@Test
fun `storing EdDSA key in java keystore`() {
val tmpKeyStore = tempFile("keystore.jks")
val keyPair = generateKeyPair(EDDSA_ED25519_SHA512)
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
assertTrue(Arrays.equals(selfSignCert.subjectPublicKeyInfo.encoded, keyPair.public.encoded))
// Save the EdDSA private key with self sign cert in the keystore.
val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(),
Stream.of(selfSignCert).map { it.cert }.toTypedArray())
keyStore.save(tmpKeyStore, "keystorepass")
// Load the keystore from file and make sure keys are intact.
val keyStore2 = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
val privateKey = keyStore2.getKey("Key", "password".toCharArray())
val pubKey = keyStore2.getCertificate("Key").publicKey
assertNotNull(pubKey)
assertNotNull(privateKey)
assertEquals(keyPair.public, pubKey)
assertEquals(keyPair.private, privateKey)
}
@Test
fun `signing EdDSA key with EcDSA certificate`() {
val tmpKeyStore = tempFile("keystore.jks")
val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
val ecDSACert = X509Utilities.createSelfSignedCACertificate(testName, ecDSAKey)
val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512)
val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, X500Name("CN=TestEdDSA"), edDSAKeypair.public)
// Save the EdDSA private key with cert chains.
val keyStore = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(),
Stream.of(ecDSACert, edDSACert).map { it.cert }.toTypedArray())
keyStore.save(tmpKeyStore, "keystorepass")
// Load the keystore from file and make sure keys are intact.
val keyStore2 = loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
val privateKey = keyStore2.getKey("Key", "password".toCharArray())
val certs = keyStore2.getCertificateChain("Key")
val pubKey = certs.last().publicKey
assertEquals(2, certs.size)
assertNotNull(pubKey)
assertNotNull(privateKey)
assertEquals(edDSAKeypair.public, pubKey)
assertEquals(edDSAKeypair.private, privateKey)
}
@Test
fun `create full CA keystore`() {
val tmpKeyStore = tempFile("keystore.jks")
val tmpTrustStore = tempFile("truststore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
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 = loadKeyStore(tmpKeyStore, "keystorepass")
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA, "keypass".toCharArray()) as PrivateKey
val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) 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 caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData)
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) }
// Load back generated intermediate CA Cert and private key
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate
val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA, "keypass".toCharArray()) as PrivateKey
intermediateCaCert.checkValidity(Date())
intermediateCaCert.verify(rootCaCert.publicKey)
// Now sign something with private key and verify against certificate public key
val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData)
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) }
}
@Test
fun `create server certificate in keystore for SSL`() {
val tmpCAKeyStore = tempFile("keystore.jks")
val tmpTrustStore = tempFile("truststore.jks")
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
val tmpServerKeyStore = tempFile("serverkeystore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
createCAKeyStoreAndTrustStore(tmpCAKeyStore,
"cakeystorepass",
"cakeypass",
tmpTrustStore,
"trustpass")
// Load signing intermediate CA cert
val caKeyStore = loadKeyStore(tmpCAKeyStore, "cakeystorepass")
val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, "cakeypass")
// Generate server cert and private key and populate another keystore suitable for SSL
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name)
// Load back server certificate
val serverKeyStore = loadKeyStore(tmpServerKeyStore, "serverstorepass")
val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass")
serverCertAndKey.certificate.isValidOn(Date())
serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo))
assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
// Load back server certificate
val sslKeyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass")
sslCertAndKey.certificate.isValidOn(Date())
sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo))
assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) }
// Now sign something with private key and verify against certificate public key
val testData = "123456".toByteArray()
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData)
val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo)
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
}
@Test
fun `create server cert and use in SSL socket`() {
val tmpCAKeyStore = tempFile("keystore.jks")
val tmpTrustStore = tempFile("truststore.jks")
val tmpSSLKeyStore = tempFile("sslkeystore.jks")
val tmpServerKeyStore = tempFile("serverkeystore.jks")
// Generate Root and Intermediate CA cert and put both into key store and root ca cert into trust store
val caKeyStore = createCAKeyStoreAndTrustStore(tmpCAKeyStore,
"cakeystorepass",
"cakeypass",
tmpTrustStore,
"trustpass")
// Generate server cert and private key and populate another keystore suitable for SSL
createKeystoreForCordaNode(tmpSSLKeyStore, tmpServerKeyStore, "serverstorepass", "serverstorepass", caKeyStore, "cakeypass", MEGA_CORP.name)
val keyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass")
val trustStore = loadKeyStore(tmpTrustStore, "trustpass")
val context = SSLContext.getInstance("TLS")
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, "serverstorepass".toCharArray())
val keyManagers = keyManagerFactory.keyManagers
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 = null // Reconfirm default no server name indication, use our own validator.
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 = null // Reconfirm default no server name indication, use our own validator.
clientSocket.sslParameters = clientParams
clientSocket.useClientMode = true
// We need to specify this explicitly because by default the client binds to 'localhost' and we want it to bind
// to whatever <hostname> resolves to(as that's what the server binds to). In particular on Debian <hostname>
// resolves to 127.0.1.1 instead of the external address of the interface, so the ssl handshake fails.
clientSocket.bind(InetSocketAddress(InetAddress.getLocalHost(), 0))
val lock = Object()
var done = false
var serverError = false
val serverThread = thread {
try {
val sslServerSocket = serverSocket.accept()
assertTrue(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
assertEquals(MEGA_CORP.name.x500Principal, peerX500Principal)
X509Utilities.validateCertificateChain(trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA), *peerChain)
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)
}
private fun tempFile(name: String): Path = tempFolder.root.toPath() / name
/**
* 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
*/
private fun createCAKeyStoreAndTrustStore(keyStoreFilePath: Path,
storePassword: String,
keyPassword: String,
trustStoreFilePath: Path,
trustStorePassword: String
): KeyStore {
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val baseName = CordaX500Name(organisation = "R3CEV", locality = "London", country = "GB")
val rootCACert = X509Utilities.createSelfSignedCACertificate(baseName.copy(commonName = "Corda Node Root CA"), rootCAKey)
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val intermediateCACert = X509Utilities.createCertificate(
CertificateType.INTERMEDIATE_CA,
rootCACert,
rootCAKey,
baseName.copy(commonName = "Corda Node Intermediate CA"),
intermediateCAKeyPair.public)
val keyPass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf<Certificate>(rootCACert.cert))
keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA,
intermediateCAKeyPair.private,
keyPass,
Stream.of(intermediateCACert, rootCACert).map { it.cert }.toTypedArray<Certificate>())
keyStore.save(keyStoreFilePath, storePassword)
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert)
trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert.cert)
trustStore.save(trustStoreFilePath, trustStorePassword)
return keyStore
}
@Test
fun `Get correct private key type from Keystore`() {
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
val testName = CordaX500Name(commonName = "Test", organisation = "R3 Ltd", locality = "London", country = "GB")
val selfSignCert = X509Utilities.createSelfSignedCACertificate(testName, keyPair)
val keyStore = loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword")
keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.cert))
val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray())
val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword")
assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key
assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey)
}
@Test
fun `serialize - deserialize X509CertififcateHolder`() {
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
val context = SerializationContextImpl(KryoHeaderV0_1,
javaClass.classLoader,
AllWhitelist,
emptyMap(),
true,
SerializationContext.UseCase.P2P)
val expected: X509CertificateHolder = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val serialized = expected.serialize(factory, context).bytes
val actual: X509CertificateHolder = serialized.deserialize(factory, context)
assertEquals(expected, actual)
}
@Test
fun `serialize - deserialize X509CertPath`() {
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
val context = SerializationContextImpl(KryoHeaderV0_1,
javaClass.classLoader,
AllWhitelist,
emptyMap(),
true,
SerializationContext.UseCase.P2P)
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name.x500Name, BOB_PUBKEY)
val expected = X509CertificateFactory().delegate.generateCertPath(listOf(certificate.cert, rootCACert.cert))
val serialized = expected.serialize(factory, context).bytes
val actual: CertPath = serialized.deserialize(factory, context)
assertEquals(expected, actual)
}
}

View File

@ -0,0 +1,43 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.SerializedBytes
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.File
import java.io.NotSerializableException
import java.net.URI
// NOTE: To recreate the test files used by these tests uncomment the original test classes and comment
// the new ones out, then change each test to write out the serialized bytes rather than read
// the file.
class EnumEvolveTests {
var localPath = projectRootDir.toUri().resolve(
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
// Version of the class as it was serialised
//
// @CordaSerializationTransformEnumDefault("D", "C")
// enum class DeserializeNewerSetToUnknown { A, B, C, D }
//
// Version of the class as it's used in the test
enum class DeserializeNewerSetToUnknown { A, B, C }
@Test
fun deserialiseNewerSetToUnknown() {
val resource = "${this.javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C (val e : DeserializeNewerSetToUnknown)
// Uncomment to re-generate test files
// File(URI("$localPath/$resource")).writeBytes(
// SerializationOutput(sf).serialize(C(DeserializeNewerSetToUnknown.D)).bytes)
Assertions.assertThatThrownBy {
DeserializationInput(sf).deserialize(SerializedBytes<C>(
File(EvolvabilityTests::class.java.getResource(resource).toURI()).readBytes()))
}.isInstanceOf(NotSerializableException::class.java)
}
}

View File

@ -17,7 +17,7 @@ class OverridePKSerializerTest {
throw SerializerTestException("Custom write call")
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): PublicKey {
throw SerializerTestException("Custom read call")
}