diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index f25c568efa..6877f2c76f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -336,13 +336,14 @@ val KClass<*>.packageName: String get() = java.`package`.name fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection -fun URL.post(serializedData: OpaqueBytes) { - openHttpConnection().apply { +fun URL.post(serializedData: OpaqueBytes): ByteArray { + return openHttpConnection().run { doOutput = true requestMethod = "POST" setRequestProperty("Content-Type", "application/octet-stream") outputStream.use { serializedData.open().copyTo(it) } checkOkResponse() + inputStream.use { it.readBytes() } } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt index d557c08022..45e9fa949c 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt @@ -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_INTERMEDIATE_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 java.io.ByteArrayOutputStream import java.io.InputStream @@ -43,8 +45,11 @@ class RegistrationWebService(private val csrHandler: CsrHandler, private val cli @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.TEXT_PLAIN) fun submitRequest(input: InputStream): Response { - val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) } - val requestId = csrHandler.saveRequest(certificationRequest) + val csr = input.use { JcaPKCS10CertificationRequest(it.readBytes()) } + if (!csr.isSignatureValid()) { + return status(Response.Status.BAD_REQUEST).entity("Invalid CSR signature").build() + } + val requestId = csrHandler.saveRequest(csr) return ok(requestId).build() } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt index e55f2fe4f6..b85fe54525 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt @@ -15,26 +15,31 @@ import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer import com.r3.corda.networkmanage.doorman.signer.CsrHandler +import net.corda.core.CordaOID import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash 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.OpaqueBytes import net.corda.core.utilities.seconds import net.corda.node.utilities.registration.cacheControl -import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair -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.* import net.corda.nodeapi.internal.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME import net.corda.testing.internal.createDevIntermediateCaCertPath import org.apache.commons.io.IOUtils 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.style.BCStyle import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.junit.After import org.junit.Before import org.junit.Test @@ -43,13 +48,14 @@ import java.net.HttpURLConnection import java.net.HttpURLConnection.* import java.net.URL import java.nio.charset.StandardCharsets.UTF_8 +import java.security.KeyPair import java.security.cert.CertPath import java.security.cert.X509Certificate import java.util.* import java.util.zip.ZipInputStream import javax.security.auth.x500.X500Principal -import javax.ws.rs.core.MediaType import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class RegistrationWebServiceTest : TestBase() { private lateinit var webServer: NetworkManagementWebServer @@ -75,7 +81,7 @@ class RegistrationWebServiceTest : TestBase() { } @Test - fun `submit request`() { + fun `submit request succeeds`() { val id = SecureHash.randomSHA256().toString() val requestProcessor = mock { @@ -97,6 +103,22 @@ class RegistrationWebServiceTest : TestBase() { 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("Invalid CSR signature") { + submitRequest(request) + } + } + @Test fun `retrieve certificate`() { val keyPair = Crypto.generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) @@ -213,12 +235,7 @@ class RegistrationWebServiceTest : TestBase() { } private fun submitRequest(request: PKCS10CertificationRequest): String { - val conn = URL("http://${webServer.hostAndPort}/certificate").openConnection() as HttpURLConnection - 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() } + return String(URL("http://${webServer.hostAndPort}/certificate").post(OpaqueBytes(request.encoded))) } 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 { data class NotReady(val pollInterval: Int) : PollResponse data class Ready(val certChain: List) : PollResponse diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index 7f9a9e4e4e..f2cba25503 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -14,7 +14,10 @@ import net.corda.core.CordaOID import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureScheme 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.millis import org.bouncycastle.asn1.* @@ -36,6 +39,7 @@ import java.math.BigInteger import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey +import java.security.SignatureException import java.security.cert.* import java.security.cert.Certificate import java.time.Duration @@ -275,7 +279,11 @@ object X509Utilities { return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public) .addAttribute(BCStyle.E, DERUTF8String(email)) .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 { @@ -321,6 +329,13 @@ val Certificate.x509: X509Certificate get() = requireNotNull(this as? X509Certif val Array.x509: List 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 * so assume this class is not.