Skip to content

Commit

Permalink
Make PrivateKeyUtils#load method file extension agnostic
Browse files Browse the repository at this point in the history
As outlined in #264, this is achieved by trying to parse the key file
according to one of the supported formats in sequence until one works.
Given that there are only two supported formats at the moment, and that
PEM files are attempted first and more common in the wild than PVK, this
approach should have good enough performance. Because a Java exception
can only have a single cause, I've attached the underlying parse
exceptions to the higher-level `KeyException` as supressed exceptions.
These get displayed to consumers on e.g. stacktraces, allowing them to
know what exactly went wrong when parsing either format.

While at it, I've added a test to ensure that this extension-agnostic
behavior is maintained over time.

Resolves #264.
  • Loading branch information
AlexTMjugador committed Dec 7, 2024
1 parent 6acd186 commit 227e617
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ openssl genrsa -aes128 -traditional -passout pass:password -out privatekey-encry
openssl pkcs8 -topk8 -in privatekey-encrypted.pkcs1.pem -out privatekey-encrypted.pkcs8.pem -passin pass:password -passout pass:password -v2 aes128
openssl pkcs8 -topk8 -in privatekey-encrypted.pkcs1.pem -out privatekey.pkcs8.pem -passin pass:password -nocrypt
openssl pkcs8 -traditional -in privatekey.pkcs8.pem -out privatekey.pkcs1.pem -nocrypt
cp privatekey.pkcs1.pem privatekey.pkcs1.pem.key

# Generate the PVK files
# This requires an OpenSSL version with RC4 enabled (works on Debian Stretch)
Expand Down
27 changes: 27 additions & 0 deletions jsign-core/src/test/resources/keystores/privatekey.pkcs1.pem.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAppBS5ZCsecfKOR2HBZ2pP5bSD23q9OdCZTjL9emDnCwXU1Ey
Ha6MuPs9DH76Xt9knHe8LUknX9EzoJUUFi5rMvB8OOXj82PieCWK36KM4m1ecHFI
YikplNo5HD1phwwi8dGLFkWqlUh8DQjuCMQH9aQjoer54+7RkYA2iuCAnH9A/kiE
E/MpKa/7zdxAD3ngk3wSN3K4zNWzZKo7KBRso1qLiOWaK0RTIoH/b2T2mj732knc
pRB74EeWLd/IQU3NR0/2cHoVTD72Sjjz8LSruop6sqSdhbfKxnAPvRYx3jRr/hTV
wAFIVtW/IhUBrh3FGhyjnYaS37HUaE4LO0u1DwIDAQABAoIBAQCc+3MVp8fWswUV
1Y13LoPgSb5LCnaiQP9jtScN6vq+ixOk0+be8K7yfN+p0lcYaYVCrtqs98BjXyFA
XKDk0vT3uo3pdknkD6TXRdLgOSx9D0UtxqbI1TC6eP3QbtTxke+xZ6Ol5x0Bu6In
Ct6FZnR2ADARIAxK1b+wWV2OgE7Wrtd/vkSdkn9NPjBHpTUNM0gYe9A6am55KomH
5OyfwWNJw8da7PNod89kgBNjcYZ+02SQxeFzDKVgAttcGXEai4qNJoFScNiJ87WC
SmX32w6i3yfna4gM3KNSj93wGqE3+71NthIOMBNlSaZufw3S+w7mMFNRruJs3ge2
G0IOCydxAoGBANINXPxZ0zRHv+GIB7etvU8791a2Dh7782okD1uA/ewpPhJRkZSa
zMnsNwMionwkJox/ouGZU/crcBjzYXShe4LsezrosmkAiHmNeOqImjesRIcWO7dx
w1fjYXNpnC73By+rwfpLZyFHmaVqFU3XQbcKcXxIfYurk+wvLImjpfZZAoGBAMr/
quKfhBdAMDrf/KQwvvZMFXnMzjbWCi7y6qN8mwm3fhp3rAbmu14B5RQRKAvfmMr2
E84ApjCLFGjDOZM3D0YYbhOx2NgfJiQeB1O7GVg92I48UcHoBOkPsqbz9jYi2dc9
stOc4QCSOIQ8DFV8tfENVIAORgYdVP++RbvLOemnAoGAa/HAGlLS7ef9XJo6VRMs
2R4Y8m+mfBfANIiJd92nIAjlxCY06ShQG2iPsMXIuIEfak5hVwwjkT66YagZKgWe
Yl7CyTgyDzHd8JFaVTSUBA48PSuYzqHg4DaSquvX/m6mO8JJciXzvq977vzAK/t1
4umz/kmGcxNedh6cBbOaoykCgYAhkTdbtA7unVGcWq93Iwxgw+IFOwWacbhLXSXJ
lPA6Ihp7G/DZT0wKVnvf3pplpDqqzRgnR1ozyru8OxQJMOCYsa96GD2IN1ZiQIjr
opOlUMy/cGAAlXJCa7MaAltjRk4JVo18ioN2SbeIvjk35aBcVNz1M+cGWdFVXMxB
KnDQHwKBgQDAF5W0wSiNzN7Z7BV4djwL/zCMPRl3qQxwVxahbkPhHRNysYmQ3x0Y
c+X0tr6lMMda61+TdafuwAM4ROX06YcRS9/K2XAz9Q+AWCWS/zx/ylMc6+RvLKuA
FlLvSLKSXTBjAC2S2Gj6BiqJKd3a5XgEecq/rgoctIpkmA/Ms1MPCA==
-----END RSA PRIVATE KEY-----
315 changes: 161 additions & 154 deletions jsign-crypto/src/main/java/net/jsign/PrivateKeyUtils.java
Original file line number Diff line number Diff line change
@@ -1,154 +1,161 @@
/**
* Copyright 2017 Emmanuel Bourg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.jsign;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.KeyException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.function.Function;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import sun.misc.Unsafe;

/**
* Helper class for loading private keys (PVK or PEM, encrypted or not).
*
* @author Emmanuel Bourg
* @since 2.0
*/
public class PrivateKeyUtils {

private PrivateKeyUtils() {
}

/**
* Load the private key from the specified file. Supported formats are PVK and PEM,
* encrypted or not. The type of the file is inferred from its extension (<code>.pvk</code>
* for PVK files, <code>.pem</code> for PEM files).
*
* @param file the file to load the key from
* @param password the password protecting the key
* @return the private key loaded
* @throws KeyException if the key cannot be loaded
*/
public static PrivateKey load(File file, String password) throws KeyException {
try {
if (file.getName().endsWith(".pvk")) {
return PVK.parse(file, password);
} else if (file.getName().endsWith(".pem")) {
return readPrivateKeyPEM(file, password != null ? password.toCharArray() : null);
}
} catch (Exception e) {
throw new KeyException("Failed to load the private key from " + file, e);
}

throw new IllegalArgumentException("Unsupported private key format (PEM or PVK file expected");
}

/**
* Disables the signature verification of the jar containing the BouncyCastle provider.
*/
private static void disableJceSecurity() {
try {
Class<?> jceSecurityClass = Class.forName("javax.crypto.JceSecurity");
Field field = jceSecurityClass.getDeclaredField("verificationResults");
field.setAccessible(true);

Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

unsafe.putObject(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field), new HashMap<Object, Boolean>() {
@Override
public Boolean get(Object key) {
// This is not the provider you are looking for, you don't need to see its identification, move along
return Boolean.TRUE;
}

@Override
public Boolean computeIfAbsent(Object key, Function<? super Object, ? extends Boolean> mappingFunction) {
return super.computeIfAbsent(key, object -> Boolean.TRUE);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}

private static PrivateKey readPrivateKeyPEM(File file, char[] password) throws IOException, OperatorCreationException, PKCSException {
try (FileReader reader = new FileReader(file)) {
PEMParser parser = new PEMParser(reader);
Object object = parser.readObject();
if (object instanceof ASN1ObjectIdentifier) {
// ignore the EC key parameters
object = parser.readObject();
}

if (object == null) {
throw new IllegalArgumentException("No key found in " + file);
}

if (BouncyCastleProvider.class.getName().startsWith("net.jsign")) {
// disable JceSecurity to allow the use of the repackaged BouncyCastle provider
disableJceSecurity();
}
BouncyCastleProvider provider = new BouncyCastleProvider();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(provider);

if (object instanceof PEMEncryptedKeyPair) {
// PKCS1 encrypted key
PEMDecryptorProvider decryptionProvider = new JcePEMDecryptorProviderBuilder().setProvider(provider).build(password);
PEMKeyPair keypair = ((PEMEncryptedKeyPair) object).decryptKeyPair(decryptionProvider);
return converter.getPrivateKey(keypair.getPrivateKeyInfo());

} else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
// PKCS8 encrypted key
InputDecryptorProvider decryptionProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider(provider).build(password);
PrivateKeyInfo info = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(decryptionProvider);
return converter.getPrivateKey(info);

} else if (object instanceof PEMKeyPair) {
// PKCS1 unencrypted key
return converter.getKeyPair((PEMKeyPair) object).getPrivate();

} else if (object instanceof PrivateKeyInfo) {
// PKCS8 unencrypted key
return converter.getPrivateKey((PrivateKeyInfo) object);

} else {
throw new UnsupportedOperationException("Unsupported PEM object: " + object.getClass().getSimpleName());
}
}
}
}
/**
* Copyright 2017 Emmanuel Bourg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.jsign;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.KeyException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.function.Function;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import sun.misc.Unsafe;

/**
* Helper class for loading private keys (PVK or PEM, encrypted or not).
*
* @author Emmanuel Bourg
* @since 2.0
*/
public class PrivateKeyUtils {

private PrivateKeyUtils() {
}

/**
* Load the private key from the specified file. Supported formats are PVK and PEM,
* encrypted or not. The type of the file is inferred by trying the supported formats
* in sequence until one parses successfully.
*
* @param file the file to load the key from
* @param password the password protecting the key
* @return the private key loaded
* @throws KeyException if the key cannot be loaded
*/
public static PrivateKey load(File file, String password) throws KeyException {
Exception pemParseException;
try {
return readPrivateKeyPEM(file, password != null ? password.toCharArray() : null);
} catch (Exception e) {
pemParseException = e;
}

Exception pvkParseException;
try {
return PVK.parse(file, password);
} catch (Exception e) {
pvkParseException = e;
}

KeyException keyException = new KeyException("Failed to load the private key from " + file + " (valid PEM or PVK file expected)");
keyException.addSuppressed(pemParseException);
keyException.addSuppressed(pvkParseException);
throw keyException;
}

/**
* Disables the signature verification of the jar containing the BouncyCastle provider.
*/
private static void disableJceSecurity() {
try {
Class<?> jceSecurityClass = Class.forName("javax.crypto.JceSecurity");
Field field = jceSecurityClass.getDeclaredField("verificationResults");
field.setAccessible(true);

Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

unsafe.putObject(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field), new HashMap<Object, Boolean>() {
@Override
public Boolean get(Object key) {
// This is not the provider you are looking for, you don't need to see its identification, move along
return Boolean.TRUE;
}

@Override
public Boolean computeIfAbsent(Object key, Function<? super Object, ? extends Boolean> mappingFunction) {
return super.computeIfAbsent(key, object -> Boolean.TRUE);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}

private static PrivateKey readPrivateKeyPEM(File file, char[] password) throws IOException, OperatorCreationException, PKCSException {
try (FileReader reader = new FileReader(file)) {
PEMParser parser = new PEMParser(reader);
Object object = parser.readObject();
if (object instanceof ASN1ObjectIdentifier) {
// ignore the EC key parameters
object = parser.readObject();
}

if (object == null) {
throw new IllegalArgumentException("No key found in " + file);
}

if (BouncyCastleProvider.class.getName().startsWith("net.jsign")) {
// disable JceSecurity to allow the use of the repackaged BouncyCastle provider
disableJceSecurity();
}
BouncyCastleProvider provider = new BouncyCastleProvider();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(provider);

if (object instanceof PEMEncryptedKeyPair) {
// PKCS1 encrypted key
PEMDecryptorProvider decryptionProvider = new JcePEMDecryptorProviderBuilder().setProvider(provider).build(password);
PEMKeyPair keypair = ((PEMEncryptedKeyPair) object).decryptKeyPair(decryptionProvider);
return converter.getPrivateKey(keypair.getPrivateKeyInfo());

} else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
// PKCS8 encrypted key
InputDecryptorProvider decryptionProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider(provider).build(password);
PrivateKeyInfo info = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(decryptionProvider);
return converter.getPrivateKey(info);

} else if (object instanceof PEMKeyPair) {
// PKCS1 unencrypted key
return converter.getKeyPair((PEMKeyPair) object).getPrivate();

} else if (object instanceof PrivateKeyInfo) {
// PKCS8 unencrypted key
return converter.getPrivateKey((PrivateKeyInfo) object);

} else {
throw new UnsupportedOperationException("Unsupported PEM object: " + object.getClass().getSimpleName());
}
}
}
}
9 changes: 7 additions & 2 deletions jsign-crypto/src/test/java/net/jsign/PrivateKeyUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public void testLoadPKCS1PEM() throws Exception {
testLoadPEM(new File("target/test-classes/keystores/privatekey.pkcs1.pem"), null);
}

@Test
public void testLoadPKCS1PEMNonPEMExtension() throws Exception {
testLoadPEM(new File("target/test-classes/keystores/privatekey.pkcs1.pem.key"), null);
}

@Test
public void testLoadEncryptedPKCS1PEM() throws Exception {
testLoadPEM(new File("target/test-classes/keystores/privatekey-encrypted.pkcs1.pem"), "password");
Expand All @@ -71,7 +76,7 @@ private void testLoadPEM(File file, String password) throws Exception {
@Test
public void testLoadWrongPEMObject() {
Exception e = assertThrows(KeyException.class, () -> PrivateKeyUtils.load(new File("target/test-classes/keystores/jsign-test-certificate.pem"), null));
assertEquals("message", "Unsupported PEM object: X509CertificateHolder", e.getCause().getMessage());
assertEquals("message", "Unsupported PEM object: X509CertificateHolder", e.getSuppressed()[0].getMessage());
}

@Test
Expand All @@ -82,7 +87,7 @@ public void testLoadEmptyPEM() throws Exception {
writer.close();

Exception e = assertThrows(KeyException.class, () -> PrivateKeyUtils.load(file, null));
assertTrue(e.getCause().getMessage().startsWith("No key found in"));
assertTrue(e.getSuppressed()[0].getMessage().startsWith("No key found in"));
}

@Test
Expand Down

0 comments on commit 227e617

Please sign in to comment.