CORDA-967 Multi-tx signature verification (#2488)

This commit is contained in:
Konstantinos Chalkias 2018-02-09 15:33:48 +00:00 committed by GitHub
parent c8cf46c657
commit 86fb1ed852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 17 deletions

View File

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

View File

@ -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.
}
/**

View File

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

View File

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

View File

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

View File

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