Skip to content

Commit 3f94a96

Browse files
committed
Fix parsing of PBES2 encrypted PKCS#8 keys
This commit adds support for decrypting PKCS#8 encoded private keys that have been encrypted using a PBES2 based scheme (AES only). Unfortunately `java.crypto.EncryptedPrivateKeyInfo` doesn't make this easy as the underlying encryption algorithm is hidden within the `AlgorithmParameters`, and can only be extracted by calling `toString()` on the parameters object. See: https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4 See: AlgorithmParameters#toString() See: com.sun.crypto.provider.PBES2Parameters#toString() Backport of: elastic#78904
1 parent 88ed45c commit 3f94a96

File tree

13 files changed

+490
-70
lines changed

13 files changed

+490
-70
lines changed

libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,22 @@ final class DerParser {
3636
private static final int CONSTRUCTED = 0x20;
3737

3838
// Tag and data types
39-
private static final int INTEGER = 0x02;
40-
private static final int OCTET_STRING = 0x04;
41-
private static final int OBJECT_OID = 0x06;
42-
private static final int NUMERIC_STRING = 0x12;
43-
private static final int PRINTABLE_STRING = 0x13;
44-
private static final int VIDEOTEX_STRING = 0x15;
45-
private static final int IA5_STRING = 0x16;
46-
private static final int GRAPHIC_STRING = 0x19;
47-
private static final int ISO646_STRING = 0x1A;
48-
private static final int GENERAL_STRING = 0x1B;
49-
50-
private static final int UTF8_STRING = 0x0C;
51-
private static final int UNIVERSAL_STRING = 0x1C;
52-
private static final int BMP_STRING = 0x1E;
53-
39+
static final class Type {
40+
static final int INTEGER = 0x02;
41+
static final int OCTET_STRING = 0x04;
42+
static final int OBJECT_OID = 0x06;
43+
static final int SEQUENCE = 0x10;
44+
static final int NUMERIC_STRING = 0x12;
45+
static final int PRINTABLE_STRING = 0x13;
46+
static final int VIDEOTEX_STRING = 0x15;
47+
static final int IA5_STRING = 0x16;
48+
static final int GRAPHIC_STRING = 0x19;
49+
static final int ISO646_STRING = 0x1A;
50+
static final int GENERAL_STRING = 0x1B;
51+
static final int UTF8_STRING = 0x0C;
52+
static final int UNIVERSAL_STRING = 0x1C;
53+
static final int BMP_STRING = 0x1E;
54+
}
5455

5556
private InputStream derInputStream;
5657
private int maxAsnObjectLength;
@@ -60,6 +61,22 @@ final class DerParser {
6061
this.maxAsnObjectLength = bytes.length;
6162
}
6263

64+
/**
65+
* Read an object and verify its type
66+
* @param requiredType The expected type code
67+
* @throws IOException if data can not be parsed
68+
* @throws IllegalStateException if the parsed object is of the wrong type
69+
*/
70+
Asn1Object readAsn1Object(int requiredType) throws IOException {
71+
final Asn1Object obj = readAsn1Object();
72+
if (obj.type != requiredType) {
73+
throw new IllegalStateException(
74+
"Expected ASN.1 object of type 0x" + Integer.toHexString(requiredType) + " but was 0x" + Integer.toHexString(obj.type)
75+
);
76+
}
77+
return obj;
78+
}
79+
6380
Asn1Object readAsn1Object() throws IOException {
6481
int tag = derInputStream.read();
6582
if (tag == -1) {
@@ -207,7 +224,7 @@ public DerParser getParser() throws IOException {
207224
* @return BigInteger
208225
*/
209226
public BigInteger getInteger() throws IOException {
210-
if (type != DerParser.INTEGER)
227+
if (type != Type.INTEGER)
211228
throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$
212229

213230
return new BigInteger(value);
@@ -218,28 +235,28 @@ public String getString() throws IOException {
218235
String encoding;
219236

220237
switch (type) {
221-
case DerParser.OCTET_STRING:
238+
case Type.OCTET_STRING:
222239
// octet string is basically a byte array
223240
return toHexString(value);
224-
case DerParser.NUMERIC_STRING:
225-
case DerParser.PRINTABLE_STRING:
226-
case DerParser.VIDEOTEX_STRING:
227-
case DerParser.IA5_STRING:
228-
case DerParser.GRAPHIC_STRING:
229-
case DerParser.ISO646_STRING:
230-
case DerParser.GENERAL_STRING:
241+
case Type.NUMERIC_STRING:
242+
case Type.PRINTABLE_STRING:
243+
case Type.VIDEOTEX_STRING:
244+
case Type.IA5_STRING:
245+
case Type.GRAPHIC_STRING:
246+
case Type.ISO646_STRING:
247+
case Type.GENERAL_STRING:
231248
encoding = "ISO-8859-1"; //$NON-NLS-1$
232249
break;
233250

234-
case DerParser.BMP_STRING:
251+
case Type.BMP_STRING:
235252
encoding = "UTF-16BE"; //$NON-NLS-1$
236253
break;
237254

238-
case DerParser.UTF8_STRING:
255+
case Type.UTF8_STRING:
239256
encoding = "UTF-8"; //$NON-NLS-1$
240257
break;
241258

242-
case DerParser.UNIVERSAL_STRING:
259+
case Type.UNIVERSAL_STRING:
243260
throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$
244261

245262
default:
@@ -251,7 +268,7 @@ public String getString() throws IOException {
251268

252269
public String getOid() throws IOException {
253270

254-
if (type != DerParser.OBJECT_OID) {
271+
if (type != Type.OBJECT_OID) {
255272
throw new IOException("Ivalid DER: object is not object OID");
256273
}
257274
StringBuilder sb = new StringBuilder(64);

libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.nio.file.Files;
2727
import java.nio.file.NoSuchFileException;
2828
import java.nio.file.Path;
29+
import java.security.AccessControlException;
30+
import java.security.AlgorithmParameters;
2931
import java.security.GeneralSecurityException;
3032
import java.security.KeyFactory;
3133
import java.security.KeyPairGenerator;
@@ -69,6 +71,9 @@ public final class PemUtils {
6971
private static final String OPENSSL_EC_PARAMS_FOOTER = "-----END EC PARAMETERS-----";
7072
private static final String HEADER = "-----BEGIN";
7173

74+
private static final String PBES2_OID = "1.2.840.113549.1.5.13";
75+
private static final String AES_OID = "2.16.840.1.101.3.4.1";
76+
7277
private PemUtils() {
7378
throw new IllegalStateException("Utility class should not be instantiated");
7479
}
@@ -336,17 +341,70 @@ private static PrivateKey parsePKCS8Encrypted(BufferedReader bReader, char[] key
336341
}
337342
byte[] keyBytes = Base64.getDecoder().decode(sb.toString());
338343

339-
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(keyBytes);
340-
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
344+
final EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = getEncryptedPrivateKeyInfo(keyBytes);
345+
String algorithm = encryptedPrivateKeyInfo.getAlgName();
346+
if (algorithm.equals("PBES2") || algorithm.equals("1.2.840.113549.1.5.13")) {
347+
algorithm = getPBES2Algorithm(encryptedPrivateKeyInfo);
348+
}
349+
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
341350
SecretKey secretKey = secretKeyFactory.generateSecret(new PBEKeySpec(keyPassword));
342-
Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
351+
Cipher cipher = Cipher.getInstance(algorithm);
343352
cipher.init(Cipher.DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());
344353
PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
345354
String keyAlgo = getKeyAlgorithmIdentifier(keySpec.getEncoded());
346355
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo);
347356
return keyFactory.generatePrivate(keySpec);
348357
}
349358

359+
private static EncryptedPrivateKeyInfo getEncryptedPrivateKeyInfo(byte[] keyBytes) throws IOException, GeneralSecurityException {
360+
try {
361+
return new EncryptedPrivateKeyInfo(keyBytes);
362+
} catch (IOException e) {
363+
// The Sun JCE provider can't handle non-AES PBES2 data (but it can handle PBES1 DES data - go figure)
364+
// It's not worth our effort to try and decrypt it ourselves, but we can detect it and give a good error message
365+
DerParser parser = new DerParser(keyBytes);
366+
final DerParser.Asn1Object rootSeq = parser.readAsn1Object(DerParser.Type.SEQUENCE);
367+
parser = rootSeq.getParser();
368+
final DerParser.Asn1Object algSeq = parser.readAsn1Object(DerParser.Type.SEQUENCE);
369+
parser = algSeq.getParser();
370+
final String algId = parser.readAsn1Object(DerParser.Type.OBJECT_OID).getOid();
371+
if (PBES2_OID.equals(algId)) {
372+
final DerParser.Asn1Object algData = parser.readAsn1Object(DerParser.Type.SEQUENCE);
373+
parser = algData.getParser();
374+
final DerParser.Asn1Object ignoreKdf = parser.readAsn1Object(DerParser.Type.SEQUENCE);
375+
final DerParser.Asn1Object cryptSeq = parser.readAsn1Object(DerParser.Type.SEQUENCE);
376+
parser = cryptSeq.getParser();
377+
final String encryptionId = parser.readAsn1Object(DerParser.Type.OBJECT_OID).getOid();
378+
if (encryptionId.startsWith(AES_OID) == false) {
379+
final String name = getAlgorithmNameFromOid(encryptionId);
380+
throw new GeneralSecurityException(
381+
"PKCS#8 Private Key is encrypted with unsupported PBES2 algorithm ["
382+
+ encryptionId
383+
+ "]"
384+
+ (name == null ? "" : " (" + name + ")"),
385+
e
386+
);
387+
}
388+
}
389+
throw e;
390+
}
391+
}
392+
393+
/**
394+
* This is horrible, but it's the only option other than to parse the encoded ASN.1 value ourselves
395+
* @see AlgorithmParameters#toString() and com.sun.crypto.provider.PBES2Parameters#toString()
396+
*/
397+
private static String getPBES2Algorithm(EncryptedPrivateKeyInfo encryptedPrivateKeyInfo) {
398+
final AlgorithmParameters algParameters = encryptedPrivateKeyInfo.getAlgParameters();
399+
if (algParameters != null) {
400+
return algParameters.toString();
401+
} else {
402+
// AlgorithmParameters can be null when running on BCFIPS.
403+
// However, since BCFIPS doesn't support any PBE specs, nothing we do here would work, so we just do enough to avoid an NPE
404+
return encryptedPrivateKeyInfo.getAlgName();
405+
}
406+
}
407+
350408
/**
351409
* Decrypts the password protected contents using the algorithm and IV that is specified in the PEM Headers of the file
352410
*
@@ -575,7 +633,7 @@ private static String getKeyAlgorithmIdentifier(byte[] keyBytes) throws IOExcept
575633
return "EC";
576634
}
577635
throw new GeneralSecurityException("Error parsing key algorithm identifier. Algorithm with OID [" + oidString +
578-
"] is not żsupported");
636+
"] is not supported");
579637
}
580638

581639
public static List<Certificate> readCertificates(Collection<Path> certPaths) throws CertificateException, IOException {
@@ -593,6 +651,56 @@ public static List<Certificate> readCertificates(Collection<Path> certPaths) thr
593651
return certificates;
594652
}
595653

654+
private static String getAlgorithmNameFromOid(String oidString) throws GeneralSecurityException {
655+
switch (oidString) {
656+
case "1.2.840.10040.4.1":
657+
return "DSA";
658+
case "1.2.840.113549.1.1.1":
659+
return "RSA";
660+
case "1.2.840.10045.2.1":
661+
return "EC";
662+
case "1.3.14.3.2.7":
663+
return "DES-CBC";
664+
case "2.16.840.1.101.3.4.1.1":
665+
return "AES-128_ECB";
666+
case "2.16.840.1.101.3.4.1.2":
667+
return "AES-128_CBC";
668+
case "2.16.840.1.101.3.4.1.3":
669+
return "AES-128_OFB";
670+
case "2.16.840.1.101.3.4.1.4":
671+
return "AES-128_CFB";
672+
case "2.16.840.1.101.3.4.1.6":
673+
return "AES-128_GCM";
674+
case "2.16.840.1.101.3.4.1.21":
675+
return "AES-192_ECB";
676+
case "2.16.840.1.101.3.4.1.22":
677+
return "AES-192_CBC";
678+
case "2.16.840.1.101.3.4.1.23":
679+
return "AES-192_OFB";
680+
case "2.16.840.1.101.3.4.1.24":
681+
return "AES-192_CFB";
682+
case "2.16.840.1.101.3.4.1.26":
683+
return "AES-192_GCM";
684+
case "2.16.840.1.101.3.4.1.41":
685+
return "AES-256_ECB";
686+
case "2.16.840.1.101.3.4.1.42":
687+
return "AES-256_CBC";
688+
case "2.16.840.1.101.3.4.1.43":
689+
return "AES-256_OFB";
690+
case "2.16.840.1.101.3.4.1.44":
691+
return "AES-256_CFB";
692+
case "2.16.840.1.101.3.4.1.46":
693+
return "AES-256_GCM";
694+
case "2.16.840.1.101.3.4.1.5":
695+
return "AESWrap-128";
696+
case "2.16.840.1.101.3.4.1.25":
697+
return "AESWrap-192";
698+
case "2.16.840.1.101.3.4.1.45":
699+
return "AESWrap-256";
700+
}
701+
return null;
702+
}
703+
596704
private static String getEcCurveNameFromOid(String oidString) throws GeneralSecurityException {
597705
switch (oidString) {
598706
// see https://tools.ietf.org/html/rfc5480#section-2.1.1.1

libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/PemUtilsTests.java

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.nio.file.Files;
1515
import java.nio.file.Path;
1616
import java.security.AlgorithmParameters;
17+
import java.security.GeneralSecurityException;
1718
import java.security.Key;
1819
import java.security.KeyStore;
1920
import java.security.PrivateKey;
@@ -25,6 +26,7 @@
2526
import static org.hamcrest.Matchers.equalTo;
2627
import static org.hamcrest.Matchers.instanceOf;
2728
import static org.hamcrest.Matchers.notNullValue;
29+
import static org.hamcrest.Matchers.startsWith;
2830
import static org.hamcrest.core.StringContains.containsString;
2931

3032
public class PemUtilsTests extends ESTestCase {
@@ -78,17 +80,49 @@ public void testReadPKCS8EcKey() throws Exception {
7880
assertThat(privateKey, equalTo(key));
7981
}
8082

81-
public void testReadEncryptedPKCS8Key() throws Exception {
83+
public void testReadEncryptedPKCS8PBES1Key() throws Exception {
8284
assumeFalse("Can't run in a FIPS JVM, PBE KeySpec is not available", inFipsJvm());
8385
Key key = getKeyFromKeystore("RSA");
8486
assertThat(key, notNullValue());
8587
assertThat(key, instanceOf(PrivateKey.class));
86-
PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath
87-
("/certs/pem-utils/key_pkcs8_encrypted.pem"), TESTNODE_PASSWORD);
88+
PrivateKey privateKey = PemUtils.parsePrivateKey(
89+
getDataPath("/certs/pem-utils/key_pkcs8_encrypted_pbes1_des.pem"),
90+
TESTNODE_PASSWORD
91+
);
8892
assertThat(privateKey, notNullValue());
8993
assertThat(privateKey, equalTo(key));
9094
}
9195

96+
public void testReadEncryptedPKCS8PBES2AESKey() throws Exception {
97+
assumeFalse("Can't run in a FIPS JVM, PBE KeySpec is not available", inFipsJvm());
98+
Key key = getKeyFromKeystore("RSA");
99+
assertThat(key, notNullValue());
100+
assertThat(key, instanceOf(PrivateKey.class));
101+
PrivateKey privateKey = PemUtils.parsePrivateKey(
102+
getDataPath("/certs/pem-utils/key_pkcs8_encrypted_pbes2_aes.pem"),
103+
TESTNODE_PASSWORD
104+
);
105+
assertThat(privateKey, notNullValue());
106+
assertThat(privateKey, equalTo(key));
107+
}
108+
109+
public void testReadEncryptedPKCS8PBES2DESKey() throws Exception {
110+
assumeFalse("Can't run in a FIPS JVM, PBE KeySpec is not available", inFipsJvm());
111+
112+
// Sun JSE cannot read keys encrypted with PBES2 DES (but does support AES with PBES2 and DES with PBES1)
113+
// Rather than add our own support for this we just detect that our error message is clear and meaningful
114+
final GeneralSecurityException exception = expectThrows(
115+
GeneralSecurityException.class,
116+
() -> PemUtils.parsePrivateKey(getDataPath("/certs/pem-utils/key_pkcs8_encrypted_pbes2_des.pem"), TESTNODE_PASSWORD)
117+
);
118+
assertThat(
119+
exception.getMessage(),
120+
equalTo("PKCS#8 Private Key is encrypted with unsupported PBES2 algorithm [1.3.14.3.2.7] (DES-CBC)")
121+
);
122+
assertThat(exception.getCause(), instanceOf(IOException.class));
123+
assertThat(exception.getCause().getMessage(), startsWith("PBE parameter parsing error"));
124+
}
125+
92126
public void testReadDESEncryptedPKCS1Key() throws Exception {
93127
Key key = getKeyFromKeystore("RSA");
94128
assertThat(key, notNullValue());
@@ -133,8 +167,15 @@ public void testReadOpenSslDsaKeyWithParams() throws Exception {
133167
Key key = getKeyFromKeystore("DSA");
134168
assertThat(key, notNullValue());
135169
assertThat(key, instanceOf(PrivateKey.class));
170+
<<<<<<< HEAD
136171
PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/dsa_key_openssl_plain_with_params.pem"),
137172
EMPTY_PASSWORD);
173+
=======
174+
PrivateKey privateKey = PemUtils.parsePrivateKey(
175+
getDataPath("/certs/pem-utils/dsa_key_openssl_plain_with_params.pem"),
176+
EMPTY_PASSWORD
177+
);
178+
>>>>>>> 7cc9edbf1bb (Fix parsing of PBES2 encrypted PKCS#8 keys (#78904))
138179

139180
assertThat(privateKey, notNullValue());
140181
assertThat(privateKey, equalTo(key));
@@ -164,8 +205,15 @@ public void testReadOpenSslEcKeyWithParams() throws Exception {
164205
Key key = getKeyFromKeystore("EC");
165206
assertThat(key, notNullValue());
166207
assertThat(key, instanceOf(PrivateKey.class));
208+
<<<<<<< HEAD
167209
PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath("/certs/pem-utils/ec_key_openssl_plain_with_params.pem"),
168210
EMPTY_PASSWORD);
211+
=======
212+
PrivateKey privateKey = PemUtils.parsePrivateKey(
213+
getDataPath("/certs/pem-utils/ec_key_openssl_plain_with_params.pem"),
214+
EMPTY_PASSWORD
215+
);
216+
>>>>>>> 7cc9edbf1bb (Fix parsing of PBES2 encrypted PKCS#8 keys (#78904))
169217

170218
assertThat(privateKey, notNullValue());
171219
assertThat(privateKey, equalTo(key));

0 commit comments

Comments
 (0)