diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt new file mode 100644 index 0000000000..896a101863 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -0,0 +1,84 @@ +package net.corda.core.crypto + +import net.corda.core.serialization.deserialize +import java.io.ByteArrayOutputStream +import java.security.* +import java.security.spec.AlgorithmParameterSpec + +/** + * Dedicated class for storing a set of signatures that comprise [CompositeKey]. + */ +class CompositeSignature : Signature(ALGORITHM) { + companion object { + val ALGORITHM = "X-Corda-CompositeSig" + } + + private var signatureState: State? = null + + /** + * Check that the signature state has been initialised, then return it. + */ + @Throws(SignatureException::class) + private fun assertInitialised(): State { + if (signatureState == null) + throw SignatureException("Engine has not been initialised") + return signatureState!! + } + + @Throws(InvalidAlgorithmParameterException::class) + override fun engineGetParameter(param: String?): Any { + throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters") + } + + @Throws(InvalidKeyException::class) + override fun engineInitSign(privateKey: PrivateKey?) { + throw InvalidKeyException("Composite signatures must be assembled independently from signatures provided by the component private keys") + } + + @Throws(InvalidKeyException::class) + override fun engineInitVerify(publicKey: PublicKey?) { + if (publicKey is CompositeKey) { + signatureState = State(ByteArrayOutputStream(1024), publicKey) + } else { + throw InvalidKeyException("Key to verify must be a composite key") + } + } + + @Throws(InvalidAlgorithmParameterException::class) + override fun engineSetParameter(param: String?, value: Any?) { + throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters") + } + + @Throws(InvalidAlgorithmParameterException::class) + override fun engineSetParameter(params: AlgorithmParameterSpec) { + throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters") + } + + @Throws(SignatureException::class) + override fun engineSign(): ByteArray { + throw SignatureException("Composite signatures must be assembled independently from signatures provided by the component private keys") + } + + override fun engineUpdate(b: Byte) { + assertInitialised().buffer.write(b.toInt()) + } + + override fun engineUpdate(b: ByteArray, off: Int, len: Int) { + assertInitialised().buffer.write(b, off, len) + } + + @Throws(SignatureException::class) + override fun engineVerify(sigBytes: ByteArray): Boolean = assertInitialised().engineVerify(sigBytes) + + data class State(val buffer: ByteArrayOutputStream, val verifyKey: CompositeKey) { + fun engineVerify(sigBytes: ByteArray): Boolean { + val sig = sigBytes.deserialize() + return if (verifyKey.isFulfilledBy(sig.sigs.map { it.by })) { + val clearData = buffer.toByteArray() + sig.sigs.all { it.isValidForECDSA(clearData) } + } else { + false + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt new file mode 100644 index 0000000000..6edac6ce43 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt @@ -0,0 +1,14 @@ +package net.corda.core.crypto + +import net.corda.core.serialization.CordaSerializable + +/** + * Custom class for holding signature data. This exists for later extension work to provide a standardised cross-platform + * serialization format (i.e. not Kryo). + */ +@CordaSerializable +data class CompositeSignaturesWithKeys(val sigs: List) { + companion object { + val EMPTY = CompositeSignaturesWithKeys(emptyList()) + } +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt index 7e4e9d8983..d474eb320d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt @@ -21,13 +21,44 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.SignatureException +// TODO: Is there a use-case for bare [DigitalSignature], or is everything a [DigitalSignature.WithKey]? If there's no +// actual use-case, we should merge the with key version into the parent class. In that case [CompositeSignatureWithKeys] +// should be renamed to match. /** A wrapper around a digital signature. */ @CordaSerializable open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) { /** A digital signature that identifies who the public key is owned by. */ open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) { + /** + * Utility to simplify the act of verifying a signature. + * + * @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the + * signature). + * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). + */ + @Throws(InvalidKeyException::class, SignatureException::class) fun verifyWithECDSA(content: ByteArray) = by.verifyWithECDSA(content, this) + /** + * Utility to simplify the act of verifying a signature. + * + * @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the + * signature). + * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). + */ + @Throws(InvalidKeyException::class, SignatureException::class) fun verifyWithECDSA(content: OpaqueBytes) = by.verifyWithECDSA(content.bytes, this) + /** + * Utility to simplify the act of verifying a signature. In comparison to [verifyWithECDSA] doesn't throw an + * exception, making it more suitable where a boolean is required, but normally you should use the function + * which throws, as it avoids the risk of failing to test the result. + * + * @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the + * signature). + * @throws SignatureException if the signature is invalid (i.e. damaged). + * @return whether the signature is correct for this key. + */ + @Throws(InvalidKeyException::class, SignatureException::class) + fun isValidForECDSA(content: ByteArray) = by.isValidForECDSA(content, this) } // TODO: consider removing this as whoever needs to identify the signer should be able to derive it from the public key @@ -97,9 +128,35 @@ fun KeyPair.signWithECDSA(bytesToSign: ByteArray, party: Party): DigitalSignatur return DigitalSignature.LegallyIdentifiable(party, sig.bytes) } -/** Utility to simplify the act of verifying a signature */ -@Throws(SignatureException::class, IllegalStateException::class) +/** + * Utility to simplify the act of verifying a signature. + * + * @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the + * signature). + * @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect). + */ +// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`, +// we should use another exception (perhaps IllegalArgumentException) for indicating the signature is valid but does +// not match. +@Throws(IllegalStateException::class, SignatureException::class) fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) { + if (!isValidForECDSA(content, signature)) + throw SignatureException("Signature did not match") +} + +/** + * Utility to simplify the act of verifying a signature. In comparison to [verifyWithECDSA] 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, but this is for uses such as [java.security.Signature.verify] + * implementations. + * + * @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the + * signature). + * @throws SignatureException if the signature is invalid (i.e. damaged). + * @return whether the signature is correct for this key. + */ +@Throws(IllegalStateException::class, SignatureException::class) +fun PublicKey.isValidForECDSA(content: ByteArray, signature: DigitalSignature) : Boolean { val pubKey = when (this) { is CompositeKey -> throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification. else -> this @@ -107,8 +164,7 @@ fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) { val verifier = EdDSAEngine() verifier.initVerify(pubKey) verifier.update(content) - if (!verifier.verify(signature.bytes)) - throw SignatureException("Signature did not match") + return verifier.verify(signature.bytes) } /** Render a public key to a string, using a short form if it's an elliptic curve public key */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index dec29f4027..c3d6bea73a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -23,6 +23,10 @@ import java.util.* * map to the same key (and they could be different in important ways, like validity!). The signatures on a * SignedTransaction might be invalid or missing: the type does not imply validity. * A transaction ID should be the hash of the [WireTransaction] Merkle tree root. Thus adding or removing a signature does not change it. + * + * @param sigs a list of signatures from individual (non-composite) public keys. This is passed as a list of signatures + * when verifying composite key signatures, but may be used as individual signatures where a single key is expected to + * sign. */ data class SignedTransaction(val txBits: SerializedBytes, val sigs: List diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index a7d6f01639..7890412991 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,6 +1,7 @@ package net.corda.core.crypto import net.corda.core.serialization.OpaqueBytes +import net.corda.core.serialization.serialize import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -20,16 +21,22 @@ class CompositeKeyTests { val aliceSignature = aliceKey.signWithECDSA(message) val bobSignature = bobKey.signWithECDSA(message) + val charlieSignature = charlieKey.signWithECDSA(message) + val compositeAliceSignature = CompositeSignaturesWithKeys(listOf(aliceSignature)) @Test fun `(Alice) fulfilled by Alice signature`() { assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) } + assertFalse { alicePublicKey.isFulfilledBy(charlieSignature.by) } } @Test - fun `(Alice or Bob) fulfilled by Bob signature`() { + fun `(Alice or Bob) fulfilled by either signature`() { val aliceOrBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build(threshold = 1) + assertTrue { aliceOrBob.isFulfilledBy(aliceSignature.by) } assertTrue { aliceOrBob.isFulfilledBy(bobSignature.by) } + assertTrue { aliceOrBob.isFulfilledBy(listOf(aliceSignature.by, bobSignature.by)) } + assertFalse { aliceOrBob.isFulfilledBy(charlieSignature.by) } } @Test @@ -39,6 +46,14 @@ class CompositeKeyTests { assertTrue { aliceAndBob.isFulfilledBy(signatures.byKeys()) } } + @Test + fun `(Alice and Bob) requires both signatures to fulfil`() { + val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() + assertFalse { aliceAndBob.isFulfilledBy(listOf(aliceSignature).byKeys()) } + assertFalse { aliceAndBob.isFulfilledBy(listOf(bobSignature).byKeys()) } + assertTrue { aliceAndBob.isFulfilledBy(listOf(aliceSignature, bobSignature).byKeys()) } + } + @Test fun `((Alice and Bob) or Charlie) signature verifies`() { // TODO: Look into a DSL for building multi-level composite keys if that becomes a common use case @@ -87,4 +102,27 @@ class CompositeKeyTests { // Chain of single nodes should throw. assertEquals(CompositeKey.Builder().addKeys(tree1).build(), tree1) } + + /** + * Check that verifying a composite signature using the [CompositeSignature] engine works. + */ + @Test + fun `composite signature verification`() { + val twoOfThree = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey, charliePublicKey).build(threshold = 2) + val engine = CompositeSignature() + engine.initVerify(twoOfThree) + engine.update(message.bytes) + + assertFalse { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature)).serialize().bytes) } + assertFalse { engine.verify(CompositeSignaturesWithKeys(listOf(bobSignature)).serialize().bytes) } + assertFalse { engine.verify(CompositeSignaturesWithKeys(listOf(charlieSignature)).serialize().bytes) } + assertTrue { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature, bobSignature)).serialize().bytes) } + assertTrue { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature, charlieSignature)).serialize().bytes) } + assertTrue { engine.verify(CompositeSignaturesWithKeys(listOf(bobSignature, charlieSignature)).serialize().bytes) } + assertTrue { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature, bobSignature, charlieSignature)).serialize().bytes) } + + // Check the underlying signature is validated + val brokenBobSignature = DigitalSignature.WithKey(bobSignature.by, aliceSignature.bytes) + assertFalse { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature, brokenBobSignature)).serialize().bytes) } + } } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 41bd9905e1..4ef67799d3 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,9 @@ UNRELEASED * API changes: * Added extension function ``Database.transaction`` to replace ``databaseTransaction``, which is now deprecated. + * Added ``CompositeSignature`` and ``CompositeSignatureData`` as part of enabling ``java.security`` classes to work with + composite keys and signatures. + Milestone 10.0 -------------- diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 4c9b9dd45f..9c5b8f6217 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -4,6 +4,14 @@ Release notes Here are release notes for each snapshot release from M9 onwards. This includes guidance on how to upgrade code from the previous milestone release. +Unreleased +---------- + +Work has continued on confidential identities, introducing code to enable the Java standard libraries to work with +composite key signatures. This will form the underlying basis of future work to standardise the public key and signature +formats to enable interoperability with other systems, as well as enabling the use of composite signatures on X.509 +certificates to prove association between transaction keys and identity keys. + Milestone 10 ------------ diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index fe161205d0..989e457e4d 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -156,20 +156,20 @@ class CordaRPCOpsImplTest { transactions.expectEvents { sequence( // ISSUE - expect { tx -> - require(tx.tx.inputs.isEmpty()) - require(tx.tx.outputs.size == 1) - val signaturePubKeys = tx.sigs.map { it.by }.toSet() + expect { stx -> + require(stx.tx.inputs.isEmpty()) + require(stx.tx.outputs.size == 1) + val signaturePubKeys = stx.sigs.map { it.by }.toSet() // Only Alice signed val aliceKey = aliceNode.info.legalIdentity.owningKey require(signaturePubKeys.size <= aliceKey.keys.size) require(aliceKey.isFulfilledBy(signaturePubKeys)) }, // MOVE - expect { tx -> - require(tx.tx.inputs.size == 1) - require(tx.tx.outputs.size == 1) - val signaturePubKeys = tx.sigs.map { it.by }.toSet() + expect { stx -> + require(stx.tx.inputs.size == 1) + require(stx.tx.outputs.size == 1) + val signaturePubKeys = stx.sigs.map { it.by }.toSet() // Alice and Notary signed require(aliceNode.info.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys)) require(notaryNode.info.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))