mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +00:00
Adding CSR signature verification (#537)
This commit is contained in:
parent
34800ab527
commit
341e060424
@ -336,13 +336,14 @@ val KClass<*>.packageName: String get() = java.`package`.name
|
|||||||
|
|
||||||
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
|
||||||
|
|
||||||
fun URL.post(serializedData: OpaqueBytes) {
|
fun URL.post(serializedData: OpaqueBytes): ByteArray {
|
||||||
openHttpConnection().apply {
|
return openHttpConnection().run {
|
||||||
doOutput = true
|
doOutput = true
|
||||||
requestMethod = "POST"
|
requestMethod = "POST"
|
||||||
setRequestProperty("Content-Type", "application/octet-stream")
|
setRequestProperty("Content-Type", "application/octet-stream")
|
||||||
outputStream.use { serializedData.open().copyTo(it) }
|
outputStream.use { serializedData.open().copyTo(it) }
|
||||||
checkOkResponse()
|
checkOkResponse()
|
||||||
|
inputStream.use { it.readBytes() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import com.r3.corda.networkmanage.doorman.signer.CsrHandler
|
|||||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
|
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||||
|
import net.corda.nodeapi.internal.crypto.isSignatureValid
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -43,8 +45,11 @@ class RegistrationWebService(private val csrHandler: CsrHandler, private val cli
|
|||||||
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
fun submitRequest(input: InputStream): Response {
|
fun submitRequest(input: InputStream): Response {
|
||||||
val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) }
|
val csr = input.use { JcaPKCS10CertificationRequest(it.readBytes()) }
|
||||||
val requestId = csrHandler.saveRequest(certificationRequest)
|
if (!csr.isSignatureValid()) {
|
||||||
|
return status(Response.Status.BAD_REQUEST).entity("Invalid CSR signature").build()
|
||||||
|
}
|
||||||
|
val requestId = csrHandler.saveRequest(csr)
|
||||||
return ok(requestId).build()
|
return ok(requestId).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,26 +15,31 @@ import com.r3.corda.networkmanage.TestBase
|
|||||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||||
import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
|
import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
|
||||||
import com.r3.corda.networkmanage.doorman.signer.CsrHandler
|
import com.r3.corda.networkmanage.doorman.signer.CsrHandler
|
||||||
|
import net.corda.core.CordaOID
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.CertRole
|
||||||
|
import net.corda.core.internal.post
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.utilities.registration.cacheControl
|
import net.corda.node.utilities.registration.cacheControl
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
|
import org.bouncycastle.asn1.DERUTF8String
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
import org.bouncycastle.asn1.x509.GeneralName
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
import org.bouncycastle.asn1.x509.NameConstraints
|
import org.bouncycastle.asn1.x509.NameConstraints
|
||||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||||
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -43,13 +48,14 @@ import java.net.HttpURLConnection
|
|||||||
import java.net.HttpURLConnection.*
|
import java.net.HttpURLConnection.*
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.StandardCharsets.UTF_8
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
|
import java.security.KeyPair
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
import javax.ws.rs.core.MediaType
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class RegistrationWebServiceTest : TestBase() {
|
class RegistrationWebServiceTest : TestBase() {
|
||||||
private lateinit var webServer: NetworkManagementWebServer
|
private lateinit var webServer: NetworkManagementWebServer
|
||||||
@ -75,7 +81,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `submit request`() {
|
fun `submit request succeeds`() {
|
||||||
val id = SecureHash.randomSHA256().toString()
|
val id = SecureHash.randomSHA256().toString()
|
||||||
|
|
||||||
val requestProcessor = mock<CsrHandler> {
|
val requestProcessor = mock<CsrHandler> {
|
||||||
@ -97,6 +103,22 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
verify(requestProcessor, times(2)).saveRequest(any())
|
verify(requestProcessor, times(2)).saveRequest(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `submit request fails with invalid public key`() {
|
||||||
|
startSigningServer(mock())
|
||||||
|
|
||||||
|
val keyPairGenuine = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val keyPairMalicious = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
val request = createUnverifiedCertificateSigningRequest(
|
||||||
|
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
|
||||||
|
"my@mail.com",
|
||||||
|
KeyPair(keyPairMalicious.public, keyPairGenuine.private))
|
||||||
|
// Post request to signing server via http.
|
||||||
|
assertFailsWith<IOException>("Invalid CSR signature") {
|
||||||
|
submitRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `retrieve certificate`() {
|
fun `retrieve certificate`() {
|
||||||
val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
@ -213,12 +235,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun submitRequest(request: PKCS10CertificationRequest): String {
|
private fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||||
val conn = URL("http://${webServer.hostAndPort}/certificate").openConnection() as HttpURLConnection
|
return String(URL("http://${webServer.hostAndPort}/certificate").post(OpaqueBytes(request.encoded)))
|
||||||
conn.doOutput = true
|
|
||||||
conn.requestMethod = "POST"
|
|
||||||
conn.setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM)
|
|
||||||
conn.outputStream.write(request.encoded)
|
|
||||||
return conn.inputStream.bufferedReader().use { it.readLine() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pollForResponse(id: String): PollResponse {
|
private fun pollForResponse(id: String): PollResponse {
|
||||||
@ -241,6 +258,14 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createUnverifiedCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
|
||||||
|
val signer = ContentSignerBuilder.build(DEFAULT_TLS_SIGNATURE_SCHEME, keyPair.private, Crypto.findProvider(DEFAULT_TLS_SIGNATURE_SCHEME.providerName))
|
||||||
|
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public)
|
||||||
|
.addAttribute(BCStyle.E, DERUTF8String(email))
|
||||||
|
.addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), CertRole.NODE_CA)
|
||||||
|
.build(signer)
|
||||||
|
}
|
||||||
|
|
||||||
private interface PollResponse {
|
private interface PollResponse {
|
||||||
data class NotReady(val pollInterval: Int) : PollResponse
|
data class NotReady(val pollInterval: Int) : PollResponse
|
||||||
data class Ready(val certChain: List<X509Certificate>) : PollResponse
|
data class Ready(val certChain: List<X509Certificate>) : PollResponse
|
||||||
|
@ -14,7 +14,10 @@ import net.corda.core.CordaOID
|
|||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SignatureScheme
|
import net.corda.core.crypto.SignatureScheme
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.CertRole
|
||||||
|
import net.corda.core.internal.reader
|
||||||
|
import net.corda.core.internal.uncheckedCast
|
||||||
|
import net.corda.core.internal.writer
|
||||||
import net.corda.core.utilities.days
|
import net.corda.core.utilities.days
|
||||||
import net.corda.core.utilities.millis
|
import net.corda.core.utilities.millis
|
||||||
import org.bouncycastle.asn1.*
|
import org.bouncycastle.asn1.*
|
||||||
@ -36,6 +39,7 @@ import java.math.BigInteger
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.security.SignatureException
|
||||||
import java.security.cert.*
|
import java.security.cert.*
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@ -275,7 +279,11 @@ object X509Utilities {
|
|||||||
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public)
|
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public)
|
||||||
.addAttribute(BCStyle.E, DERUTF8String(email))
|
.addAttribute(BCStyle.E, DERUTF8String(email))
|
||||||
.addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), certRole)
|
.addAttribute(ASN1ObjectIdentifier(CordaOID.X509_EXTENSION_CORDA_ROLE), certRole)
|
||||||
.build(signer)
|
.build(signer).apply {
|
||||||
|
if (!isSignatureValid()) {
|
||||||
|
throw SignatureException("The certificate signing request signature validation failed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest {
|
fun createCertificateSigningRequest(subject: X500Principal, email: String, keyPair: KeyPair, certRole: CertRole = CertRole.NODE_CA): PKCS10CertificationRequest {
|
||||||
@ -321,6 +329,13 @@ val Certificate.x509: X509Certificate get() = requireNotNull(this as? X509Certif
|
|||||||
|
|
||||||
val Array<Certificate>.x509: List<X509Certificate> get() = map { it.x509 }
|
val Array<Certificate>.x509: List<X509Certificate> get() = map { it.x509 }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the signature of the CSR
|
||||||
|
*/
|
||||||
|
fun PKCS10CertificationRequest.isSignatureValid(): Boolean {
|
||||||
|
return this.isSignatureValid(JcaContentVerifierProviderBuilder().build(this.subjectPublicKeyInfo))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
|
* Wraps a [CertificateFactory] to remove boilerplate. It's unclear whether [CertificateFactory] is threadsafe so best
|
||||||
* so assume this class is not.
|
* so assume this class is not.
|
||||||
|
Loading…
Reference in New Issue
Block a user