Add infrastructure for X.500 name support (#534)

This introduces new functions for fetching parties by their X.500 name, Kryo serialization support for X500Name objects, an X500Name generator and some X509 utility support in preparation for full X.500 name support.
This commit is contained in:
Ross Nicoll
2017-04-13 15:32:34 +01:00
committed by GitHub
parent 1a88ca4bee
commit 6d48667d91
10 changed files with 132 additions and 14 deletions

View File

@ -38,6 +38,7 @@ import java.security.spec.ECGenParameterSpec
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.* import java.util.*
import javax.security.auth.x500.X500Principal
object X509Utilities { object X509Utilities {
@ -236,6 +237,18 @@ object X509Utilities {
return keyGen.generateKeyPair() return keyGen.generateKeyPair()
} }
/**
* Create certificate signing request using provided information.
*
* @param commonName The legal name of your organization. This should not be abbreviated and should include suffixes such as Inc, Corp, or LLC.
* @param nearestCity The city where your organization is located.
* @param email An email address used to contact your organization.
* @param keyPair Standard curve ECDSA KeyPair generated for TLS.
* @return The generated Certificate signing request.
*/
@Deprecated("Use [createCertificateSigningRequest(X500Name, KeyPair)] instead, specifying full legal name")
fun createCertificateSigningRequest(commonName: String, nearestCity: String, email: String, keyPair: KeyPair): PKCS10CertificationRequest = createCertificateSigningRequest(getX509Name(commonName, nearestCity, email), keyPair)
/** /**
* Create certificate signing request using provided information. * Create certificate signing request using provided information.
* *
@ -245,8 +258,7 @@ object X509Utilities {
* @param keyPair Standard curve ECDSA KeyPair generated for TLS. * @param keyPair Standard curve ECDSA KeyPair generated for TLS.
* @return The generated Certificate signing request. * @return The generated Certificate signing request.
*/ */
fun createCertificateSigningRequest(myLegalName: String, nearestCity: String, email: String, keyPair: KeyPair): PKCS10CertificationRequest { fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair): PKCS10CertificationRequest {
val subject = getX509Name(myLegalName, nearestCity, email)
val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM)
.setProvider(BouncyCastleProvider.PROVIDER_NAME) .setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.private) .build(keyPair.private)
@ -261,16 +273,24 @@ object X509Utilities {
/** /**
* Create a de novo root self-signed X509 v3 CA cert and [KeyPair]. * Create a de novo root self-signed X509 v3 CA cert and [KeyPair].
* @param domain The Common (CN) field of the cert Subject will be populated with the domain string * @param commonName The Common (CN) field of the cert Subject will be populated with the domain string
* @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates. * @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
*/ */
fun createSelfSignedCACert(myLegalName: String): CACertAndKey { @Deprecated("Use [createSelfSignedCACert(X500Name)] instead, specifying full legal name")
fun createSelfSignedCACert(commonName: String): CACertAndKey = createSelfSignedCACert(getDevX509Name(commonName))
/**
* Create a de novo root self-signed X509 v3 CA cert and [KeyPair].
* @param subject the cert Subject will be populated with the domain string
* @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
*/
fun createSelfSignedCACert(subject: X500Name): CACertAndKey {
val keyPair = generateECDSAKeyPairForSSL() val keyPair = generateECDSAKeyPairForSSL()
val issuer = getDevX509Name(myLegalName) val issuer = subject
val serial = BigInteger.valueOf(random63BitValue()) val serial = BigInteger.valueOf(random63BitValue())
val subject = issuer
val pubKey = keyPair.public val pubKey = keyPair.public
// Ten year certificate validity // Ten year certificate validity
@ -305,18 +325,29 @@ object X509Utilities {
/** /**
* Create a de novo root intermediate X509 v3 CA cert and KeyPair. * Create a de novo root intermediate X509 v3 CA cert and KeyPair.
* @param domain The Common (CN) field of the cert Subject will be populated with the domain string * @param commonName The Common (CN) field of the cert Subject will be populated with the domain string
* @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it * @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it
* @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates. * @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates
*/ */
fun createIntermediateCert(domain: String, @Deprecated("Use [createIntermediateCert(X500Name, CACertAndKey)] instead, specifying full legal name")
fun createIntermediateCert(commonName: String,
certificateAuthority: CACertAndKey): CACertAndKey
= createIntermediateCert(getDevX509Name(commonName), certificateAuthority)
/**
* Create a de novo root intermediate X509 v3 CA cert and KeyPair.
* @param subject subject of the generated certificate.
* @param certificateAuthority The Public certificate and KeyPair of the root CA certificate above this used to sign it
* @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates
*/
fun createIntermediateCert(subject: X500Name,
certificateAuthority: CACertAndKey): CACertAndKey { certificateAuthority: CACertAndKey): CACertAndKey {
val keyPair = generateECDSAKeyPairForSSL() val keyPair = generateECDSAKeyPairForSSL()
val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject val issuer = X509CertificateHolder(certificateAuthority.certificate.encoded).subject
val serial = BigInteger.valueOf(random63BitValue()) val serial = BigInteger.valueOf(random63BitValue())
val subject = getDevX509Name(domain)
val pubKey = keyPair.public val pubKey = keyPair.public
// Ten year certificate validity // Ten year certificate validity
@ -517,8 +548,8 @@ object X509Utilities {
trustStoreFilePath: Path, trustStoreFilePath: Path,
trustStorePassword: String trustStorePassword: String
): KeyStore { ): KeyStore {
val rootCA = createSelfSignedCACert("Corda Node Root CA") val rootCA = createSelfSignedCACert(getDevX509Name("Corda Node Root CA"))
val intermediateCA = createIntermediateCert("Corda Node Intermediate CA", rootCA) val intermediateCA = createIntermediateCert(getDevX509Name("Corda Node Intermediate CA"), rootCA)
val keyPass = keyPassword.toCharArray() val keyPass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
@ -574,7 +605,25 @@ object X509Utilities {
keyPassword: String, keyPassword: String,
caKeyStore: KeyStore, caKeyStore: KeyStore,
caKeyPassword: String, caKeyPassword: String,
commonName: String): KeyStore { commonName: String): KeyStore = createKeystoreForSSL(keyStoreFilePath, storePassword, keyPassword,
caKeyStore, caKeyPassword, getDevX509Name(commonName))
/**
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine
* @param keyStoreFilePath KeyStore path to save output to
* @param storePassword access password for KeyStore
* @param keyPassword PrivateKey access password for the generated keys.
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore
* @param caKeyPassword password to unlock private keys in the CA KeyStore
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications
*/
fun createKeystoreForSSL(keyStoreFilePath: Path,
storePassword: String,
keyPassword: String,
caKeyStore: KeyStore,
caKeyPassword: String,
commonName: X500Name): KeyStore {
val rootCA = X509Utilities.loadCertificateAndKey( val rootCA = X509Utilities.loadCertificateAndKey(
caKeyStore, caKeyStore,
caKeyPassword, caKeyPassword,
@ -587,7 +636,7 @@ object X509Utilities {
val serverKey = generateECDSAKeyPairForSSL() val serverKey = generateECDSAKeyPairForSSL()
val host = InetAddress.getLocalHost() val host = InetAddress.getLocalHost()
val serverCert = createServerCert( val serverCert = createServerCert(
getDevX509Name(commonName), commonName,
serverKey.public, serverKey.public,
intermediateCA, intermediateCA,
listOf(host.hostName), listOf(host.hostName),

View File

@ -15,6 +15,7 @@ import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import org.bouncycastle.asn1.x500.X500Name
import rx.Observable import rx.Observable
import java.io.InputStream import java.io.InputStream
import java.security.PublicKey import java.security.PublicKey
@ -165,6 +166,11 @@ interface CordaRPCOps : RPCOps {
*/ */
fun partyFromName(name: String): Party? fun partyFromName(name: String): Party?
/**
* Returns the [Party] with the X.500 principal as it's [Party.name]
*/
fun partyFromX500Name(x500Name: X500Name): Party?
/** Enumerates the class names of the flows that this node knows about. */ /** Enumerates the class names of the flows that this node knows about. */
fun registeredFlows(): List<String> fun registeredFlows(): List<String>
} }

View File

@ -4,6 +4,7 @@ import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.AnonymousParty import net.corda.core.crypto.AnonymousParty
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import java.security.PublicKey import java.security.PublicKey
import org.bouncycastle.asn1.x500.X500Name
/** /**
* 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,7 +15,7 @@ interface IdentityService {
fun registerIdentity(party: Party) fun registerIdentity(party: Party)
/** /**
* Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromName] 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.
*/ */
fun getAllIdentities(): Iterable<Party> fun getAllIdentities(): Iterable<Party>
@ -25,6 +26,7 @@ interface IdentityService {
fun partyFromKey(key: PublicKey): Party? fun partyFromKey(key: PublicKey): Party?
fun partyFromName(name: String): Party? fun partyFromName(name: String): Party?
fun partyFromX500Name(principal: X500Name): Party?
fun partyFromAnonymous(party: AnonymousParty): Party? fun partyFromAnonymous(party: AnonymousParty): Party?
fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)

View File

@ -16,6 +16,7 @@ import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.NonEmptySetSerializer import net.corda.core.utilities.NonEmptySetSerializer
import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.x500.X500Name
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.Logger import org.slf4j.Logger
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -88,6 +89,8 @@ object DefaultKryoCustomizer {
// Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...).
addDefaultSerializer(InputStream::class.java, InputStreamSerializer) addDefaultSerializer(InputStream::class.java, InputStreamSerializer)
register(X500Name::class.java, X500NameSerializer)
val customization = KryoSerializationCustomization(this) val customization = KryoSerializationCustomization(this)
pluginRegistries.forEach { it.customizeSerialization(customization) } pluginRegistries.forEach { it.customizeSerialization(customization) }
} }

View File

@ -16,6 +16,9 @@ import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.x500.X500Name
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.* import java.io.*
@ -584,3 +587,17 @@ object LoggerSerializer : Serializer<Logger>() {
return LoggerFactory.getLogger(input.readString()) return LoggerFactory.getLogger(input.readString())
} }
} }
/**
* For serialising an [X500Name] without touching Sun internal classes.
*/
@ThreadSafe
object X500NameSerializer : Serializer<X500Name>() {
override fun read(kryo: Kryo, input: Input, type: Class<X500Name>): X500Name {
return X500Name.getInstance(ASN1InputStream(input.readBytes()).readObject())
}
override fun write(kryo: Kryo, output: Output, obj: X500Name) {
output.writeBytes(obj.encoded)
}
}

View File

@ -161,6 +161,8 @@ class ContractUpgradeFlowTest {
// Create some cash. // Create some cash.
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary)).resultFuture val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val baseState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() }
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0) val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0)
// Starts contract upgrade flow. // Starts contract upgrade flow.
a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2::class.java)) a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2::class.java))

View File

@ -8,6 +8,9 @@ import com.pholser.junit.quickcheck.random.SourceOfRandomness
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
@ -119,3 +122,32 @@ class TimestampGenerator : Generator<Timestamp>(Timestamp::class.java) {
} }
} }
class X500NameGenerator : Generator<X500Name>(X500Name::class.java) {
companion object {
private val charset = Charset.forName("US-ASCII")
private val asciiA = charset.encode("A")[0]
private val asciia = charset.encode("a")[0]
}
/**
* Append something that looks a bit like a proper noun to the string builder.
*/
private fun appendProperNoun(builder: StringBuilder, random: SourceOfRandomness, status: GenerationStatus) : StringBuilder {
val length = random.nextByte(1, 8)
val encoded = ByteBuffer.allocate(length.toInt())
encoded.put((random.nextByte(0, 25) + asciiA).toByte())
for (charIdx in 1..length - 1) {
encoded.put((random.nextByte(0, 25) + asciia).toByte())
}
return builder.append(charset.decode(encoded))
}
override fun generate(random: SourceOfRandomness, status: GenerationStatus): X500Name {
val wordCount = random.nextByte(1, 3)
val cn = StringBuilder()
for (word in 0..wordCount) {
appendProperNoun(cn, random, status).append(" ")
}
return X509Utilities.getDevX509Name(cn.trim().toString())
}
}

View File

@ -4,6 +4,7 @@ import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.UpgradedContract
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
@ -23,6 +24,7 @@ import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import org.bouncycastle.asn1.x500.X500Name
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import rx.Observable import rx.Observable
import java.io.InputStream import java.io.InputStream
@ -144,6 +146,7 @@ class CordaRPCOpsImpl(
override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered
override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key) override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key)
override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromName(name: String) = services.identityService.partyFromName(name)
override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name)
override fun registeredFlows(): List<String> = services.flowLogicRefFactory.flowWhitelist.keys.sorted() override fun registeredFlows(): List<String> = services.flowLogicRefFactory.flowWhitelist.keys.sorted()

View File

@ -8,6 +8,7 @@ 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 java.security.PublicKey import java.security.PublicKey
import org.bouncycastle.asn1.x500.X500Name
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
@ -35,6 +36,7 @@ class InMemoryIdentityService : SingletonSerializeAsToken(), IdentityService {
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key] override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
override fun partyFromName(name: String): Party? = nameToParties[name] override fun partyFromName(name: String): Party? = nameToParties[name]
override fun partyFromX500Name(principal: X500Name): Party? = nameToParties[principal.toString()]
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)
} }

View File

@ -22,6 +22,7 @@ import net.corda.node.services.vault.NodeVaultService
import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP
import net.corda.testing.MINI_CORP import net.corda.testing.MINI_CORP
import net.corda.testing.MOCK_VERSION import net.corda.testing.MOCK_VERSION
import org.bouncycastle.asn1.x500.X500Name
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -94,6 +95,7 @@ class MockIdentityService(val identities: List<Party>) : IdentityService, Single
override fun partyFromAnonymous(partyRef: PartyAndReference): Party? = partyFromAnonymous(partyRef.party) override fun partyFromAnonymous(partyRef: PartyAndReference): Party? = partyFromAnonymous(partyRef.party)
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key] override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
override fun partyFromName(name: String): Party? = nameToParties[name] override fun partyFromName(name: String): Party? = nameToParties[name]
override fun partyFromX500Name(principal: X500Name): Party? = nameToParties[principal.toString()]
} }