mirror of
https://github.com/corda/corda.git
synced 2024-12-21 05:53:23 +00:00
CORDA-967 Multi-tx signature verification (#2488)
This commit is contained in:
parent
c8cf46c657
commit
86fb1ed852
@ -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:
|
||||
* <p><ul>
|
||||
* <li>RSA_SHA256 (RSA using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function).
|
||||
* <li>RSA_SHA256 (RSA PKCS#1 using SHA256 as hash algorithm).
|
||||
* <li>ECDSA_SECP256K1_SHA256 (ECDSA using the secp256k1 Koblitz curve and SHA256 as hash algorithm).
|
||||
* <li>ECDSA_SECP256R1_SHA256 (ECDSA using the secp256r1 (NIST P-256) curve and SHA256 as hash algorithm).
|
||||
* <li>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<SecureHash>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<SecureHash>): Boolean {
|
||||
val usedHashes = ArrayList<SecureHash>()
|
||||
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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,8 +5,10 @@ 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.
|
||||
* 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.
|
||||
* @param txId transaction's id or root of multi-transaction Merkle tree in case of multi-transaction signing.
|
||||
* @param signatureMetadata meta data required.
|
||||
*/
|
||||
@CordaSerializable
|
||||
|
@ -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.
|
||||
|
@ -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")
|
||||
}
|
@ -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<SecureHash> = 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<SignatureException> { 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<IllegalArgumentException> { Crypto.doVerify(it, txSignatureWithTree) }
|
||||
}
|
||||
|
||||
// Test that the Merkle tree consists of hash(txId), not txId.
|
||||
assertFailsWith<MerkleTreeException> { 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<SignatureException> { Crypto.doVerify(txId.sha256(), txSignature) }
|
||||
}
|
||||
|
||||
// Returns a TransactionSignature over the Merkle root, but the partial tree is null.
|
||||
private fun signMultipleTx(txIds: List<SecureHash>, 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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user