From 53276c1f06e2065659e238a024c507ea40761465 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Mon, 22 May 2017 11:14:05 +0100 Subject: [PATCH] faster key encoding/decoding and generic converters between key implementations --- .../kotlin/net/corda/core/crypto/Crypto.kt | 132 +++++++++++++----- .../net/corda/core/crypto/CryptoUtils.kt | 6 +- .../corda/core/crypto/KeyStoreUtilities.kt | 80 ++++++----- .../net/corda/core/crypto/X509Utilities.kt | 81 ++++++----- .../kotlin/net/corda/core/identity/Party.kt | 4 +- .../corda/core/crypto/X509UtilitiesTest.kt | 19 ++- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../network/InMemoryIdentityServiceTests.kt | 1 - 8 files changed, 208 insertions(+), 117 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 5be19a4d71..6e1a2fec15 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -21,13 +21,20 @@ 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.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider +import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey +import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec +import sun.security.pkcs.PKCS8Key +import sun.security.util.DerValue +import sun.security.x509.X509Key import java.math.BigInteger import java.security.* import java.security.KeyFactory @@ -140,6 +147,10 @@ object Crypto { SPHINCS256_SHA256 ).associateBy { it.schemeCodeName } + // We need to group signature schemes per algorithm, so to quickly identify them during decoding. + // Please note there are schemes with the same algorithm, e.g. EC (or ECDSA) keys are used for both ECDSA_SECP256K1_SHA256 and ECDSA_SECP256R1_SHA256. + private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName } + // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // that could cause unexpected and suspicious behaviour. // i.e. if someone removes a Provider and then he/she adds a new one with the same name. @@ -167,37 +178,20 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: $schemeCodeName") + fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") /** * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. * This function is usually called when requiring to verify signatures and the signing schemes must be defined. - * Note that only the Corda platform standard schemes are supported (see [Crypto]). - * Note that we always need to add an additional if-else statement when there are signature schemes - * with the same algorithmName, but with different parameters (e.g. now there are two ECDSA schemes, each using its own curve). + * For the supported signature schemes see [Crypto]. * @param key either private or public. * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ fun findSignatureScheme(key: Key): SignatureScheme { - for (sig in supportedSignatureSchemes.values) { - var algorithm = key.algorithm - if (algorithm == "EC") algorithm = "ECDSA" // required to read ECC keys from Keystore, because encoding may change algorithm name from ECDSA to EC. - if (algorithm == "SPHINCS-256") algorithm = "SPHINCS256" // because encoding may change algorithm name from SPHINCS256 to SPHINCS-256. - if (algorithm == sig.algorithmName) { - // If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters. - if (algorithm == "EdDSA") { - if ((key is EdDSAPublicKey && publicKeyOnCurve(sig, key)) || (key is EdDSAPrivateKey && key.params == sig.algSpec)) { - return sig - } else break // use continue if in the future we support more than one Edwards curves. - } else if (algorithm == "ECDSA") { - if ((key is BCECPublicKey && publicKeyOnCurve(sig, key)) || (key is BCECPrivateKey && key.parameters == sig.algSpec)) { - return sig - } else continue - } else return sig // it's either RSA_SHA256 or SPHINCS-256. - } - } - throw IllegalArgumentException("Unsupported key/algorithm for the key: ${key.encoded.toBase58()}") + val algorithm = matchingAlgorithmName(key.algorithm) + algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it } + throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format") } /** @@ -209,11 +203,16 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { - for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) { + val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm) + // There are cases where the same key algorithm is applied to different signature schemes. + // Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. + // In such a case, we should try and identify which of the candidate schemes is the correct one so as + // to generate the appropriate key. + for (signatureScheme in algorithmGroups[algorithm]!!) { try { - return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - // ignore it - only used to bypass the scheme that causes an exception. + // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params. } } throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.") @@ -258,11 +257,16 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun decodePublicKey(encodedKey: ByteArray): PublicKey { - for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) { + val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm) + // There are cases where the same key algorithm is applied to different signature schemes. + // Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. + // In such a case, we should try and identify which of the candidate schemes is the correct one so as + // to generate the appropriate key. + for (signatureScheme in algorithmGroups[algorithm]!!) { try { - return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) + return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - // ignore it - only used to bypass the scheme that causes an exception. + // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params. } } throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.") @@ -273,7 +277,7 @@ object Crypto { * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers. * @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). * @param encodedKey an X509 encoded public key. - * @throws IllegalArgumentException if the requested scheme is not supported + * @throws IllegalArgumentException if the requested scheme is not supported. * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ @@ -285,7 +289,7 @@ object Crypto { * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers. * @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256). * @param encodedKey an X509 encoded public key. - * @throws IllegalArgumentException if the requested scheme is not supported + * @throws IllegalArgumentException if the requested scheme is not supported. * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ @@ -444,7 +448,7 @@ object Crypto { */ @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) fun doVerify(publicKey: PublicKey, transactionSignature: TransactionSignature): Boolean { - if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.encoded.toBase58()} does not match the input clearData: ${publicKey.encoded.toBase58()}") + if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.toStringShort()} does not match") return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes()) } @@ -598,9 +602,9 @@ object Crypto { * Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks. * Point-at-infinity is not permitted as well. * @see Small subgroup and invalid-curve attacks for a more descriptive explanation on such attacks. - * We use this function on [findSignatureScheme] for a [PublicKey]; currently used for signature verification only. + * We use this function on [validatePublicKey], which is currently used for signature verification only. * Thus, as these attacks are mostly not relevant to signature verification, we should note that - * we're doing it out of an abundance of caution and specifically to proactively protect developers + * we are doing it out of an abundance of caution and specifically to proactively protect developers * against using these points as part of a DH key agreement or for use cases as yet unimagined. * This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve). * @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve. @@ -625,4 +629,66 @@ object Crypto { /** Check if the requested [SignatureScheme] is supported by the system. */ fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme + + // map algorithm names returned from Keystore (or after encode/decode) to the supported algorithm names. + private fun matchingAlgorithmName(algorithm: String): String { + return when (algorithm) { + "EC" -> "ECDSA" + "SPHINCS-256" -> "SPHINCS256" + "1.3.6.1.4.1.22554.2.1" -> "SPHINCS256" // Unfortunately, PKCS8Key and X509Key parsing return the OID as the algorithm name and not SPHINCS256. + else -> algorithm + } + } + + // validate a key, by checking its algorithmic params. + private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { + return when (key) { + is PublicKey -> validatePublicKey(signatureScheme, key) + is PrivateKey -> validatePrivateKey(signatureScheme, key) + else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") + } + } + + // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). + private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { + when (key) { + is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key) + is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") + } + } + + // check if a private key satisfies algorithm specs. + private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { + when (key) { + is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec + is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec + is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") + } + } + + /** + * Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. + * This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations. + * @param key a public key. + * @return a supported implementation of the input public key. + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for a supported key factory to produce a private key. + */ + fun toSupportedPublicKey(key: PublicKey): PublicKey { + return Crypto.decodePublicKey(key.encoded) + } + + /** + * Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. + * This method is usually required to retrieve keys from JKS keystores that by default return SUN implementations. + * @param key a private key. + * @return a supported implementation of the input private key. + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for a supported key factory to produce a private key. + */ + fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { + return Crypto.decodePrivateKey(key.encoded) + } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 66a4ec85ac..276a48f824 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -71,11 +71,9 @@ fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.byte // implementation of CompositeSignature. @Throws(InvalidKeyException::class) fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable { + // Quick workaround when we have CompositeKey as Party owningKey. + if (party.owningKey is CompositeKey) throw InvalidKeyException("Signing for parties with CompositeKey not supported.") val sig = sign(bytesToSign) - val sigKey = when (party.owningKey) { // Quick workaround when we have CompositeKey as Party owningKey. - is CompositeKey -> throw InvalidKeyException("Signing for parties with CompositeKey not supported.") - else -> party.owningKey - } return DigitalSignature.LegallyIdentifiable(party, sig.bytes) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt b/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt index a4375b5305..138eed3c56 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt @@ -16,10 +16,10 @@ object KeyStoreUtilities { /** * Helper method to either open an existing keystore for modification, or create a new blank keystore. - * @param keyStoreFilePath location of KeyStore file + * @param keyStoreFilePath location of KeyStore file. * @param storePassword password to open the store. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @return returns the KeyStore opened/created + * @return returns the KeyStore opened/created. */ fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore { val pass = storePassword.toCharArray() @@ -34,11 +34,11 @@ object KeyStoreUtilities { } /** - * Helper method to open an existing keystore for modification/read - * @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException + * Helper method to open an existing keystore for modification/read. + * @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException. * @param storePassword password to open the store. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @return returns the KeyStore opened + * @return returns the KeyStore opened. * @throws IOException if there was an error reading the key store from the file. * @throws KeyStoreException if the password is incorrect or the key store is damaged. */ @@ -48,11 +48,11 @@ object KeyStoreUtilities { } /** - * Helper method to open an existing keystore for modification/read - * @param input stream containing a KeyStore e.g. loaded from a resource file + * Helper method to open an existing keystore for modification/read. + * @param input stream containing a KeyStore e.g. loaded from a resource file. * @param storePassword password to open the store. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @return returns the KeyStore opened + * @return returns the KeyStore opened. * @throws IOException if there was an error reading the key store from the stream. * @throws KeyStoreException if the password is incorrect or the key store is damaged. */ @@ -68,12 +68,12 @@ object KeyStoreUtilities { } /** - * Helper extension method to add, or overwrite any key data in store - * @param alias name to record the private key and certificate chain under - * @param key cryptographic key to store + * Helper extension method to add, or overwrite any key data in store. + * @param alias name to record the private key and certificate chain under. + * @param key cryptographic key to store. * @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert + * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert. */ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array) { if (containsAlias(alias)) { @@ -83,9 +83,9 @@ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain } /** - * Helper extension method to add, or overwrite any public certificate data in store - * @param alias name to record the public certificate under - * @param cert certificate to store + * Helper extension method to add, or overwrite any public certificate data in store. + * @param alias name to record the public certificate under. + * @param cert certificate to store. */ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) { if (containsAlias(alias)) { @@ -96,8 +96,8 @@ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) { /** - * Helper method save KeyStore to storage - * @param keyStoreFilePath the file location to save to + * Helper method save KeyStore to storage. + * @param keyStoreFilePath the file location to save to. * @param storePassword password to access the store in future. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. */ @@ -108,31 +108,47 @@ fun KeyStore.store(out: OutputStream, password: String) = store(out, password.to /** * Extract public and private keys from a KeyStore file assuming storage alias is known. - * @param keyPassword Password to unlock the private key entries - * @param alias The name to lookup the Key and Certificate chain from - * @return The KeyPair found in the KeyStore under the specified alias + * @param alias The name to lookup the Key and Certificate chain from. + * @param keyPassword Password to unlock the private key entries. + * @return The KeyPair found in the KeyStore under the specified alias. */ -fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKey(alias, keyPassword).keyPair +fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKeyPair(alias, keyPassword).keyPair /** * Helper method to load a Certificate and KeyPair from their KeyStore. * The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys. - * @param keyPassword The password for the PrivateKey (not the store access password) * @param alias The name to search for the data. Typically if generated with the methods here this will be one of - * CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above + * CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above. + * @param keyPassword The password for the PrivateKey (not the store access password). */ -fun KeyStore.getCertificateAndKey(alias: String, keyPassword: String): CertificateAndKey { - val keyPass = keyPassword.toCharArray() - val key = getKey(alias, keyPass) as PrivateKey +fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair { val cert = getCertificate(alias) as X509Certificate - // Using Crypto.decodePublicKey to convert X509Key to bouncy castle public key implementation. - // Using Crypto.decodePrivateKey to convert sun provider key implementation to bouncy castle private key implementation. - return CertificateAndKey(cert, KeyPair(Crypto.decodePublicKey(cert.publicKey.encoded), Crypto.decodePrivateKey(key.encoded))) + return CertificateAndKeyPair(cert, KeyPair(Crypto.toSupportedPublicKey(cert.publicKey), getSupportedKey(alias, keyPassword))) } /** - * Extract public X509 certificate from a KeyStore file assuming storage alias is know - * @param alias The name to lookup the Key and Certificate chain from - * @return The X509Certificate found in the KeyStore under the specified alias + * Extract public X509 certificate from a KeyStore file assuming storage alias is known. + * @param alias The name to lookup the Key and Certificate chain from. + * @return The X509Certificate found in the KeyStore under the specified alias. */ fun KeyStore.getX509Certificate(alias: String): X509Certificate = getCertificate(alias) as X509Certificate + +/** + * Extract a private key from a KeyStore file assuming storage alias is known. + * By default, a JKS keystore returns PrivateKey implementations supported by the SUN provider. + * For instance, if one imports a BouncyCastle ECC key, JKS will return a SUN ECC key implementation on getKey. + * To convert to a supported implementation, an encode->decode method is applied to the keystore's returned object. + * @param alias The name to lookup the Key. + * @param keyPassword Password to unlock the private key entries. + * @return the requested private key in supported type. + * @throws KeyStoreException if the keystore has not been initialized. + * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found (not supported from the Keystore provider). + * @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong). + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for a supported key factory to produce a private key. + */ +fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey { + val keyPass = keyPassword.toCharArray() + val key = getKey(alias, keyPass) as PrivateKey + return Crypto.toSupportedPrivateKey(key) +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt index c7b0db0904..b2622a23f8 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -44,12 +44,12 @@ object X509Utilities { private val DEFAULT_VALIDITY_WINDOW = Pair(0, 365 * 10) /** - * Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range - * @param daysBefore number of days to roll back returned start date relative to current date - * @param daysAfter number of days to roll forward returned end date relative to current date - * @param parentNotBefore if provided is used to lower bound the date interval returned - * @param parentNotAfter if provided is used to upper bound the date interval returned - * Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date + * Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range. + * @param daysBefore number of days to roll back returned start date relative to current date. + * @param daysAfter number of days to roll forward returned end date relative to current date. + * @param parentNotBefore if provided is used to lower bound the date interval returned. + * @param parentNotAfter if provided is used to upper bound the date interval returned. + * Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date. * Thus we avoid too many round trip conversions. */ private fun getCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair { @@ -96,57 +96,56 @@ object X509Utilities { /* * Create a de novo root self-signed X509 v3 CA cert and [KeyPair]. - * @param subject the cert Subject will be populated with the domain string + * @param subject the cert Subject will be populated with the domain string. * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates. - * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates + * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates. */ @JvmStatic - fun createSelfSignedCACert(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKey { + fun createSelfSignedCACert(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 2) - return CertificateAndKey(cert, keyPair) + return CertificateAndKeyPair(cert, keyPair) } @JvmStatic fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKey + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair = createSelfSignedCACert(subject, generateKeyPair(signatureScheme), validityWindow) /** * Create a de novo root intermediate X509 v3 CA cert and KeyPair. * @param subject subject of the generated certificate. - * @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it + * @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it. * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates. - * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates + * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates. */ @JvmStatic - fun createIntermediateCert(subject: X500Name, ca: CertificateAndKey, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKey { + fun createIntermediateCert(subject: X500Name, ca: CertificateAndKeyPair, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { val keyPair = generateKeyPair(signatureScheme) val issuer = X509CertificateHolder(ca.certificate.encoded).subject val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter) val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 1) - return CertificateAndKey(cert, keyPair) + return CertificateAndKeyPair(cert, keyPair) } /** * Create an X509v3 certificate suitable for use in TLS roles. - * @param subject The contents to put in the subject field of the certificate - * @param publicKey The PublicKey to be wrapped in the certificate - * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate - * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes - * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes - * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. + * @param subject The contents to put in the subject field of the certificate. + * @param publicKey The PublicKey to be wrapped in the certificate. + * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate. + * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes. + * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS. * This certificate is not marked as a CA cert to be similar in nature to commercial certificates. */ @JvmStatic fun createServerCert(subject: X500Name, publicKey: PublicKey, - ca: CertificateAndKey, + ca: CertificateAndKeyPair, subjectAlternativeNameDomains: List, subjectAlternativeNameIps: List, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { @@ -168,8 +167,8 @@ object X509Utilities { * @param targetCertAndKey certificate the path ends at. * @param revocationEnabled whether revocation of certificates in the path should be checked. */ - fun createCertificatePath(rootCertAndKey: CertificateAndKey, - targetCertAndKey: CertificateAndKey, + fun createCertificatePath(rootCertAndKey: CertificateAndKeyPair, + targetCertAndKey: CertificateAndKeyPair, revocationEnabled: Boolean): CertPathBuilderResult { val intermediateCertificates = setOf(targetCertAndKey.certificate) val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates)) @@ -189,9 +188,9 @@ object X509Utilities { } /** - * Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection - * @param x509Certificate certificate to save - * @param filename Target filename + * Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection. + * @param x509Certificate certificate to save. + * @param filename Target filename. */ @JvmStatic fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) { @@ -203,9 +202,9 @@ object X509Utilities { } /** - * Helper method to load back a .pem/.cer format file copy of a certificate - * @param filename Source filename - * @return The X509Certificate that was encoded in the file + * Helper method to load back a .pem/.cer format file copy of a certificate. + * @param filename Source filename. + * @return The X509Certificate that was encoded in the file. */ @JvmStatic fun loadCertificateFromPEMFile(filename: Path): X509Certificate { @@ -217,14 +216,14 @@ object X509Utilities { } /** - * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine - * @param keyStoreFilePath KeyStore path to save output to - * @param storePassword access password for KeyStore + * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine. + * @param keyStoreFilePath KeyStore path to save output to. + * @param storePassword access password for KeyStore. * @param keyPassword PrivateKey access password for the generated keys. * It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same. - * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore - * @param caKeyPassword password to unlock private keys in the CA KeyStore - * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications + * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore. + * @param caKeyPassword password to unlock private keys in the CA KeyStore. + * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications. */ fun createKeystoreForSSL(keyStoreFilePath: Path, storePassword: String, @@ -234,8 +233,8 @@ object X509Utilities { commonName: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore { - val rootCA = caKeyStore.getCertificateAndKey(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) - val intermediateCA = caKeyStore.getCertificateAndKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) + val rootCA = caKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) + val intermediateCA = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) val serverKey = generateKeyPair(signatureScheme) val host = InetAddress.getLocalHost() @@ -259,7 +258,7 @@ object X509Utilities { /** * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an - * exception + * exception. */ @Throws(IllegalArgumentException::class) fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } @@ -269,7 +268,7 @@ fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName * adds one. */ @Throws(IllegalArgumentException::class) -fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { attr -> commonName } +fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } /** * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. @@ -307,4 +306,4 @@ class CertificateStream(val input: InputStream) { fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate } -data class CertificateAndKey(val certificate: X509Certificate, val keyPair: KeyPair) +data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair) diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 8039f4945c..b94f266b8c 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -1,7 +1,7 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.CertificateAndKey +import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.serialization.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name @@ -27,7 +27,7 @@ import java.security.PublicKey * @see CompositeKey */ class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { - constructor(certAndKey: CertificateAndKey) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public) + constructor(certAndKey: CertificateAndKeyPair) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public) override fun toString() = name.toString() override fun nameOrNull(): X500Name? = name diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt index faedf36c5d..6d224c707d 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt @@ -2,7 +2,6 @@ package net.corda.core.crypto import net.corda.core.div import net.corda.testing.MEGA_CORP -import net.i2p.crypto.eddsa.EdDSAEngine import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.GeneralName @@ -177,14 +176,14 @@ class X509UtilitiesTest { // Load signing intermediate CA cert val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass") - val caCertAndKey = caKeyStore.getCertificateAndKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") + val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") // Generate server cert and private key and populate another keystore suitable for SSL X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) // Load back server certificate val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") - val serverCertAndKey = serverKeyStore.getCertificateAndKey(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") + val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") serverCertAndKey.certificate.checkValidity(Date()) serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) @@ -349,4 +348,18 @@ class X509UtilitiesTest { return keyStore } + @Test + fun `Get correct private key type from Keystore`() { + val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), keyPair) + val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") + keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.certificate)) + + val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray()) + val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword") + + assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key + assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey) + } + } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index c4c9c517e3..dc99af9e4c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -605,7 +605,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val identityAndKey = if (configuration.keyStoreFile.exists() && keystore.containsAlias(privateKeyAlias)) { // Get keys from keystore. - val (cert, keyPair) = keystore.getCertificateAndKey(privateKeyAlias, configuration.keyStorePassword) + val (cert, keyPair) = keystore.getCertificateAndKeyPair(privateKeyAlias, configuration.keyStorePassword) val loadedServiceName = X509CertificateHolder(cert.encoded).subject if (X509CertificateHolder(cert.encoded).subject != serviceName) { throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:" + diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index a3028d0f4f..5733b2b6f3 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -1,6 +1,5 @@ package net.corda.node.services.network -import net.corda.core.crypto.CertificateAndKey import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty