Skip to content

Commit 3c3093d

Browse files
authored
Undo reflective modification of SunPKCS11-NSS algorithms mapping (#46)
* Resolves #45 by undoing the reflective modification of SunPKCS11-NSS algorithms mapping and providing a KeyPairLoader#getKeyPair which accepts a provider name. * Checkstyle * Explicit check for requested provider potentially not being available and the relevant test case * Add a BC-specific test * Clean up some more
1 parent a4d7f07 commit 3c3093d

File tree

9 files changed

+223
-65
lines changed

9 files changed

+223
-65
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ local.properties
8181
# TeXlipse plugin
8282
.texlipse
8383

84+
# OS X
85+
.DS_Store
8486

8587
### Maven ###
8688
target/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2017, Joyent, Inc. All rights reserved.
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
package com.joyent.http.signature;
9+
10+
/**
11+
* Exception that can occur while loading a {@link java.security.KeyPair}.
12+
*
13+
* @since 4.0.4
14+
* @author <a href="https://github.com/tjcelaya">Tomas Celayac</a>
15+
*/
16+
public class KeyLoadException extends HttpSignatureException {
17+
18+
private static final long serialVersionUID = 3842266217250311085L;
19+
20+
/**
21+
* Creates a new exception with the specified message.
22+
* @param message Message to embed
23+
*/
24+
public KeyLoadException(final String message) {
25+
super(message);
26+
}
27+
}

common/src/main/java/com/joyent/http/signature/KeyPairLoader.java

Lines changed: 111 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
*/
88
package com.joyent.http.signature;
99

10-
import com.joyent.http.signature.crypto.NssBridgeKeyConverter;
1110
import org.bouncycastle.jce.provider.BouncyCastleProvider;
1211
import org.bouncycastle.openssl.PEMDecryptorProvider;
1312
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
1413
import org.bouncycastle.openssl.PEMKeyPair;
1514
import org.bouncycastle.openssl.PEMParser;
15+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
1616
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
1717

1818
import java.io.BufferedReader;
@@ -26,6 +26,7 @@
2626
import java.nio.file.Files;
2727
import java.nio.file.Path;
2828
import java.security.KeyPair;
29+
import java.security.Provider;
2930
import java.security.Security;
3031

3132

@@ -35,18 +36,66 @@
3536
*/
3637
public final class KeyPairLoader {
3738

39+
/**
40+
* Provider name for libnss.
41+
*/
42+
public static final String PROVIDER_PKCS11_NSS = "SunPKCS11-NSS";
3843

39-
@SuppressWarnings("checkstyle:javadocmethod")
40-
private KeyPairLoader() {
41-
}
44+
/**
45+
* Provider name for Bouncy Castle.
46+
*/
47+
public static final String PROVIDER_BOUNCY_CASTLE = "BC";
48+
49+
/**
50+
* The key format converter to use when reading key pairs and libnss is enabled (or specifically requested).
51+
*/
52+
private static final JcaPEMKeyConverter CONVERTER_PKCS11_NSS;
53+
54+
/**
55+
* The key format converter to use when libnss is disabled (or BC is specifically requested).
56+
*/
57+
private static final JcaPEMKeyConverter CONVERTER_BOUNCY_CASTLE;
4258

4359
/**
44-
* The key format converter to use when reading key pairs.
60+
* Set of security providers users can request.
4561
*/
46-
private static final NssBridgeKeyConverter CONVERTER =
47-
new NssBridgeKeyConverter();
48-
{
49-
CONVERTER.setProvider("BC");
62+
@SuppressWarnings({"checkstyle:JavaDocVariable", "checkstyle:JavadocMethod"})
63+
public enum DesiredSecurityProvider {
64+
BC(PROVIDER_BOUNCY_CASTLE),
65+
NSS(PROVIDER_PKCS11_NSS);
66+
67+
private final String providerCode;
68+
69+
DesiredSecurityProvider(final String providerCode) {
70+
this.providerCode = providerCode;
71+
}
72+
73+
@Override
74+
public String toString() {
75+
return providerCode;
76+
}
77+
}
78+
79+
static {
80+
final Provider providerPkcs11NSS = Security.getProvider(PROVIDER_PKCS11_NSS);
81+
82+
if (providerPkcs11NSS != null) {
83+
CONVERTER_PKCS11_NSS = new JcaPEMKeyConverter().setProvider(PROVIDER_PKCS11_NSS);
84+
} else {
85+
CONVERTER_PKCS11_NSS = null;
86+
}
87+
88+
final Provider providerBouncyCastle = Security.getProvider(PROVIDER_BOUNCY_CASTLE);
89+
90+
if (providerBouncyCastle == null) {
91+
Security.addProvider(new BouncyCastleProvider());
92+
}
93+
94+
CONVERTER_BOUNCY_CASTLE = new JcaPEMKeyConverter().setProvider(PROVIDER_BOUNCY_CASTLE);
95+
}
96+
97+
@SuppressWarnings("checkstyle:javadocmethod")
98+
private KeyPairLoader() {
5099
}
51100

52101
/**
@@ -150,36 +199,71 @@ public static KeyPair getKeyPair(final byte[] pKeyBytes, final char[] password)
150199
/**
151200
* Read KeyPair from an input stream, optionally using password.
152201
*
153-
* @param is private key content as a stream
202+
* @param is private key content as a stream
203+
* @param password password associated with key
204+
* @return public/private keypair object
205+
* @throws IOException If unable to read the private key from the string
206+
*/
207+
public static KeyPair getKeyPair(final InputStream is,
208+
final char[] password) throws IOException {
209+
return getKeyPair(is, password, null);
210+
}
211+
212+
/**
213+
* Read KeyPair from an input stream, optionally using password and desired Security Provider. Most implementations
214+
* should continue calling the one and two-argument methods
215+
*
216+
* @param is private key content as a stream
154217
* @param password password associated with key
218+
* @param provider security provider to use when loading the key
155219
* @return public/private keypair object
156220
* @throws IOException If unable to read the private key from the string
157221
*/
158222
public static KeyPair getKeyPair(final InputStream is,
159-
final char[] password) throws IOException {
223+
final char[] password,
224+
final DesiredSecurityProvider provider) throws IOException {
225+
final Object pemObject;
160226
try (InputStreamReader isr = new InputStreamReader(is, StandardCharsets.US_ASCII);
161227
BufferedReader br = new BufferedReader(isr);
162228
PEMParser pemParser = new PEMParser(br)) {
163229

230+
pemObject = pemParser.readObject();
231+
}
232+
233+
final PEMKeyPair pemKeyPair;
234+
235+
if (pemObject instanceof PEMEncryptedKeyPair) {
164236
if (password == null) {
165-
Security.addProvider(new BouncyCastleProvider());
166-
final Object object = pemParser.readObject();
167-
return CONVERTER.getKeyPair((PEMKeyPair) object);
168-
} else {
169-
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password);
170-
171-
Object object = pemParser.readObject();
172-
173-
final KeyPair kp;
174-
if (object instanceof PEMEncryptedKeyPair) {
175-
kp = CONVERTER.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv));
176-
} else {
177-
kp = CONVERTER.getKeyPair((PEMKeyPair) object);
178-
}
179-
180-
return kp;
237+
throw new KeyLoadException("Loaded key is encrypted but no password was supplied.");
181238
}
239+
240+
final PEMDecryptorProvider decryptorProvider = new JcePEMDecryptorProviderBuilder().build(password);
241+
final PEMEncryptedKeyPair encryptedPemObject = ((PEMEncryptedKeyPair) pemObject);
242+
pemKeyPair = encryptedPemObject.decryptKeyPair(decryptorProvider);
243+
} else if (pemObject instanceof PEMKeyPair) {
244+
if (password != null) {
245+
throw new KeyLoadException("Loaded key is not encrypted but a password was supplied.");
246+
}
247+
248+
pemKeyPair = (PEMKeyPair) pemObject;
249+
} else {
250+
throw new KeyLoadException("Unexpected PEM object loaded: " + pemObject.getClass().getCanonicalName());
182251
}
252+
253+
// throw if the user has specifically requested NSS and it is unavailable
254+
if (provider != null && provider.equals(DesiredSecurityProvider.NSS) && CONVERTER_PKCS11_NSS == null) {
255+
throw new KeyLoadException(PROVIDER_PKCS11_NSS + " provider requested but unavailable. "
256+
+ "Is java.security configured correctly?");
257+
}
258+
259+
// Attempt to load with NSS if it is available and requested (or no provider was specified)
260+
final boolean attemptPKCS11NSS = provider == null || provider.equals(DesiredSecurityProvider.NSS);
261+
262+
if (CONVERTER_PKCS11_NSS != null && attemptPKCS11NSS) {
263+
return CONVERTER_PKCS11_NSS.getKeyPair(pemKeyPair);
264+
}
265+
266+
return CONVERTER_BOUNCY_CASTLE.getKeyPair(pemKeyPair);
183267
}
184268

185269
}

common/src/main/java/com/joyent/http/signature/crypto/NssBridgeKeyConverter.java

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,8 @@
77
*/
88
package com.joyent.http.signature.crypto;
99

10-
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
1110
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
1211

13-
import java.lang.reflect.Field;
14-
import java.lang.reflect.Method;
15-
import java.security.Provider;
16-
import java.security.Security;
17-
1812
/**
1913
* There is an unfortunate naming discrepancy between BouncyCastle and
2014
* other security providers over the name of ECDSA. BouncyCastle uses
@@ -31,35 +25,6 @@
3125
* @see <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/security/p11guide.html">PKCS#11 Reference Guide</a>
3226
* @see <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS">Network Security Services</a>
3327
*/
28+
@Deprecated
3429
public class NssBridgeKeyConverter extends JcaPEMKeyConverter {
35-
36-
{
37-
Provider[] providers = Security.getProviders();
38-
try {
39-
if (providers == null || providers.length == 0) {
40-
/* A lack of any security providers should be
41-
* an "impossible" condition. But if it occurs we print
42-
* to stderr instead of throwing in a static
43-
* initializer.
44-
*/
45-
System.err.println("Unable to configure ECDSA, no security providers present");
46-
} else if (providers[0].getName().equals("SunPKCS11-NSS")) {
47-
/* JcaPEMKeyConverter maintains an internal mapping of
48-
* algorithms identifies to string codes. If SunPKCS11-NSS
49-
* is the most preferred provider, reflection is used to
50-
* adjust that mapping to match the expectations of
51-
* SunPKCS11.
52-
*/
53-
Field fieldDefinition = JcaPEMKeyConverter.class.getDeclaredField("algorithms");
54-
fieldDefinition.setAccessible(true);
55-
Object fieldValue = fieldDefinition.get(null);
56-
Method put = fieldValue.getClass().getDeclaredMethod("put", Object.class, Object.class);
57-
put.invoke(fieldValue, X9ObjectIdentifiers.id_ecPublicKey, "EC");
58-
}
59-
} catch (ReflectiveOperationException e) {
60-
System.err.println("SunPKCS11-NSS is preferred security provider, "
61-
+ "but failed to enable for ECDSA via reflection");
62-
e.printStackTrace();
63-
}
64-
}
6530
}

common/src/test/java/com/joyent/http/signature/KeyPairLoaderTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
11
package com.joyent.http.signature;
22

3+
import com.joyent.http.signature.KeyPairLoader.DesiredSecurityProvider;
34
import org.bouncycastle.openssl.PEMEncryptor;
45
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
56
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
67
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
78
import org.testng.Assert;
89
import org.testng.AssertJUnit;
10+
import org.testng.SkipException;
911
import org.testng.annotations.Test;
1012

13+
import java.io.ByteArrayInputStream;
1114
import java.io.ByteArrayOutputStream;
1215
import java.io.File;
1316
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
1419
import java.io.OutputStreamWriter;
1520
import java.nio.charset.StandardCharsets;
1621
import java.nio.file.Files;
1722
import java.nio.file.Path;
1823
import java.security.KeyPair;
1924
import java.security.KeyPairGenerator;
2025
import java.security.NoSuchAlgorithmException;
26+
import java.security.Security;
2127
import java.util.UUID;
2228

29+
import static com.joyent.http.signature.KeyPairLoader.PROVIDER_PKCS11_NSS;
30+
2331
@Test
2432
public class KeyPairLoaderTest {
2533

2634
private static final String RSA_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
2735

36+
private static final ClassLoader CLASS_LOADER = KeyPairLoaderTest.class.getClassLoader();
37+
2838
public void willThrowOnNullInputs() {
2939
Assert.assertThrows(() ->
3040
KeyPairLoader.getKeyPair((String) null, null));
@@ -86,6 +96,50 @@ public void canLoadPasswordProtectedKeyFromFile() throws Exception {
8696
compareKeyContents(keyPair, loadedKeyPair);
8797
}
8898

99+
public void canLoadKeyPairUsingSpecifiedProvider() throws Exception {
100+
101+
if (Security.getProvider(PROVIDER_PKCS11_NSS) == null) {
102+
throw new SkipException(PROVIDER_PKCS11_NSS + " provider is missing.");
103+
}
104+
105+
for (final String keyId : SignerTestUtil.keys.keySet()) {
106+
final SignerTestUtil.TestKeyResource keyResource = SignerTestUtil.keys.get(keyId);
107+
final KeyPair bouncyKeyPair = loadTestKeyPair(keyResource.resourcePath, DesiredSecurityProvider.BC);
108+
final String bouncyAlgo = bouncyKeyPair.getPrivate().getAlgorithm().toUpperCase();
109+
String classAlgoName = bouncyAlgo;
110+
if (bouncyAlgo.equals("ECDSA")) {
111+
classAlgoName = "EC";
112+
}
113+
114+
Assert.assertEquals(KeyFingerprinter.md5Fingerprint(bouncyKeyPair), keyResource.md5Fingerprint);
115+
Assert.assertTrue(bouncyKeyPair.getPrivate().getClass().getSimpleName().contains("BC" + classAlgoName + "Private"));
116+
Assert.assertTrue(bouncyKeyPair.getPublic().getClass().getSimpleName().contains("BC" + classAlgoName + "Public"));
117+
118+
final String nssAlgo = bouncyKeyPair.getPrivate().getAlgorithm().toUpperCase();
119+
Assert.assertEquals(bouncyAlgo, nssAlgo);
120+
121+
final KeyPair nssKeyPair = loadTestKeyPair(keyResource.resourcePath, DesiredSecurityProvider.NSS);
122+
Assert.assertEquals(KeyFingerprinter.md5Fingerprint(nssKeyPair), keyResource.md5Fingerprint);
123+
Assert.assertTrue(nssKeyPair.getPrivate().getClass().getSimpleName().contains("P11" + classAlgoName));
124+
Assert.assertTrue(nssKeyPair.getPublic().getClass().getSimpleName().contains("P11" + classAlgoName));
125+
126+
}
127+
}
128+
129+
public void willThrowWhenPkcs11IsRequestedButUnavailable() throws Exception {
130+
if (Security.getProvider(PROVIDER_PKCS11_NSS) != null) {
131+
throw new SkipException("PKCS11 provider is available, can't perform skip test");
132+
}
133+
134+
final KeyPair keyPair = generateKeyPair();
135+
final byte[] serializedKey = serializePrivateKey(keyPair, null);
136+
137+
Assert.assertTrue(new String(serializedKey, StandardCharsets.UTF_8).startsWith(RSA_HEADER));
138+
139+
Assert.assertThrows(KeyLoadException.class, () ->
140+
KeyPairLoader.getKeyPair(new ByteArrayInputStream(serializedKey), null, DesiredSecurityProvider.NSS));
141+
}
142+
89143
// TEST UTILITY METHODS
90144

91145
private KeyPair generateKeyPair() throws NoSuchAlgorithmException {
@@ -122,4 +176,13 @@ private void compareKeyContents(KeyPair expectedKeyPair, KeyPair actualKeyPair)
122176
actualKeyPair.getPublic().getEncoded());
123177
}
124178

179+
private KeyPair loadTestKeyPair(final String resourcePath,
180+
final DesiredSecurityProvider provider) throws IOException {
181+
final KeyPair loadedKeyPair;
182+
183+
try (final InputStream inputKey = CLASS_LOADER.getResourceAsStream(resourcePath)) {
184+
loadedKeyPair = KeyPairLoader.getKeyPair(inputKey, null, provider);
185+
}
186+
return loadedKeyPair;
187+
}
125188
}

common/src/test/java/com/joyent/http/signature/SignerTestUtil.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import java.util.HashMap;
1414
import java.util.Map;
1515

16-
1716
public class SignerTestUtil {
1817
@SuppressWarnings("serial")
1918
public static final Map<String,TestKeyResource> keys = new HashMap<String,TestKeyResource>() {{
@@ -56,7 +55,7 @@ public static KeyPair testKeyPair(String keyId) throws IOException {
5655
final ClassLoader loader = SignerTestUtil.class.getClassLoader();
5756

5857
try (InputStream is = loader.getResourceAsStream(keys.get(keyId).resourcePath)) {
59-
KeyPair classPathPair = KeyPairLoader.getKeyPair(is, null);
58+
KeyPair classPathPair = KeyPairLoader.getKeyPair(is, null, null);
6059
return classPathPair;
6160
}
6261
}

0 commit comments

Comments
 (0)