diff --git a/.ci/api-current.txt b/.ci/api-current.txt index cab36647d2..5a07958353 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1259,6 +1259,8 @@ public final class net.corda.core.crypto.Crypto extends java.lang.Object @NotNull public static final java.security.Provider findProvider(String) @NotNull + public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(int) + @NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(String) @NotNull public static final net.corda.core.crypto.SignatureScheme findSignatureScheme(java.security.PrivateKey) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 9208ede561..988d6e4a25 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -106,6 +106,10 @@ + + + + diff --git a/build.gradle b/build.gradle index 3dd4b3feb0..8efe2c9314 100644 --- a/build.gradle +++ b/build.gradle @@ -167,6 +167,9 @@ allprojects { suppressionFile = '.ci/dependency-checker/suppressedLibraries.xml' cveValidForHours = 1 format = 'ALL' + failOnError = project.getProperty('owasp.failOnError') + // by default CVSS is '11' which passes everything. Set between 0-10 to catch vulnerable deps + failBuildOnCVSS = project.getProperty('owasp.failBuildOnCVSS').toFloat() } sourceCompatibility = 1.8 targetCompatibility = 1.8 diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index bb2bdbeb70..408d9574d5 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -12,7 +12,7 @@ - + - + diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 9da4417c7d..8f88762592 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -199,6 +199,12 @@ object Crypto { + signatureSchemeMap.values.map { Pair(it.signatureOID, it) }) .toMap() + /** + * Map of supported digital signature schemes associated by [SignatureScheme.schemeNumberID]. + * SchemeNumberID is the scheme identifier attached to [SignatureMetadata]. + */ + private val signatureSchemeNumberIDMap: Map = Crypto.supportedSignatureSchemes().associateBy { it.schemeNumberID } + @JvmStatic fun supportedSignatureSchemes(): List = ArrayList(signatureSchemeMap.values) @@ -224,6 +230,13 @@ object Crypto { ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}") } + /** Find [SignatureScheme] by platform specific schemeNumberID. */ + @JvmStatic + fun findSignatureScheme(schemeNumberID: Int): SignatureScheme { + return signatureSchemeNumberIDMap[schemeNumberID] + ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeNumberID: $schemeNumberID") + } + /** * Factory pattern to retrieve the corresponding [SignatureScheme] based on [SignatureScheme.schemeCodeName]. * This function is usually called by key generators and verify signature functions. @@ -458,9 +471,14 @@ object Crypto { @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { - val sigKey: SignatureScheme = findSignatureScheme(keyPair.private) - val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public) - require(sigKey == sigMetaData) { + val sigKey: SignatureScheme = Crypto.findSignatureScheme(keyPair.private) + val sigMetaData: SignatureScheme = Crypto.findSignatureScheme(signableData.signatureMetadata.schemeNumberID) + // Special handling if the advertised SignatureScheme is CompositeKey. + // TODO fix notaries that advertise [CompositeKey] in their signature Metadata. Currently, clustered notary nodes + // mention Crypto.COMPOSITE_KEY in their SignatureMetadata, but they are actually signing with a leaf-key + // (and if they refer to it as a Composite key, then we lose info about the actual type of their signing key). + // In short, their metadata should be the leaf key-type, until we support CompositeKey signatures. + require(sigKey == sigMetaData || sigMetaData == Crypto.COMPOSITE_KEY) { "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}." } val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes) diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/AliasPrivateKey.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/AliasPrivateKey.kt new file mode 100644 index 0000000000..50d75fdb31 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/AliasPrivateKey.kt @@ -0,0 +1,39 @@ +package net.corda.core.crypto.internal + +import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import java.security.KeyPair +import java.security.PrivateKey +import java.security.spec.PKCS8EncodedKeySpec + +/** + * [PrivateKey] wrapper to just store the alias of a private key. + * Usually, HSM (hardware secure module) key entries are accessed via unique aliases and the private key material never + * leaves the box. This class wraps a [String] key alias into a [PrivateKey] object, which helps on transferring + * [KeyPair] objects without exposing the private key material. Then, whenever we need to sign with the actual private + * key, we provide the [alias] from this [AliasPrivateKey] to the underlying HSM implementation. + */ +data class AliasPrivateKey(val alias: String): PrivateKey { + + companion object { + // UUID-based OID + // TODO: Register for an OID space and issue our own shorter OID. + @JvmField + val ALIAS_PRIVATE_KEY = ASN1ObjectIdentifier("2.26.40086077608615255153862931087626791001") + + const val ALIAS_KEY_ALGORITHM = "ALIAS" + } + + override fun getAlgorithm() = ALIAS_KEY_ALGORITHM + + override fun getEncoded(): ByteArray { + val keyVector = ASN1EncodableVector() + keyVector.add(DERUTF8String(alias)) + val privateKeyInfoBytes = PrivateKeyInfo(AlgorithmIdentifier(ALIAS_PRIVATE_KEY), DERSequence(keyVector)).getEncoded(ASN1Encoding.DER) + val keySpec = PKCS8EncodedKeySpec(privateKeyInfoBytes) + return keySpec.encoded + } + + override fun getFormat() = "PKCS#8" +} diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 17d2ff5b65..9ba4c1adb8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -14,9 +14,10 @@ object JarSignatureCollector { /** * @see - * also accepting *.EC as this can be created and accepted by jarsigner tool @see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html - * and Java Security Manager. */ - private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA|EC)|SIG-.*)".toRegex() + * Additionally accepting *.EC as its valid for [java.util.jar.JarVerifier] and jarsigner @see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html, + * temporally treating META-INF/INDEX.LIST as unsignable entry because [java.util.jar.JarVerifier] doesn't load its signers. + */ + private val unsignableEntryName = "META-INF/(?:(?:.*[.](?:SF|DSA|RSA|EC)|SIG-.*)|INDEX\\.LIST)".toRegex() /** * Returns an ordered list of every [Party] which has signed every signable item in the given [JarInputStream]. diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index aec3891862..2c1c807633 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -86,7 +86,6 @@ data class NetworkParameters( require(epoch > 0) { "epoch must be at least 1" } require(maxMessageSize > 0) { "maxMessageSize must be at least 1" } require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" } - require(maxTransactionSize <= maxMessageSize) { "maxTransactionSize cannot be bigger than maxMessageSize" } require(!eventHorizon.isNegative) { "eventHorizon must be positive value" } require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index 30adb39244..cd3a5a511e 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -21,18 +21,24 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.interfaces.ECKey import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec +import org.bouncycastle.operator.ContentSigner import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.junit.Assert.assertNotEquals import org.junit.Test import java.math.BigInteger +import java.security.KeyPair import java.security.KeyPairGenerator +import java.security.cert.X509Certificate import java.util.* import kotlin.test.* @@ -936,4 +942,32 @@ class CryptoUtilsTest { // Just in case, test that signatures of different messages are not the same. assertNotEquals(OpaqueBytes(signedData1stTime), OpaqueBytes(signedZeroArray1stTime)) } + + fun ContentSigner.write(message: ByteArray) { + this.outputStream.write(message) + this.outputStream.close() + } + + private fun createCert(signer: ContentSigner, keyPair: KeyPair): X509Certificate { + val dname = X500Name("CN=TestEntity") + val startDate = Calendar.getInstance().let { cal -> + cal.time = Date() + cal.add(Calendar.HOUR, -1) + cal.time + } + val endDate = Calendar.getInstance().let { cal -> + cal.time = startDate + cal.add(Calendar.YEAR, 1) + cal.time + } + val certificate = JcaX509v3CertificateBuilder( + dname, + BigInteger.TEN, + startDate, + endDate, + dname, + keyPair.public + ).build(signer) + return JcaX509CertificateConverter().getCertificate(certificate) + } } diff --git a/gradle.properties b/gradle.properties index 0c6bf2d49f..3e359435f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ kotlin.incremental=true org.gradle.jvmargs=-XX:+UseG1GC -Xmx1g -Dfile.encoding=UTF-8 org.gradle.caching=true +owasp.failOnError=false +owasp.failBuildOnCVSS=11.0 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt index 632e2ad313..57c5ff659b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStore.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.config +import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.internal.outputStream import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities @@ -8,6 +9,7 @@ import java.io.InputStream import java.io.OutputStream import java.nio.file.OpenOption import java.nio.file.Path +import java.security.PrivateKey import java.security.cert.X509Certificate interface CertificateStore : Iterable> { @@ -42,7 +44,6 @@ interface CertificateStore : Iterable> { } operator fun set(alias: String, certificate: X509Certificate) { - update { internal.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certificate) } @@ -65,7 +66,6 @@ interface CertificateStore : Iterable> { * @throws IllegalArgumentException if no certificate for the alias is found, or if the certificate is not an [X509Certificate]. */ operator fun get(alias: String): X509Certificate { - return query { getCertificate(alias) } @@ -79,6 +79,21 @@ interface CertificateStore : Iterable> { this@CertificateStore.forEach(::setCertificate) } } + + fun setCertPathOnly(alias: String, certificates: List) { + // In case CryptoService and CertificateStore share the same KeyStore (i.e., when BCCryptoService is used), + // extract the existing key from the Keystore and store it again along with the new certificate chain. + // This is because KeyStores do not support updateKeyEntry and thus we cannot update the certificate chain + // without overriding the key entry. + // Note that if the given alias already exists, the keystore information associated with it + // is overridden by the given key (and associated certificate chain). + val privateKey: PrivateKey = if (this.contains(alias)) { + this.value.getPrivateKey(alias, entryPassword) + } else { + AliasPrivateKey(alias) + } + this.value.setPrivateKey(alias, privateKey, certificates, entryPassword) + } } private class DelegatingCertificateStore(override val value: X509KeyStore, override val password: String, override val entryPassword: String) : CertificateStore \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt index 7cdfeee79b..f56d2f52d1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/CertificateStoreSupplier.kt @@ -19,6 +19,5 @@ interface CertificateStoreSupplier { // TODO replace reference to FileBasedCertificateStoreSupplier with CertificateStoreSupplier, after coming up with a way of passing certificate stores to Artemis. class FileBasedCertificateStoreSupplier(val path: Path, val storePassword: String, val entryPassword: String) : CertificateStoreSupplier { - override fun get(createNew: Boolean) = CertificateStore.fromFile(path, storePassword, entryPassword, createNew) -} \ No newline at end of file +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt index dad42d37ca..f877f6ffd2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/SslConfiguration.kt @@ -16,7 +16,6 @@ interface SslConfiguration { } interface MutualSslConfiguration : SslConfiguration { - override val keyStore: FileBasedCertificateStoreSupplier override val trustStore: FileBasedCertificateStoreSupplier } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt index bfcf1f6631..01ce3e5b51 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/ContentSignerBuilder.kt @@ -10,8 +10,8 @@ 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. + * Provide extra OID look up for signature algorithm not supported by BouncyCastle. + * This builder will use BouncyCastle's JcaContentSignerBuilder as fallback for unknown algorithm. */ object ContentSignerBuilder { fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.kt new file mode 100644 index 0000000000..77062a6bc0 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.kt @@ -0,0 +1,14 @@ +package net.corda.nodeapi.internal.crypto + +import net.corda.core.crypto.Crypto +import net.corda.nodeapi.internal.crypto.X509Utilities.createSelfSignedCACertificate +import java.math.BigInteger +import javax.security.auth.x500.X500Principal + +/** + * Dummy keys and certificates mainly required when we need to store dummy entries to KeyStores, i.e., as progress + * indicators in node registration. */ +object NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS { + val ECDSAR1_KEYPAIR by lazy { Crypto.deriveKeyPairFromEntropy(Crypto.ECDSA_SECP256R1_SHA256, BigInteger.valueOf(0)) } + val ECDSAR1_CERT by lazy { createSelfSignedCACertificate(X500Principal("CN=DUMMY"), ECDSAR1_KEYPAIR) } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt index f039af0b3d..04703466f6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509KeyStore.kt @@ -7,14 +7,15 @@ import java.nio.file.Path import java.security.KeyPair import java.security.KeyStore import java.security.PrivateKey +import java.security.PublicKey import java.security.cert.X509Certificate /** * Wrapper around a [KeyStore] object but only dealing with [X509Certificate]s and with a better API. */ -class X509KeyStore private constructor(val internal: KeyStore, private val storePassword: String, private val keyStoreFile: Path? = null) { +class X509KeyStore private constructor(val internal: KeyStore, private val storePassword: String, private val keyStoreFile: Path? = null, private val saveSupported: Boolean = true) { /** Wrap an existing [KeyStore]. [save] is not supported. */ - constructor(keyStore: KeyStore, storePassword: String) : this(keyStore, storePassword, null) + constructor(keyStore: KeyStore, storePassword: String) : this(keyStore, storePassword, null, false) /** Create an empty [KeyStore] using the given password. [save] is not supported. */ constructor(storePassword: String) : this( @@ -55,24 +56,32 @@ class X509KeyStore private constructor(val internal: KeyStore, private val store fun getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair { val cert = getCertificate(alias) - val publicKey = Crypto.toSupportedPublicKey(cert.publicKey) + val publicKey = getPublicKey(alias) return CertificateAndKeyPair(cert, KeyPair(publicKey, getPrivateKey(alias, keyPassword))) } + fun getPublicKey(alias: String): PublicKey { + return Crypto.toSupportedPublicKey(getCertificate(alias).publicKey) + } + fun getPrivateKey(alias: String, keyPassword: String): PrivateKey { return internal.getSupportedKey(alias, keyPassword) } fun setPrivateKey(alias: String, key: PrivateKey, certificates: List, keyPassword: String) { internal.setKeyEntry(alias, key, keyPassword.toCharArray(), certificates.toTypedArray()) + save() } fun setCertificate(alias: String, certificate: X509Certificate) { internal.setCertificateEntry(alias, certificate) + save() } fun save() { - internal.save(checkWritableToFile(), storePassword) + if (saveSupported) { + internal.save(checkWritableToFile(), storePassword) + } } fun update(action: X509KeyStore.() -> Unit) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index dafa767440..9a21bfac72 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -301,24 +301,21 @@ object X509Utilities { /** * Create certificate signing request using provided information. */ - private fun createCertificateSigningRequest(subject: X500Principal, - email: String, - keyPair: KeyPair, - signatureScheme: SignatureScheme, - certRole: CertRole): PKCS10CertificationRequest { - val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName)) - return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public) + fun createCertificateSigningRequest(subject: X500Principal, email: String, publicKey: PublicKey, contentSigner: ContentSigner, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest { + return JcaPKCS10CertificationRequestBuilder(subject, publicKey) .addAttribute(BCStyle.E, DERUTF8String(email)) .addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), certRole) - .build(signer).apply { - if (!isSignatureValid()) { - throw SignatureException("The certificate signing request signature validation failed.") - } - } + .build(contentSigner).apply { + if (!isSignatureValid()) { + throw SignatureException("The certificate signing request signature validation failed.") + } + } } fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest { - return createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME, certRole) + val signatureScheme = Crypto.findSignatureScheme(keyPair.public) + val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName)) + return createCertificateSigningRequest(subject, email, keyPair.public, signer, certRole) } fun buildCertPath(first: X509Certificate, remaining: List): CertPath { @@ -356,7 +353,7 @@ object X509Utilities { val CertRole.certificateType: CertificateType get() = CertificateType.values().first { it.role == this } /** - * Convert a [X509Certificate] into Bouncycastle's [X509CertificateHolder]. + * Convert a [X509Certificate] into BouncyCastle's [X509CertificateHolder]. * * NOTE: To avoid unnecessary copying use [X509Certificate] where possible. */ @@ -376,7 +373,7 @@ val Certificate.x509: X509Certificate get() = requireNotNull(this as? X509Certif val Array.x509: List get() = map { it.x509 } /** - * Validates the signature of the CSR + * Validates the signature of the CSR. */ fun PKCS10CertificationRequest.isSignatureValid(): Boolean { return this.isSignatureValid(JcaContentVerifierProviderBuilder().build(this.subjectPublicKeyInfo)) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt new file mode 100644 index 0000000000..e24b53c39d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cryptoservice/CryptoService.kt @@ -0,0 +1,40 @@ +package net.corda.nodeapi.internal.cryptoservice + +import net.corda.core.DoNotImplement +import org.bouncycastle.operator.ContentSigner +import java.security.KeyPair +import java.security.PublicKey + +@DoNotImplement +interface CryptoService { + + /** + * Generate and store a new [KeyPair]. + * Note that schemeNumberID is Corda specific. Cross-check with the network operator for supported schemeNumberID + * and their corresponding signature schemes. The main reason for using schemeNumberID and not algorithm OIDs is + * because some schemes might not be standardised and thus an official OID might for this scheme not exist yet. + * + * Returns the [PublicKey] of the generated [KeyPair]. + */ + fun generateKeyPair(alias: String, schemeNumberID: Int): PublicKey + + /** Check if this [CryptoService] has a private key entry for the input alias. */ + fun containsKey(alias: String): Boolean + + /** + * Returns the [PublicKey] of the input alias or null if it doesn't exist. + */ + fun getPublicKey(alias: String): PublicKey? + + /** + * Sign a [ByteArray] using the private key identified by the input alias. + * Returns the signature bytes whose format depends on the underlying signature scheme and it should + * be Java BouncyCastle compatible (i.e., ASN.1 DER-encoded for ECDSA). + */ + fun sign(alias: String, data: ByteArray): ByteArray + + /** + * Returns [ContentSigner] for the key identified by the input alias. + */ + fun getSigner(alias: String): ContentSigner +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index db539a2c4d..ff94acacae 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -394,7 +394,7 @@ internal constructor(private val initSerEnv: Boolean, notaries = notaryInfos, modifiedTime = Instant.now(), maxMessageSize = 10485760, - maxTransactionSize = 10485760, + maxTransactionSize = 524288000, whitelistedContractImplementations = whitelist, packageOwnership = packageOwnership.filterNotNullValues(), epoch = 1, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AliasPrivateKeyTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AliasPrivateKeyTest.kt new file mode 100644 index 0000000000..93bc1e24f4 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/AliasPrivateKeyTest.kt @@ -0,0 +1,34 @@ +package net.corda.nodeapi.internal.crypto + +import net.corda.core.crypto.internal.AliasPrivateKey +import net.corda.testing.internal.stubs.CertificateStoreStubs +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class AliasPrivateKeyTest { + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + @Test + fun `store AliasPrivateKey entry and cert to keystore`() { + val alias = "01234567890" + val aliasPrivateKey = AliasPrivateKey(alias) + val certificatesDirectory = tempFolder.root.toPath() + val signingCertStore = CertificateStoreStubs.Signing.withCertificatesDirectory(certificatesDirectory, "keystorepass").get(createNew = true) + signingCertStore.query { setPrivateKey(alias, aliasPrivateKey, listOf(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT), "entrypassword") } + // We can retrieve the certificate. + assertTrue { signingCertStore.contains(alias) } + // We can retrieve the certificate. + assertEquals(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT, signingCertStore[alias]) + // Although we can store an AliasPrivateKey, we cannot retrieve it. But, it's fine as we use certStore for storing/handling certs only. + assertFailsWith("Unrecognised algorithm: 2.26.40086077608615255153862931087626791001") { + signingCertStore.query { getPrivateKey(alias, "entrypassword") } + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 2a10fa56c5..47a290f158 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -95,7 +95,7 @@ class NodeRegistrationTest : IntegrationTest() { cordappsForAllNodes = cordappsInCurrentAndAdditionalPackages("net.corda.finance"), notaryCustomOverrides = mapOf("devMode" to false) ) { - val (alice, genevieve) = listOf( + val (alice, genevieve) = listOf( startNode(providedName = aliceName, customOverrides = mapOf("devMode" to false)), startNode(providedName = genevieveName, customOverrides = mapOf("devMode" to false)) ).transpose().getOrThrow() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index f07e5024f0..aa619cb5bc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -9,22 +9,20 @@ import net.corda.confidential.SwapIdentitiesHandler import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.context.InvocationContext +import net.corda.core.crypto.internal.AliasPrivateKey +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isCRLDistributionPointBlacklisted import net.corda.core.crypto.newSecureRandom -import net.corda.core.crypto.sign import net.corda.core.flows.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.NamedCacheFactory -import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.* import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.notary.NotaryService -import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.* import net.corda.core.node.* import net.corda.core.node.services.* @@ -54,8 +52,9 @@ import net.corda.node.services.config.shouldInitCrashShell import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver import net.corda.node.services.identity.PersistentIdentityService +import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.KeyManagementServiceInternal -import net.corda.node.services.keys.PersistentKeyManagementService +import net.corda.node.services.keys.cryptoservice.BCCryptoService import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapClient @@ -73,14 +72,15 @@ import net.corda.node.utilities.* import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX import net.corda.nodeapi.internal.persistence.* -import net.corda.nodeapi.internal.storeLegalIdentity import net.corda.tools.shell.InteractiveShell import org.apache.activemq.artemis.utils.ReusableLatch import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry @@ -165,6 +165,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize() + val cryptoService = configuration.makeCryptoService() val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() @@ -260,6 +261,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun initKeyStores(): X509Certificate { if (configuration.devMode) { configuration.configureWithDevSSLCertificate() + // configureWithDevSSLCertificate is a devMode process that writes directly to keystore files, so + // we should re-synchronise BCCryptoService with the updated keystore file. + if (cryptoService is BCCryptoService) { + cryptoService.resyncKeystore() + } } return validateKeyStores() } @@ -298,7 +304,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.info("Node starting up ...") val trustRoot = initKeyStores() - val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] initialiseJVMAgents() schemaService.mappedSchemasWarnings().forEach { @@ -324,6 +329,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, startDatabase() val (identity, identityKeyPair) = obtainIdentity() + X509Utilities.validateCertPath(trustRoot, identity.certPath) + + val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA] identityService.start(trustRoot, listOf(identity.certificate, nodeCa)) val mutualExclusionConfiguration = configuration.enterpriseConfiguration.mutualExclusionConfiguration @@ -357,6 +365,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, attachments.start() cordappProvider.start(netParams.whitelistedContractImplementations) nodeProperties.start() + // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because + // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with + // the identity key. But the infrastructure to make that easy isn't here yet. keyManagementService.start(keyPairs) val notaryService = makeNotaryService(myNotaryIdentity) installCordaServices(myNotaryIdentity) @@ -434,7 +445,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val nodeInfoFromDb = getPreviousNodeInfoIfPresent(identity) - val nodeInfo = if (potentialNodeInfo == nodeInfoFromDb?.copy(serial = 0)) { // The node info hasn't changed. We use the one from the database to preserve the serial. log.debug("Node-info hasn't changed") @@ -449,7 +459,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val nodeInfoAndSigned = NodeInfoAndSigned(nodeInfo) { publicKey, serialised -> val privateKey = keyPairs.single { it.public == publicKey }.private - privateKey.sign(serialised.bytes) + DigitalSignature(cryptoService.sign((privateKey as AliasPrivateKey).alias, serialised.bytes)) } // Write the node-info file even if nothing's changed, just in case the file has been deleted. @@ -558,7 +568,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.error("${it.name}, as a Corda service, must have a constructor with a single parameter of type " + ServiceHub::class.java.name) } catch (e: ServiceInstantiationException) { - log.error("Corda service ${it.name} failed to instantiate", e.cause) + if (e.cause != null) { + log.error("Corda service ${it.name} failed to instantiate. Reason was: ${e.cause?.rootMessage}", e.cause) + } else { + log.error("Corda service ${it.name} failed to instantiate", e) + } } catch (e: Exception) { log.error("Unable to install Corda service ${it.name}", e) } @@ -664,9 +678,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return try { // The following will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. val sslKeyStore = configuration.p2pSslOptions.keyStore.get() - val identitiesKeyStore = configuration.signingCertificateStore.get() + val signingCertificateStore = configuration.signingCertificateStore.get() val trustStore = configuration.p2pSslOptions.trustStore.get() - AllCertificateStores(trustStore, sslKeyStore, identitiesKeyStore) + AllCertificateStores(trustStore, sslKeyStore, signingCertificateStore) } catch (e: IOException) { log.error("IO exception while trying to validate keystores and truststore", e) null @@ -788,7 +802,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. - return PersistentKeyManagementService(cacheFactory, identityService, database) + return BasicHSMKeyManagementService(cacheFactory,identityService, database, cryptoService) } open fun stop() { @@ -813,50 +827,47 @@ abstract class AbstractNode(val configuration: NodeConfiguration, nodeInfo: NodeInfo, myNotaryIdentity: PartyAndCertificate?, networkParameters: NetworkParameters) - - /** Loads or generates the node's legal identity and key-pair. */ + /** + * Loads or generates the node's legal identity and key-pair. + * Note that obtainIdentity returns a KeyPair with an [AliasPrivateKey]. + */ private fun obtainIdentity(): Pair { - val keyStore = configuration.signingCertificateStore.get() - val legalName = configuration.myLegalName + val legalIdentityPrivateKeyAlias = "$NODE_IDENTITY_ALIAS_PREFIX-private-key" - // TODO: Integrate with Key management service? - val privateKeyAlias = "$NODE_IDENTITY_ALIAS_PREFIX-private-key" - if (privateKeyAlias !in keyStore) { - log.info("$privateKeyAlias not found in key store, generating fresh key!") - keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair()) + if (!cryptoService.containsKey(legalIdentityPrivateKeyAlias)) { + log.info("$legalIdentityPrivateKeyAlias not found in key store, generating fresh key!") + storeLegalIdentity(legalIdentityPrivateKeyAlias) } - - val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) } + val signingCertificateStore = configuration.signingCertificateStore.get() + val x509Cert = signingCertificateStore.query { getCertificate(legalIdentityPrivateKeyAlias) } // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. - val certificates = keyStore.query { getCertificateChain(privateKeyAlias) } + val certificates: List = signingCertificateStore.query { getCertificateChain(legalIdentityPrivateKeyAlias) } check(certificates.first() == x509Cert) { "Certificates from key store do not line up!" } val subject = CordaX500Name.build(certificates.first().subjectX500Principal) + val legalName = configuration.myLegalName if (subject != legalName) { throw ConfigurationException("The name '$legalName' for $NODE_IDENTITY_ALIAS_PREFIX doesn't match what's in the key store: $subject") } - val certPath = X509Utilities.buildCertPath(certificates) - return Pair(PartyAndCertificate(certPath), keyPair) + return getPartyAndCertificatePlusAliasKeyPair(certificates, legalIdentityPrivateKeyAlias) } /** Loads pre-generated notary service cluster identity. */ private fun loadNotaryClusterIdentity(serviceLegalName: CordaX500Name): Pair { - val keyStore = configuration.signingCertificateStore.get() - val privateKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key" - val keyPair = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) }.keyPair - val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key" - val certificates = if (compositeKeyAlias in keyStore) { - val certificate = keyStore[compositeKeyAlias] + + val signingCertificateStore = configuration.signingCertificateStore.get() + val certificates = if (cryptoService.containsKey(compositeKeyAlias)) { + val certificate = signingCertificateStore[compositeKeyAlias] // We have to create the certificate chain for the composite key manually, this is because we don't have a keystore // provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate + // the tail of the private key certificates, as they are both signed by the same certificate chain. - listOf(certificate) + keyStore.query { getCertificateChain(privateKeyAlias) }.drop(1) + listOf(certificate) + signingCertificateStore.query { getCertificateChain(privateKeyAlias) }.drop(1) } else throw IllegalStateException("The identity public key for the notary service $serviceLegalName was not found in the key store.") val subject = CordaX500Name.build(certificates.first().subjectX500Principal) @@ -864,11 +875,43 @@ abstract class AbstractNode(val configuration: NodeConfiguration, throw ConfigurationException("The name of the notary service '$serviceLegalName' for $DISTRIBUTED_NOTARY_ALIAS_PREFIX doesn't " + "match what's in the key store: $subject. You might need to adjust the configuration of `notary.serviceLegalName`.") } + return getPartyAndCertificatePlusAliasKeyPair(certificates, privateKeyAlias) + } + + // Method to create a Pair, where KeyPair uses an AliasPrivateKey. + private fun getPartyAndCertificatePlusAliasKeyPair(certificates: List, privateKeyAlias: String): Pair { val certPath = X509Utilities.buildCertPath(certificates) + val keyPair = KeyPair(cryptoService.getPublicKey(privateKeyAlias), AliasPrivateKey(privateKeyAlias)) return Pair(PartyAndCertificate(certPath), keyPair) } - protected open fun generateKeyPair() = cryptoGenerateKeyPair() + private fun storeLegalIdentity(alias: String): PartyAndCertificate { + val legalIdentityPublicKey = generateKeyPair(alias) + val signingCertificateStore = configuration.signingCertificateStore.get() + + val nodeCaCertPath = signingCertificateStore.value.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + val nodeCaCert = nodeCaCertPath[0] // This should be the same with signingCertificateStore[alias] + + val identityCert = X509Utilities.createCertificate( + CertificateType.LEGAL_IDENTITY, + nodeCaCert.subjectX500Principal, + nodeCaCert.publicKey, + cryptoService.getSigner(X509Utilities.CORDA_CLIENT_CA), + nodeCaCert.subjectX500Principal, + legalIdentityPublicKey, + // TODO this might be smaller than DEFAULT_VALIDITY_WINDOW, shall we strictly apply DEFAULT_VALIDITY_WINDOW? + X509Utilities.getCertificateValidityWindow( + DEFAULT_VALIDITY_WINDOW.first, + DEFAULT_VALIDITY_WINDOW.second, + nodeCaCert) + ) + + val identityCertPath = listOf(identityCert) + nodeCaCertPath + signingCertificateStore.setCertPathOnly(alias, identityCertPath) + return PartyAndCertificate(X509Utilities.buildCertPath(identityCertPath)) + } + + protected open fun generateKeyPair(alias: String) = cryptoService.generateKeyPair(alias, X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME.schemeNumberID) protected open fun makeVaultService(keyManagementService: KeyManagementService, services: ServicesForResolution, diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt index 2f550be957..ca046ea199 100644 --- a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt @@ -71,7 +71,7 @@ class InitialRegistration(val baseDirectory: Path, private val networkRootTrustS HTTPNetworkRegistrationService( requireNotNull(conf.networkServices), versionInfo), - nodeRegistration).buildKeystore() + nodeRegistration).generateKeysAndRegister() // Minimal changes to make registration tool create node identity. // TODO: Move node identity generation logic from node to registration helper. @@ -107,4 +107,3 @@ class InitialRegistration(val baseDirectory: Path, private val networkRootTrustS initialRegistration(node.configuration) } } - diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 95b4e27c44..e31d46626e 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -82,10 +82,11 @@ object ConfigHelper { * Strictly for dev only automatically construct a server certificate/private key signed from * the CA certs in Node resources. Then provision KeyStores into certificates folder under node path. */ -// TODO Move this to KeyStoreConfigHelpers +// TODO Move this to KeyStoreConfigHelpers. +// TODO consider taking CryptoService as an input. fun NodeConfiguration.configureWithDevSSLCertificate() = p2pSslOptions.configureDevKeyAndTrustStores(myLegalName, signingCertificateStore, certificatesDirectory) -// TODO Move this to KeyStoreConfigHelpers +// TODO Move this to KeyStoreConfigHelpers. fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path) { val specifiedTrustStore = trustStore.getOptional() @@ -100,7 +101,7 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N loadDevCaTrustStore().copyTo(trustStore.get(true)) } - if (keyStore.getOptional() == null || signingCertificateStore.getOptional() == null) { + if (specifiedKeyStore == null || specifiedSigningStore == null) { val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.storePassword, signingCertificateStore.entryPassword).get(true).also { it.registerDevSigningCertificates(myLegalName) } FileBasedCertificateStoreSupplier(keyStore.path, keyStore.storePassword, keyStore.entryPassword).get(true).also { it.registerDevP2pCertificates(myLegalName) } diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 6e57e50669..25727f6695 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -10,11 +10,14 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds import net.corda.node.services.config.rpc.NodeRpcOptions +import net.corda.node.services.keys.cryptoservice.BCCryptoService +import net.corda.node.services.keys.cryptoservice.SupportedCryptoServices import net.corda.nodeapi.BrokerRpcSslOptions +import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.nodeapi.internal.config.* +import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.tools.shell.SSHDConfiguration import org.slf4j.Logger import java.net.URL @@ -79,6 +82,8 @@ interface NodeConfiguration { val baseDirectory: Path val certificatesDirectory: Path + // signingCertificateStore is used to store certificate chains. + // However, BCCryptoService is reusing this to store keys as well. val signingCertificateStore: FileBasedCertificateStoreSupplier val p2pSslOptions: MutualSslConfiguration @@ -88,6 +93,11 @@ interface NodeConfiguration { val cordappSignerKeyFingerprintBlacklist: List + // TODO At the moment this is just an identifier for the desired CryptoService engine. Consider using a classname to + // to allow for pluggable implementations. + val cryptoServiceName: SupportedCryptoServices? + val cryptoServiceConf: String? // Location for the cryptoService conf file. + fun validate(): List companion object { @@ -106,6 +116,13 @@ interface NodeConfiguration { val defaultJmxReporterType = JmxReporterType.JOLOKIA } + + fun makeCryptoService(): CryptoService { + return when(cryptoServiceName) { + SupportedCryptoServices.BC_SIMPLE -> BCCryptoService(this) + null -> BCCryptoService(this) // Pick default BCCryptoService when null. + } + } } data class FlowOverrideConfig(val overrides: List = listOf()) @@ -240,11 +257,13 @@ data class NodeConfigurationImpl( override val enableSNI: Boolean = true, private val useOpenSsl: Boolean = false, override val flowOverrides: FlowOverrideConfig?, - override val cordappSignerKeyFingerprintBlacklist: List = DEV_PUB_KEY_HASHES.map { it.toString() } + override val cordappSignerKeyFingerprintBlacklist: List = DEV_PUB_KEY_HASHES.map { it.toString() }, + override val cryptoServiceName: SupportedCryptoServices? = null, + override val cryptoServiceConf: String? = null ) : NodeConfiguration { companion object { private val logger = loggerFor() - + // private val supportedCryptoServiceNames = setOf("BC", "UTIMACO", "GEMALTO-LUNA", "AZURE-KEY-VAULT") } private val actualRpcSettings: NodeRpcSettings @@ -297,6 +316,14 @@ data class NodeConfigurationImpl( return errors } + private fun validateCryptoService(): List { + val errors = mutableListOf() + if (cryptoServiceName == null && cryptoServiceConf != null) { + errors += "cryptoServiceName is null, but cryptoServiceConf is set to $cryptoServiceConf" + } + return errors + } + override fun validate(): List { val errors = mutableListOf() errors += validateDevModeOptions() @@ -309,6 +336,7 @@ data class NodeConfigurationImpl( errors += validateTlsCertCrlConfig() errors += validateNetworkServices() errors += validateH2Settings() + errors += validateCryptoService() return errors } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt new file mode 100644 index 0000000000..17df59ddb3 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/keys/BasicHSMKeyManagementService.kt @@ -0,0 +1,155 @@ +package net.corda.node.services.keys + +import net.corda.core.crypto.* +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.serialize +import net.corda.core.utilities.MAX_HASH_HEX_SIZE +import net.corda.node.services.identity.PersistentIdentityService +import net.corda.core.crypto.internal.AliasPrivateKey +import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX +import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY +import org.bouncycastle.operator.ContentSigner +import java.security.KeyPair +import java.security.PrivateKey +import java.security.PublicKey +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Lob + +/** + * A persistent re-implementation of [E2ETestKeyManagementService] to support CryptoService for initial keys and + * database storage for anonymous fresh keys. + * + * This is not the long-term implementation. See the list of items in the above class. + * + * This class needs database transactions to be in-flight during method calls and init. + */ +class BasicHSMKeyManagementService(cacheFactory: NamedCacheFactory, val identityService: PersistentIdentityService, + private val database: CordaPersistence, private val cryptoService: CryptoService) : SingletonSerializeAsToken(), KeyManagementServiceInternal { + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}our_key_pairs") + class PersistentKey( + + @Id + @Column(name = "public_key_hash", length = MAX_HASH_HEX_SIZE, nullable = false) + var publicKeyHash: String, + + @Lob + @Column(name = "public_key", nullable = false) + var publicKey: ByteArray = EMPTY_BYTE_ARRAY, + @Lob + @Column(name = "private_key", nullable = false) + var privateKey: ByteArray = EMPTY_BYTE_ARRAY + ) { + constructor(publicKey: PublicKey, privateKey: PrivateKey) + : this(publicKey.toStringShort(), publicKey.encoded, privateKey.encoded) + } + + private companion object { + fun createKeyMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + cacheFactory = cacheFactory, + name = "BasicHSMKeyManagementService_keys", + toPersistentEntityKey = { it.toStringShort() }, + fromPersistentEntity = { Pair(Crypto.decodePublicKey(it.publicKey), Crypto.decodePrivateKey( + it.privateKey)) }, + toPersistentEntity = { key: PublicKey, value: PrivateKey -> + PersistentKey(key, value) + }, + persistentEntityClass = PersistentKey::class.java + ) + } + } + + // Maintain a map from PublicKey to alias for the initial keys. + private val originalKeysMap = mutableMapOf() + // A map for anonymous keys. + private val keysMap = createKeyMap(cacheFactory) + + override fun start(initialKeyPairs: Set) { + initialKeyPairs.forEach { + require(it.private is AliasPrivateKey) { "${this.javaClass.name} supports AliasPrivateKeys only, but ${it.private.algorithm} key was found" } + originalKeysMap[Crypto.toSupportedPublicKey(it.public)] = (it.private as AliasPrivateKey).alias + } + } + + override val keys: Set get() = database.transaction { originalKeysMap.keys.plus(keysMap.allPersisted().map { it.first }.toSet()) } + + private fun containsPublicKey(publicKey: PublicKey): Boolean { + return (publicKey in originalKeysMap || publicKey in keysMap) + } + + override fun filterMyKeys(candidateKeys: Iterable): Iterable = database.transaction { + identityService.stripCachedPeerKeys(candidateKeys).filter { containsPublicKey(it) } // TODO: bulk cache access. + } + + // Unlike initial keys, freshkey() is related confidential keys and it utilises platform's software key generation + // thus, without using [cryptoService]). + override fun freshKey(): PublicKey { + val keyPair = generateKeyPair() + database.transaction { + keysMap[keyPair.public] = keyPair.private + } + return keyPair.public + } + + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): PartyAndCertificate { + return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey)) + } + + private fun getSigner(publicKey: PublicKey): ContentSigner { + val signingPublicKey = getSigningPublicKey(publicKey) + return if (signingPublicKey in originalKeysMap) { + cryptoService.getSigner(originalKeysMap[signingPublicKey]!!) + } else { + getSigner(getSigningKeyPair(signingPublicKey)) + } + } + + // Get [KeyPair] for the input [publicKey]. This is used for fresh keys, in which we have access to the private key material. + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { + return database.transaction { + KeyPair(publicKey, keysMap[publicKey]!!) + } + } + + // It looks for the PublicKey in the (potentially) CompositeKey that is ours. + // TODO what if we own two or more leaves of a CompositeKey? + private fun getSigningPublicKey(publicKey: PublicKey): PublicKey { + return publicKey.keys.first { containsPublicKey(it) } + } + + override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { + val signingPublicKey = getSigningPublicKey(publicKey) + return if (signingPublicKey in originalKeysMap) { + DigitalSignature.WithKey(signingPublicKey, cryptoService.sign(originalKeysMap[signingPublicKey]!!, bytes)) + } else { + val keyPair = getSigningKeyPair(signingPublicKey) + keyPair.sign(bytes) + } + } + + // TODO: A full KeyManagementService implementation needs to record activity to the Audit Service and to limit + // signing to appropriately authorised contexts and initiating users. + override fun sign(signableData: SignableData, publicKey: PublicKey): TransactionSignature { + val signingPublicKey = getSigningPublicKey(publicKey) + return if (signingPublicKey in originalKeysMap) { + val sigKey: SignatureScheme = Crypto.findSignatureScheme(signingPublicKey) + val sigMetaData: SignatureScheme = Crypto.findSignatureScheme(signableData.signatureMetadata.schemeNumberID) + require(sigKey == sigMetaData || sigMetaData == Crypto.COMPOSITE_KEY) { + "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}." + } + val signatureBytes = cryptoService.sign(originalKeysMap[signingPublicKey]!!, signableData.serialize().bytes) + TransactionSignature(signatureBytes, signingPublicKey, signableData.signatureMetadata) + } else { + val keyPair = getSigningKeyPair(signingPublicKey) + keyPair.sign(signableData) + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index c99732a356..c33d28e642 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -5,6 +5,9 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.ThreadBox import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.crypto.internal.AliasPrivateKey +import net.corda.node.services.keys.cryptoservice.BCCryptoService +import net.corda.nodeapi.internal.cryptoservice.CryptoService import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey @@ -24,7 +27,7 @@ import javax.annotation.concurrent.ThreadSafe * etc. */ @ThreadSafe -class E2ETestKeyManagementService(val identityService: IdentityService) : SingletonSerializeAsToken(), KeyManagementServiceInternal { +class E2ETestKeyManagementService(val identityService: IdentityService, private val cryptoService: CryptoService? = null) : SingletonSerializeAsToken(), KeyManagementServiceInternal { private class InnerState { val keys = HashMap() } @@ -32,12 +35,20 @@ class E2ETestKeyManagementService(val identityService: IdentityService) : Single private val mutex = ThreadBox(InnerState()) // Accessing this map clones it. override val keys: Set get() = mutex.locked { keys.keys } + // Maintain a map from PublicKey to alias for the initial keys. + val keyPairs: Set get() = mutex.locked { keys.map { KeyPair(it.key, it.value) }.toSet() } override fun start(initialKeyPairs: Set) { mutex.locked { for (key in initialKeyPairs) { - keys[key.public] = key.private + var privateKey = key.private + if (privateKey is AliasPrivateKey && cryptoService is BCCryptoService) { + privateKey = cryptoService.certificateStore.query { + getPrivateKey((privateKey as AliasPrivateKey).alias, cryptoService.certificateStore.entryPassword) + } + } + keys[key.public] = privateKey } } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt b/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt new file mode 100644 index 0000000000..744b6a15d1 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/BCCryptoService.kt @@ -0,0 +1,66 @@ +package net.corda.node.services.keys.cryptoservice + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.newSecureRandom +import net.corda.node.services.config.NodeConfiguration +import net.corda.nodeapi.internal.config.CertificateStore +import net.corda.nodeapi.internal.crypto.ContentSignerBuilder +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.cryptoservice.CryptoService +import org.bouncycastle.operator.ContentSigner +import java.security.KeyPair +import java.security.KeyStore +import java.security.PublicKey + +/** + * Basic implementation of a [CryptoService] that uses BouncyCastle for cryptographic operations + * and a Java KeyStore in the form of [CertificateStore] to store private keys. + * This service reuses the [NodeConfiguration.signingCertificateStore] to store keys. + */ +class BCCryptoService(private val nodeConf: NodeConfiguration) : CryptoService { + + // TODO check if keyStore exists. + // TODO make it private when E2ETestKeyManagementService does not require direct access to the private key. + internal var certificateStore: CertificateStore = nodeConf.signingCertificateStore.get(true) + + override fun generateKeyPair(alias: String, schemeNumberID: Int): PublicKey { + val keyPair = Crypto.generateKeyPair(Crypto.findSignatureScheme(schemeNumberID)) + importKey(alias, keyPair) + return keyPair.public + } + + override fun containsKey(alias: String): Boolean { + return certificateStore.contains(alias) + } + + override fun getPublicKey(alias: String): PublicKey { + return certificateStore.query { getPublicKey(alias) } + } + + override fun sign(alias: String, data: ByteArray): ByteArray { + return Crypto.doSign(certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) } , data) + } + + override fun getSigner(alias: String): ContentSigner { + val privateKey = certificateStore.query { getPrivateKey(alias, certificateStore.entryPassword) } + val signatureScheme = Crypto.findSignatureScheme(privateKey) + return ContentSignerBuilder.build(signatureScheme, privateKey, Crypto.findProvider(signatureScheme.providerName), newSecureRandom()) + } + + /** + * If a node is running in [NodeConfiguration.devMode] and for backwards compatibility purposes, the same [KeyStore] + * is reused outside [BCCryptoService] to update certificate paths. [resyncKeystore] will sync [BCCryptoService]'s + * loaded [certificateStore] in memory with the contents of the corresponding [KeyStore] file. + */ + fun resyncKeystore() { + certificateStore = nodeConf.signingCertificateStore.get(true) + } + + /** Import an already existing [KeyPair] to this [CryptoService]. */ + fun importKey(alias: String, keyPair: KeyPair) { + // Store a self-signed certificate, as Keystore requires to store certificates instead of public keys. + // We could probably add a null cert, but we store a self-signed cert that will be used to retrieve the public key. + val cert = X509Utilities.createSelfSignedCACertificate(nodeConf.myLegalName.x500Principal, keyPair) + certificateStore.query { setPrivateKey(alias, keyPair.private, listOf(cert), certificateStore.entryPassword) } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/SupportedCryptoServices.kt b/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/SupportedCryptoServices.kt new file mode 100644 index 0000000000..b76b25cdc1 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/keys/cryptoservice/SupportedCryptoServices.kt @@ -0,0 +1,9 @@ +package net.corda.node.services.keys.cryptoservice + +enum class SupportedCryptoServices { + /** Identifier for [BCCryptoService]. */ + BC_SIMPLE + // UTIMACO, // Utimaco HSM. + // GEMALTO_LUNA, // Gemalto Luna HSM. + // AZURE_KV // Azure key Vault. +} diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 842af5589c..344ab468c1 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -12,6 +12,7 @@ import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService.SchemaOptions import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.identity.PersistentIdentityService +import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.messaging.P2PMessageDeduplicator import net.corda.node.services.persistence.DBCheckpointStorage @@ -35,6 +36,7 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() object NodeCoreV1 : MappedSchema(schemaFamily = NodeCore.javaClass, version = 1, mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java, DBTransactionStorage.DBTransaction::class.java, + BasicHSMKeyManagementService.PersistentKey::class.java, PersistentKeyManagementService.PersistentKey::class.java, NodeSchedulerService.PersistentScheduledState::class.java, NodeAttachmentService.DBAttachment::class.java, diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index 5c2b9a1241..11c01ccfd1 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -56,6 +56,7 @@ open class DefaultNamedCacheFactory protected constructor(private val metricRegi name == "DeduplicationChecker_watermark" -> caffeine name == "BFTNonValidatingNotaryService_transactions" -> caffeine.maximumSize(defaultCacheSize) name == "RaftUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize) + name == "BasicHSMKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize) else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index e8f916dbb7..06b429b194 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -1,28 +1,31 @@ package net.corda.node.utilities.registration +import net.corda.core.crypto.internal.AliasPrivateKey import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.utilities.contextLogger import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.keys.cryptoservice.BCCryptoService import net.corda.nodeapi.internal.config.CertificateStore -import net.corda.nodeapi.internal.config.CertificateStoreSupplier import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA +import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_VALIDITY_WINDOW import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.openssl.jcajce.JcaPEMWriter +import org.bouncycastle.operator.ContentSigner import org.bouncycastle.util.io.pem.PemObject import java.io.IOException import java.io.StringWriter import java.net.ConnectException import java.nio.file.Path import java.security.KeyPair -import java.security.KeyStore import java.security.PublicKey import java.security.cert.X509Certificate import java.time.Duration @@ -33,23 +36,25 @@ import javax.security.auth.x500.X500Principal * Helper for managing the node registration process, which checks for any existing certificates and requests them if * needed. */ -// TODO: Use content signer instead of keypairs. -open class NetworkRegistrationHelper(private val certificatesDirectory: Path, - private val signingCertificateStore: CertificateStoreSupplier, - private val myLegalName: CordaX500Name, - private val emailAddress: String, - private val certService: NetworkRegistrationService, - private val networkRootTrustStorePath: Path, - networkRootTrustStorePassword: String, - private val keyAlias: String, - private val certRole: CertRole, - private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) { +open class NetworkRegistrationHelper( + config: NodeConfiguration, + private val certService: NetworkRegistrationService, + private val networkRootTrustStorePath: Path, + networkRootTrustStorePassword: String, + private val nodeCaKeyAlias: String, + private val certRole: CertRole, + private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1)) +) { companion object { const val SELF_SIGNED_PRIVATE_KEY = "SelfSignedPrivateKey" val logger = contextLogger() } - + private val certificatesDirectory: Path = config.certificatesDirectory + private val myLegalName: CordaX500Name = config.myLegalName + private val emailAddress: String = config.emailAddress + private val cryptoService = config.makeCryptoService() + private val certificateStore = config.signingCertificateStore.get(true) private val requestIdStore = certificatesDirectory / "certificate-request-id.txt" protected val rootTrustStore: X509KeyStore protected val rootCert: X509Certificate @@ -64,57 +69,67 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, } /** - * Ensure the initial keystore for a node is set up. + * Ensure the initial keys and certificates for a node are set up. * * This checks the "config.certificatesDirectory" field for certificates required to connect to a Corda network. * If the certificates are not found, a PKCS #10 certification request will be submitted to the * Corda network permissioning server via [NetworkRegistrationService]. This process will enter a polling loop until - * the request has been approved, and then the certificate chain will be downloaded and stored in [KeyStore] reside in - * the certificates directory. + * the request has been approved, and then the certificate chain will be downloaded and stored in [certificateStore]. * * @throws CertificateRequestException if the certificate retrieved by doorman is invalid. */ - fun buildKeystore() { + fun generateKeysAndRegister() { certificatesDirectory.createDirectories() - val nodeKeyStore = signingCertificateStore.get(createNew = true) - if (keyAlias in nodeKeyStore) { + // We need this in case cryptoService and certificateStore share the same KeyStore (for backwards compatibility purposes). + // If we didn't, then an update to cryptoService wouldn't be reflected to certificateStore that is already loaded in memory. + val certStore: CertificateStore = if (cryptoService is BCCryptoService) cryptoService.certificateStore else certificateStore + + // SELF_SIGNED_PRIVATE_KEY is used as progress indicator. + if (certStore.contains(nodeCaKeyAlias) && !certStore.contains(SELF_SIGNED_PRIVATE_KEY)) { println("Certificate already exists, Corda node will now terminate...") return } - // TODO: Use different password for private key. - val privateKeyPassword = nodeKeyStore.password + + val tlsCrlIssuerCert = getTlsCrlIssuerCert() + + // We use SELF_SIGNED_PRIVATE_KEY as progress indicator so we just store a dummy key and cert. + // When registration succeeds, this entry should be deleted. + certStore.query { setPrivateKey(SELF_SIGNED_PRIVATE_KEY, AliasPrivateKey(SELF_SIGNED_PRIVATE_KEY), listOf(NOT_YET_REGISTERED_MARKER_KEYS_AND_CERTS.ECDSAR1_CERT), certificateStore.entryPassword) } + + val nodeCaPublicKey = loadOrGenerateKeyPair() + + val requestId = submitOrResumeCertificateSigningRequest(nodeCaPublicKey, cryptoService.getSigner(nodeCaKeyAlias)) + + val nodeCaCertificates = pollServerForCertificates(requestId) + validateCertificates(nodeCaPublicKey, nodeCaCertificates) + + certStore.setCertPathOnly(nodeCaKeyAlias, nodeCaCertificates) + certStore.value.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + certStore.value.save() + println("Private key '$nodeCaKeyAlias' and its certificate-chain stored successfully.") + + onSuccess(nodeCaPublicKey, cryptoService.getSigner(nodeCaKeyAlias), nodeCaCertificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name()) + // All done, clean up temp files. + requestIdStore.deleteIfExists() + } + + private fun loadOrGenerateKeyPair(): PublicKey { + return if (cryptoService.containsKey(nodeCaKeyAlias)) { + cryptoService.getPublicKey(nodeCaKeyAlias)!! + } else { + cryptoService.generateKeyPair(nodeCaKeyAlias, X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME.schemeNumberID) + } + } + + private fun getTlsCrlIssuerCert(): X509Certificate? { val tlsCrlIssuerCert = validateAndGetTlsCrlIssuerCert() if (tlsCrlIssuerCert == null && isTlsCrlIssuerCertRequired()) { System.err.println("""tlsCrlIssuerCert config does not match the root certificate issuer and nor is there any other certificate in the trust store with a matching issuer. - | Please make sure the config is correct or that the correct certificate for the CRL issuer is added to the node's trust store. - | The node will now terminate.""".trimMargin()) + | Please make sure the config is correct or that the correct certificate for the CRL issuer is added to the node's trust store. + | The node will now terminate.""".trimMargin()) throw IllegalArgumentException("TLS CRL issuer certificate not found in the trust store.") } - - val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) - - val requestId = try { - submitOrResumeCertificateSigningRequest(keyPair) - } catch (e: Exception) { - throw if (e is ConnectException || e is ServiceUnavailableException || e is IOException) { - NodeRegistrationException(e.message, e) - } else e - } - - val certificates = try { - pollServerForCertificates(requestId) - } catch (certificateRequestException: CertificateRequestException) { - System.err.println(certificateRequestException.message) - System.err.println("Please make sure the details in configuration file are correct and try again.") - System.err.println("Corda node will now terminate.") - requestIdStore.deleteIfExists() - throw certificateRequestException - } - validateCertificates(keyPair.public, certificates) - storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias, privateKeyPassword) - onSuccess(keyPair, certificates, tlsCrlIssuerCert?.subjectX500Principal?.toX500Name()) - // All done, clean up temp files. - requestIdStore.deleteIfExists() + return tlsCrlIssuerCert } private fun validateCertificates(registeringPublicKey: PublicKey, certificates: List) { @@ -149,18 +164,6 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, println("Certificate signing request approved, storing private key with the certificate chain.") } - private fun storePrivateKeyWithCertificates(nodeKeystore: CertificateStore, keyPair: KeyPair, certificates: List, keyAlias: String, keyPassword: String) { - // Save private key and certificate chain to the key store. - with(nodeKeystore.value) { - setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = keyPassword) - // The key was temporarily stored as SELF_SIGNED_PRIVATE_KEY, but now that it's signed by the Doorman we - // can delete this old record. - internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - save() - } - println("Private key '$keyAlias' and certificate stored in node signing keystore.") - } - private fun CertificateStore.loadOrCreateKeyPair(alias: String, entryPassword: String = password): KeyPair { // Create or load self signed keypair from the key store. // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. @@ -184,65 +187,80 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, * @return List of certificate chain. */ private fun pollServerForCertificates(requestId: String): List { - println("Start polling server for certificate signing approval.") - // Poll server to download the signed certificate once request has been approved. - var idlePeriodDuration: Duration? = null - while (true) { - try { - val (pollInterval, certificates) = certService.retrieveCertificates(requestId) - if (certificates != null) { - return certificates - } - Thread.sleep(pollInterval.toMillis()) - } catch (e: ServiceUnavailableException) { - idlePeriodDuration = nextIdleDuration(idlePeriodDuration) - if (idlePeriodDuration != null) { - Thread.sleep(idlePeriodDuration.toMillis()) - } else { - throw NodeRegistrationException("Compatibility Zone registration service is currently unavailable, " - + "try again later!.", e) + try { + println("Start polling server for certificate signing approval.") + // Poll server to download the signed certificate once request has been approved. + var idlePeriodDuration: Duration? = null + while (true) { + try { + val (pollInterval, certificates) = certService.retrieveCertificates(requestId) + if (certificates != null) { + return certificates + } + Thread.sleep(pollInterval.toMillis()) + } catch (e: ServiceUnavailableException) { + idlePeriodDuration = nextIdleDuration(idlePeriodDuration) + if (idlePeriodDuration != null) { + Thread.sleep(idlePeriodDuration.toMillis()) + } else { + throw NodeRegistrationException("Compatibility Zone registration service is currently unavailable, " + + "try again later!.", e) + } } } + } catch (certificateRequestException: CertificateRequestException) { + System.err.println(certificateRequestException.message) + System.err.println("Please make sure the details in configuration file are correct and try again.") + System.err.println("Corda node will now terminate.") + requestIdStore.deleteIfExists() + throw certificateRequestException } } /** - * Submit Certificate Signing Request to Certificate signing service if request ID not found in file system + * Submit Certificate Signing Request to Certificate signing service if request ID not found in file system. * New request ID will be stored in requestId.txt - * @param keyPair Public Private key pair generated for SSL certification. + * @param publicKey public key for which we need a certificate. + * @param contentSigner the [ContentSigner] that will sign the CSR. * @return Request ID return from the server. */ - private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String { - // Retrieve request id from file if exists, else post a request to server. - return if (!requestIdStore.exists()) { - val request = X509Utilities.createCertificateSigningRequest(myLegalName.x500Principal, emailAddress, keyPair, certRole) - val writer = StringWriter() - JcaPEMWriter(writer).use { - it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded)) + private fun submitOrResumeCertificateSigningRequest(publicKey: PublicKey, contentSigner: ContentSigner): String { + try { + // Retrieve request id from file if exists, else post a request to server. + return if (!requestIdStore.exists()) { + val request = X509Utilities.createCertificateSigningRequest(myLegalName.x500Principal, emailAddress, publicKey, contentSigner, certRole) + val writer = StringWriter() + JcaPEMWriter(writer).use { + it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded)) + } + println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.") + println() + println("Legal Name: $myLegalName") + println("Email: $emailAddress") + println() + println("Public Key: $publicKey") + println() + println("$writer") + // Post request to signing server via http. + println("Submitting certificate signing request to Corda certificate signing server.") + val requestId = certService.submitRequest(request) + // Persists request ID to file in case of node shutdown. + requestIdStore.writeLines(listOf(requestId)) + println("Successfully submitted request to Corda certificate signing server, request ID: $requestId.") + requestId + } else { + val requestId = requestIdStore.readLines { it.findFirst().get() } + println("Resuming from previous certificate signing request, request ID: $requestId.") + requestId } - println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.") - println() - println("Legal Name: $myLegalName") - println("Email: $emailAddress") - println() - println("Public Key: ${keyPair.public}") - println() - println("$writer") - // Post request to signing server via http. - println("Submitting certificate signing request to Corda certificate signing server.") - val requestId = certService.submitRequest(request) - // Persists request ID to file in case of node shutdown. - requestIdStore.writeLines(listOf(requestId)) - println("Successfully submitted request to Corda certificate signing server, request ID: $requestId.") - requestId - } else { - val requestId = requestIdStore.readLines { it.findFirst().get() } - println("Resuming from previous certificate signing request, request ID: $requestId.") - requestId + } catch (e: Exception) { + throw if (e is ConnectException || e is ServiceUnavailableException || e is IOException) { + NodeRegistrationException(e.message, e) + } else e } } - protected open fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List, tlsCrlCertificateIssuer: X500Name?) {} + protected open fun onSuccess(publicKey: PublicKey, contentSigner: ContentSigner, certificates: List, tlsCrlCertificateIssuer: X500Name?) {} protected open fun validateAndGetTlsCrlIssuerCert(): X509Certificate? = null @@ -256,14 +274,9 @@ class NodeRegistrationException( class NodeRegistrationHelper( private val config: NodeConfiguration, - certService: NetworkRegistrationService, - regConfig: NodeRegistrationOption, - computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1)) -) : NetworkRegistrationHelper( - config.certificatesDirectory, - config.signingCertificateStore, - config.myLegalName, - config.emailAddress, + certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) : + NetworkRegistrationHelper( + config, certService, regConfig.networkRootTrustStorePath, regConfig.networkRootTrustStorePassword, @@ -275,29 +288,38 @@ class NodeRegistrationHelper( val logger = contextLogger() } - override fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List, tlsCrlCertificateIssuer: X500Name?) { - createSSLKeystore(nodeCAKeyPair, certificates, tlsCrlCertificateIssuer) + override fun onSuccess(publicKey: PublicKey, contentSigner: ContentSigner, certificates: List, tlsCrlCertificateIssuer: X500Name?) { + createSSLKeystore(publicKey, contentSigner, certificates, tlsCrlCertificateIssuer) createTruststore(certificates.last()) } - private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List, tlsCertCrlIssuer: X500Name?) { + private fun createSSLKeystore(nodeCaPublicKey: PublicKey, nodeCaContentSigner: ContentSigner, nodeCaCertificateChain: List, tlsCertCrlIssuer: X500Name?) { val keyStore = config.p2pSslOptions.keyStore val certificateStore = keyStore.get(createNew = true) certificateStore.update { println("Generating SSL certificate for node messaging service.") val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val issuerCertificate = nodeCaCertificateChain.first() + val validityWindow = X509Utilities.getCertificateValidityWindow(DEFAULT_VALIDITY_WINDOW.first, DEFAULT_VALIDITY_WINDOW.second, issuerCertificate) + val sslCert = X509Utilities.createCertificate( CertificateType.TLS, - certificates.first(), - nodeCAKeyPair, + issuerCertificate.subjectX500Principal, + nodeCaPublicKey, + nodeCaContentSigner, config.myLegalName.x500Principal, sslKeyPair.public, + validityWindow, crlDistPoint = config.tlsCertCrlDistPoint?.toString(), crlIssuer = tlsCertCrlIssuer) + logger.info("Generated TLS certificate: $sslCert") - setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates, certificateStore.entryPassword) + + val sslCertificateChain: List = listOf(sslCert) + nodeCaCertificateChain + X509Utilities.validateCertificateChain(rootCert, sslCertificateChain) + setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, sslCertificateChain, keyStore.entryPassword) } - println("SSL private key and certificate stored in ${keyStore.path}.") + println("SSL private key and certificate chain stored in ${keyStore.path}.") } private fun createTruststore(rootCertificate: X509Certificate) { @@ -365,4 +387,4 @@ private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: return if (counter-- > 0) period else null } } -} \ No newline at end of file +} diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index 3544ee5e85..09faa02886 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -76,19 +76,6 @@ class NetworkParametersTest { } } - @Test - fun `maxTransactionSize must be bigger than maxMesssageSize`() { - assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { - NetworkParameters(1, - emptyList(), - 2000, - 2001, - Instant.now(), - 1, - emptyMap()) - }.withMessage("maxTransactionSize cannot be bigger than maxMessageSize") - } - @Test fun `package ownership checks are correct`() { val key1 = generateKeyPair().public diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index ac22fe3696..aef48b5558 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -139,12 +139,11 @@ class NodeConfigurationImplTest { private fun getConfig(cfgName: String, overrides: Config = ConfigFactory.empty()): Config { val path = this::class.java.classLoader.getResource(cfgName).toPath() - val cfg = ConfigHelper.loadConfig( + return ConfigHelper.loadConfig( baseDirectory = path.parent, configFile = path, configOverrides = overrides ) - return cfg } @Test @@ -214,6 +213,24 @@ class NodeConfigurationImplTest { } } + @Test + fun `validation has error on non-null cryptoServiceConf for null cryptoServiceName`() { + val configuration = testConfiguration.copy(cryptoServiceConf = "unsupported.conf") + + val errors = configuration.validate() + + assertThat(errors).hasOnlyOneElementSatisfying { + error -> error.contains("cryptoServiceName is null, but cryptoServiceConf is set to unsupported.conf") + } + } + + @Test + fun `fail on wrong cryptoServiceName`() { + var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false)) + rawConfig = rawConfig.withValue("cryptoServiceName", ConfigValueFactory.fromAnyRef("UNSUPPORTED")) + assertThatThrownBy { rawConfig.parseAsNodeConfiguration() }.hasMessageStartingWith("UNSUPPORTED is not one of") + } + @Test fun `rpcAddress and rpcSettings_address are equivalent`() { var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false)) @@ -247,7 +264,7 @@ class NodeConfigurationImplTest { @Test fun `jmxReporterType is null and defaults to Jokolia`() { - var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true))) + val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true))) val nodeConfig = rawConfig.parseAsNodeConfiguration() assertTrue(JmxReporterType.JOLOKIA.toString() == nodeConfig.jmxReporterType.toString()) } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 9e12b9b60e..292fec8a2d 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -35,6 +35,7 @@ import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.junit.* import java.io.ByteArrayOutputStream +import java.io.Closeable import java.io.OutputStream import java.net.URI import java.nio.charset.StandardCharsets @@ -80,25 +81,30 @@ class NodeAttachmentServiceTest { @After fun tearDown() { - dir.list { subdir -> - subdir.forEach(Path::deleteRecursively) - } database.close() } @Test fun `importing a signed jar saves the signers to the storage`() { - val jarAndSigner = makeTestSignedContractJar("com.example.MyContract") - val signedJar = jarAndSigner.first - val attachmentId = storage.importAttachment(signedJar.inputStream(), "test", null) - assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash }) + SelfCleaningDir().use { file -> + val jarAndSigner = makeTestSignedContractJar(file.path, "com.example.MyContract") + val signedJar = jarAndSigner.first + signedJar.inputStream().use { jarStream -> + val attachmentId = storage.importAttachment(jarStream, "test", null) + assertEquals(listOf(jarAndSigner.second.hash), storage.openAttachment(attachmentId)!!.signers.map { it.hash }) + } + } } @Test fun `importing a non-signed jar will save no signers`() { - val jarName = makeTestContractJar("com.example.MyContract") - val attachmentId = storage.importAttachment(dir.resolve(jarName).inputStream(), "test", null) - assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size) + SelfCleaningDir().use { + val jarName = makeTestContractJar(it.path, "com.example.MyContract") + it.path.resolve(jarName).inputStream().use { jarStream -> + val attachmentId = storage.importAttachment(jarStream, "test", null) + assertEquals(0, storage.openAttachment(attachmentId)!!.signers.size) + } + } } @Test @@ -127,25 +133,27 @@ class NodeAttachmentServiceTest { @Test fun `insert contract attachment as an untrusted uploader and then as trusted CorDapp uploader`() { - val contractJarName = makeTestContractJar("com.example.MyContract") - val testJar = dir.resolve(contractJarName) - val expectedHash = testJar.readAll().sha256() + SelfCleaningDir().use { file -> + val contractJarName = makeTestContractJar(file.path, "com.example.MyContract") + val testJar = file.path.resolve(contractJarName) + val expectedHash = testJar.readAll().sha256() - // PRIVILEGED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, P2P_UPLOADER, UNKNOWN_UPLOADER) - // TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER) + // PRIVILEGED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, P2P_UPLOADER, UNKNOWN_UPLOADER) + // TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER) - database.transaction { - val id = testJar.read { storage.privilegedImportOrGetAttachment(it, P2P_UPLOADER, null) } - assertEquals(expectedHash, id) - val attachment1 = storage.openAttachment(expectedHash) + database.transaction { + val id = testJar.read { storage.privilegedImportOrGetAttachment(it, P2P_UPLOADER, null) } + assertEquals(expectedHash, id) + val attachment1 = storage.openAttachment(expectedHash) - val id2 = testJar.read { storage.privilegedImportOrGetAttachment(it, DEPLOYED_CORDAPP_UPLOADER, null) } - assertEquals(expectedHash, id2) - val attachment2 = storage.openAttachment(expectedHash) + val id2 = testJar.read { storage.privilegedImportOrGetAttachment(it, DEPLOYED_CORDAPP_UPLOADER, null) } + assertEquals(expectedHash, id2) + val attachment2 = storage.openAttachment(expectedHash) - assertNotEquals(attachment1, attachment2) - assertEquals(P2P_UPLOADER, (attachment1 as ContractAttachment).uploader) - assertEquals(DEPLOYED_CORDAPP_UPLOADER, (attachment2 as ContractAttachment).uploader) + assertNotEquals(attachment1, attachment2) + assertEquals(P2P_UPLOADER, (attachment1 as ContractAttachment).uploader) + assertEquals(DEPLOYED_CORDAPP_UPLOADER, (attachment2 as ContractAttachment).uploader) + } } } @@ -350,20 +358,17 @@ class NodeAttachmentServiceTest { return Pair(file, file.readAll().sha256()) } + /** + * Class to create an automatically delete a temporary directory. + */ + class SelfCleaningDir : Closeable { + val path: Path = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName) + override fun close() { + path.deleteRecursively() + } + } + companion object { - private val dir = Files.createTempDirectory(NodeAttachmentServiceTest::class.simpleName) - - @BeforeClass - @JvmStatic - fun beforeClass() { - } - - @AfterClass - @JvmStatic - fun afterClass() { - dir.deleteRecursively() - } - private fun makeTestJar(output: OutputStream, extraEntries: List> = emptyList()) { output.use { val jar = JarOutputStream(it) @@ -372,33 +377,33 @@ class NodeAttachmentServiceTest { jar.closeEntry() jar.putNextEntry(JarEntry("test2.txt")) jar.write("Some more useful content".toByteArray()) - extraEntries.forEach { - jar.putNextEntry(JarEntry(it.first)) - jar.write(it.second.toByteArray()) + extraEntries.forEach { entry -> + jar.putNextEntry(JarEntry(entry.first)) + jar.write(entry.second.toByteArray()) } jar.closeEntry() } } - private fun makeTestSignedContractJar(contractName: String): Pair { + private fun makeTestSignedContractJar(workingDir: Path, contractName: String): Pair { val alias = "testAlias" val pwd = "testPassword" - dir.generateKey(alias, pwd, ALICE_NAME.toString()) - val jarName = makeTestContractJar(contractName) - val signer = dir.signJar(jarName, alias, pwd) - return dir.resolve(jarName) to signer + workingDir.generateKey(alias, pwd, ALICE_NAME.toString()) + val jarName = makeTestContractJar(workingDir, contractName) + val signer = workingDir.signJar(jarName, alias, pwd) + return workingDir.resolve(jarName) to signer } - private fun makeTestContractJar(contractName: String): String { + private fun makeTestContractJar(workingDir: Path, contractName: String): String { val packages = contractName.split(".") val jarName = "testattachment.jar" val className = packages.last() - createTestClass(className, packages.subList(0, packages.size - 1)) - dir.createJar(jarName, "${contractName.replace(".", "/")}.class") + createTestClass(workingDir, className, packages.subList(0, packages.size - 1)) + workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class") return jarName } - private fun createTestClass(className: String, packages: List): Path { + private fun createTestClass(workingDir: Path, className: String, packages: List): Path { val newClass = """package ${packages.joinToString(".")}; import net.corda.core.contracts.*; import net.corda.core.transactions.*; @@ -410,15 +415,15 @@ class NodeAttachmentServiceTest { } """.trimIndent() val compiler = ToolProvider.getSystemJavaCompiler() - val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/${className}.java"), JavaFileObject.Kind.SOURCE) { + val source = object : SimpleJavaFileObject(URI.create("string:///${packages.joinToString("/")}/$className.java"), JavaFileObject.Kind.SOURCE) { override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence { return newClass } } val fileManager = compiler.getStandardFileManager(null, null, null) - fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(dir.toFile())) + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, listOf(workingDir.toFile())) - val compile = compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call() + compiler.getTask(System.out.writer(), fileManager, null, null, null, listOf(source)).call() return Paths.get(fileManager.list(StandardLocation.CLASS_OUTPUT, "", setOf(JavaFileObject.Kind.CLASS), true).single().name) } } diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 5f60bb6618..320e499718 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -69,6 +69,8 @@ class NetworkRegistrationHelperTest { doReturn(null).whenever(it).tlsCertCrlDistPoint doReturn(null).whenever(it).tlsCertCrlIssuer doReturn(true).whenever(it).crlCheckSoftFail + doReturn(null).whenever(it).cryptoServiceName + doReturn(null).whenever(it).cryptoServiceConf } } @@ -85,7 +87,7 @@ class NetworkRegistrationHelperTest { val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(CORDA_ROOT_CA to it.first.certificate) } - createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA).buildKeystore() + createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA).generateKeysAndRegister() val nodeKeystore = config.signingCertificateStore.get() val sslKeystore = config.p2pSslOptions.keyStore.get() @@ -130,7 +132,7 @@ class NetworkRegistrationHelperTest { saveNetworkTrustStore(CORDA_ROOT_CA to nodeCaCertPath.last()) val registrationHelper = createFixedResponseRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) - .isThrownBy { registrationHelper.buildKeystore() } + .isThrownBy { registrationHelper.generateKeysAndRegister() } .withMessageContaining(CertificateType.TLS.toString()) } @@ -141,7 +143,7 @@ class NetworkRegistrationHelperTest { saveNetworkTrustStore(CORDA_ROOT_CA to nodeCaCertPath.last()) val registrationHelper = createFixedResponseRegistrationHelper(nodeCaCertPath) assertThatExceptionOfType(CertificateRequestException::class.java) - .isThrownBy { registrationHelper.buildKeystore() } + .isThrownBy { registrationHelper.generateKeysAndRegister() } .withMessageContaining(invalidName.toString()) } @@ -156,7 +158,7 @@ class NetworkRegistrationHelperTest { } val registrationHelper = createRegistrationHelper(rootAndIntermediateCA = rootAndIntermediateCA) - registrationHelper.buildKeystore() + registrationHelper.generateKeysAndRegister() val trustStore = config.p2pSslOptions.trustStore.get() trustStore.run { assertTrue(contains(extraTrustedCertAlias)) @@ -174,7 +176,7 @@ class NetworkRegistrationHelperTest { val registrationHelper = createRegistrationHelper() assertThatThrownBy { - registrationHelper.buildKeystore() + registrationHelper.generateKeysAndRegister() }.isInstanceOf(CertPathValidatorException::class.java) } @@ -186,7 +188,7 @@ class NetworkRegistrationHelperTest { val rootAndIntermediateCA = createDevIntermediateCaCertPath().also { saveNetworkTrustStore(CORDA_ROOT_CA to it.first.certificate) } - createRegistrationHelper(CertRole.SERVICE_IDENTITY, rootAndIntermediateCA).buildKeystore() + createRegistrationHelper(CertRole.SERVICE_IDENTITY, rootAndIntermediateCA).generateKeysAndRegister() val nodeKeystore = config.signingCertificateStore.get() @@ -251,10 +253,7 @@ class NetworkRegistrationHelperTest { return when (certRole) { CertRole.NODE_CA -> NodeRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) CertRole.SERVICE_IDENTITY -> NetworkRegistrationHelper( - config.certificatesDirectory, - config.signingCertificateStore, - config.myLegalName, - config.emailAddress, + config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 0e7bad2e20..13b4b334e6 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -356,7 +356,7 @@ class DriverDSLImpl( config.corda, HTTPNetworkRegistrationService(networkServicesConfig, versionInfo), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword) - ).buildKeystore() + ).generateKeysAndRegister() config } } else { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 929b40ac77..e7cbd4717c 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -38,6 +38,7 @@ import net.corda.node.services.config.* import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.keys.KeyManagementServiceInternal +import net.corda.node.services.keys.cryptoservice.BCCryptoService import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.MessagingService import net.corda.node.services.persistence.NodeAttachmentService @@ -65,7 +66,7 @@ import rx.internal.schedulers.CachedThreadScheduler import java.math.BigInteger import java.nio.file.Path import java.nio.file.Paths -import java.security.KeyPair +import java.security.PublicKey import java.time.Clock import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -378,7 +379,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } override fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal { - return E2ETestKeyManagementService(identityService) + return E2ETestKeyManagementService(identityService, cryptoService) } override fun startShell() { @@ -386,10 +387,13 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } // This is not thread safe, but node construction is done on a single thread, so that should always be fine - override fun generateKeyPair(): KeyPair { + override fun generateKeyPair(alias: String): PublicKey { + require(cryptoService is BCCryptoService) { "MockNode supports BCCryptoService only, but it is ${cryptoService.javaClass.name}" } counter = counter.add(BigInteger.ONE) // The StartedMockNode specifically uses EdDSA keys as they are fixed and stored in json files for some tests (e.g IRSSimulation). - return Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, counter) + val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, counter) + (cryptoService as BCCryptoService).importKey(alias, keyPair) + return keyPair.public } // NodeInfo requires a non-empty addresses list and so we give it a dummy value for mock nodes. @@ -613,6 +617,8 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio doReturn(FlowTimeoutConfiguration(1.hours, 3, backoffBase = 1.0)).whenever(it).flowTimeout doReturn(5.seconds.toMillis()).whenever(it).additionalNodeInfoPollingFrequencyMsec doReturn(null).whenever(it).devModeOptions + doReturn(null).whenever(it).cryptoServiceName + doReturn(null).whenever(it).cryptoServiceConf doReturn(EnterpriseConfiguration( mutualExclusionConfiguration = MutualExclusionConfiguration(false, "", 20000, 40000), useMultiThreadedSMM = false diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt index e9f4902c58..99d60846b8 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -13,7 +13,7 @@ fun testNetworkParameters( modifiedTime: Instant = Instant.now(), maxMessageSize: Int = 10485760, // TODO: Make this configurable and consistence across driver, bootstrapper, demobench and NetworkMapServer - maxTransactionSize: Int = maxMessageSize, + maxTransactionSize: Int = maxMessageSize * 50, whitelistedContractImplementations: Map> = emptyMap(), epoch: Int = 1, eventHorizon: Duration = 30.days diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt index 903d05fca7..fb13a38de8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/JarSignatureTestUtils.kt @@ -31,6 +31,10 @@ object JarSignatureTestUtils { fun Path.createJar(fileName: String, vararg contents: String) = executeProcess(*(arrayOf("jar", "cvf", fileName) + contents)) + fun Path.addIndexList(fileName: String) { + executeProcess(*(arrayOf("jar", "i", fileName))) + } + fun Path.updateJar(fileName: String, vararg contents: String) = executeProcess(*(arrayOf("jar", "uvf", fileName) + contents)) diff --git a/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt b/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt index 3f356d5e7f..dd7fb5c90d 100644 --- a/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt +++ b/testing/test-utils/src/test/kotlin/net/corda/testing/core/JarSignatureCollectorTest.kt @@ -5,10 +5,12 @@ import net.corda.testing.core.JarSignatureTestUtils.generateKey import net.corda.testing.core.JarSignatureTestUtils.getJarSigners import net.corda.testing.core.JarSignatureTestUtils.signJar import net.corda.testing.core.JarSignatureTestUtils.updateJar +import net.corda.testing.core.JarSignatureTestUtils.addIndexList import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.AfterClass @@ -36,6 +38,7 @@ class JarSignatureCollectorTest { fun beforeClass() { dir.generateKey(ALICE, "storepass", ALICE_NAME.toString(), keyPassword = ALICE_PASS) dir.generateKey(BOB, "storepass", BOB_NAME.toString(), keyPassword = BOB_PASS) + dir.generateKey(CHARLIE, "storepass", CHARLIE_NAME.toString(), "EC", CHARLIE_PASS) (dir / "_signable1").writeLines(listOf("signable1")) (dir / "_signable2").writeLines(listOf("signable2")) @@ -134,12 +137,19 @@ class JarSignatureCollectorTest { // and our JarSignatureCollector @Test fun `one signer with EC algorithm`() { - dir.generateKey(CHARLIE, "storepass", CHARLIE_NAME.toString(), "EC", CHARLIE_PASS) dir.createJar(FILENAME, "_signable1", "_signable2") val key = dir.signJar(FILENAME, CHARLIE, "storepass", CHARLIE_PASS) assertEquals(listOf(key), dir.getJarSigners(FILENAME)) // We only used CHARLIE's distinguished name, so the keys will be different. } + @Test + fun `jar with jar index file`() { + dir.createJar(FILENAME, "_signable1") + dir.addIndexList(FILENAME) + val key = signAsAlice() + assertEquals(listOf(key), dir.getJarSigners(FILENAME)) + } + private fun signAsAlice() = dir.signJar(FILENAME, ALICE, "storepass", ALICE_PASS) private fun signAsBob() = dir.signJar(FILENAME, BOB, "storepass", BOB_PASS) } diff --git a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt index 3caf0285d3..cbc9b53cd4 100644 --- a/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt +++ b/tools/bootstrapper/src/test/kotlin/net/corda/bootstrapper/PackageOwnerParsingTest.kt @@ -8,10 +8,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.JarSignatureTestUtils.generateKey import org.assertj.core.api.Assertions.assertThat -import org.junit.AfterClass -import org.junit.BeforeClass -import org.junit.Rule -import org.junit.Test +import org.junit.* import org.junit.rules.ExpectedException import picocli.CommandLine import java.nio.file.Files @@ -124,6 +121,7 @@ class PackageOwnerParsingTest { assertThat(networkBootstrapper.registerPackageOwnership).hasSize(3) } + @Ignore("Ignoring this test as the delimiters don't work correctly, see CORDA-2191") @Test fun `parse registration request with delimiter inclusive passwords`() { val aliceKeyStorePath1 = dirAlice / "_alicestore1"