Add certificate path storage to identity service

Add functionality for generating certificate paths from identity
certificates to transaction certificates, validating, storing and
retrieving those certificate paths.
This commit is contained in:
Ross Nicoll 2017-05-04 15:34:25 +01:00
parent af7ba082a4
commit edfc4dd7d9
7 changed files with 181 additions and 8 deletions

View File

@ -1,6 +1,8 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.security.* import java.security.*
import java.security.spec.AlgorithmParameterSpec import java.security.spec.AlgorithmParameterSpec
@ -10,7 +12,12 @@ import java.security.spec.AlgorithmParameterSpec
*/ */
class CompositeSignature : Signature(ALGORITHM) { class CompositeSignature : Signature(ALGORITHM) {
companion object { companion object {
val ALGORITHM = "X-Corda-CompositeSig" val ALGORITHM = "2.25.30086077608615255153862931087626791003"
// UUID-based OID
// 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

@ -17,11 +17,11 @@ import java.io.FileWriter
import java.io.InputStream import java.io.InputStream
import java.net.InetAddress import java.net.InetAddress
import java.nio.file.Path import java.nio.file.Path
import java.security.InvalidAlgorithmParameterException
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyStore import java.security.KeyStore
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertificateFactory import java.security.cert.*
import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.* import java.util.*
@ -64,7 +64,7 @@ object X509Utilities {
} }
/** /**
* Return a bogus X509 for dev purposes. * Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
*/ */
@Deprecated("Full legal names should be specified in all configurations") @Deprecated("Full legal names should be specified in all configurations")
fun getDevX509Name(commonName: String): X500Name { fun getDevX509Name(commonName: String): X500Name {
@ -160,6 +160,34 @@ object X509Utilities {
return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses) return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses)
} }
/**
* Build a certificate path from a trusted root certificate to a target certificate. This will always return a path
* directly from the root to the target, with no intermediate certificates (presuming that path is valid).
*
* @param rootCertAndKey trusted root certificate that will be the start of the path.
* @param targetCertAndKey certificate the path ends at.
* @param revocationEnabled whether revocation of certificates in the path should be checked.
*/
fun createCertificatePath(rootCertAndKey: CertificateAndKey,
targetCertAndKey: CertificateAndKey,
revocationEnabled: Boolean): CertPathBuilderResult {
val intermediateCertificates = setOf(targetCertAndKey.certificate)
val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates))
val certPathFactory = CertPathBuilder.getInstance("PKIX")
val trustAnchor = TrustAnchor(rootCertAndKey.certificate, null)
val certPathParameters = try {
PKIXBuilderParameters(setOf(trustAnchor), X509CertSelector().apply {
certificate = targetCertAndKey.certificate
})
} catch (ex: InvalidAlgorithmParameterException) {
throw RuntimeException(ex)
}.apply {
addCertStore(certStore)
isRevocationEnabled = revocationEnabled
}
return certPathFactory.build(certPathParameters)
}
/** /**
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection * Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection
* @param x509Certificate certificate to save * @param x509Certificate certificate to save

View File

@ -1,6 +1,7 @@
package net.corda.core.identity package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.CertificateAndKey
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -27,6 +28,7 @@ import java.security.PublicKey
*/ */
// TODO: Remove "open" from [Party] once deprecated crypto.Party class is removed // TODO: Remove "open" from [Party] once deprecated crypto.Party class is removed
open class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { open class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
constructor(certAndKey: CertificateAndKey) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public)
override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey) override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey)
override fun toString() = "${owningKey.toBase58String()} ($name)" override fun toString() = "${owningKey.toBase58String()} ($name)"
override fun nameOrNull(): X500Name? = name override fun nameOrNull(): X500Name? = name

View File

@ -5,6 +5,8 @@ import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
/** /**
* An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports * An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports
@ -14,6 +16,29 @@ import java.security.PublicKey
interface IdentityService { interface IdentityService {
fun registerIdentity(party: Party) fun registerIdentity(party: Party)
/**
* Verify and then store the certificates proving that an anonymous party's key is owned by the given full
* party.
*
* @param trustedRoot trusted root certificate, typically the R3 master signing certificate.
* @param anonymousParty an anonymised party belonging to the legal entity.
* @param path certificate path from the trusted root to the anonymised party.
* @throws IllegalArgumentException if the chain does not link the two parties, or if there is already an existing
* certificate chain for the anonymous party. Anonymous parties must always resolve to a single owning party.
*/
// TODO: Move this into internal identity service once available
@Throws(IllegalArgumentException::class)
fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath)
/**
* Asserts that an anonymous party maps to the given full party, by looking up the certificate chain associated with
* the anonymous party and resolving it back to the given full party.
*
* @throws IllegalStateException if the anonymous party is not owned by the full party.
*/
@Throws(IllegalStateException::class)
fun assertOwnership(party: Party, anonymousParty: AnonymousParty)
/** /**
* Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be * Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be
* used in preference where possible. * used in preference where possible.
@ -31,4 +56,11 @@ interface IdentityService {
fun partyFromAnonymous(party: AnonymousParty): Party? fun partyFromAnonymous(party: AnonymousParty): Party?
fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)
/**
* Get the certificate chain showing an anonymous party is owned by the given party.
*/
fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath?
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
} }

View File

@ -1,6 +1,8 @@
package net.corda.node.services.identity package net.corda.node.services.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.contracts.requireThat
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
@ -8,10 +10,13 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.security.InvalidAlgorithmParameterException
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.*
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import javax.security.auth.x500.X500Principal
/** /**
* Simple identity service which caches parties and provides functionality for efficient lookup. * Simple identity service which caches parties and provides functionality for efficient lookup.
@ -24,6 +29,7 @@ class InMemoryIdentityService : SingletonSerializeAsToken(), IdentityService {
private val keyToParties = ConcurrentHashMap<PublicKey, Party>() private val keyToParties = ConcurrentHashMap<PublicKey, Party>()
private val principalToParties = ConcurrentHashMap<X500Name, Party>() private val principalToParties = ConcurrentHashMap<X500Name, Party>()
private val partyToPath = ConcurrentHashMap<AnonymousParty, CertPath>()
override fun registerIdentity(party: Party) { override fun registerIdentity(party: Party) {
log.trace { "Registering identity $party" } log.trace { "Registering identity $party" }
@ -40,4 +46,36 @@ class InMemoryIdentityService : SingletonSerializeAsToken(), IdentityService {
override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal] override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal]
override fun partyFromAnonymous(party: AnonymousParty): Party? = partyFromKey(party.owningKey) override fun partyFromAnonymous(party: AnonymousParty): Party? = partyFromKey(party.owningKey)
override fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) override fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)
@Throws(IdentityService.UnknownAnonymousPartyException::class)
override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) {
val path = partyToPath[anonymousParty] ?: throw IdentityService.UnknownAnonymousPartyException("Unknown anonymous party ${anonymousParty.owningKey.toStringShort()}")
val target = path.certificates.last() as X509Certificate
requireThat {
"Certificate path ends with \"${target.issuerX500Principal}\" expected \"${party.name}\"" using (X500Name(target.subjectX500Principal.name) == party.name)
"Certificate path ends with correct public key" using (target.publicKey == anonymousParty.owningKey)
}
// Verify there's a previous certificate in the path, which matches
val root = path.certificates.first() as X509Certificate
require(X500Name(root.issuerX500Principal.name) == party.name) { "Certificate path starts with \"${root.issuerX500Principal}\" expected \"${party.name}\"" }
}
override fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? = partyToPath[anonymousParty]
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
override fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath) {
val expectedTrustAnchor = TrustAnchor(trustedRoot, null)
require(path.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
val target = path.certificates.last() as X509Certificate
require(target.publicKey == anonymousParty.owningKey) { "Certificate path must end with anonymous party's public key" }
val validator = CertPathValidator.getInstance("PKIX")
val validatorParameters = PKIXParameters(setOf(expectedTrustAnchor)).apply {
isRevocationEnabled = false
}
val result = validator.validate(path, validatorParameters) as PKIXCertPathValidatorResult
require(result.trustAnchor == expectedTrustAnchor)
require(result.publicKey == anonymousParty.owningKey)
partyToPath[anonymousParty] = path
}
} }

View File

@ -1,23 +1,26 @@
package net.corda.node.services.network package net.corda.node.services.network
import net.corda.core.identity.Party import net.corda.core.crypto.CertificateAndKey
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.services.IdentityService
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.testing.ALICE_PUBKEY import net.corda.testing.ALICE_PUBKEY
import net.corda.testing.BOB_PUBKEY import net.corda.testing.BOB_PUBKEY
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull import kotlin.test.assertNull
/** /**
* Tests for the in memory identity service. * Tests for the in memory identity service.
*/ */
class InMemoryIdentityServiceTests { class InMemoryIdentityServiceTests {
@Test @Test
fun `get all identities`() { fun `get all identities`() {
val service = InMemoryIdentityService() val service = InMemoryIdentityService()
@ -58,4 +61,54 @@ class InMemoryIdentityServiceTests {
identities.forEach { service.registerIdentity(it) } identities.forEach { service.registerIdentity(it) }
identities.forEach { assertEquals(it, service.partyFromX500Name(it.name)) } identities.forEach { assertEquals(it, service.partyFromX500Name(it.name)) }
} }
/**
* Generate a certificate path from a root CA, down to a transaction key, store and verify the association.
*/
@Test
fun `assert unknown anonymous key is unrecognised`() {
val rootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name)
val txCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, rootCertAndKey)
val service = InMemoryIdentityService()
val rootKey = rootCertAndKey.keyPair
// TODO: Generate certificate with an EdDSA key rather than ECDSA
val identity = Party(rootCertAndKey)
val txIdentity = AnonymousParty(txCertAndKey.keyPair.public)
assertFailsWith<IdentityService.UnknownAnonymousPartyException> {
service.assertOwnership(identity, txIdentity)
}
}
/**
* Generate a pair of certificate paths from a root CA, down to a transaction key, store and verify the associations.
* Also checks that incorrect associations are rejected.
*/
@Test
fun `assert ownership`() {
val aliceRootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name)
val aliceTxCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, aliceRootCertAndKey)
val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCertAndKey, aliceTxCertAndKey, false).certPath
val bobRootCertAndKey = X509Utilities.createSelfSignedCACert(BOB.name)
val bobTxCertAndKey = X509Utilities.createIntermediateCert(BOB.name, bobRootCertAndKey)
val bobCertPath = X509Utilities.createCertificatePath(bobRootCertAndKey, bobTxCertAndKey, false).certPath
val service = InMemoryIdentityService()
val alice = Party(aliceRootCertAndKey)
val anonymousAlice = AnonymousParty(aliceTxCertAndKey.keyPair.public)
val bob = Party(bobRootCertAndKey)
val anonymousBob = AnonymousParty(bobTxCertAndKey.keyPair.public)
service.registerPath(aliceRootCertAndKey.certificate, anonymousAlice, aliceCertPath)
service.registerPath(bobRootCertAndKey.certificate, anonymousBob, bobCertPath)
// Verify that paths are verified
service.assertOwnership(alice, anonymousAlice)
service.assertOwnership(bob, anonymousBob)
assertFailsWith<IllegalArgumentException> {
service.assertOwnership(alice, anonymousBob)
}
assertFailsWith<IllegalArgumentException> {
service.assertOwnership(bob, anonymousAlice)
}
}
} }

View File

@ -28,10 +28,13 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.time.Clock import java.time.Clock
import java.util.* import java.util.*
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
@ -73,11 +76,14 @@ open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub {
} }
@ThreadSafe @ThreadSafe
class MockIdentityService(val identities: List<Party>) : IdentityService, SingletonSerializeAsToken() { class MockIdentityService(val identities: List<Party>,
val certificates: List<Triple<AnonymousParty, Party, CertPath>> = emptyList()) : IdentityService, SingletonSerializeAsToken() {
private val keyToParties: Map<PublicKey, Party> private val keyToParties: Map<PublicKey, Party>
get() = synchronized(identities) { identities.associateBy { it.owningKey } } get() = synchronized(identities) { identities.associateBy { it.owningKey } }
private val nameToParties: Map<X500Name, Party> private val nameToParties: Map<X500Name, Party>
get() = synchronized(identities) { identities.associateBy { it.name } } get() = synchronized(identities) { identities.associateBy { it.name } }
private val anonymousToPath: Map<AnonymousParty, Pair<Party, CertPath>>
get() = synchronized(certificates) { certificates.map { Pair(it.first, Pair(it.second, it.third)) }.toMap() }
override fun registerIdentity(party: Party) { override fun registerIdentity(party: Party) {
throw UnsupportedOperationException() throw UnsupportedOperationException()
@ -89,6 +95,13 @@ class MockIdentityService(val identities: List<Party>) : IdentityService, Single
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key] override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
override fun partyFromName(name: String): Party? = nameToParties[X500Name(name)] override fun partyFromName(name: String): Party? = nameToParties[X500Name(name)]
override fun partyFromX500Name(principal: X500Name): Party? = nameToParties[principal] override fun partyFromX500Name(principal: X500Name): Party? = nameToParties[principal]
@Throws(IllegalStateException::class)
override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) {
check(anonymousToPath[anonymousParty]?.first == party)
}
override fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? = anonymousToPath[anonymousParty]?.second
override fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath) { throw UnsupportedOperationException() }
} }