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 98c150b5b8..ec994b450f 100644
--- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt
+++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt
@@ -55,7 +55,7 @@ import javax.crypto.spec.SecretKeySpec
* However, only the schemes returned by {@link #listSupportedSignatureSchemes()} are supported.
* Note that Corda currently supports the following signature schemes by their code names:
*
- * - RSA_SHA256 (RSA using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function).
+ *
- RSA_SHA256 (RSA PKCS#1 using SHA256 as hash algorithm).
*
- ECDSA_SECP256K1_SHA256 (ECDSA using the secp256k1 Koblitz curve and SHA256 as hash algorithm).
*
- ECDSA_SECP256R1_SHA256 (ECDSA using the secp256r1 (NIST P-256) curve and SHA256 as hash algorithm).
*
- EDDSA_ED25519_SHA512 (EdDSA using the ed255519 twisted Edwards curve and SHA512 as hash algorithm).
@@ -64,7 +64,8 @@ import javax.crypto.spec.SecretKeySpec
*/
object Crypto {
/**
- * RSA signature scheme using SHA256 for message hashing.
+ * RSA PKCS#1 signature scheme using SHA256 for message hashing.
+ * The actual algorithm id is 1.2.840.113549.1.1.1
* Note: Recommended key size >= 3072 bits.
*/
@JvmField
@@ -75,7 +76,7 @@ object Crypto {
listOf(AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, null)),
BouncyCastleProvider.PROVIDER_NAME,
"RSA",
- "SHA256WITHRSAEncryption",
+ "SHA256WITHRSA",
null,
3072,
"RSA_SHA256 signature scheme using SHA256 as hash algorithm."
@@ -547,7 +548,7 @@ object Crypto {
/**
* 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 txId transaction's id (Merkle root).
+ * @param txId transaction's id.
* @param transactionSignature the signature on the transaction.
* @return true if verification passes or throw exception if verification fails.
* @throws InvalidKeyException if the key is invalid.
@@ -559,7 +560,7 @@ object Crypto {
@JvmStatic
@Throws(InvalidKeyException::class, SignatureException::class)
fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
- val signableData = SignableData(txId, transactionSignature.signatureMetadata)
+ val signableData = SignableData(originalSignedHash(txId, transactionSignature.partialMerkleTree), transactionSignature.signatureMetadata)
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
}
@@ -569,7 +570,7 @@ object Crypto {
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
* as it avoids the risk of failing to test the result.
- * @param txId transaction's id (Merkle root).
+ * @param txId transaction's id.
* @param transactionSignature the signature on the transaction.
* @throws SignatureException if this signatureData object is not initialized properly,
* the passed-in signatureData is improperly encoded or of the wrong type,
@@ -578,7 +579,7 @@ object Crypto {
@JvmStatic
@Throws(SignatureException::class)
fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
- val signableData = SignableData(txId, transactionSignature.signatureMetadata)
+ val signableData = SignableData(originalSignedHash(txId, transactionSignature.partialMerkleTree), transactionSignature.signatureMetadata)
return isValid(
findSignatureScheme(transactionSignature.by),
transactionSignature.by,
@@ -1011,4 +1012,21 @@ object Crypto {
else -> decodePrivateKey(key.encoded)
}
}
+
+ /**
+ * Get the hash value that is actually signed.
+ * The txId is returned when [partialMerkleTree] is null,
+ * else the root of the tree is computed and returned.
+ * Note that the hash of the txId should be a leaf in the tree, not the txId itself.
+ */
+ private fun originalSignedHash(txId: SecureHash, partialMerkleTree: PartialMerkleTree?): SecureHash {
+ return if (partialMerkleTree != null) {
+ val usedHashes = mutableListOf()
+ val root = PartialMerkleTree.rootAndUsedHashes(partialMerkleTree.root, usedHashes)
+ require(txId.sha256() in usedHashes) { "Transaction with id:$txId is not a leaf in the provided partial Merkle tree" }
+ root
+ } else {
+ txId
+ }
+ }
}
diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt
index b46eb7f631..5ba48577dd 100644
--- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt
+++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt
@@ -140,23 +140,24 @@ class PartialMerkleTree(val root: PartialTree) {
is PartialTree.Node -> {
val leftHash = rootAndUsedHashes(node.left, usedHashes)
val rightHash = rootAndUsedHashes(node.right, usedHashes)
- return leftHash.hashConcat(rightHash)
+ leftHash.hashConcat(rightHash)
}
}
}
}
/**
+ * Function to verify a [PartialMerkleTree] against an input Merkle root and a list of leaves.
+ * The tree should only contain the leaves defined in [hashesToCheck].
* @param merkleRootHash Hash that should be checked for equality with root calculated from this partial tree.
* @param hashesToCheck List of included leaves hashes that should be found in this partial tree.
*/
fun verify(merkleRootHash: SecureHash, hashesToCheck: List): Boolean {
val usedHashes = ArrayList()
val verifyRoot = rootAndUsedHashes(root, usedHashes)
- // It means that we obtained more/fewer hashes than needed or different sets of hashes.
- if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
- return false
- return (verifyRoot == merkleRootHash)
+ return verifyRoot == merkleRootHash // Tree roots match.
+ && hashesToCheck.size == usedHashes.size // Obtained the same number of hashes (leaves).
+ && hashesToCheck.toSet().containsAll(usedHashes) // Lists contain the same elements.
}
/**
diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt
index 5027247980..3083df4946 100644
--- a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt
+++ b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt
@@ -5,8 +5,9 @@ import net.corda.core.serialization.CordaSerializable
/**
* A [SignableData] object is the packet actually signed.
* It works as a wrapper over transaction id and signature metadata.
- *
- * @param txId transaction's id.
+ * Note that when multi-transaction signing (signing a block of transactions) is used, the root of the Merkle tree
+ * (having transaction IDs as leaves) is actually signed and thus [txId] refers to this root and not a specific transaction.
+ * @param txId transaction's id or root of multi-transaction Merkle tree in case of multi-transaction signing.
* @param signatureMetadata meta data required.
*/
@CordaSerializable
diff --git a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt
index 4f5f7eb207..fed8e88fef 100644
--- a/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt
+++ b/core/src/main/kotlin/net/corda/core/crypto/TransactionSignature.kt
@@ -8,13 +8,24 @@ import java.util.*
/**
* A wrapper over the signature output accompanied by signer's public key and signature metadata.
- * This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction signatures.
+ * This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction (or block of transactions) signatures.
+ * @property bytes actual bytes of the cryptographic signature.
+ * @property by [PublicKey] of the signer.
+ * @property signatureMetadata attached [SignatureMetadata] for this signature.
+ * @property partialMerkleTree required when multi-transaction signing is utilised.
*/
@CordaSerializable
-class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata) : DigitalSignature(bytes) {
+class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata, val partialMerkleTree: PartialMerkleTree?) : DigitalSignature(bytes) {
+ /**
+ * Construct a [TransactionSignature] with [partialMerkleTree] set to null.
+ * This is the recommended constructor when signing over a single transaction.
+ */
+ constructor(bytes: ByteArray, by: PublicKey, signatureMetadata: SignatureMetadata) : this(bytes, by, signatureMetadata, null)
+
/**
* Function to verify a [SignableData] object's signature.
* Note that [SignableData] contains the id of the transaction and extra metadata, such as DLT's platform version.
+ * A non-null [partialMerkleTree] implies multi-transaction signing and the signature is over the root of this tree.
*
* @param txId transaction's id (Merkle root), which along with [signatureMetadata] will be used to construct the [SignableData] object to be signed.
* @throws InvalidKeyException if the key is invalid.
diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt
index fc24174df4..bc77fe9683 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt
@@ -104,15 +104,19 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
return NotaryException(NotaryError.Conflict(txId, signedConflict))
}
+ /** Sign a [ByteArray] input. */
fun sign(bits: ByteArray): DigitalSignature.WithKey {
return services.keyManagementService.sign(bits, notaryIdentityKey)
}
+ /** Sign a single transaction. */
fun sign(txId: SecureHash): TransactionSignature {
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
return services.keyManagementService.sign(signableData, notaryIdentityKey)
}
+ // TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root.
+
@Deprecated("This property is no longer used") @Suppress("DEPRECATION")
protected open val timeWindowChecker: TimeWindowChecker get() = throw UnsupportedOperationException("No default implementation, need to override")
}
\ No newline at end of file
diff --git a/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt b/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt
index dbb0027a7c..4958d58de6 100644
--- a/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt
+++ b/core/src/test/kotlin/net/corda/core/crypto/TransactionSignatureTest.kt
@@ -3,17 +3,21 @@ package net.corda.core.crypto
import net.corda.testing.core.SerializationEnvironmentRule
import org.junit.Rule
import org.junit.Test
+import java.math.BigInteger
+import java.security.KeyPair
import java.security.SignatureException
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
- * Digital signature MetaData tests.
+ * Transaction signature tests.
*/
class TransactionSignatureTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
- val testBytes = "12345678901234567890123456789012".toByteArray()
+ private val testBytes = "12345678901234567890123456789012".toByteArray()
/** Valid sign and verify. */
@Test
@@ -41,4 +45,83 @@ class TransactionSignatureTest {
val transactionSignature = keyPair.sign(signableData)
Crypto.doVerify((testBytes + testBytes).sha256(), transactionSignature)
}
+
+ @Test
+ fun `Verify multi-tx signature`() {
+ val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger.valueOf(1234567890L))
+ // Deterministically create 5 txIds.
+ val txIds: List = IntRange(0, 4).map { byteArrayOf(it.toByte()).sha256() }
+ // Multi-tx signature.
+ val txSignature = signMultipleTx(txIds, keyPair)
+
+ // The hash of all txIds are used as leaves.
+ val merkleTree = MerkleTree.getMerkleTree(txIds.map { it.sha256() })
+
+ // We haven't added the partial tree yet.
+ assertNull(txSignature.partialMerkleTree)
+ // Because partial tree is still null, but we signed over a block of txs, verifying a single tx will fail.
+ assertFailsWith { Crypto.doVerify(txIds[3], txSignature) }
+
+ // Create a partial tree for one tx.
+ val pmt = PartialMerkleTree.build(merkleTree, listOf(txIds[0].sha256()))
+ // Add the partial Merkle tree to the tx signature.
+ val txSignatureWithTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmt)
+
+ // Verify the corresponding txId with every possible way.
+ assertTrue(Crypto.doVerify(txIds[0], txSignatureWithTree))
+ assertTrue(txSignatureWithTree.verify(txIds[0]))
+ assertTrue(Crypto.isValid(txIds[0], txSignatureWithTree))
+ assertTrue(txSignatureWithTree.isValid(txIds[0]))
+
+ // Verify the rest txs in the block, which are not included in the partial Merkle tree.
+ txIds.subList(1, txIds.size).forEach {
+ assertFailsWith { Crypto.doVerify(it, txSignatureWithTree) }
+ }
+
+ // Test that the Merkle tree consists of hash(txId), not txId.
+ assertFailsWith { PartialMerkleTree.build(merkleTree, listOf(txIds[0])) }
+
+ // What if we send the Full tree. This could be used if notaries didn't want to create a per tx partial tree.
+ // Create a partial tree for all txs, thus all leaves are included.
+ val pmtFull = PartialMerkleTree.build(merkleTree, txIds.map { it.sha256() })
+ // Add the partial Merkle tree to the tx.
+ val txSignatureWithFullTree = TransactionSignature(txSignature.bytes, txSignature.by, txSignature.signatureMetadata, pmtFull)
+
+ // All txs can be verified, as they are all included in the provided partial tree.
+ txIds.forEach {
+ assertTrue(Crypto.doVerify(it, txSignatureWithFullTree))
+ }
+ }
+
+ @Test
+ fun `Verify one-tx signature`() {
+ val keyPair = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, BigInteger.valueOf(1234567890L))
+ val txId = "aTransaction".toByteArray().sha256()
+ // One-tx signature.
+ val txSignature = signOneTx(txId, keyPair)
+
+ // partialMerkleTree should be null.
+ assertNull(txSignature.partialMerkleTree)
+ // Verify the corresponding txId with every possible way.
+ assertTrue(Crypto.doVerify(txId, txSignature))
+ assertTrue(txSignature.verify(txId))
+ assertTrue(Crypto.isValid(txId, txSignature))
+ assertTrue(txSignature.isValid(txId))
+
+ // We signed the txId itself, not its hash (because it was a signature over one tx only and no partial tree has been received).
+ assertFailsWith { Crypto.doVerify(txId.sha256(), txSignature) }
+ }
+
+ // Returns a TransactionSignature over the Merkle root, but the partial tree is null.
+ private fun signMultipleTx(txIds: List, keyPair: KeyPair): TransactionSignature {
+ val merkleTreeRoot = MerkleTree.getMerkleTree(txIds.map { it.sha256() }).hash
+ return signOneTx(merkleTreeRoot, keyPair)
+ }
+
+ // Returns a TransactionSignature over one SecureHash.
+ // Note that if one tx is to be signed, we don't create a Merkle tree and we directly sign over the txId.
+ private fun signOneTx(txId: SecureHash, keyPair: KeyPair): TransactionSignature {
+ val signableData = SignableData(txId, SignatureMetadata(3, Crypto.findSignatureScheme(keyPair.public).schemeNumberID))
+ return keyPair.sign(signableData)
+ }
}