Merge commit '22af527b7943c0c4376af582de6505893817111a' into andr3ej-ross-merge

# Conflicts:
#	docs/source/changelog.rst
This commit is contained in:
Andrzej Cichocki
2017-12-19 17:07:17 +00:00
25 changed files with 405 additions and 106 deletions

View 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"
}

View File

@ -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
}
}

View 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)
}

View File

@ -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"

View File

@ -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(

View File

@ -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)) }
}
}