mirror of
https://github.com/corda/corda.git
synced 2024-12-21 13:57:54 +00:00
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:
parent
af7ba082a4
commit
edfc4dd7d9
@ -1,6 +1,8 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.deserialize
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.*
|
||||
import java.security.spec.AlgorithmParameterSpec
|
||||
@ -10,7 +12,12 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
*/
|
||||
class CompositeSignature : Signature(ALGORITHM) {
|
||||
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
|
||||
|
@ -17,11 +17,11 @@ import java.io.FileWriter
|
||||
import java.io.InputStream
|
||||
import java.net.InetAddress
|
||||
import java.nio.file.Path
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.cert.*
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param x509Certificate certificate to save
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.CertificateAndKey
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
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
|
||||
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 toString() = "${owningKey.toBase58String()} ($name)"
|
||||
override fun nameOrNull(): X500Name? = name
|
||||
|
@ -5,6 +5,8 @@ import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
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
|
||||
@ -14,6 +16,29 @@ import java.security.PublicKey
|
||||
interface IdentityService {
|
||||
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
|
||||
* used in preference where possible.
|
||||
@ -31,4 +56,11 @@ interface IdentityService {
|
||||
|
||||
fun partyFromAnonymous(party: AnonymousParty): 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)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package net.corda.node.services.identity
|
||||
|
||||
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.Party
|
||||
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.trace
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
/**
|
||||
* 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 principalToParties = ConcurrentHashMap<X500Name, Party>()
|
||||
private val partyToPath = ConcurrentHashMap<AnonymousParty, CertPath>()
|
||||
|
||||
override fun registerIdentity(party: Party) {
|
||||
log.trace { "Registering identity $party" }
|
||||
@ -40,4 +46,36 @@ class InMemoryIdentityService : SingletonSerializeAsToken(), IdentityService {
|
||||
override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal]
|
||||
override fun partyFromAnonymous(party: AnonymousParty): Party? = partyFromKey(party.owningKey)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,26 @@
|
||||
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.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.BOB
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* Tests for the in memory identity service.
|
||||
*/
|
||||
class InMemoryIdentityServiceTests {
|
||||
|
||||
@Test
|
||||
fun `get all identities`() {
|
||||
val service = InMemoryIdentityService()
|
||||
@ -58,4 +61,54 @@ class InMemoryIdentityServiceTests {
|
||||
identities.forEach { service.registerIdentity(it) }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,10 +28,13 @@ import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
@ -73,11 +76,14 @@ open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub {
|
||||
}
|
||||
|
||||
@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>
|
||||
get() = synchronized(identities) { identities.associateBy { it.owningKey } }
|
||||
private val nameToParties: Map<X500Name, Party>
|
||||
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) {
|
||||
throw UnsupportedOperationException()
|
||||
@ -89,6 +95,13 @@ class MockIdentityService(val identities: List<Party>) : IdentityService, Single
|
||||
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
|
||||
override fun partyFromName(name: String): Party? = nameToParties[X500Name(name)]
|
||||
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() }
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user