mirror of
https://github.com/corda/corda.git
synced 2025-06-20 08:03:53 +00:00
Merge commit '22af527b7943c0c4376af582de6505893817111a' into andr3ej-ross-merge
# Conflicts: # docs/source/changelog.rst
This commit is contained in:
17
core/src/main/kotlin/net/corda/core/CordaOID.kt
Normal file
17
core/src/main/kotlin/net/corda/core/CordaOID.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package net.corda.core
|
||||
|
||||
/**
|
||||
* OIDs used for the Corda platform. Entries MUST NOT be removed from this file; if an OID is incorrectly assigned it
|
||||
* should be marked deprecated.
|
||||
*/
|
||||
object CordaOID {
|
||||
/** Assigned to R3, see http://www.oid-info.com/cgi-bin/display?oid=1.3.6.1.4.1.50530&action=display */
|
||||
const val R3_ROOT = "1.3.6.1.4.1.50530"
|
||||
/** OIDs issued for the Corda platform */
|
||||
const val CORDA_PLATFORM = R3_ROOT + ".1"
|
||||
/**
|
||||
* Identifier for the X.509 certificate extension specifying the Corda role. See
|
||||
* https://r3-cev.atlassian.net/wiki/spaces/AWG/pages/156860572/Certificate+identity+type+extension for details.
|
||||
*/
|
||||
const val X509_EXTENSION_CORDA_ROLE = CORDA_PLATFORM + ".1"
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.*
|
||||
@ -19,6 +20,8 @@ class PartyAndCertificate(val certPath: CertPath) {
|
||||
val certs = certPath.certificates
|
||||
require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
|
||||
certificate = certs[0] as X509Certificate
|
||||
val role = CertRole.extract(certificate)
|
||||
require(role?.isIdentity ?: false) { "Party certificate ${certificate.subjectDN} does not have a well known or confidential identity role. Found: $role" }
|
||||
}
|
||||
|
||||
@Transient
|
||||
@ -38,6 +41,24 @@ class PartyAndCertificate(val certPath: CertPath) {
|
||||
fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult {
|
||||
val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false }
|
||||
val validator = CertPathValidator.getInstance("PKIX")
|
||||
return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult
|
||||
val result = validator.validate(certPath, parameters) as PKIXCertPathValidatorResult
|
||||
// Apply Corda-specific validity rules to the chain. This only applies to chains with any roles present, so
|
||||
// an all-null chain is in theory valid.
|
||||
var parentRole: CertRole? = CertRole.extract(result.trustAnchor.trustedCert)
|
||||
for (certIdx in (0 until certPath.certificates.size).reversed()) {
|
||||
val certificate = certPath.certificates[certIdx]
|
||||
val role = CertRole.extract(certificate)
|
||||
if (parentRole != null) {
|
||||
if (role == null) {
|
||||
throw CertPathValidatorException("Child certificate whose issuer includes a Corda role, must also specify Corda role")
|
||||
}
|
||||
if (!role.isValidParent(parentRole)) {
|
||||
val certificateString = (certificate as? X509Certificate)?.subjectDN?.toString() ?: certificate.toString()
|
||||
throw CertPathValidatorException("The issuing certificate for $certificateString has role $parentRole, expected one of ${role.validParents}")
|
||||
}
|
||||
}
|
||||
parentRole = role
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
109
core/src/main/kotlin/net/corda/core/internal/CertRole.kt
Normal file
109
core/src/main/kotlin/net/corda/core/internal/CertRole.kt
Normal file
@ -0,0 +1,109 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.CordaOID
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.ASN1Primitive
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Describes the Corda role a certificate is used for. This is used both to verify the hierarchy of certificates is
|
||||
* correct, and to determine which is the well known identity's certificate.
|
||||
*
|
||||
* @property validParents the parent role of this role - must match exactly for the certificate hierarchy to be valid for use
|
||||
* in Corda. Null indicates the parent certificate must have no role (the extension must be absent).
|
||||
* @property isIdentity true if the role is valid for use as a [net.corda.core.identity.Party] identity, false otherwise (the role is Corda
|
||||
* infrastructure of some kind).
|
||||
* @property isWellKnown true if the role is a well known identity type (legal entity or service). This only makes sense
|
||||
* where [isIdentity] is true.
|
||||
*/
|
||||
// NOTE: The order of the entries in the enum MUST NOT be changed, as their ordinality is used as an identifier. Please
|
||||
// also note that IDs are numbered from 1 upwards, matching numbering of other enum types in ASN.1 specifications.
|
||||
// TODO: Link to the specification once it has a permanent URL
|
||||
enum class CertRole(val validParents: Set<CertRole?>, val isIdentity: Boolean, val isWellKnown: Boolean) : ASN1Encodable {
|
||||
/**
|
||||
* Intermediate CA (Doorman service).
|
||||
*/
|
||||
INTERMEDIATE_CA(setOf(null), false, false),
|
||||
/** Signing certificate for the network map. */
|
||||
NETWORK_MAP(setOf(null), false, false),
|
||||
/** Well known (publicly visible) identity of a service (such as notary). */
|
||||
SERVICE_IDENTITY(setOf(INTERMEDIATE_CA), true, true),
|
||||
/** Node level CA from which the TLS and well known identity certificates are issued. */
|
||||
NODE_CA(setOf(INTERMEDIATE_CA), false, false),
|
||||
/** Transport layer security certificate for a node. */
|
||||
TLS(setOf(NODE_CA), false, false),
|
||||
/** Well known (publicly visible) identity of a legal entity. */
|
||||
LEGAL_IDENTITY(setOf(INTERMEDIATE_CA, NODE_CA), true, true),
|
||||
/** Confidential (limited visibility) identity of a legal entity. */
|
||||
CONFIDENTIAL_LEGAL_IDENTITY(setOf(LEGAL_IDENTITY), true, false);
|
||||
|
||||
companion object {
|
||||
private var cachedRoles: Array<CertRole>? = null
|
||||
/**
|
||||
* Get a role from its ASN.1 encoded form.
|
||||
*
|
||||
* @throws IllegalArgumentException if the encoded data is not a valid role.
|
||||
*/
|
||||
fun getInstance(id: ASN1Integer): CertRole {
|
||||
if (cachedRoles == null) {
|
||||
cachedRoles = CertRole.values()
|
||||
}
|
||||
val idVal = id.value
|
||||
require(idVal.compareTo(BigInteger.ZERO) > 0) { "Invalid role ID" }
|
||||
return try {
|
||||
val ordinal = idVal.intValueExact() - 1
|
||||
cachedRoles!![ordinal]
|
||||
} catch (ex: ArithmeticException) {
|
||||
throw IllegalArgumentException("Invalid role ID")
|
||||
} catch (ex: ArrayIndexOutOfBoundsException) {
|
||||
throw IllegalArgumentException("Invalid role ID")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role from its ASN.1 encoded form.
|
||||
*
|
||||
* @throws IllegalArgumentException if the encoded data is not a valid role.
|
||||
*/
|
||||
fun getInstance(data: ByteArray): CertRole = getInstance(ASN1Integer.getInstance(data))
|
||||
|
||||
/**
|
||||
* Get a role from a certificate.
|
||||
*
|
||||
* @return the role if the extension is present, or null otherwise.
|
||||
* @throws IllegalArgumentException if the extension is present but is invalid.
|
||||
*/
|
||||
fun extract(cert: Certificate): CertRole? {
|
||||
val x509Cert = cert as? X509Certificate
|
||||
return if (x509Cert != null) {
|
||||
extract(x509Cert)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role from a certificate.
|
||||
*
|
||||
* @return the role if the extension is present, or null otherwise.
|
||||
* @throws IllegalArgumentException if the extension is present but is invalid.
|
||||
*/
|
||||
fun extract(cert: X509Certificate): CertRole? {
|
||||
val extensionData: ByteArray? = cert.getExtensionValue(CordaOID.X509_EXTENSION_CORDA_ROLE)
|
||||
return if (extensionData != null) {
|
||||
val extensionString = DEROctetString.getInstance(extensionData)
|
||||
getInstance(extensionString.octets)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isValidParent(parent: CertRole?): Boolean = parent in validParents
|
||||
|
||||
override fun toASN1Primitive(): ASN1Primitive = ASN1Integer(this.ordinal + 1L)
|
||||
}
|
@ -337,7 +337,7 @@ class CompositeKeyTests {
|
||||
val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair)
|
||||
|
||||
// Sign the composite key with the self sign CA.
|
||||
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey)
|
||||
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.LEGAL_IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey)
|
||||
|
||||
// Store certificate to keystore.
|
||||
val keystorePath = tempFolder.root.toPath() / "keystore.jks"
|
||||
|
@ -1,11 +1,14 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.read
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.crypto.KEYSTORE_TYPE
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.save
|
||||
import net.corda.testing.DEV_CA
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.getTestPartyAndCertificate
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -14,12 +17,19 @@ import org.junit.Test
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyStore
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class PartyAndCertificateTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
@Test
|
||||
fun `should reject a path with no roles`() {
|
||||
val path = X509CertificateFactory().generateCertPath(DEV_CA.certificate.cert)
|
||||
assertFailsWith<IllegalArgumentException> { PartyAndCertificate(path) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `kryo serialisation`() {
|
||||
val original = getTestPartyAndCertificate(Party(
|
||||
|
@ -0,0 +1,25 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CertRoleTests {
|
||||
@Test
|
||||
fun `should deserialize valid value`() {
|
||||
val expected = CertRole.INTERMEDIATE_CA
|
||||
val actual = CertRole.getInstance(ASN1Integer(1L))
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject invalid values`() {
|
||||
// Below the lowest used value
|
||||
assertFailsWith<IllegalArgumentException> { CertRole.getInstance(ASN1Integer(0L)) }
|
||||
// Outside of the array size, but a valid integer
|
||||
assertFailsWith<IllegalArgumentException> { CertRole.getInstance(ASN1Integer(Integer.MAX_VALUE - 1L)) }
|
||||
// Outside of the range of integers
|
||||
assertFailsWith<IllegalArgumentException> { CertRole.getInstance(ASN1Integer(Integer.MAX_VALUE + 1L)) }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user