mirror of
https://github.com/corda/corda.git
synced 2025-06-17 14:48:16 +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:
@ -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) }
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user