mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
[CORDA-2011] [CORDA-2057] CryptoService interface and BC HSM simulation (#4099)
This commit is contained in:
parent
1f5436dcfc
commit
106eb9df4a
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -73,6 +73,10 @@
|
||||
<module name="core_main" target="1.8" />
|
||||
<module name="core_smokeTest" target="1.8" />
|
||||
<module name="core_test" target="1.8" />
|
||||
<module name="crypto-service_main" target="1.8" />
|
||||
<module name="crypto-service_test" target="1.8" />
|
||||
<module name="crypto-services_main" target="1.8" />
|
||||
<module name="crypto-services_test" target="1.8" />
|
||||
<module name="data_main" target="1.8" />
|
||||
<module name="data_test" target="1.8" />
|
||||
<module name="demobench_main" target="1.8" />
|
||||
|
@ -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<Int, SignatureScheme> = Crypto.supportedSignatureSchemes().associateBy { it.schemeNumberID }
|
||||
|
||||
@JvmStatic
|
||||
fun supportedSignatureSchemes(): List<SignatureScheme> = 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)
|
||||
|
@ -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"
|
||||
}
|
@ -16,18 +16,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.*
|
||||
|
||||
@ -922,4 +928,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)
|
||||
}
|
||||
}
|
||||
|
@ -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<Pair<String, X509Certificate>> {
|
||||
@ -42,7 +44,6 @@ interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
|
||||
}
|
||||
|
||||
operator fun set(alias: String, certificate: X509Certificate) {
|
||||
|
||||
update {
|
||||
internal.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, certificate)
|
||||
}
|
||||
@ -64,7 +65,6 @@ interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
|
||||
* @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)
|
||||
}
|
||||
@ -78,6 +78,21 @@ interface CertificateStore : Iterable<Pair<String, X509Certificate>> {
|
||||
this@CertificateStore.forEach(::setCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCertPathOnly(alias: String, certificates: List<X509Certificate>) {
|
||||
// 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
|
@ -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)
|
||||
}
|
@ -15,7 +15,6 @@ interface SslConfiguration {
|
||||
}
|
||||
|
||||
interface MutualSslConfiguration : SslConfiguration {
|
||||
|
||||
override val keyStore: FileBasedCertificateStoreSupplier
|
||||
override val trustStore: FileBasedCertificateStoreSupplier
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) }
|
||||
}
|
@ -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,25 +56,33 @@ 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<X509Certificate>, keyPassword: String) {
|
||||
internal.setKeyEntry(alias, key, keyPassword.toCharArray(), certificates.toTypedArray())
|
||||
save()
|
||||
}
|
||||
|
||||
fun setCertificate(alias: String, certificate: X509Certificate) {
|
||||
internal.setCertificateEntry(alias, certificate)
|
||||
save()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
if (saveSupported) {
|
||||
internal.save(checkWritableToFile(), storePassword)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(action: X509KeyStore.() -> Unit) {
|
||||
checkWritableToFile()
|
||||
|
@ -301,16 +301,11 @@ 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 {
|
||||
.build(contentSigner).apply {
|
||||
if (!isSignatureValid()) {
|
||||
throw SignatureException("The certificate signing request signature validation failed.")
|
||||
}
|
||||
@ -318,7 +313,9 @@ object X509Utilities {
|
||||
}
|
||||
|
||||
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<X509Certificate>): 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<Certificate>.x509: List<X509Certificate> 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))
|
||||
|
@ -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
|
||||
}
|
@ -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<IllegalArgumentException>("Unrecognised algorithm: 2.26.40086077608615255153862931087626791001") {
|
||||
signingCertStore.query { getPrivateKey(alias, "entrypassword") }
|
||||
}
|
||||
}
|
||||
}
|
@ -9,21 +9,19 @@ 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.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.*
|
||||
@ -56,8 +54,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
|
||||
@ -75,14 +74,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
|
||||
@ -90,7 +90,6 @@ import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Scheduler
|
||||
import java.io.IOException
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
@ -167,6 +166,7 @@ abstract class AbstractNode<S>(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()
|
||||
@ -262,6 +262,11 @@ abstract class AbstractNode<S>(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()
|
||||
}
|
||||
@ -300,7 +305,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
log.info("Node starting up ...")
|
||||
|
||||
val trustRoot = initKeyStores()
|
||||
val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA]
|
||||
initialiseJVMAgents()
|
||||
|
||||
schemaService.mappedSchemasWarnings().forEach {
|
||||
@ -326,6 +330,9 @@ abstract class AbstractNode<S>(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 (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction {
|
||||
@ -344,6 +351,9 @@ abstract class AbstractNode<S>(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)
|
||||
@ -421,7 +431,6 @@ abstract class AbstractNode<S>(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")
|
||||
@ -436,7 +445,7 @@ abstract class AbstractNode<S>(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.
|
||||
@ -651,9 +660,9 @@ abstract class AbstractNode<S>(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
|
||||
@ -762,7 +771,7 @@ abstract class AbstractNode<S>(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() {
|
||||
@ -787,50 +796,47 @@ abstract class AbstractNode<S>(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<PartyAndCertificate, KeyPair> {
|
||||
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<X509Certificate> = 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<PartyAndCertificate, KeyPair> {
|
||||
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)
|
||||
@ -838,11 +844,43 @@ abstract class AbstractNode<S>(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<PartyAndCertificate, KeyPair>, where KeyPair uses an AliasPrivateKey.
|
||||
private fun getPartyAndCertificatePlusAliasKeyPair(certificates: List<X509Certificate>, privateKeyAlias: String): Pair<PartyAndCertificate, KeyPair> {
|
||||
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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,10 +67,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()
|
||||
@ -85,7 +86,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) }
|
||||
|
@ -10,10 +10,13 @@ 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.config.*
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
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.DatabaseConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
import org.slf4j.Logger
|
||||
import java.net.URL
|
||||
@ -73,6 +76,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
|
||||
|
||||
@ -81,6 +86,11 @@ interface NodeConfiguration {
|
||||
|
||||
val cordappSignerKeyFingerprintBlacklist: List<String>
|
||||
|
||||
// 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<String>
|
||||
|
||||
companion object {
|
||||
@ -99,6 +109,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<FlowOverride> = listOf())
|
||||
@ -219,11 +236,13 @@ data class NodeConfigurationImpl(
|
||||
override val cordappDirectories: List<Path> = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT),
|
||||
override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA,
|
||||
override val flowOverrides: FlowOverrideConfig?,
|
||||
override val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() }
|
||||
override val cordappSignerKeyFingerprintBlacklist: List<String> = DEV_PUB_KEY_HASHES.map { it.toString() },
|
||||
override val cryptoServiceName: SupportedCryptoServices? = null,
|
||||
override val cryptoServiceConf: String? = null
|
||||
) : NodeConfiguration {
|
||||
companion object {
|
||||
private val logger = loggerFor<NodeConfigurationImpl>()
|
||||
|
||||
// private val supportedCryptoServiceNames = setOf("BC", "UTIMACO", "GEMALTO-LUNA", "AZURE-KEY-VAULT")
|
||||
}
|
||||
|
||||
private val actualRpcSettings: NodeRpcSettings
|
||||
@ -276,6 +295,14 @@ data class NodeConfigurationImpl(
|
||||
return errors
|
||||
}
|
||||
|
||||
private fun validateCryptoService(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
if (cryptoServiceName == null && cryptoServiceConf != null) {
|
||||
errors += "cryptoServiceName is null, but cryptoServiceConf is set to $cryptoServiceConf"
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
override fun validate(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
errors += validateDevModeOptions()
|
||||
@ -288,6 +315,7 @@ data class NodeConfigurationImpl(
|
||||
errors += validateTlsCertCrlConfig()
|
||||
errors += validateNetworkServices()
|
||||
errors += validateH2Settings()
|
||||
errors += validateCryptoService()
|
||||
return errors
|
||||
}
|
||||
|
||||
|
@ -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<PublicKey, PrivateKey, PersistentKey, String> {
|
||||
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<PublicKey, String>()
|
||||
// A map for anonymous keys.
|
||||
private val keysMap = createKeyMap(cacheFactory)
|
||||
|
||||
override fun start(initialKeyPairs: Set<KeyPair>) {
|
||||
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<PublicKey> 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<PublicKey>): Iterable<PublicKey> = 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<PublicKey, PrivateKey>()
|
||||
}
|
||||
@ -32,12 +35,20 @@ class E2ETestKeyManagementService(val identityService: IdentityService) : Single
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
// Accessing this map clones it.
|
||||
override val keys: Set<PublicKey> get() = mutex.locked { keys.keys }
|
||||
// Maintain a map from PublicKey to alias for the initial keys.
|
||||
|
||||
val keyPairs: Set<KeyPair> get() = mutex.locked { keys.map { KeyPair(it.key, it.value) }.toSet() }
|
||||
|
||||
override fun start(initialKeyPairs: Set<KeyPair>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
@ -12,12 +12,12 @@ 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
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
|
||||
import net.corda.node.services.vault.VaultSchemaV1
|
||||
|
||||
@ -35,6 +35,7 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = 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,
|
||||
|
@ -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?")
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
open class NetworkRegistrationHelper(
|
||||
config: NodeConfiguration,
|
||||
private val certService: NetworkRegistrationService,
|
||||
private val networkRootTrustStorePath: Path,
|
||||
networkRootTrustStorePassword: String,
|
||||
private val keyAlias: String,
|
||||
private val nodeCaKeyAlias: String,
|
||||
private val certRole: CertRole,
|
||||
private val nextIdleDuration: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) {
|
||||
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,25 +69,59 @@ 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.
|
||||
@ -90,31 +129,7 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
| 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<X509Certificate>) {
|
||||
@ -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<X509Certificate>, 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,6 +187,7 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
* @return List of certificate chain.
|
||||
*/
|
||||
private fun pollServerForCertificates(requestId: String): List<X509Certificate> {
|
||||
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
|
||||
@ -204,18 +208,27 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
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, keyPair, certRole)
|
||||
val request = X509Utilities.createCertificateSigningRequest(myLegalName.x500Principal, emailAddress, publicKey, contentSigner, certRole)
|
||||
val writer = StringWriter()
|
||||
JcaPEMWriter(writer).use {
|
||||
it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded))
|
||||
@ -225,7 +238,7 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
println("Legal Name: $myLegalName")
|
||||
println("Email: $emailAddress")
|
||||
println()
|
||||
println("Public Key: ${keyPair.public}")
|
||||
println("Public Key: $publicKey")
|
||||
println()
|
||||
println("$writer")
|
||||
// Post request to signing server via http.
|
||||
@ -240,9 +253,14 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
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<X509Certificate>, tlsCrlCertificateIssuer: X500Name?) {}
|
||||
protected open fun onSuccess(publicKey: PublicKey, contentSigner: ContentSigner, certificates: List<X509Certificate>, 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<X509Certificate>, tlsCrlCertificateIssuer: X500Name?) {
|
||||
createSSLKeystore(nodeCAKeyPair, certificates, tlsCrlCertificateIssuer)
|
||||
override fun onSuccess(publicKey: PublicKey, contentSigner: ContentSigner, certificates: List<X509Certificate>, tlsCrlCertificateIssuer: X500Name?) {
|
||||
createSSLKeystore(publicKey, contentSigner, certificates, tlsCrlCertificateIssuer)
|
||||
createTruststore(certificates.last())
|
||||
}
|
||||
|
||||
private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>, tlsCertCrlIssuer: X500Name?) {
|
||||
private fun createSSLKeystore(nodeCaPublicKey: PublicKey, nodeCaContentSigner: ContentSigner, nodeCaCertificateChain: List<X509Certificate>, 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<X509Certificate> = 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) {
|
||||
|
@ -108,12 +108,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
|
||||
@ -177,6 +176,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))
|
||||
@ -210,7 +227,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())
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -316,7 +316,7 @@ class DriverDSLImpl(
|
||||
config.corda,
|
||||
HTTPNetworkRegistrationService(networkServicesConfig, versionInfo),
|
||||
NodeRegistrationOption(rootTruststorePath, rootTruststorePassword)
|
||||
).buildKeystore()
|
||||
).generateKeysAndRegister()
|
||||
config
|
||||
}
|
||||
} else {
|
||||
|
@ -40,6 +40,7 @@ import net.corda.node.services.config.VerifierType
|
||||
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
|
||||
@ -67,7 +68,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
|
||||
@ -380,7 +381,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
}
|
||||
|
||||
override fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal {
|
||||
return E2ETestKeyManagementService(identityService)
|
||||
return E2ETestKeyManagementService(identityService, cryptoService)
|
||||
}
|
||||
|
||||
override fun startShell() {
|
||||
@ -388,10 +389,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.
|
||||
@ -614,6 +618,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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user