[CORDA-2011] [CORDA-2057] CryptoService interface and BC HSM simulation (#4099)

This commit is contained in:
Konstantinos Chalkias
2018-11-06 12:57:13 +00:00
committed by GitHub
parent 1f5436dcfc
commit 106eb9df4a
29 changed files with 775 additions and 220 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -15,7 +15,6 @@ interface SslConfiguration {
}
interface MutualSslConfiguration : SslConfiguration {
override val keyStore: FileBasedCertificateStoreSupplier
override val trustStore: FileBasedCertificateStoreSupplier
}

View File

@ -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 {

View File

@ -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) }
}

View File

@ -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) {

View File

@ -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))

View File

@ -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
}

View File

@ -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") }
}
}
}