Added composite key provider for storing composite keys in keystore (#1006)

* Add unit tests around decoding composite keys

(cherry picked from commit 9ccdd8e)

* Start writing a Composite signature scheme

(cherry picked from commit 72ac3a5)

* Composite key serialisation

* refactoring

* * Address PR issues

* * Address PR issues

* * Address PR issues

* * Address PR issues

* fix up after rebase
This commit is contained in:
Patrick Kuo
2017-07-12 12:13:29 +01:00
committed by GitHub
parent 5f7b8f6ec3
commit 78ecff7933
20 changed files with 269 additions and 51 deletions

View File

@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.contracts.BusinessCalendar import net.corda.contracts.BusinessCalendar
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party

View File

@ -1,6 +1,9 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.composite.CompositeSignature
import net.corda.core.crypto.provider.CordaObjectIdentifier
import net.corda.core.crypto.provider.CordaSecurityProvider
import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
@ -147,6 +150,22 @@ object Crypto {
"at the cost of larger key sizes and loss of compatibility." "at the cost of larger key sizes and loss of compatibility."
) )
/**
* Corda composite key type
*/
val COMPOSITE_KEY = SignatureScheme(
6,
"COMPOSITE",
AlgorithmIdentifier(CordaObjectIdentifier.compositeKey),
emptyList(),
CordaSecurityProvider.PROVIDER_NAME,
CompositeKey.KEY_ALGORITHM,
CompositeSignature.SIGNATURE_ALGORITHM,
null,
null,
"Composite keys composed from individual public keys"
)
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
@ -159,7 +178,8 @@ object Crypto {
ECDSA_SECP256K1_SHA256, ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256, ECDSA_SECP256R1_SHA256,
EDDSA_ED25519_SHA512, EDDSA_ED25519_SHA512,
SPHINCS256_SHA256 SPHINCS256_SHA256,
COMPOSITE_KEY
).associateBy { it.schemeCodeName } ).associateBy { it.schemeCodeName }
/** /**
@ -177,6 +197,7 @@ object Crypto {
// The val is private to avoid any harmful state changes. // The val is private to avoid any harmful state changes.
private val providerMap: Map<String, Provider> = mapOf( private val providerMap: Map<String, Provider> = mapOf(
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(), BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(),
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. "BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply { private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
@ -188,6 +209,7 @@ object Crypto {
// This registration is needed for reading back EdDSA key from java keystore. // This registration is needed for reading back EdDSA key from java keystore.
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
Security.addProvider(getBouncyCastleProvider()) Security.addProvider(getBouncyCastleProvider())
Security.addProvider(CordaSecurityProvider())
} }
/** /**
@ -202,7 +224,7 @@ object Crypto {
} }
fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme { fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme {
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm}") return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
} }
/** /**
@ -543,7 +565,7 @@ object Crypto {
if (signatureScheme.algSpec != null) if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
else else
keyPairGenerator.initialize(signatureScheme.keySize, newSecureRandom()) keyPairGenerator.initialize(signatureScheme.keySize!!, newSecureRandom())
return keyPairGenerator.generateKeyPair() return keyPairGenerator.generateKeyPair()
} }

View File

@ -2,6 +2,7 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import java.math.BigInteger import java.math.BigInteger

View File

@ -28,5 +28,5 @@ data class SignatureScheme(
val algorithmName: String, val algorithmName: String,
val signatureName: String, val signatureName: String,
val algSpec: AlgorithmParameterSpec?, val algSpec: AlgorithmParameterSpec?,
val keySize: Int, val keySize: Int?,
val desc: String) val desc: String)

View File

@ -1,8 +1,14 @@
package net.corda.core.crypto package net.corda.core.crypto.composite
import net.corda.core.crypto.CompositeKey.NodeAndWeight import net.corda.core.crypto.Crypto
import net.corda.core.crypto.composite.CompositeKey.NodeAndWeight
import net.corda.core.crypto.keys
import net.corda.core.crypto.provider.CordaObjectIdentifier
import net.corda.core.crypto.toSHA256Bytes
import net.corda.core.crypto.toStringShort
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.asn1.* import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.PublicKey import java.security.PublicKey
@ -26,9 +32,35 @@ import java.util.*
* signatures required) to satisfy the sub-tree rooted at this node. * signatures required) to satisfy the sub-tree rooted at this node.
*/ */
@CordaSerializable @CordaSerializable
class CompositeKey private constructor (val threshold: Int, class CompositeKey private constructor(val threshold: Int, children: List<NodeAndWeight>) : PublicKey {
children: List<NodeAndWeight>) : PublicKey { companion object {
val KEY_ALGORITHM = "COMPOSITE"
/**
* Build a composite key from a DER encoded form.
*/
fun getInstance(encoded: ByteArray) = getInstance(ASN1Primitive.fromByteArray(encoded))
fun getInstance(asn1: ASN1Primitive): PublicKey {
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.compositeKey)
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
val builder = Builder()
val listOfChildren = sequenceOfChildren.objects.toList()
listOfChildren.forEach { childAsn1 ->
require(childAsn1 is ASN1Sequence)
val childSeq = childAsn1 as ASN1Sequence
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1))
builder.addKey(key, weight.positiveValue.toInt())
}
return builder.build(threshold)
}
}
val children = children.sorted() val children = children.sorted()
init { init {
// TODO: replace with the more extensive, but slower, checkValidity() test. // TODO: replace with the more extensive, but slower, checkValidity() test.
checkConstraints() checkConstraints()
@ -47,8 +79,9 @@ class CompositeKey private constructor (val threshold: Int,
require(threshold > 0) { "CompositeKey threshold is set to $threshold, but it should be a positive integer." } require(threshold > 0) { "CompositeKey threshold is set to $threshold, but it should be a positive integer." }
// If threshold is bigger than total weight, then it will never be satisfied. // If threshold is bigger than total weight, then it will never be satisfied.
val totalWeight = totalWeight() val totalWeight = totalWeight()
require(threshold <= totalWeight) { "CompositeKey threshold: $threshold cannot be bigger than aggregated weight of " + require(threshold <= totalWeight) {
"child nodes: $totalWeight"} "CompositeKey threshold: $threshold cannot be bigger than aggregated weight of child nodes: $totalWeight"
}
} }
// Graph cycle detection in the composite key structure to avoid infinite loops on CompositeKey graph traversal and // Graph cycle detection in the composite key structure to avoid infinite loops on CompositeKey graph traversal and
@ -75,7 +108,7 @@ class CompositeKey private constructor (val threshold: Int,
* TODO: Always call this method when deserialising [CompositeKey]s. * TODO: Always call this method when deserialising [CompositeKey]s.
*/ */
fun checkValidity() { fun checkValidity() {
val visitedMap = IdentityHashMap<CompositeKey,Boolean>() val visitedMap = IdentityHashMap<CompositeKey, Boolean>()
visitedMap.put(this, true) visitedMap.put(this, true)
cycleDetection(visitedMap) // Graph cycle testing on the root node. cycleDetection(visitedMap) // Graph cycle testing on the root node.
checkConstraints() checkConstraints()
@ -93,7 +126,7 @@ class CompositeKey private constructor (val threshold: Int,
private fun totalWeight(): Int { private fun totalWeight(): Int {
var sum = 0 var sum = 0
for ((_, weight) in children) { for ((_, weight) in children) {
require (weight > 0) { "Non-positive weight: $weight detected." } require(weight > 0) { "Non-positive weight: $weight detected." }
sum = Math.addExact(sum, weight) // Add and check for integer overflow. sum = Math.addExact(sum, weight) // Add and check for integer overflow.
} }
return sum return sum
@ -104,17 +137,17 @@ class CompositeKey private constructor (val threshold: Int,
* Each node should be assigned with a positive weight to avoid certain types of weight underflow attacks. * Each node should be assigned with a positive weight to avoid certain types of weight underflow attacks.
*/ */
@CordaSerializable @CordaSerializable
data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable<NodeAndWeight>, ASN1Object() { data class NodeAndWeight(val node: PublicKey, val weight: Int) : Comparable<NodeAndWeight>, ASN1Object() {
init { init {
// We don't allow zero or negative weights. Minimum weight = 1. // We don't allow zero or negative weights. Minimum weight = 1.
require (weight > 0) { "A non-positive weight was detected. Node info: $this" } require(weight > 0) { "A non-positive weight was detected. Node info: $this" }
} }
override fun compareTo(other: NodeAndWeight): Int { override fun compareTo(other: NodeAndWeight): Int {
return if (weight == other.weight) { return if (weight == other.weight)
ByteBuffer.wrap(node.toSHA256Bytes()).compareTo(ByteBuffer.wrap(other.node.toSHA256Bytes())) ByteBuffer.wrap(node.toSHA256Bytes()).compareTo(ByteBuffer.wrap(other.node.toSHA256Bytes()))
} else weight.compareTo(other.weight) else
weight.compareTo(other.weight)
} }
override fun toASN1Primitive(): ASN1Primitive { override fun toASN1Primitive(): ASN1Primitive {
@ -129,16 +162,13 @@ class CompositeKey private constructor (val threshold: Int,
} }
} }
companion object {
val ALGORITHM = CompositeSignature.ALGORITHM_IDENTIFIER.algorithm.toString()
}
/** /**
* Takes single PublicKey and checks if CompositeKey requirements hold for that key. * Takes single PublicKey and checks if CompositeKey requirements hold for that key.
*/ */
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key)) fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
override fun getAlgorithm() = ALGORITHM override fun getAlgorithm() = KEY_ALGORITHM
override fun getEncoded(): ByteArray { override fun getEncoded(): ByteArray {
val keyVector = ASN1EncodableVector() val keyVector = ASN1EncodableVector()
val childrenVector = ASN1EncodableVector() val childrenVector = ASN1EncodableVector()
@ -147,13 +177,14 @@ class CompositeKey private constructor (val threshold: Int,
} }
keyVector.add(ASN1Integer(threshold.toLong())) keyVector.add(ASN1Integer(threshold.toLong()))
keyVector.add(DERSequence(childrenVector)) keyVector.add(DERSequence(childrenVector))
return SubjectPublicKeyInfo(CompositeSignature.ALGORITHM_IDENTIFIER, DERSequence(keyVector)).encoded return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), DERSequence(keyVector)).encoded
} }
override fun getFormat() = ASN1Encoding.DER override fun getFormat() = ASN1Encoding.DER
// Extracted method from isFulfilledBy. // Extracted method from isFulfilledBy.
private fun checkFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean { private fun checkFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
if (keysToCheck.any { it is CompositeKey } ) return false if (keysToCheck.any { it is CompositeKey }) return false
val totalWeight = children.map { (node, weight) -> val totalWeight = children.map { (node, weight) ->
if (node is CompositeKey) { if (node is CompositeKey) {
if (node.checkFulfilledBy(keysToCheck)) weight else 0 if (node.checkFulfilledBy(keysToCheck)) weight else 0
@ -221,18 +252,18 @@ class CompositeKey private constructor (val threshold: Int,
* Builds the [CompositeKey]. If [threshold] is not specified, it will default to * Builds the [CompositeKey]. If [threshold] is not specified, it will default to
* the total (aggregated) weight of the children, effectively generating an "N of N" requirement. * the total (aggregated) weight of the children, effectively generating an "N of N" requirement.
* During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes. * During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes.
*
* @throws IllegalArgumentException
*/ */
@Throws(IllegalArgumentException::class)
fun build(threshold: Int? = null): PublicKey { fun build(threshold: Int? = null): PublicKey {
val n = children.size val n = children.size
if (n > 1) return if (n > 1)
return CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children) CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children)
else if (n == 1) { else if (n == 1) {
require(threshold == null || threshold == children.first().weight) require(threshold == null || threshold == children.first().weight)
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." } { "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
return children.first().node // We can assume that this node is a correct CompositeKey. children.first().node // We can assume that this node is a correct CompositeKey.
} } else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
} }
} }
} }

View File

@ -1,4 +1,4 @@
package net.corda.core.crypto package net.corda.core.crypto.composite
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1ObjectIdentifier
@ -10,14 +10,10 @@ import java.security.spec.AlgorithmParameterSpec
/** /**
* Dedicated class for storing a set of signatures that comprise [CompositeKey]. * Dedicated class for storing a set of signatures that comprise [CompositeKey].
*/ */
class CompositeSignature : Signature(ALGORITHM) { class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
companion object { companion object {
val ALGORITHM = "2.25.30086077608615255153862931087626791003" val SIGNATURE_ALGORITHM = "COMPOSITESIG"
// UUID-based OID fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
// TODO: Register for an OID space and issue our own shorter OID
val ALGORITHM_IDENTIFIER = AlgorithmIdentifier(ASN1ObjectIdentifier(ALGORITHM))
fun getService(provider: Provider) = Provider.Service(provider, "Signature", ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
} }
private var signatureState: State? = null private var signatureState: State? = null

View File

@ -1,5 +1,6 @@
package net.corda.core.crypto package net.corda.core.crypto.composite
import net.corda.core.crypto.DigitalSignature
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
/** /**

View File

@ -0,0 +1,34 @@
package net.corda.core.crypto.composite
import java.security.*
import java.security.spec.InvalidKeySpecException
import java.security.spec.KeySpec
import java.security.spec.X509EncodedKeySpec
class KeyFactory : KeyFactorySpi() {
@Throws(InvalidKeySpecException::class)
override fun engineGeneratePrivate(keySpec: KeySpec): PrivateKey {
// Private composite key not supported.
throw InvalidKeySpecException("key spec not recognised: " + keySpec.javaClass)
}
@Throws(InvalidKeySpecException::class)
override fun engineGeneratePublic(keySpec: KeySpec): PublicKey? {
return when (keySpec) {
is X509EncodedKeySpec -> CompositeKey.getInstance(keySpec.encoded)
else -> throw InvalidKeySpecException("key spec not recognised: " + keySpec.javaClass)
}
}
@Throws(InvalidKeySpecException::class)
override fun <T : KeySpec> engineGetKeySpec(key: Key, keySpec: Class<T>): T {
// Only support [X509EncodedKeySpec].
throw InvalidKeySpecException("Not implemented yet $key $keySpec")
}
@Throws(InvalidKeyException::class)
override fun engineTranslateKey(key: Key): Key {
throw InvalidKeyException("No other composite key providers known")
}
}

View File

@ -0,0 +1,37 @@
package net.corda.core.crypto.provider
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.composite.CompositeSignature
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.security.AccessController
import java.security.PrivilegedAction
import java.security.Provider
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
companion object {
val PROVIDER_NAME = "Corda"
}
init {
AccessController.doPrivileged(PrivilegedAction<Unit> { setup() })
}
private fun setup() {
put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory")
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature")
val compositeKeyOID = CordaObjectIdentifier.compositeKey.id
put("Alg.Alias.KeyFactory.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
put("Alg.Alias.Signature.OID.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
}
}
object CordaObjectIdentifier {
// UUID-based OID
// TODO: Register for an OID space and issue our own shorter OID
val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
}

View File

@ -3,7 +3,7 @@ package net.corda.core.node.services
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.keys import net.corda.core.crypto.keys

View File

@ -8,7 +8,7 @@ import de.javakaffee.kryoserializers.ArraysAsListSerializer
import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.BitSetSerializer
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
import de.javakaffee.kryoserializers.guava.* import de.javakaffee.kryoserializers.guava.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.MetaData import net.corda.core.crypto.MetaData
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction

View File

@ -9,6 +9,7 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.AttachmentsClassLoader import net.corda.core.node.AttachmentsClassLoader
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction

View File

@ -2,7 +2,7 @@ package net.corda.core.contracts
import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.sign import net.corda.core.crypto.sign

View File

@ -1,14 +1,25 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.composite.CompositeSignature
import net.corda.core.crypto.composite.CompositeSignaturesWithKeys
import net.corda.core.div
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class CompositeKeyTests { class CompositeKeyTests {
@Rule
@JvmField
val tempFolder: TemporaryFolder = TemporaryFolder()
val aliceKey = generateKeyPair() val aliceKey = generateKeyPair()
val bobKey = generateKeyPair() val bobKey = generateKeyPair()
val charlieKey = generateKeyPair() val charlieKey = generateKeyPair()
@ -65,7 +76,7 @@ class CompositeKeyTests {
} }
@Test @Test
fun `encoded tree decodes correctly`() { fun `kryo encoded tree decodes correctly`() {
val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build()
val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1)
@ -75,6 +86,35 @@ class CompositeKeyTests {
assertEquals(decoded, aliceAndBobOrCharlie) assertEquals(decoded, aliceAndBobOrCharlie)
} }
@Test
fun `der encoded tree decodes correctly`() {
val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build()
val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1)
val encoded = aliceAndBobOrCharlie.encoded
val decoded = CompositeKey.getInstance(encoded)
assertEquals(decoded, aliceAndBobOrCharlie)
}
@Test
fun `der encoded tree decodes correctly with weighting`() {
val aliceAndBob = CompositeKey.Builder()
.addKey(alicePublicKey, 2)
.addKey(bobPublicKey, 1)
.build(threshold = 2)
val aliceAndBobOrCharlie = CompositeKey.Builder()
.addKey(aliceAndBob, 3)
.addKey(charliePublicKey, 2)
.build(threshold = 3)
val encoded = aliceAndBobOrCharlie.encoded
val decoded = CompositeKey.getInstance(encoded)
assertEquals(decoded, aliceAndBobOrCharlie)
}
@Test @Test
fun `tree canonical form`() { fun `tree canonical form`() {
assertEquals(CompositeKey.Builder().addKeys(alicePublicKey).build(), alicePublicKey) assertEquals(CompositeKey.Builder().addKeys(alicePublicKey).build(), alicePublicKey)
@ -260,6 +300,59 @@ class CompositeKeyTests {
assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) }
} }
@Test
fun `Test save to keystore`() {
// From test case [CompositeKey from multiple signature schemes and signature verification]
val (privRSA, pubRSA) = Crypto.generateKeyPair(Crypto.RSA_SHA256)
val (privK1, pubK1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256)
val (privR1, pubR1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
val (privEd, pubEd) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512)
val (privSP, pubSP) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256)
val RSASignature = privRSA.sign(message.bytes, pubRSA)
val K1Signature = privK1.sign(message.bytes, pubK1)
val R1Signature = privR1.sign(message.bytes, pubR1)
val EdSignature = privEd.sign(message.bytes, pubEd)
val SPSignature = privSP.sign(message.bytes, pubSP)
val compositeKey = CompositeKey.Builder().addKeys(pubRSA, pubK1, pubR1, pubEd, pubSP).build() as CompositeKey
val signatures = listOf(RSASignature, K1Signature, R1Signature, EdSignature, SPSignature)
assertTrue { compositeKey.isFulfilledBy(signatures.byKeys()) }
// One signature is missing.
val signaturesWithoutRSA = listOf(K1Signature, R1Signature, EdSignature, SPSignature)
assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) }
// Create self sign CA.
val caKeyPair = Crypto.generateKeyPair()
val ca = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test CA"), caKeyPair)
// Sign the composite key with the self sign CA.
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, X500Name("CN=CompositeKey"), compositeKey)
// Store certificate to keystore.
val keystorePath = tempFolder.root.toPath() / "keystore.jks"
val keystore = KeyStoreUtilities.loadOrCreateKeyStore(keystorePath, "password")
keystore.setCertificateEntry("CompositeKey", compositeKeyCert.cert)
keystore.save(keystorePath, "password")
// Load keystore from disk.
val keystore2 = KeyStoreUtilities.loadKeyStore(keystorePath, "password")
assertTrue { keystore2.containsAlias("CompositeKey") }
val key = keystore2.getCertificate("CompositeKey").publicKey
// Convert sun public key to Composite key.
val compositeKey2 = Crypto.toSupportedPublicKey(key)
assertTrue { compositeKey2 is CompositeKey }
// Run the same composite key test again.
assertTrue { compositeKey2.isFulfilledBy(signatures.byKeys()) }
assertFalse { compositeKey2.isFulfilledBy(signaturesWithoutRSA.byKeys()) }
// Ensure keys are the same before and after keystore.
assertEquals(compositeKey, compositeKey2)
}
@Test @Test
fun `CompositeKey deterministic children sorting`() { fun `CompositeKey deterministic children sorting`() {
val (_, pub1) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) val (_, pub1) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512)

View File

@ -344,7 +344,7 @@ class CryptoUtilsTest {
@Test @Test
fun `Check supported algorithms`() { fun `Check supported algorithms`() {
val algList: List<String> = Crypto.supportedSignatureSchemes.keys.toList() val algList: List<String> = Crypto.supportedSignatureSchemes.keys.toList()
val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512") val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE")
assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); } assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); }
} }

View File

@ -9,7 +9,7 @@ import io.requery.sql.*
import io.requery.sql.platform.Generic import io.requery.sql.platform.Generic
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String

View File

@ -4,7 +4,8 @@ import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.CompositeKey import net.corda.testing.contracts.DummyContract
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.div import net.corda.core.div
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
@ -22,7 +23,6 @@ import net.corda.node.services.transactions.minClusterSize
import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.ServiceIdentityGenerator
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.After import org.junit.After

View File

@ -10,6 +10,7 @@ import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.* import net.corda.core.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate

View File

@ -1,7 +1,6 @@
package net.corda.node.utilities package net.corda.node.utilities
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize

View File

@ -2,6 +2,7 @@ package net.corda.testing
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.expandedCompositeKeys
import net.corda.core.crypto.testing.NullSignature import net.corda.core.crypto.testing.NullSignature
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub