mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
parent
37dc6ead82
commit
1a88ca4bee
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
@ -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>
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
--------------
|
||||
|
||||
|
@ -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
|
||||
------------
|
||||
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user