+ * + * RFC 5915 defines the following ASN.1 syntax for an EC private key: + *
+ *+ * ECPrivateKey ::= SEQUENCE { + * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + * privateKey OCTET STRING, + * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + * publicKey [1] BIT STRING OPTIONAL + * } + *+ *
+ * A private key encoded like this will most often be found in a PEM file which will + * contain the Base64 encoded DER-encoding of an ECPrivateKey sandwiched between + *
+ * -----BEGIN EC PRIVATE KEY----- + * -----END EC PRIVATE KEY----- + *+ * as described in + * RFC 5915, Section 4 + * + * @see "https://datatracker.ietf.org/doc/html/rfc5915" + * @param keyBytes The encoded key. + * @return The spec that can be used to instantiate the private key. + * @throws VertxException if the byte array does not represent an ASN.1 ECPrivateKey structure. + */ + public static ECPrivateKeySpec getECKeySpec(byte[] keyBytes) throws VertxException { + DerParser parser = new DerParser(keyBytes); + + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) { + throw new VertxException("Invalid DER: not a sequence"); + } + + // Parse inside the sequence + parser = sequence.getParser(); + + Asn1Object version = parser.read(); + if (version.getType() != DerParser.INTEGER) { + throw new VertxException(String.format( + "Invalid DER: 'version' field must be of type INTEGER (2) but found type `%d`", + version.getType())); + } else if (version.getInteger().intValue() != 1) { + throw new VertxException(String.format( + "Invalid DER: expected 'version' field to have value '1' but found '%d'", + version.getInteger().intValue())); + } + byte[] privateValue = parser.read().getValue(); + parser = parser.read().getParser(); + Asn1Object params = parser.read(); + // ECParameters are mandatory according to RFC 5915, Section 3 + if (params.getType() != DerParser.OBJECT_IDENTIFIER) { + throw new VertxException(String.format( + "Invalid DER: expected to find an OBJECT_IDENTIFIER (6) in 'parameters' but found type '%d'", + params.getType())); + } + byte[] namedCurveOid = params.getValue(); + ECParameterSpec spec = getECParameterSpec(oidToString(namedCurveOid)); + return new ECPrivateKeySpec(new BigInteger(1, privateValue), spec); + } + /** * Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec. * diff --git a/src/test/java/io/vertx/core/http/HttpTLSTest.java b/src/test/java/io/vertx/core/http/HttpTLSTest.java index ee98d9a9cda..c524477ae2f 100755 --- a/src/test/java/io/vertx/core/http/HttpTLSTest.java +++ b/src/test/java/io/vertx/core/http/HttpTLSTest.java @@ -11,24 +11,8 @@ package io.vertx.core.http; -import io.netty.util.internal.PlatformDependent; -import io.vertx.core.Future; -import io.vertx.core.Vertx; -import io.vertx.core.VertxException; -import io.vertx.core.VertxOptions; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.*; -import io.vertx.core.net.impl.TrustAllTrustManager; -import io.vertx.test.core.TestUtils; -import io.vertx.test.proxy.HAProxy; -import io.vertx.test.tls.Cert; -import io.vertx.test.tls.Trust; -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import static org.hamcrest.core.StringEndsWith.endsWith; -import javax.net.ssl.*; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -46,7 +30,39 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import static org.hamcrest.core.StringEndsWith.endsWith; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.TrustManagerFactorySpi; + +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import io.netty.util.internal.PlatformDependent; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.VertxException; +import io.vertx.core.VertxOptions; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.net.JdkSSLEngineOptions; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.KeyStoreOptions; +import io.vertx.core.net.OpenSSLEngineOptions; +import io.vertx.core.net.PemTrustOptions; +import io.vertx.core.net.ProxyOptions; +import io.vertx.core.net.ProxyType; +import io.vertx.core.net.SelfSignedCertificate; +import io.vertx.core.net.SocketAddress; +import io.vertx.core.net.TrustOptions; +import io.vertx.core.net.impl.TrustAllTrustManager; +import io.vertx.test.core.TestUtils; +import io.vertx.test.proxy.HAProxy; +import io.vertx.test.tls.Cert; +import io.vertx.test.tls.Trust; /** * @author Julien Viet @@ -1384,17 +1400,23 @@ public void testKeyCertInvalidPem() throws IOException { "", "-----BEGIN PRIVATE KEY-----", "-----BEGIN RSA PRIVATE KEY-----", + "-----BEGIN EC PRIVATE KEY-----", "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", "-----BEGIN RSA PRIVATE KEY-----\n-----END RSA PRIVATE KEY-----", + "-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----", "-----BEGIN PRIVATE KEY-----\n*\n-----END PRIVATE KEY-----", - "-----BEGIN RSA PRIVATE KEY-----\n*\n-----END RSA PRIVATE KEY-----" + "-----BEGIN RSA PRIVATE KEY-----\n*\n-----END RSA PRIVATE KEY-----", + "-----BEGIN EC PRIVATE KEY-----\n*\n-----END EC PRIVATE KEY-----" }; String[] messages = { - "Missing -----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY----- delimiter", + "Missing -----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY----- or -----BEGIN EC PRIVATE KEY----- delimiter", "Missing -----END PRIVATE KEY----- delimiter", "Missing -----END RSA PRIVATE KEY----- delimiter", + "Missing -----END EC PRIVATE KEY----- delimiter", "Empty pem file", "Empty pem file", + "Empty pem file", + "Input byte[] should at least have 2 bytes for base64 bytes", "Input byte[] should at least have 2 bytes for base64 bytes", "Input byte[] should at least have 2 bytes for base64 bytes" }; diff --git a/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java b/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java index c7ac4612b83..ed32fea549c 100644 --- a/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java +++ b/src/test/java/io/vertx/core/net/KeyStoreHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -15,7 +15,6 @@ import static org.hamcrest.CoreMatchers.instanceOf; import java.security.GeneralSecurityException; -import java.security.KeyFactory; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.cert.X509Certificate; @@ -23,13 +22,12 @@ import java.security.interfaces.RSAPrivateKey; import java.util.Enumeration; -import io.vertx.core.net.impl.KeyStoreHelper; -import io.vertx.test.core.VertxTestBase; import org.junit.Assume; import org.junit.Test; -import io.vertx.core.impl.VertxInternal; -import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.impl.KeyStoreHelper; +import io.vertx.test.core.TestUtils; +import io.vertx.test.core.VertxTestBase; /** @@ -47,8 +45,8 @@ public class KeyStoreHelperTest extends VertxTestBase { @Test public void testKeyStoreHelperSupportsRSAPrivateKeys() throws Exception { PemKeyCertOptions options = new PemKeyCertOptions() - .addKeyPath("target/test-classes/tls/server-key.pem") - .addCertPath("target/test-classes/tls/server-cert.pem"); + .addKeyPath("tls/server-key.pem") + .addCertPath("tls/server-cert.pem"); KeyStoreHelper helper = options.getHelper(vertx); assertKeyType(helper.store(), RSAPrivateKey.class); } @@ -60,12 +58,29 @@ public void testKeyStoreHelperSupportsRSAPrivateKeys() throws Exception { * @throws Exception if the key cannot be read. */ @Test - public void testKeyStoreHelperSupportsECPrivateKeys() throws Exception { + public void testKeyStoreHelperSupportsPKCS8ECPrivateKey() throws Exception { - Assume.assumeTrue("ECC is not supported by VM's security providers", isECCSupportedByVM()); + Assume.assumeTrue("ECC is not supported by VM's security providers", TestUtils.isECCSupportedByVM()); PemKeyCertOptions options = new PemKeyCertOptions() - .addKeyPath("target/test-classes/tls/server-key-ec.pem") - .addCertPath("target/test-classes/tls/server-cert-ec.pem"); + .addKeyPath("tls/server-key-ec.pem") + .addCertPath("tls/server-cert-ec.pem"); + KeyStoreHelper helper = options.getHelper(vertx); + assertKeyType(helper.store(), ECPrivateKey.class); + } + + /** + * Verifies that the key store helper can read a DER encoded EC private key + * from a PEM file. + * + * @throws Exception if the key cannot be read. + */ + @Test + public void testKeyStoreHelperSupportsReadingECPrivateKeyFromPEMFile() throws Exception { + + Assume.assumeTrue("ECC is not supported by VM's security providers", TestUtils.isECCSupportedByVM()); + PemKeyCertOptions options = new PemKeyCertOptions() + .addKeyPath("tls/server-key-ec-pkcs1.pem") + .addCertPath("tls/server-cert-ec.pem"); KeyStoreHelper helper = options.getHelper(vertx); assertKeyType(helper.store(), ECPrivateKey.class); } @@ -80,13 +95,4 @@ private void assertKeyType(KeyStore store, Class> expectedKeyType) throws KeyS assertThat(store.getCertificate(alias), instanceOf(X509Certificate.class)); } } - - private boolean isECCSupportedByVM() { - try { - KeyFactory.getInstance("EC"); - return true; - } catch (GeneralSecurityException e) { - return false; - } - } } diff --git a/src/test/java/io/vertx/core/net/impl/pkcs1/PrivateKeyParserTest.java b/src/test/java/io/vertx/core/net/impl/pkcs1/PrivateKeyParserTest.java index 5a9d6d7d365..cc65ace546d 100644 --- a/src/test/java/io/vertx/core/net/impl/pkcs1/PrivateKeyParserTest.java +++ b/src/test/java/io/vertx/core/net/impl/pkcs1/PrivateKeyParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -13,10 +13,22 @@ package io.vertx.core.net.impl.pkcs1; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECPrivateKeySpec; +import java.util.Base64; + +import org.junit.Assume; import org.junit.Test; +import io.vertx.core.Vertx; +import io.vertx.test.core.TestUtils; + /** * Verifies behavior of {@link PrivateKeyParser}. @@ -66,7 +78,30 @@ public void testGetPKCS8EncodedKeySpecSupportsEC() { } private void assertKeySpecType(byte[] encodedKey, String expectedAlgorithm) { - String keyAlgorithm = PrivateKeyParser.getPKCS8EncodedKeyAlgorithm(encodedKey); - assertThat(keyAlgorithm, is(expectedAlgorithm)); + String keyAlgorithm = PrivateKeyParser.getPKCS8EncodedKeyAlgorithm(encodedKey); + assertThat(keyAlgorithm, is(expectedAlgorithm)); + } + + /** + * Verifies that the parser can read a DER encoded ECPrivateKey. + * + * @throws GeneralSecurityException if the JVM does not support + */ + @Test + public void testGetECKeySpecSucceedsForDEREncodedECPrivateKey() throws GeneralSecurityException { + + Assume.assumeTrue("ECC is not supported by VM's security providers", TestUtils.isECCSupportedByVM()); + Vertx vertx = Vertx.vertx(); + String b = vertx.fileSystem().readFileBlocking("tls/server-key-ec-pkcs1.pem") + .toString(StandardCharsets.US_ASCII) + .replaceAll("-----BEGIN EC PRIVATE KEY-----", "") + .replaceAll("-----END EC PRIVATE KEY-----", "") + .replaceAll("\\s", ""); + byte[] derEncoding = Base64.getDecoder().decode(b); + ECPrivateKeySpec spec = PrivateKeyParser.getECKeySpec(derEncoding); + KeyFactory factory = KeyFactory.getInstance("EC"); + ECPrivateKey key = (ECPrivateKey) factory.generatePrivate(spec); + assertThat(key, notNullValue()); + assertThat(key.getAlgorithm(), is("EC")); } } diff --git a/src/test/java/io/vertx/test/core/TestUtils.java b/src/test/java/io/vertx/test/core/TestUtils.java index bc14606be54..eecb14079b7 100644 --- a/src/test/java/io/vertx/test/core/TestUtils.java +++ b/src/test/java/io/vertx/test/core/TestUtils.java @@ -11,20 +11,9 @@ package io.vertx.test.core; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.util.NetUtil; -import io.netty.util.internal.logging.InternalLoggerFactory; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.Http2Settings; -import io.vertx.core.net.*; -import io.vertx.core.net.impl.KeyStoreHelper; -import io.vertx.test.netty.TestLoggerFactory; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; -import javax.security.cert.X509Certificate; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.RandomAccessFile; @@ -32,20 +21,36 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; import java.util.EnumSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.function.Supplier; -import java.util.jar.JarEntry; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import java.util.zip.GZIPOutputStream; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import javax.security.cert.X509Certificate; + +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.util.NetUtil; +import io.netty.util.internal.logging.InternalLoggerFactory; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.Http2Settings; +import io.vertx.core.net.JksOptions; +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.PemTrustOptions; +import io.vertx.core.net.PfxOptions; +import io.vertx.core.net.TrustOptions; +import io.vertx.core.net.impl.KeyStoreHelper; +import io.vertx.test.netty.TestLoggerFactory; /** * @author Tim Fox @@ -512,4 +517,18 @@ public static TestLoggerFactory testLogging(Runnable runnable) { } return factory; } + + /** + * Checks if the JVM supports ECC algorithms. + * + * @return {@code true} if the JVM supports ECC. + */ + public static boolean isECCSupportedByVM() { + try { + KeyFactory.getInstance("EC"); + return true; + } catch (GeneralSecurityException e) { + return false; + } + } }