Add composite signature engine (#446)

Add CompositeSignature and CompositeSignatureWithKeys classes as part of preliminary work to make CompositeKey signature validation compatible with java.security classes, so that these keys and signatures can be used readily in X.509 certificates.
This commit is contained in:
Ross Nicoll 2017-04-13 13:25:12 +01:00 committed by GitHub
parent 37dc6ead82
commit 1a88ca4bee
8 changed files with 220 additions and 13 deletions

View File

@ -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<CompositeSignaturesWithKeys>()
return if (verifyKey.isFulfilledBy(sig.sigs.map { it.by })) {
val clearData = buffer.toByteArray()
sig.sigs.all { it.isValidForECDSA(clearData) }
} else {
false
}
}
}
}

View File

@ -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<DigitalSignature.WithKey>) {
companion object {
val EMPTY = CompositeSignaturesWithKeys(emptyList())
}
}

View File

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

View File

@ -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<WireTransaction>,
val sigs: List<DigitalSignature.WithKey>

View File

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

View File

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

View File

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

View File

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