Skip to content

Commit

Permalink
Support reading PEM file containing EC Private Key
Browse files Browse the repository at this point in the history
Added support for reading a PEM file that contains the Base64 encoding
of a DER-encoded EC private key as described in
https://datatracker.ietf.org/doc/html/rfc5915#section-4

Signed-off-by: Kai Hudalla <[email protected]>
  • Loading branch information
sophokles73 authored and vietj committed Jun 23, 2022
1 parent 9f64678 commit 52f95ab
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 82 deletions.
59 changes: 44 additions & 15 deletions src/main/java/io/vertx/core/net/impl/KeyStoreHelper.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2021 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
Expand All @@ -11,33 +11,54 @@

package io.vertx.core.net.impl;

import io.netty.util.internal.PlatformDependent;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.net.impl.pkcs1.PrivateKeyParser;

import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.security.*;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;

import io.netty.util.internal.PlatformDependent;
import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.net.impl.pkcs1.PrivateKeyParser;

/**
* @author <a href="mailto:[email protected]">Julien Viet</a>
*/
Expand Down Expand Up @@ -257,6 +278,14 @@ private static PrivateKey loadPrivateKey(Buffer keyValue) throws Exception {
List<PrivateKey> pems = loadPems(keyValue, (delimiter, content) -> {
try {
switch (delimiter) {
case "EC PRIVATE KEY":
if (ecKeyFactory == null) {
// ECC is not supported by JVM
return Collections.emptyList();
} else {
// read PEM file as described in https://datatracker.ietf.org/doc/html/rfc5915#section-4
return Collections.singletonList(ecKeyFactory.generatePrivate(PrivateKeyParser.getECKeySpec(content)));
}
case "RSA PRIVATE KEY":
return Collections.singletonList(rsaKeyFactory.generatePrivate(PrivateKeyParser.getRSAKeySpec(content)));
case "PRIVATE KEY":
Expand All @@ -265,10 +294,10 @@ private static PrivateKey loadPrivateKey(Buffer keyValue) throws Exception {
String algorithm = PrivateKeyParser.getPKCS8EncodedKeyAlgorithm(content);
if (rsaKeyFactory.getAlgorithm().equals(algorithm)) {
return Collections.singletonList(rsaKeyFactory.generatePrivate(new PKCS8EncodedKeySpec(content)));
} else if (ecKeyFactory != null &&
ecKeyFactory.getAlgorithm().equals(algorithm)) {
} else if (ecKeyFactory != null && ecKeyFactory.getAlgorithm().equals(algorithm)) {
return Collections.singletonList(ecKeyFactory.generatePrivate(new PKCS8EncodedKeySpec(content)));
}
// fall through if ECC is not supported by JVM
default:
return Collections.emptyList();
}
Expand All @@ -277,7 +306,7 @@ private static PrivateKey loadPrivateKey(Buffer keyValue) throws Exception {
}
});
if (pems.isEmpty()) {
throw new RuntimeException("Missing -----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY----- delimiter");
throw new RuntimeException("Missing -----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY----- or -----BEGIN EC PRIVATE KEY----- delimiter");
}
return pems.get(0);
}
Expand Down
112 changes: 107 additions & 5 deletions src/main/java/io/vertx/core/net/impl/pkcs1/PrivateKeyParser.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,15 +11,20 @@

package io.vertx.core.net.impl.pkcs1;

import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.GeneralSecurityException;
import java.security.KeyPairGenerator;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.Arrays;

import io.vertx.core.VertxException;
import io.vertx.core.buffer.Buffer;

/**
* This code is copies and modifies over from net.oauth.java.jmeter:ApacheJMeter_oauth
* <p/>
Expand All @@ -40,6 +45,39 @@ public class PrivateKeyParser {
*/
private static final byte[] OID_EC_PUBLIC_KEY = { 0x2A, (byte) 0x86, 0x48, (byte) 0xCE, 0x3D, 0x02, 0x01 };

private static String oidToString(byte[] oid) {
StringBuilder result = new StringBuilder();
int value = oid[0] & 0xff;
result.append(value / 40).append(".").append(value % 40);
for (int index = 1; index < oid.length; ++index) {
byte bValue = oid[index];
if (bValue < 0) {
value = (bValue & 0b01111111);
++index;
if (index == oid.length) {
throw new IllegalArgumentException("Invalid OID");
}
value <<= 7;
value |= (oid[index] & 0b01111111);
result.append(".").append(value);
} else {
result.append(".").append(bValue);
}
}
return result.toString();
}

private static ECParameterSpec getECParameterSpec(String curveName) throws VertxException {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(new ECGenParameterSpec(curveName));
ECPublicKey publicKey = (ECPublicKey) keyPairGenerator.generateKeyPair().getPublic();
return publicKey.getParams();
} catch (GeneralSecurityException e) {
throw new VertxException("Cannot determine EC parameter spec for curve name/OID", e);
}
}

/**
* Gets the algorithm used by a PKCS#8 encoded private key.
*
Expand Down Expand Up @@ -79,6 +117,70 @@ public static String getPKCS8EncodedKeyAlgorithm(byte[] encodedKey) {
}
}

/**
* Converts a DER encoded ECPrivateKey into a Java ECPrivateKeySpec.
* <p>
* <a href="https://datatracker.ietf.org/doc/html/rfc5915#section-3">
* RFC 5915</a> defines the following ASN.1 syntax for an EC private key:
* </p>
* <pre>
* ECPrivateKey ::= SEQUENCE {
* version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
* privateKey OCTET STRING,
* parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
* publicKey [1] BIT STRING OPTIONAL
* }
* </pre>
* <p>
* 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
* <pre>
* -----BEGIN EC PRIVATE KEY-----
* -----END EC PRIVATE KEY-----
* </pre>
* as described in <a href="https://datatracker.ietf.org/doc/html/rfc5915#section-4">
* RFC 5915, Section 4</a>
*
* @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.
* <p/>
Expand Down
62 changes: 42 additions & 20 deletions src/test/java/io/vertx/core/http/HttpTLSTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <a href="mailto:[email protected]">Julien Viet</a>
Expand Down Expand Up @@ -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"
};
Expand Down
Loading

0 comments on commit 52f95ab

Please sign in to comment.