From ecb1acdb8d0347e1528e6434d2bce7d911632f42 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Mon, 3 Oct 2016 10:20:44 +0100 Subject: [PATCH] Add threshold signature support: introduce PublicKeyTree which allows composing public keys into a tree structure with nodes containing thresholds for how many child node signatures it requires. --- .../r3corda/core/crypto/CryptoUtilities.kt | 6 + .../com/r3corda/core/crypto/PublicKeyTree.kt | 137 ++++++++++++++++++ .../r3corda/core/crypto/PublicKeyTreeTests.kt | 49 +++++++ 3 files changed, 192 insertions(+) create mode 100644 core/src/main/kotlin/com/r3corda/core/crypto/PublicKeyTree.kt create mode 100644 core/src/test/kotlin/com/r3corda/core/crypto/PublicKeyTreeTests.kt diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt b/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt index df44710f8e..2ccb54b762 100644 --- a/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt +++ b/core/src/main/kotlin/com/r3corda/core/crypto/CryptoUtilities.kt @@ -101,6 +101,12 @@ fun PublicKey.toStringShort(): String { fun Iterable.toStringsShort(): String = map { it.toStringShort() }.toString() +/** Creates a [PublicKeyTree] with a single leaf node containing the public key */ +val PublicKey.tree: PublicKeyTree get() = PublicKeyTree.Leaf(this) + +/** Returns the set of all [PublicKey]s of the signatures */ +fun Iterable.byKeys() = map { it.by }.toSet() + // Allow Kotlin destructuring: val (private, public) = keyPair operator fun KeyPair.component1() = this.private diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/PublicKeyTree.kt b/core/src/main/kotlin/com/r3corda/core/crypto/PublicKeyTree.kt new file mode 100644 index 0000000000..d8e1c1514a --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/crypto/PublicKeyTree.kt @@ -0,0 +1,137 @@ +package com.r3corda.core.crypto + +import com.r3corda.core.crypto.PublicKeyTree.Leaf +import com.r3corda.core.crypto.PublicKeyTree.Node +import com.r3corda.core.serialization.deserialize +import com.r3corda.core.serialization.serialize +import java.security.PublicKey + +/** + * A tree data structure that enables the representation of composite public keys. + * + * It the simplest case it may just contain a single node encapsulating a [PublicKey] – a [Leaf]. + * + * For more complex scenarios, such as *"Both Alice and Bob need to sign to consume a sate S"*, we can represent + * the requirement by creating a tree with a root [Node], and Alice and Bob as children – [Leaf]s. + * The root node would specify *weights* for each of its children and a *threshold* – the minimum total weight required + * (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement. + * + * Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can + * create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*. + */ +sealed class PublicKeyTree { + /** Checks whether [keys] match a sufficient amount of leaf nodes */ + abstract fun isFulfilledBy(keys: Iterable): Boolean + + fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key)) + + /** Returns all [PublicKey]s contained within the tree leaves */ + abstract fun getKeys(): Set + + /** Checks whether any of the given [keys] matches a leaf on the tree */ + fun containsAny(keys: Iterable) = getKeys().intersect(keys).isNotEmpty() + + // TODO: implement a proper encoding/decoding mechanism + fun toBase58String(): String = Base58.encode(this.serialize().bits) + + companion object { + fun parseFromBase58(encoded: String) = Base58.decode(encoded).deserialize() + } + + /** The leaf node of the public key tree – a wrapper around a [PublicKey] primitive */ + class Leaf(val publicKey: PublicKey) : PublicKeyTree() { + override fun isFulfilledBy(keys: Iterable) = publicKey in keys + + override fun getKeys(): Set = setOf(publicKey) + + // Auto-generated. TODO: remove once data class inheritance is enabled + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Leaf + + if (publicKey != other.publicKey) return false + + return true + } + + override fun hashCode() = publicKey.hashCode() + } + + /** + * Represents a node in the [PublicKeyTree]. It maintains a list of child nodes – sub-trees, and associated + * [weights] carried by child node signatures. + * + * The [threshold] specifies the minimum total weight required (in the simple case – the minimum number of child + * signatures required) to satisfy the public key sub-tree rooted at this node. + */ + class Node(val threshold: Int, + val children: List, + val weights: List) : PublicKeyTree() { + + override fun isFulfilledBy(keys: Iterable): Boolean { + val totalWeight = children.mapIndexed { i, childNode -> + if (childNode.isFulfilledBy(keys)) weights[i] else 0 + }.sum() + + return totalWeight >= threshold + } + + override fun getKeys(): Set = children.flatMap { it.getKeys() }.toSet() + + // Auto-generated. TODO: remove once data class inheritance is enabled + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Node + + if (threshold != other.threshold) return false + if (weights != other.weights) return false + if (children != other.children) return false + + return true + } + + override fun hashCode(): Int { + var result = threshold + result = 31 * result + weights.hashCode() + result = 31 * result + children.hashCode() + return result + } + } + + /** A helper class for building a [PublicKeyTree.Node]. */ + class Builder() { + private val children: MutableList = mutableListOf() + private val weights: MutableList = mutableListOf() + + /** Adds a child [PublicKeyTree] node. Specifying a [weight] for the child is optional and will default to 1. */ + fun addKey(publicKey: PublicKeyTree, weight: Int = 1): Builder { + children.add(publicKey) + weights.add(weight) + return this + } + + fun addKeys(vararg publicKeys: PublicKeyTree): Builder { + publicKeys.forEach { addKey(it) } + return this + } + + fun addLeaves(publicKeys: List): Builder = addLeaves(*publicKeys.toTypedArray()) + fun addLeaves(vararg publicKeys: PublicKey) = addKeys(*publicKeys.map { it.tree }.toTypedArray()) + + /** + * Builds the [PublicKeyTree.Node]. If [threshold] is not specified, it will default to + * the size of the children, effectively generating an "N of N" requirement. + */ + fun build(threshold: Int? = null): PublicKeyTree { + return if (children.size == 1) children.first() + else Node(threshold ?: children.size, children.toList(), weights.toList()) + } + } +} + +/** Returns the set of all [PublicKey]s contained in the leaves of the [PublicKeyTree]s */ +fun Iterable.getKeys() = flatMap { it.getKeys() }.toSet() \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/crypto/PublicKeyTreeTests.kt b/core/src/test/kotlin/com/r3corda/core/crypto/PublicKeyTreeTests.kt new file mode 100644 index 0000000000..4ce89761a0 --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/crypto/PublicKeyTreeTests.kt @@ -0,0 +1,49 @@ +package com.r3corda.core.crypto + +import com.r3corda.core.serialization.OpaqueBytes +import org.junit.Test +import kotlin.test.assertTrue + +class PublicKeyTreeTests { + val aliceKey = generateKeyPair() + val bobKey = generateKeyPair() + val charlieKey = generateKeyPair() + + val alicePublicKey = PublicKeyTree.Leaf(aliceKey.public) + val bobPublicKey = PublicKeyTree.Leaf(bobKey.public) + val charliePublicKey = PublicKeyTree.Leaf(charlieKey.public) + + val message = OpaqueBytes("Transaction".toByteArray()) + + val aliceSignature = aliceKey.signWithECDSA(message) + val bobSignature = bobKey.signWithECDSA(message) + + @Test + fun `(Alice) fulfilled by Alice signature`() { + assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) } + } + + @Test + fun `(Alice or Bob) fulfilled by Bob signature`() { + val aliceOrBob = PublicKeyTree.Builder().addKeys(alicePublicKey, bobPublicKey).build(threshold = 1) + assertTrue { aliceOrBob.isFulfilledBy(bobSignature.by) } + } + + @Test + fun `(Alice and Bob) fulfilled by Alice, Bob signatures`() { + val aliceAndBob = PublicKeyTree.Builder().addKeys(alicePublicKey, bobPublicKey).build() + val signatures = listOf(aliceSignature, bobSignature) + assertTrue { aliceAndBob.isFulfilledBy(signatures.byKeys()) } + } + + @Test + fun `((Alice and Bob) or Charlie) signature verifies`() { + // TODO: Look into a DSL for building multi-level public key trees if that becomes a common use case + val aliceAndBob = PublicKeyTree.Builder().addKeys(alicePublicKey, bobPublicKey).build() + val aliceAndBobOrCharlie = PublicKeyTree.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) + + val signatures = listOf(aliceSignature, bobSignature) + + assertTrue { aliceAndBobOrCharlie.isFulfilledBy(signatures.byKeys()) } + } +} \ No newline at end of file