mirror of
https://github.com/corda/corda.git
synced 2025-06-13 20:58:19 +00:00
[CORDA-2011] [CORDA-2057] CryptoService interface and BC HSM simulation (#4099)
This commit is contained in:
committed by
GitHub
parent
1f5436dcfc
commit
106eb9df4a
@ -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,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<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() {
|
||||
internal.save(checkWritableToFile(), storePassword)
|
||||
if (saveSupported) {
|
||||
internal.save(checkWritableToFile(), storePassword)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(action: X509KeyStore.() -> Unit) {
|
||||
|
@ -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<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") }
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user