From 4a9ff84fc766798a46c37fd6c49ad06c668c111b Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Wed, 8 Mar 2017 17:45:23 +0000 Subject: [PATCH] Allowing multiple signature algorithms (#250) Basic crypto API to support 5 signature schemes and MetaData-ed signatures. Supported schemes: (1) RSA_SHA256, (2) ECDSA_SECP256K1_SHA256, (3) ECDSA_SECP256R1_SHA256, (4) EDDSA_ED25519_SHA512, (5) SPHINCS-256_SHA512. To sign a transaction, a signer should create a MetaData wrapper that contains transaction's merkle root and some extra information, such as signer's public key, timestamp and visibleInputs. Actually, MetaData is utilised to support a practical partial, blind and extra-data attached signature model. When a MetaData object is signed, the signer sends a TransactionSignature object that contains the signed output and the corresponding MetaData object. Remarks: This is an temporary solution for signature algorithmic agility. Further development is required for a robust and extensible Crypto Manager/Provider PKI that will support certificate creation, key generation, signing/verifying, deterministic key derivation, encoding formats, SGX/HSM support, identity and key management, versioning, revocation, asynchronicity, metadata, partial sig. policies etc. --- .../kotlin/net/corda/core/crypto/Crypto.kt | 421 +++++++++++ .../net/corda/core/crypto/CryptoUtils.kt | 117 ++++ .../net/corda/core/crypto/EdDSAKeyFactory.kt | 12 + .../kotlin/net/corda/core/crypto/MetaData.kt | 71 ++ .../net/corda/core/crypto/SignatureScheme.kt | 42 ++ .../net/corda/core/crypto/SignatureType.kt | 17 + .../corda/core/crypto/TransactionSignature.kt | 24 + .../serialization/DefaultKryoCustomizer.kt | 4 + .../net/corda/core/serialization/Kryo.kt | 31 + .../net/corda/core/crypto/CryptoUtilsTest.kt | 656 ++++++++++++++++++ .../corda/core/crypto/EncodingUtilsTest.kt | 3 - .../core/crypto/TransactionSignatureTest.kt | 83 +++ .../net/corda/core/serialization/KryoTests.kt | 22 +- .../node/serialization/DefaultWhitelist.kt | 1 + 14 files changed, 1499 insertions(+), 5 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/crypto/Crypto.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/EdDSAKeyFactory.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/MetaData.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/SignatureType.kt create mode 100644 core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt create mode 100644 core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt create mode 100644 core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt new file mode 100644 index 0000000000..02e0199411 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -0,0 +1,421 @@ +package net.corda.core.crypto + +import net.i2p.crypto.eddsa.EdDSAEngine +import net.i2p.crypto.eddsa.EdDSAKey +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.interfaces.ECKey +import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec +import java.security.* +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec + +/** + * This object controls and provides the available and supported signature schemes for Corda. + * Any implemented [SignatureScheme] should be strictly defined here. + * However, only the schemes returned by {@link #listSupportedSignatureSchemes()} are supported. + * Note that Corda currently supports the following signature schemes by their code names: + *

+ */ +object Crypto { + + /** + * RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function. + * Note: Recommended key size >= 3072 bits. + */ + private val RSA_SHA256 = SignatureScheme( + 1, + "RSA_SHA256", + "RSA", + Signature.getInstance("SHA256WITHRSAANDMGF1", "BC"), + KeyFactory.getInstance("RSA", "BC"), + KeyPairGenerator.getInstance("RSA", "BC"), + null, + 3072, + "RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function." + ) + + /** ECDSA signature scheme using the secp256k1 Koblitz curve. */ + private val ECDSA_SECP256K1_SHA256 = SignatureScheme( + 2, + "ECDSA_SECP256K1_SHA256", + "ECDSA", + Signature.getInstance("SHA256withECDSA", "BC"), + KeyFactory.getInstance("ECDSA", "BC"), + KeyPairGenerator.getInstance("ECDSA", "BC"), + ECNamedCurveTable.getParameterSpec("secp256k1"), + 256, + "ECDSA signature scheme using the secp256k1 Koblitz curve." + ) + + /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */ + private val ECDSA_SECP256R1_SHA256 = SignatureScheme( + 3, + "ECDSA_SECP256R1_SHA256", + "ECDSA", + Signature.getInstance("SHA256withECDSA", "BC"), + KeyFactory.getInstance("ECDSA", "BC"), + KeyPairGenerator.getInstance("ECDSA", "BC"), + ECNamedCurveTable.getParameterSpec("secp256r1"), + 256, + "ECDSA signature scheme using the secp256r1 (NIST P-256) curve." + ) + + /** EdDSA signature scheme using the ed255519 twisted Edwards curve. */ + private val EDDSA_ED25519_SHA512 = SignatureScheme( + 4, + "EDDSA_ED25519_SHA512", + "EdDSA", + EdDSAEngine(), + EdDSAKeyFactory(), + net.i2p.crypto.eddsa.KeyPairGenerator(), // EdDSA engine uses a custom KeyPairGenerator Vs BouncyCastle. + EdDSANamedCurveTable.getByName("ed25519-sha-512"), + 256, + "EdDSA signature scheme using the ed25519 twisted Edwards curve." + ) + + /** + * SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers + * at the cost of larger key sizes and loss of compatibility. + */ + private val SPHINCS256_SHA256 = SignatureScheme( + 5, + "SPHINCS-256_SHA512", + "SPHINCS-256", + Signature.getInstance("SHA512WITHSPHINCS256", "BCPQC"), + KeyFactory.getInstance("SPHINCS256", "BCPQC"), + KeyPairGenerator.getInstance("SPHINCS256", "BCPQC"), + SPHINCS256KeyGenParameterSpec(SPHINCS256KeyGenParameterSpec.SHA512_256), + 256, + "SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers " + + "at the cost of larger key sizes and loss of compatibility." + ) + + /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ + private val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 + + /** + * Supported digital signature schemes. + * Note: Only the schemes added in this map will be supported (see [Crypto]). + * Do not forget to add the DEFAULT_SIGNATURE_SCHEME as well. + */ + private val supportedSignatureSchemes = mapOf( + RSA_SHA256.schemeCodeName to RSA_SHA256, + ECDSA_SECP256K1_SHA256.schemeCodeName to ECDSA_SECP256K1_SHA256, + ECDSA_SECP256R1_SHA256.schemeCodeName to ECDSA_SECP256R1_SHA256, + EDDSA_ED25519_SHA512.schemeCodeName to EDDSA_ED25519_SHA512, + SPHINCS256_SHA256.schemeCodeName to SPHINCS256_SHA256 + ) + + /** + * Factory pattern to retrieve the corresponding [SignatureScheme] based on the type of the [String] input. + * This function is usually called by key generators and verify signature functions. + * In case the input is not a key in the supportedSignatureSchemes map, null will be returned. + * @param schemeCodeName a [String] that should match a supported signature scheme code name (e.g. ECDSA_SECP256K1_SHA256), see [Crypto]. + * @return a currently supported SignatureScheme. + * @throws IllegalArgumentException if the requested signature scheme is not supported. + */ + private fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: ${schemeCodeName}") + + /** + * Retrieve the corresponding [SignatureScheme] based on the type of the input [KeyPair]. + * Note that only the Corda platform standard schemes are supported (see [Crypto]). + * This function is usually called when requiring to sign signatures. + * @param keyPair a cryptographic [KeyPair]. + * @return a currently supported SignatureScheme or null. + * @throws IllegalArgumentException if the requested signature scheme is not supported. + */ + private fun findSignatureScheme(keyPair: KeyPair): SignatureScheme = findSignatureScheme(keyPair.private) + + /** + * 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). + * @param key either private or public. + * @return a currently supported SignatureScheme. + * @throws IllegalArgumentException if the requested key type is not supported. + */ + private fun findSignatureScheme(key: Key): SignatureScheme { + for (sig in supportedSignatureSchemes.values) { + val algorithm = key.algorithm + if (algorithm == sig.algorithmName) { + // If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters. + // TODO: change 'continue' to 'break' if only one EdDSA curve will be used. + if (algorithm == "EdDSA") { + if ((key as EdDSAKey).params == sig.algSpec) { + return sig + } else continue + } else if (algorithm == "ECDSA") { + if ((key as ECKey).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 private key: ${key.encoded.toBase58()}") + } + + /** + * Retrieve the corresponding signature scheme code name based on the type of the input [Key]. + * See [Crypto] for the supported scheme code names. + * @param key either private or public. + * @return signatureSchemeCodeName for a [Key]. + * @throws IllegalArgumentException if the requested key type is not supported. + */ + fun findSignatureSchemeCodeName(key: Key): String = findSignatureScheme(key).schemeCodeName + + /** + * Decode a PKCS8 encoded key to its [PrivateKey] object. + * @param encodedKey a PKCS8 encoded private key. + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for this key factory to produce a private key. + */ + @Throws(IllegalArgumentException::class) + fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { + for (sig in supportedSignatureSchemes.values) { + try { + return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + } catch (ikse: InvalidKeySpecException) { + // ignore it - only used to bypass the scheme that causes an exception. + } + } + throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.") + } + + /** + * Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name. + * This will be used by Kryo deserialisation. + * @param encodedKey a PKCS8 encoded private key. + * @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for this key factory to produce a private key. + */ + @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + fun decodePrivateKey(encodedKey: ByteArray, schemeCodeName: String): PrivateKey { + val sig = findSignatureScheme(schemeCodeName) + try { + return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + } catch (ikse: InvalidKeySpecException) { + throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse) + } + } + + /** + * Decode an X509 encoded key to its [PublicKey] object. + * @param encodedKey an X509 encoded public key. + * @throws UnsupportedSchemeException on not supported scheme. + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for this key factory to produce a private key. + */ + @Throws(IllegalArgumentException::class) + fun decodePublicKey(encodedKey: ByteArray): PublicKey { + for (sig in supportedSignatureSchemes.values) { + try { + return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) + } catch (ikse: InvalidKeySpecException) { + // ignore it - only used to bypass the scheme that causes an exception. + } + } + throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.") + } + + /** + * Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name. + * This will be used by Kryo deserialisation. + * @param encodedKey an X509 encoded public key. + * @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). + * @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. + */ + @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + fun decodePublicKey(encodedKey: ByteArray, schemeCodeName: String): PublicKey { + val sig = findSignatureScheme(schemeCodeName) + try { + return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) + } catch (ikse: InvalidKeySpecException) { + throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse) + } + } + + /** + * Utility to simplify the act of generating keys. + * Normally, we don't expect other errors here, assuming that key generation parameters for every supported signature scheme have been unit-tested. + * @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256). + * @return a KeyPair for the requested scheme. + * @throws IllegalArgumentException if the requested signature scheme is not supported. + */ + @Throws(IllegalArgumentException::class) + fun generateKeyPair(schemeCodeName: String): KeyPair = findSignatureScheme(schemeCodeName).keyPairGenerator.generateKeyPair() + + /** + * Generate a KeyPair using the default signature scheme. + * @return a new KeyPair. + */ + fun generateKeyPair(): KeyPair = DEFAULT_SIGNATURE_SCHEME.keyPairGenerator.generateKeyPair() + + /** + * Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based + * on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray). + * @param privateKey the signer's [PrivateKey]. + * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the digital signature (in [ByteArray]) on the input message. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ + @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey).sig, privateKey, clearData) + + /** + * Generic way to sign [ByteArray] data with a [PrivateKey] and a known schemeCodeName [String]. + * @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256). + * @param privateKey the signer's [PrivateKey]. + * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the digital signature (in [ByteArray]) on the input message. + * @throws IllegalArgumentException if the signature scheme is not supported. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ + @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName).sig, privateKey, clearData) + + /** + * Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature]. + * @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto]. + * @param privateKey the signer's [PrivateKey]. + * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the digital signature (in [ByteArray]) on the input message. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ + @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + private fun doSign(signature: Signature, privateKey: PrivateKey, clearData: ByteArray): ByteArray { + if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!") + signature.initSign(privateKey) + signature.update(clearData) + return signature.sign() + } + + /** + * Generic way to sign [MetaData] objects with a [PrivateKey]. + * [MetaData] is a wrapper over the transaction's Merkle root in order to attach extra information, such as a timestamp or partial and blind signature indicators. + * @param privateKey the signer's [PrivateKey]. + * @param metaData a [MetaData] object that adds extra information to a transaction. + * @return a [TransactionSignature] object than contains the output of a successful signing and the metaData. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key or + * if metaData.schemeCodeName is not aligned with key type. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ + @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + fun doSign(privateKey: PrivateKey, metaData: MetaData): TransactionSignature { + val sigKey: SignatureScheme = findSignatureScheme(privateKey) + val sigMetaData: SignatureScheme = findSignatureScheme(metaData.schemeCodeName) + if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${metaData.schemeCodeName} is not aligned with the key type.") + val signatureData = doSign(sigKey.schemeCodeName, privateKey, metaData.bytes()) + return TransactionSignature(signatureData, metaData) + } + + /** + * Utility to simplify the act of verifying a digital signature. + * It returns true if it succeeds, but it always throws an exception if verification fails. + * @param publicKey the signer's [PublicKey]. + * @param signatureData the signatureData on a message. + * @param clearData the clear data/message that was signed (usually the Merkle root). + * @return true if verification passes or throws an exception if verification fails. + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. + * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. + */ + @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName).sig, publicKey, signatureData, clearData) + + /** + * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. + * It returns true if it succeeds, but it always throws an exception if verification fails. + * Strategy on identifying the actual signing scheme is based on the [PublicKey] type, but if the schemeCodeName is known, + * then better use doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray). + * @param publicKey the signer's [PublicKey]. + * @param signatureData the signatureData on a message. + * @param clearData the clear data/message that was signed (usually the Merkle root). + * @return true if verification passes or throws an exception if verification fails. + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. + * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. + */ + @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey).sig, publicKey, signatureData, clearData) + + /** + * Method to verify a digital signature. + * It returns true if it succeeds, but it always throws an exception if verification fails. + * @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto]. + * @param publicKey the signer's [PublicKey]. + * @param signatureData the signatureData on a message. + * @param clearData the clear data/message that was signed (usually the Merkle root). + * @return true if verification passes or throws an exception if verification fails. + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. + * @throws IllegalArgumentException if any of the clear or signature data is empty. + */ + private fun doVerify(signature: Signature, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!") + if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") + signature.initVerify(publicKey) + signature.update(clearData) + val verificationResult = signature.verify(signatureData) + if (verificationResult) { + return true + } else { + throw SignatureException("Signature Verification failed!") + } + } + + /** + * Utility to simplify the act of verifying a [TransactionSignature]. + * It returns true if it succeeds, but it always throws an exception if verification fails. + * @param publicKey the signer's [PublicKey]. + * @param transactionSignature the signatureData on a message. + * @return true if verification passes or throws an exception if verification fails. + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. + * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. + */ + @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()}") + return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes()) + } + + /** + * Check if the requested signature scheme is supported by the system. + * @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256). + * @return true if the signature scheme is supported. + */ + fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes + + /** @return the default signature scheme's code name. */ + fun getDefaultSignatureSchemeCodeName(): String = DEFAULT_SIGNATURE_SCHEME.schemeCodeName + + /** @return a [List] of Strings with the scheme code names defined in [SignatureScheme] for all of our supported signature schemes, see [Crypto]. */ + fun listSupportedSignatureSchemes(): List = supportedSignatureSchemes.keys.toList() +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt new file mode 100644 index 0000000000..197a7a2777 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -0,0 +1,117 @@ +package net.corda.core.crypto + +import java.security.* + +/** + * Helper function for signing. + * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the digital signature (in [ByteArray]) on the input message. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ +@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +fun PrivateKey.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this, clearData) + +/** + * Helper function for signing. + * @param metaDataFull tha attached MetaData object. + * @return a [DSWithMetaDataFull] object. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ +@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(this, metaData) + +/** + * Helper function to sign with a key pair. + * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). + * @return the digital signature (in [ByteArray]) on the input message. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key. + * @throws InvalidKeyException if the private key is invalid. + * @throws SignatureException if signing is not possible due to malformed data or private key. + */ +@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) +fun KeyPair.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this.private, clearData) + +/** + * Helper function to verify a signature. + * @param signatureData the signature on a message. + * @param clearData the clear data/message that was signed (usually the Merkle root). + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData algorithm is unable to process the input data provided, etc. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. + */ +@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData) + +/** + * Helper function to verify a metadata attached signature. It is noted that the transactionSignature contains + * signatureData and a [MetaData] object that contains the signer's public key and the transaction's Merkle root. + * @param transactionSignature a [TransactionSignature] object that . + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData algorithm is unable to process the input data provided, etc. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. + */ +@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +fun PublicKey.verify(transactionSignature: TransactionSignature): Boolean { + return Crypto.doVerify(this, transactionSignature) +} + +/** + * Helper function for the signers to verify their own signature. + * @param signature the signature on a message. + * @param clearData the clear data/message that was signed (usually the Merkle root). + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData algorithm is unable to process the input data provided, etc. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. + */ +@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) +fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData) + +/** + * Generate a securely random [ByteArray] of requested number of bytes. Usually used for seeds, nonces and keys. + * @param numOfBytes how many random bytes to output. + * @return a random [ByteArray]. + * @throws NoSuchAlgorithmException thrown if "NativePRNGNonBlocking" is not supported on the JVM + * or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, + * which should never happen and suggests an unusual JVM or non-standard Java library. + */ +@Throws(NoSuchAlgorithmException::class) +fun safeRandomBytes(numOfBytes: Int): ByteArray { + return safeRandom().generateSeed(numOfBytes) +} + +/** + * Get an instance of [SecureRandom] to avoid blocking, due to waiting for additional entropy, when possible. + * In this version, the NativePRNGNonBlocking is exclusively used on Linux OS to utilize dev/urandom because in high traffic + * /dev/random may wait for a certain amount of "noise" to be generated on the host machine before returning a result. + * + * On Solaris, Linux, and OS X, if the entropy gathering device in java.security is set to file:/dev/urandom + * or file:/dev/random, then NativePRNG is preferred to SHA1PRNG. Otherwise, SHA1PRNG is preferred. + * @see SecureRandom Implementation. + * + * If both dev/random and dev/urandom are available, then dev/random is only preferred over dev/urandom during VM boot + * where it may be possible that OS didn't yet collect enough entropy to fill the randomness pool for the 1st time. + * @see Myths about urandom for a more descriptive explanation on /dev/random Vs /dev/urandom. + * TODO: check default settings per OS and random/urandom availability. + * @return a [SecureRandom] object. + * @throws NoSuchAlgorithmException thrown if "NativePRNGNonBlocking" is not supported on the JVM + * or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, + * which should never happen and suggests an unusual JVM or non-standard Java library. + */ +@Throws(NoSuchAlgorithmException::class) +fun safeRandom(): SecureRandom { + if (System.getProperty("os.name") == "Linux") { + return SecureRandom.getInstance("NativePRNGNonBlocking") + } else { + return SecureRandom.getInstanceStrong() + } +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/EdDSAKeyFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/EdDSAKeyFactory.kt new file mode 100644 index 0000000000..2cd69f6737 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/EdDSAKeyFactory.kt @@ -0,0 +1,12 @@ +package net.corda.core.crypto + +import java.security.KeyFactory + +/** + * Custom [KeyFactory] for EdDSA with null security [Provider]. + * This is required as a [SignatureScheme] requires a [java.security.KeyFactory] property, but i2p has + * its own KeyFactory for EdDSA, thus this actually a Proxy Pattern over i2p's KeyFactory. + */ +class EdDSAKeyFactory: KeyFactory { + constructor() : super(net.i2p.crypto.eddsa.KeyFactory(), null, "EDDSA_ED25519_SHA512") +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt b/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt new file mode 100644 index 0000000000..a8dc49ae8e --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt @@ -0,0 +1,71 @@ +package net.corda.core.crypto + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.opaque +import net.corda.core.serialization.serialize +import java.security.PublicKey +import java.time.Instant +import java.util.* + +/** + * A [MetaData] object adds extra information to a transaction. MetaData is used to support a universal + * digital signature model enabling full, partial, fully or partially blind and metaData attached signatures, + * (such as an attached timestamp). A MetaData object contains both the merkle root of the transaction and the signer's public key. + * When signatureType is set to FULL, then visibleInputs and signedInputs can be ignored. + * Note: We could omit signatureType as it can always be defined by combining visibleInputs and signedInputs, + * but it helps to speed up the process when FULL is used, and thus we can bypass the extra check on boolean arrays. + * + * @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256). + * @param versionID DLT's version. + * @param signatureType type of the signature, see [SignatureType] (e.g. FULL, PARTIAL, BLIND, PARTIAL_AND_BLIND). + * @param timestamp the signature's timestamp as provided by the signer. + * @param visibleInputs for partially/fully blind signatures. We use Merkle tree boolean index flags (from left to right) + * indicating what parts of the transaction were visible when the signature was calculated. + * @param signedInputs for partial signatures. We use Merkle tree boolean index flags (from left to right) + * indicating what parts of the Merkle tree are actually signed. + * @param merkleRoot the Merkle root of the transaction. + * @param publicKey the signer's public key. + */ +@CordaSerializable +open class MetaData( + val schemeCodeName: String, + val versionID: String, + val signatureType: SignatureType = SignatureType.FULL, + val timestamp: Instant?, + val visibleInputs: BitSet?, + val signedInputs: BitSet?, + val merkleRoot: ByteArray, + val publicKey: PublicKey) { + + fun bytes() = this.serialize().bytes + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as MetaData + + if (schemeCodeName != other.schemeCodeName) return false + if (versionID != other.versionID) return false + if (signatureType != other.signatureType) return false + if (timestamp != other.timestamp) return false + if (visibleInputs != other.visibleInputs) return false + if (signedInputs != other.signedInputs) return false + if (merkleRoot.opaque() != other.merkleRoot.opaque()) return false + if (publicKey != other.publicKey) return false + return true + } + + override fun hashCode(): Int { + var result = schemeCodeName.hashCode() + result = 31 * result + versionID.hashCode() + result = 31 * result + signatureType.hashCode() + result = 31 * result + (timestamp?.hashCode() ?: 0) + result = 31 * result + (visibleInputs?.hashCode() ?: 0) + result = 31 * result + (signedInputs?.hashCode() ?: 0) + result = 31 * result + Arrays.hashCode(merkleRoot) + result = 31 * result + publicKey.hashCode() + return result + } +} + diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt new file mode 100644 index 0000000000..8c86c84074 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -0,0 +1,42 @@ +package net.corda.core.crypto + +import java.security.* +import java.security.spec.AlgorithmParameterSpec + +/** + * This class is used to define a digital signature scheme. + * @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes. + * @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512). + * @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256). + * @param sig the [Signature] class that provides the functionality of a digital signature scheme. + * eg. Signature.getInstance("SHA256withECDSA", "BC"). + * @param keyFactory the KeyFactory for this scheme (e.g. KeyFactory.getInstance("RSA", "BC")). + * @param keyPairGenerator defines the Service Provider Interface (SPI) for the {@code KeyPairGenerator} class. + * e.g. KeyPairGenerator.getInstance("ECDSA", "BC"). + * @param algSpec parameter specs for the underlying algorithm. Note that RSA is defined by the key size rather than algSpec. + * eg. ECGenParameterSpec("secp256k1"). + * @param keySize the private key size (currently used for RSA only). + * @param desc a human-readable description for this scheme. + */ +data class SignatureScheme( + val schemeNumberID: Int, + val schemeCodeName: String, + val algorithmName: String, + val sig: Signature, + val keyFactory: KeyFactory, + val keyPairGenerator: KeyPairGeneratorSpi, + val algSpec: AlgorithmParameterSpec?, + val keySize: Int, + val desc: String) { + + /** + * KeyPair generators are always initialized once we create them, as no re-initialization is required. + * Note that RSA is the sole algorithm initialized specifically by its supported keySize. + */ + init { + if (algSpec != null) + keyPairGenerator.initialize(algSpec, safeRandom()) + else + keyPairGenerator.initialize(keySize, safeRandom()) + } +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureType.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureType.kt new file mode 100644 index 0000000000..5d94f8d9e4 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureType.kt @@ -0,0 +1,17 @@ +package net.corda.core.crypto + +import net.corda.core.serialization.CordaSerializable + +/** + * Supported Signature types: + *

+ */ +@CordaSerializable +enum class SignatureType { + FULL, PARTIAL, BLIND, PARTIAL_AND_BLIND +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt new file mode 100644 index 0000000000..8ba892f911 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt @@ -0,0 +1,24 @@ +package net.corda.core.crypto + +import net.corda.core.serialization.opaque +import java.security.InvalidKeyException +import java.security.PublicKey +import java.security.SignatureException + +/** + * A wrapper around a digital signature accompanied with metadata, see [MetaData.Full] and [DigitalSignature]. + * The signature protocol works as follows: s = sign(MetaData.hashBytes). + */ +open class TransactionSignature(val signatureData: ByteArray, val metaData: MetaData) : DigitalSignature(signatureData) { + /** + * Function to auto-verify a [MetaData] object's signature. + * Note that [MetaData] contains both public key and merkle root of the transaction. + * @throws InvalidKeyException if the key is invalid. + * @throws SignatureException if this signatureData object is not initialized properly, + * the passed-in signatureData is improperly encoded or of the wrong type, + * if this signatureData algorithm is unable to process the input data provided, etc. + * @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty. + */ + @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + fun verify(): Boolean = Crypto.doVerify(metaData.publicKey, signatureData, metaData.bytes()) +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index b9ef3e23d2..d961cbe195 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -8,6 +8,7 @@ import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.guava.* import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.MetaData import net.corda.core.node.CordaPluginRegistry import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction @@ -74,6 +75,9 @@ object DefaultKryoCustomizer { addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) + register(MetaData::class.java, MetaDataSerializer) + register(BitSet::class.java, ReferencesAwareJavaSerializer) + val customization = KryoSerializationCustomization(this) pluginRegistries.forEach { it.customizeSerialization(customization) } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index b86e8147da..cb6d20523c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -20,6 +20,8 @@ import java.lang.reflect.InvocationTargetException import java.nio.file.Files import java.nio.file.Path import java.security.PublicKey +import java.security.spec.InvalidKeySpecException +import java.time.Instant import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.reflect.* @@ -551,3 +553,32 @@ object OrderedSerializer : Serializer>() { return hm } } + +/** For serialising a MetaData object. */ +@ThreadSafe +object MetaDataSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: MetaData) { + output.writeString(obj.schemeCodeName) + output.writeString(obj.versionID) + kryo.writeClassAndObject(output, obj.signatureType) + kryo.writeClassAndObject(output, obj.timestamp) + kryo.writeClassAndObject(output, obj.visibleInputs) + kryo.writeClassAndObject(output, obj.signedInputs) + output.writeBytesWithLength(obj.merkleRoot) + output.writeBytesWithLength(obj.publicKey.encoded) + } + + @Suppress("UNCHECKED_CAST") + @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + override fun read(kryo: Kryo, input: Input, type: Class): MetaData { + val schemeCodeName = input.readString() + val versionID = input.readString() + val signatureType = kryo.readClassAndObject(input) as SignatureType + val timestamp = kryo.readClassAndObject(input) as Instant? + val visibleInputs = kryo.readClassAndObject(input) as BitSet? + val signedInputs = kryo.readClassAndObject(input) as BitSet? + val merkleRoot = input.readBytesWithLength() + val publicKey = Crypto.decodePublicKey(input.readBytesWithLength(), schemeCodeName) + return MetaData(schemeCodeName, versionID, signatureType, timestamp, visibleInputs, signedInputs, merkleRoot, publicKey) + } +} diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt new file mode 100644 index 0000000000..565992c896 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -0,0 +1,656 @@ +package net.corda.core.crypto + +import com.google.common.collect.Sets +import net.i2p.crypto.eddsa.EdDSAKey +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.interfaces.ECKey +import org.bouncycastle.jce.provider.BouncyCastleProvider +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.junit.Assert.assertNotEquals +import org.junit.Test +import java.security.KeyFactory +import java.security.Security +import java.util.* +import java.security.spec.* +import kotlin.test.* + +/** + * Run tests for cryptographic algorithms + */ +class CryptoUtilsTest { + + init { + Security.addProvider(BouncyCastleProvider()) + Security.addProvider(BouncyCastlePQCProvider()) + } + + val testString = "Hello World" + val testBytes = testString.toByteArray() + + // key generation test + @Test + fun `Generate key pairs`() { + // testing supported algorithms + val rsaKeyPair = Crypto.generateKeyPair("RSA_SHA256") + val ecdsaKKeyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val ecdsaRKeyPair = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256") + val eddsaKeyPair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") + val sphincsKeyPair = Crypto.generateKeyPair("SPHINCS-256_SHA512") + + // not null private keys + assertNotNull(rsaKeyPair.private); + assertNotNull(ecdsaKKeyPair.private); + assertNotNull(ecdsaRKeyPair.private); + assertNotNull(eddsaKeyPair.private); + assertNotNull(sphincsKeyPair.private); + + // not null public keys + assertNotNull(rsaKeyPair.public); + assertNotNull(ecdsaKKeyPair.public); + assertNotNull(ecdsaRKeyPair.public); + assertNotNull(eddsaKeyPair.public); + assertNotNull(sphincsKeyPair.public); + + // fail on unsupported algorithm + try { + val wrongKeyPair = Crypto.generateKeyPair("WRONG_ALG") + fail() + } catch (e: Exception) { + // expected + } + } + + // full process tests + + @Test + fun `RSA full process keygen-sign-verify`() { + + val keyPair = Crypto.generateKeyPair("RSA_SHA256") + + // test for some data + val signedData = keyPair.sign(testBytes) + val verification = keyPair.verify(signedData, testBytes) + assertTrue(verification) + + // test for empty data signing + try { + keyPair.sign(ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty source data when verifying + try { + keyPair.verify(testBytes, ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty signed data when verifying + try { + keyPair.verify(ByteArray(0), testBytes) + fail() + } catch (e: Exception) { + // expected + } + + // test for zero bytes data + val signedDataZeros = keyPair.sign(ByteArray(100)) + val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100)) + assertTrue(verificationZeros) + + // test for 1MB of data (I successfully tested it locally for 1GB as well) + val MBbyte = ByteArray(1000000) // 1.000.000 + Random().nextBytes(MBbyte) + val signedDataBig = keyPair.sign(MBbyte) + val verificationBig = keyPair.verify(signedDataBig, MBbyte) + assertTrue(verificationBig) + + // test on malformed signatures (even if they change for 1 bit) + for (i in 0..signedData.size - 1) { + val b = signedData.get(i) + signedData.set(i,b.inc()) + try { + keyPair.verify(signedData, testBytes) + fail() + } catch (e: Exception) { + // expected + } + signedData.set(i,b.dec()) + } + } + + @Test + fun `ECDSA secp256k1 full process keygen-sign-verify`() { + + val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + + // test for some data + val signedData = keyPair.sign(testBytes) + val verification = keyPair.verify(signedData, testBytes) + assertTrue(verification) + + // test for empty data signing + try { + keyPair.sign(ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty source data when verifying + try { + keyPair.verify(testBytes, ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty signed data when verifying + try { + keyPair.verify(ByteArray(0), testBytes) + fail() + } catch (e: Exception) { + // expected + } + + // test for zero bytes data + val signedDataZeros = keyPair.sign(ByteArray(100)) + val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100)) + assertTrue(verificationZeros) + + // test for 1MB of data (I successfully tested it locally for 1GB as well) + val MBbyte = ByteArray(1000000) // 1.000.000 + Random().nextBytes(MBbyte) + val signedDataBig = keyPair.sign(MBbyte) + val verificationBig = keyPair.verify(signedDataBig, MBbyte) + assertTrue(verificationBig) + + // test on malformed signatures (even if they change for 1 bit) + signedData.set(0,signedData[0].inc()) + try { + keyPair.verify(signedData, testBytes) + fail() + } catch (e: Exception) { + // expected + } + } + + @Test + fun `ECDSA secp256r1 full process keygen-sign-verify`() { + + val keyPair = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256") + + // test for some data + val signedData = keyPair.sign(testBytes) + val verification = keyPair.verify(signedData, testBytes) + assertTrue(verification) + + // test for empty data signing + try { + keyPair.sign(ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty source data when verifying + try { + keyPair.verify(testBytes, ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty signed data when verifying + try { + keyPair.verify(ByteArray(0), testBytes) + fail() + } catch (e: Exception) { + // expected + } + + // test for zero bytes data + val signedDataZeros = keyPair.sign(ByteArray(100)) + val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100)) + assertTrue(verificationZeros) + + // test for 1MB of data (I successfully tested it locally for 1GB as well) + val MBbyte = ByteArray(1000000) // 1.000.000 + Random().nextBytes(MBbyte) + val signedDataBig = keyPair.sign(MBbyte) + val verificationBig = keyPair.verify(signedDataBig, MBbyte) + assertTrue(verificationBig) + + // test on malformed signatures (even if they change for 1 bit) + signedData.set(0, signedData[0].inc()) + try { + keyPair.verify(signedData, testBytes) + fail() + } catch (e: Exception) { + // expected + } + } + + @Test + fun `EDDSA ed25519 full process keygen-sign-verify`() { + + val keyPair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") + + // test for some data + val signedData = keyPair.sign(testBytes) + val verification = keyPair.verify(signedData, testBytes) + assertTrue(verification) + + // test for empty data signing + try { + keyPair.sign(ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty source data when verifying + try { + keyPair.verify(testBytes, ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty signed data when verifying + try { + keyPair.verify(ByteArray(0), testBytes) + fail() + } catch (e: Exception) { + // expected + } + + // test for zero bytes data + val signedDataZeros = keyPair.sign(ByteArray(100)) + val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100)) + assertTrue(verificationZeros) + + // test for 1MB of data (I successfully tested it locally for 1GB as well) + val MBbyte = ByteArray(1000000) // 1.000.000 + Random().nextBytes(MBbyte) + val signedDataBig = keyPair.sign(MBbyte) + val verificationBig = keyPair.verify(signedDataBig, MBbyte) + assertTrue(verificationBig) + + // test on malformed signatures (even if they change for 1 bit) + signedData.set(0, signedData[0].inc()) + try { + keyPair.verify(signedData, testBytes) + fail() + } catch (e: Exception) { + // expected + } + } + + @Test + fun `SPHINCS-256 full process keygen-sign-verify`() { + + val keyPair = Crypto.generateKeyPair("SPHINCS-256_SHA512") + + // test for some data + val signedData = keyPair.sign(testBytes) + val verification = keyPair.verify(signedData, testBytes) + assertTrue(verification) + + // test for empty data signing + try { + keyPair.sign(ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty source data when verifying + try { + keyPair.verify(testBytes, ByteArray(0)) + fail() + } catch (e: Exception) { + // expected + } + + // test for empty signed data when verifying + try { + keyPair.verify(ByteArray(0), testBytes) + fail() + } catch (e: Exception) { + // expected + } + + // test for zero bytes data + val signedDataZeros = keyPair.sign(ByteArray(100)) + val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100)) + assertTrue(verificationZeros) + + // test for 1MB of data (I successfully tested it locally for 1GB as well) + val MBbyte = ByteArray(1000000) // 1.000.000 + Random().nextBytes(MBbyte) + val signedDataBig = keyPair.sign(MBbyte) + val verificationBig = keyPair.verify(signedDataBig, MBbyte) + assertTrue(verificationBig) + + // test on malformed signatures (even if they change for 1 bit) + signedData.set(0, signedData[0].inc()) + try { + keyPair.verify(signedData, testBytes) + fail() + } catch (e: Exception) { + // expected + } + } + + // test list of supported algorithms + @Test + fun `Check supported algorithms`() { + val algList : List = Crypto.listSupportedSignatureSchemes() + val expectedAlgSet = setOf("RSA_SHA256","ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512","SPHINCS-256_SHA512") + assertTrue { Sets.symmetricDifference(expectedAlgSet,algList.toSet()).isEmpty(); } + } + + // Unfortunately, there isn't a standard way to encode/decode keys, so we need to test per case + @Test + fun `RSA encode decode keys - required for serialization`() { + // Generate key pair. + val keyPair = Crypto.generateKeyPair("RSA_SHA256") + val (privKey, pubKey) = keyPair + + val keyFactory = KeyFactory.getInstance("RSA", "BC") + + // Encode and decode private key. + val privKey2 = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded)) + assertEquals(privKey2, privKey) + + // Encode and decode public key. + val pubKey2 = keyFactory.generatePublic(X509EncodedKeySpec(pubKey.encoded)) + assertEquals(pubKey2, pubKey) + } + + @Test + fun `ECDSA secp256k1 encode decode keys - required for serialization`() { + // Generate key pair. + val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val (privKey, pubKey) = keyPair + + val kf = KeyFactory.getInstance("ECDSA", "BC") + + // Encode and decode private key. + val privKey2 = kf.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded)) + assertEquals(privKey2, privKey) + + // Encode and decode public key. + val pubKey2 = kf.generatePublic(X509EncodedKeySpec(pubKey.encoded)) + assertEquals(pubKey2, pubKey) + } + + @Test + fun `ECDSA secp256r1 encode decode keys - required for serialization`() { + // Generate key pair. + val keyPair = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256") + val (privKey, pubKey) = keyPair + + val kf = KeyFactory.getInstance("ECDSA", "BC") + + // Encode and decode private key. + val privKey2 = kf.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded)) + assertEquals(privKey2, privKey) + + // Encode and decode public key. + val pubKey2 = kf.generatePublic(X509EncodedKeySpec(pubKey.encoded)) + assertEquals(pubKey2, pubKey) + } + + @Test + fun `EdDSA encode decode keys - required for serialization`() { + // Generate key pair. + val keyPair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") + val privKey: EdDSAPrivateKey = keyPair.private as EdDSAPrivateKey + val pubKey: EdDSAPublicKey = keyPair.public as EdDSAPublicKey + + val kf = EdDSAKeyFactory() + + // Encode and decode private key. + val privKey2 = kf.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded)) + assertEquals(privKey2, privKey) + + // Encode and decode public key. + val pubKey2 = kf.generatePublic(X509EncodedKeySpec(pubKey.encoded)) + assertEquals(pubKey2, pubKey) + } + + @Test + fun `SPHINCS-256 encode decode keys - required for serialization`() { + // Generate key pair. + val keyPair = Crypto.generateKeyPair("SPHINCS-256_SHA512") + val privKey: BCSphincs256PrivateKey = keyPair.private as BCSphincs256PrivateKey + val pubKey: BCSphincs256PublicKey = keyPair.public as BCSphincs256PublicKey + + //1st method for encoding/decoding + + val keyFactory = KeyFactory.getInstance("SPHINCS256", "BCPQC") + + // Encode and decode private key. + val privKey2 = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded)) + assertEquals(privKey2, privKey) + + // Encode and decode public key. + val pubKey2 = keyFactory.generatePublic(X509EncodedKeySpec(pubKey.encoded)) + assertEquals(pubKey2, pubKey) + + //2nd method for encoding/decoding + + // Encode and decode private key. + val privKeyInfo : PrivateKeyInfo = PrivateKeyInfo.getInstance(privKey.encoded) + val decodedPrivKey = BCSphincs256PrivateKey(privKeyInfo) + // Check that decoded private key is equal to the initial one. + assertEquals(decodedPrivKey, privKey) + + // Encode and decode public key. + val pubKeyInfo : SubjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(pubKey.encoded) + val extractedPubKey = BCSphincs256PublicKey(pubKeyInfo) + // Check that decoded private key is equal to the initial one. + assertEquals(extractedPubKey, pubKey) + } + + @Test + fun `RSA scheme finder by key type`() { + val keyPairRSA = Crypto.generateKeyPair("RSA_SHA256") + val (privRSA, pubRSA) = keyPairRSA + assertEquals(privRSA.algorithm, "RSA") + assertEquals(pubRSA.algorithm, "RSA") + } + + @Test + fun `ECDSA secp256k1 scheme finder by key type`() { + val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val (privK1, pubK1) = keyPairK1 + + // Encode and decode keys as they would be transferred. + val kf = KeyFactory.getInstance("ECDSA", "BC") + val privK1Decoded = kf.generatePrivate(PKCS8EncodedKeySpec(privK1.encoded)) + val pubK1Decoded = kf.generatePublic(X509EncodedKeySpec(pubK1.encoded)) + + assertEquals(privK1Decoded.algorithm, "ECDSA") + assertEquals((privK1Decoded as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1")) + assertEquals(pubK1Decoded.algorithm, "ECDSA") + assertEquals((pubK1Decoded as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1")) + } + + @Test + fun `ECDSA secp256r1 scheme finder by key type`() { + val keyPairR1 = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256") + val (privR1, pubR1) = keyPairR1 + assertEquals(privR1.algorithm, "ECDSA") + assertEquals((privR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) + assertEquals(pubR1.algorithm, "ECDSA") + assertEquals((pubR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) + } + + @Test + fun `EdDSA scheme finder by key type`() { + val keyPairEd = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") + val (privEd, pubEd) = keyPairEd + + assertEquals(privEd.algorithm, "EdDSA") + assertEquals((privEd as EdDSAKey).params, EdDSANamedCurveTable.getByName("ed25519-sha-512")) + assertEquals(pubEd.algorithm, "EdDSA") + assertEquals((pubEd as EdDSAKey).params, EdDSANamedCurveTable.getByName("ed25519-sha-512")) + } + + @Test + fun `SPHINCS-256 scheme finder by key type`() { + val keyPairSP = Crypto.generateKeyPair("SPHINCS-256_SHA512") + val (privSP, pubSP) = keyPairSP + assertEquals(privSP.algorithm, "SPHINCS-256") + assertEquals(pubSP.algorithm, "SPHINCS-256") + } + + @Test + fun `Automatic EdDSA key-type detection and decoding`() { + val keyPairEd = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") + val (privEd, pubEd) = keyPairEd + val encodedPrivEd = privEd.encoded + val encodedPubEd = pubEd.encoded + + val decodedPrivEd = Crypto.decodePrivateKey(encodedPrivEd) + assertEquals(decodedPrivEd.algorithm, "EdDSA") + assertEquals(decodedPrivEd, privEd) + + val decodedPubEd = Crypto.decodePublicKey(encodedPubEd) + assertEquals(decodedPubEd.algorithm, "EdDSA") + assertEquals(decodedPubEd, pubEd) + } + + @Test + fun `Automatic ECDSA secp256k1 key-type detection and decoding`() { + val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val (privK1, pubK1) = keyPairK1 + val encodedPrivK1 = privK1.encoded + val encodedPubK1 = pubK1.encoded + + val decodedPrivK1 = Crypto.decodePrivateKey(encodedPrivK1) + assertEquals(decodedPrivK1.algorithm, "ECDSA") + assertEquals(decodedPrivK1, privK1) + + val decodedPubK1 = Crypto.decodePublicKey(encodedPubK1) + assertEquals(decodedPubK1.algorithm, "ECDSA") + assertEquals(decodedPubK1, pubK1) + } + + @Test + fun `Automatic ECDSA secp256r1 key-type detection and decoding`() { + val keyPairR1 = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256") + val (privR1, pubR1) = keyPairR1 + val encodedPrivR1 = privR1.encoded + val encodedPubR1 = pubR1.encoded + + val decodedPrivR1 = Crypto.decodePrivateKey(encodedPrivR1) + assertEquals(decodedPrivR1.algorithm, "ECDSA") + assertEquals(decodedPrivR1, privR1) + + val decodedPubR1 = Crypto.decodePublicKey(encodedPubR1) + assertEquals(decodedPubR1.algorithm, "ECDSA") + assertEquals(decodedPubR1, pubR1) + } + + @Test + fun `Automatic RSA key-type detection and decoding`() { + val keyPairRSA = Crypto.generateKeyPair("RSA_SHA256") + val (privRSA, pubRSA) = keyPairRSA + val encodedPrivRSA = privRSA.encoded + val encodedPubRSA = pubRSA.encoded + + val decodedPrivRSA = Crypto.decodePrivateKey(encodedPrivRSA) + assertEquals(decodedPrivRSA.algorithm, "RSA") + assertEquals(decodedPrivRSA, privRSA) + + val decodedPubRSA = Crypto.decodePublicKey(encodedPubRSA) + assertEquals(decodedPubRSA.algorithm, "RSA") + assertEquals(decodedPubRSA, pubRSA) + } + + @Test + fun `Automatic SPHINCS-256 key-type detection and decoding`() { + val keyPairSP = Crypto.generateKeyPair("SPHINCS-256_SHA512") + val (privSP, pubSP) = keyPairSP + val encodedPrivSP = privSP.encoded + val encodedPubSP = pubSP.encoded + + val decodedPrivSP = Crypto.decodePrivateKey(encodedPrivSP) + assertEquals(decodedPrivSP.algorithm, "SPHINCS-256") + assertEquals(decodedPrivSP, privSP) + + val decodedPubSP = Crypto.decodePublicKey(encodedPubSP) + assertEquals(decodedPubSP.algorithm, "SPHINCS-256") + assertEquals(decodedPubSP, pubSP) + } + + @Test + fun `Failure test between K1 and R1 keys`() { + val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val (privK1, pubK1) = keyPairK1 + val encodedPrivK1 = privK1.encoded + val decodedPrivK1 = Crypto.decodePrivateKey(encodedPrivK1) + + val keyPairR1 = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256") + val (privR1, pubR1) = keyPairR1 + val encodedPrivR1 = privR1.encoded + val decodedPrivR1 = Crypto.decodePrivateKey(encodedPrivR1) + + assertNotEquals(decodedPrivK1, decodedPrivR1) + } + + @Test + fun `Decoding Failure on randomdata as key`() { + val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val (privK1, pubK1) = keyPairK1 + val encodedPrivK1 = privK1.encoded + + // Test on random encoded bytes. + val fakeEncodedKey = ByteArray(encodedPrivK1.size) + val r = Random() + r.nextBytes(fakeEncodedKey) + + // fail on fake key. + try { + val decodedFake = Crypto.decodePrivateKey(fakeEncodedKey) + fail() + } catch (e: Exception) { + // expected + } + } + + @Test + fun `Decoding Failure on malformed keys`() { + val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val (privK1, pubK1) = keyPairK1 + val encodedPrivK1 = privK1.encoded + + // fail on malformed key. + for (i in 0..encodedPrivK1.size - 1) { + val b = encodedPrivK1.get(i) + encodedPrivK1.set(i,b.inc()) + try { + val decodedFake = Crypto.decodePrivateKey(encodedPrivK1) + println("OK") + fail() + } catch (e: Exception) { + // expected + } + encodedPrivK1.set(i,b.dec()) + } + } +} diff --git a/core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt index 9b3a0144d3..3bbfa816fd 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt @@ -1,10 +1,7 @@ package net.corda.core.crypto import org.junit.Test -import java.math.BigInteger -import java.util.* import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlin.test.fail class EncodingUtilsTest { diff --git a/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt b/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt new file mode 100644 index 0000000000..c18d86b5ff --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt @@ -0,0 +1,83 @@ +package net.corda.core.crypto + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider +import org.junit.Test +import java.security.Security +import java.security.SignatureException +import java.time.Instant +import kotlin.test.assertTrue +import kotlin.test.fail + +/** + * Digital signature MetaData tests + */ +class TransactionSignatureTest { + + init { + Security.addProvider(BouncyCastleProvider()) + Security.addProvider(BouncyCastlePQCProvider()) + } + + val testBytes = "12345678901234567890123456789012".toByteArray() + + /** valid sign and verify. */ + @Test + fun `MetaData Full sign and verify`() { + val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + + // create a MetaData.Full object + val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair.public) + + // sign the message + val transactionSignature: TransactionSignature = keyPair.private.sign(meta) + + // check auto-verification + assertTrue(transactionSignature.verify()) + + // check manual verification + assertTrue(keyPair.public.verify(transactionSignature)) + } + + /** Signing should fail, as I sign with a secpK1 key, but set schemeCodeName is set to secpR1. */ + @Test(expected = IllegalArgumentException::class) + fun `MetaData Full failure wrong scheme`() { + val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val meta = MetaData("ECDSA_SECP256R1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair.public) + keyPair.private.sign(meta) + } + + /** Verification should fail; corrupted metadata - public key has changed. */ + @Test(expected = SignatureException::class) + fun `MetaData Full failure public key has changed`() { + val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val keyPair2 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair2.public) + val transactionSignature = keyPair1.private.sign(meta) + transactionSignature.verify() + } + + /** Verification should fail; corrupted metadata - clearData has changed. */ + @Test(expected = SignatureException::class) + fun `MetaData Full failure clearData has changed`() { + val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair1.public) + val transactionSignature = keyPair1.private.sign(meta) + + val meta2 = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes.plus(testBytes), keyPair1.public) + val transactionSignature2 = TransactionSignature(transactionSignature.signatureData, meta2) + keyPair1.public.verify(transactionSignature2) + } + + /** Verification should fail; corrupted metadata - schemeCodeName has changed from K1 to R1. */ + @Test(expected = SignatureException::class) + fun `MetaData Wrong schemeCodeName has changed`() { + val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair1.public) + val transactionSignature = keyPair1.private.sign(meta) + + val meta2 = MetaData("ECDSA_SECP256R1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes.plus(testBytes), keyPair1.public) + val transactionSignature2 = TransactionSignature(transactionSignature.signatureData, meta2) + keyPair1.public.verify(transactionSignature2) + } +} diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index f56670360b..ac4e8ebf6a 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -1,13 +1,15 @@ package net.corda.core.serialization import com.google.common.primitives.Ints -import net.corda.core.crypto.generateKeyPair -import net.corda.core.crypto.signWithECDSA +import net.corda.core.crypto.* import net.corda.core.messaging.Ack import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider import org.junit.Test import java.io.InputStream +import java.security.Security import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -94,6 +96,22 @@ class KryoTests { assertEquals(-1, readRubbishStream.read()) } + @Test + fun `serialize - deserialize MetaData`() { + Security.addProvider(BouncyCastleProvider()) + Security.addProvider(BouncyCastlePQCProvider()) + val testString = "Hello World" + val testBytes = testString.toByteArray() + val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") + val bitSet = java.util.BitSet(10) + bitSet.set(3) + + val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), bitSet, bitSet, testBytes, keyPair1.public) + val serializedMetaData = meta.bytes() + val meta2 = serializedMetaData.deserialize() + assertEquals(meta2, meta) + } + @CordaSerializable private data class Person(val name: String, val birthday: Instant?) diff --git a/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt b/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt index 144caedc1f..f94e7378c0 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt @@ -46,6 +46,7 @@ class DefaultWhitelist : CordaPluginRegistry() { addToWhitelist(BigDecimal::class.java) addToWhitelist(LocalDate::class.java) addToWhitelist(Period::class.java) + addToWhitelist(BitSet::class.java) } return true }