There's several different things going on in TLS, and the way that Java handles it with JSSE (Java Secure Socket Extension) is involved. The last post was all about working with KeyStore
. This post is all about the key manager.
A key manager is how TLS presents a certificate chain to a peer, and decrypts information using the private key associated with the certificate at the end of that chain. Think of it as a "private key manager" and the name makes more sense.
A key manager needs a source of private keys and certificate chains. This is provided by a KeyManagerFactory
, which provides a key manager with its source material. Once the key manager has been instantiated, it is effectively immutable: you can't add new private keys to it or alter its behavior. You also can't retrieve private key information from the key manager at all.
There are a very small number of private keys in the key manager – usually only one, which is the hostname. Private keys can have a password. JSSE assumes that a password is required for the private key, even if it is the empty string, so you can't pass null
in as a value, but oddly, there's no way to pass a password to a specific private key in a KeyManager. Instead, passwords are associated with key stores, and the password of a key is assumed to be the same as the password of the keystore. This has implications for the key manager API during initialization.
There are two implementations of KeyManagerFactory
, which go by "SunX509", which is the default value returned by KeyManagerFactory.getDefaultAlgorithm()
, and "NewSunX509", the new one. We'll show the SunX509
first.
The SunX509 Key Manager
The "SunX509" key manager is the default. It is backed by the sun.security.ssl.SunX509KeyManagerImpl
class. The javadoc for the implementation spells out its behavior fairly clearly:
The backing KeyStore is inspected when this object is constructed. All key entries containing a PrivateKey and a non-empty chain of X509Certificate are then copied into an internal store. This means that subsequent modifications of the KeyStore have no effect on the X509KeyManagerImpl object. Note that this class assumes that all keys are protected by the same password.
The JSSE handshake code currently calls into this class via chooseClientAlias() and chooseServerAlias() to find the certificates to use. As implemented here, both always return the first alias returned by getClientAliases() and getServerAliases(). In turn, these methods are implemented by calling getAliases(), which performs the actual lookup.
Note that this class currently implements no checking of the local certificates. In particular, it is not guaranteed that:
- the certificates are within their validity period and not revoked
- the signatures verify
- they form a PKIX compliant chain.
- the certificate extensions allow the certificate to be used for the desired purpose.
Chains that fail any of these criteria will probably be rejected by the remote peer.
Indeed, there's very little checking going on. Looking at the getAliases method, it checks on algorithm, signature and on the issuer, and that's basically it.
The default "SunX509" key manager takes one keystore, ideally consisting of a few KeyStore.PrivateKeyEntry
, and one password:
char[] passwordForAllPrivateKeys = "".toCharArray(); // cannot be null
String algorithm = KeyManagerFactory.getDefaultAlgorithm(); // returns "SunX509" by default in 1.8
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(keyStore, passwordForAllPrivateKeys);
You then get the key manager, which is always the first element in the array. The public API is javax.net.ssl.X509ExtendedKeyManager
.
KeyManagers[] keyManagers = kmf.getKeyManagers();
X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) keyManagers[0];
The JSSE documentation says:
Note: A KeyManagerFactory implementation for the SunX509 algorithm is supplied by the SunJSSE provider. The KeyManager that it specifies is a javax.net.ssl.X509KeyManager implementation.
This is both true and useless – in Java 1.8, you need to use an instance of X509ExtendedKeyManager
to do anything useful. We'll get into this a bit more later.
The NewSunX509 Key Manager
The "NewSunX509" key manager is backed by X509KeyManagerImpl implementation. This is not the default key manager, but it is a bit smarter about how it selects out the keystore alias.
The KeyStore.Builder
API
is central to the "NewSunX509" implementation, because it lets you use multiple private keys, and multiple keystores:
import javax.net.ssl.*;
import java.security.KeyStore.*;
import java.util.*;
List<KeyStore.Builder> builderList = Arrays.asList(
Builder.newInstance("PKCS11", null, new CallbackHandlerProtection(myGuiCallbackHandler)),
Builder.newInstance("PKCS12", null, new File(pkcsFileName), new PasswordProtection(pkcsKsPassword))
);
// ONLY the NewSunX509 key manager will take KeyStoreBuilderParameters...
KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509");
kmf.init(new KeyStoreBuilderParameters(builderList));
Why would a key manager have multiple private keys?
The answer is in Multiple and Dynamic Keystores:
If multiple certificates are available, it attempts to pick a certificate with the appropriate key usage and prefers valid to expired certificates.
An X.509 end entity certificate may have a key usage extension in the certificate. If "KeyUsage" is present, then it must equal "digitalSignature" for a client certificate, or "keyEncipherment" for the server certificate. Because a key manager is also used in the case where a server makes a Certificate Request to the client, there may be instances where the default SSLContext is being used in both a server and a client context and so must hand out different EE certificates in that case. The CheckResult code goes into detail on this if you're interested.
Another possible use case is that the key manager may be responsible for managing multiple hosts. This is more likely than it sounds, because TLS supports wildcard certificates, and so a key manager may implement Server Name Indication to return different certificates for different hostnames served from the same IP address. We'll describe this later in another section.
There are some things that the "NewSunX509" key manager promises that cannot be fulfilled without some significant customization and some spelunking of the source code.
From the Java documentation:
- it is based around the KeyStore.Builder API. This allows it to use other forms of KeyStore protection or password input (e.g. a CallbackHandler) or to have keys within one KeyStore protected by different keys.
- it can use multiple KeyStores at the same time.
- it is explicitly designed to accommodate KeyStores that change over the lifetime of the process.
- it makes an effort to choose the key that matches best, i.e. one that is not expired and has the appropriate certificate extensions.
What it means by "KeyStores that change over the lifetime of the process" is that it support PKCS11 devices as keystores. Specifically, you can have a smartcard / Yubikey, and swap out the smartcard while the keystore is running – the keystore is the same, but the private key entry points to a different device. You can't effectively swap out a PKCS12 or JKS keystore, even if you change the file that those keys were loaded from, as the implementation will cache entries internally. Likewise, the KeyStore.Builder
will always return the same memoized keystore instance once it has been built. If you want to swap out a private key or certificate chain, you're going to have to write a KeyStore provider like the PKCS11 provider.
What it means by "keys within one KeyStore protected by different keys"… it means "protected by different passwords", but that isn't the case: if you use the KeyStore.Builder API with a PKCS12 file, you're still matching a keystore with a PasswordProtection
, and there is no way to match a password to a specific alias. If the keystore and all private key entries in the keystore have the same password, then everything works fine, but you still can't differentiate inside of a keystore.
Technically, the key manager does use getProtectionParameter(alias)
correctly, but the KeyStore.Builder doesn't pass the alias parameter along in either FileBuilder or the anonymous inner class of newInstance:
public synchronized ProtectionParameter
getProtectionParameter(String alias) {
if (alias == null) {
throw new NullPointerException();
}
if (keyStore == null) {
throw new IllegalStateException
("getKeyStore() must be called first");
}
return keyProtection;
}
The end result is that even though the "NewSunX509" key manager works fine, you still can't use different passwords out of the box with the keystore builder API.
Another really strange thing about the "NewSunX509" key manager is that it prefixes aliases with numbers, so it's no longer the same alias as used in the keystore. This isn't listed in the code or even as javadoc – instead, it's a line comment. Given that the original alias name is still included, it's not terrible, but it does make it annoying when trying to match aliases in a keystore against the alias chosen by the key manager.
Add these two problems together, and you have to write a custom keystore builder with alias parsing code to get alias specific passwords to work:
public static class KeyStoreBuilder extends KeyStore.Builder {
private final Supplier<KeyStore> keyStoreSupplier;
private final Function<String, KeyStore.ProtectionParameter> passwordFunction;
public KeyStoreBuilder(Supplier<KeyStore> keyStoreSupplier, Function<String, KeyStore.ProtectionParameter> passwordFunction) {
Objects.requireNonNull(keyStoreSupplier);
Objects.requireNonNull(passwordFunction);
this.keyStoreSupplier = keyStoreSupplier;
this.passwordFunction = passwordFunction;
}
@Override
public KeyStore getKeyStore() throws KeyStoreException {
return keyStoreSupplier.get();
}
@Override
public KeyStore.ProtectionParameter getProtectionParameter(String alias) throws KeyStoreException {
Objects.requireNonNull(alias);
return passwordFunction.apply(alias);
}
}
@Test
public void testWithBuilder() throws GeneralSecurityException, IOException {
char[] password1 = "password1".toCharArray();
char[] password2 = "password2".toCharArray();
Map<String, ProtectionParameter> passwordsMap = new HashMap<>();
passwordsMap.put("rsaentry", new PasswordProtection(password1));
passwordsMap.put("dsaentry", new PasswordProtection(password2));
KeyStore keyStore = generateStore();
KeyStore.Builder builder = new KeyStoreBuilder(() -> keyStore, alias -> {
// alias is lowercased keystore alias with prefixed numbers :-/
// parse the alias
int firstDot = alias.indexOf('.');
int secondDot = alias.indexOf('.', firstDot + 1);
if ((firstDot == -1) || (secondDot == firstDot)) {
// invalid alias
return null;
}
String keyStoreAlias = alias.substring(secondDot + 1);
return passwordsMap.get(keyStoreAlias);
});
KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509");
kmf.init(new KeyStoreBuilderParameters(builder));
X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) kmf.getKeyManagers()[0];
String rsaAlias = keyManager.chooseServerAlias("RSA", null, null);
assertThat(rsaAlias).contains("rsaentry");
PrivateKey rsaPrivateKey = keyManager.getPrivateKey(rsaAlias);
assertThat(rsaPrivateKey).isNotNull(); // can get password
String dsaAlias = keyManager.chooseServerAlias("DSA", null, null);
assertThat(dsaAlias).contains("dsaentry");
PrivateKey dsaPrivateKey = keyManager.getPrivateKey(dsaAlias);
assertThat(dsaPrivateKey).isNotNull(); // can get password
}
Which is… bizarre. There's no mention of this in the documentation, but you'd think if you made a point of touting alias-specific passwords that you'd have a KeyStore.Builder.newInstance
factory method that took a password map to make it possible.
Using Domain Keystore
You can also use a domain keystore if you're not into the KeyStore.Builder API. The Domain KeyStore lets you aggregate several keystores together, meaning that a single keystore can be used for everything. The sun.security.provider.DomainKeyStore
implementation has more details and associated tests.
With some setting up of keystores:
Path privateKeyStorePath = Files.createTempFile(null, ".p12");
KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
pkcs12.load(null);
char[] privateKeyPassword = "".toCharArray();
pkcs12.setKeyEntry("example.com", keyPair.getPrivate(), privateKeyPassword, new Certificate[] { certificate });
pkcs12.store(new FileOutputStream(privateKeyStorePath.toFile()), privateKeyPassword);
List<String> lines = new ArrayList<>();
lines.add("domain app1 {");
lines.add("\tkeystore app1keystore");
lines.add(String.format("\t\tkeystoreURI=\"%s\";", privateKeyStorePath.toUri()));
lines.add("");
lines.add("\tkeystore systemtruststore");
lines.add("\t\tkeystoreURI=\"${java.home}/lib/security/cacerts\";");
lines.add("};");
Path tempFile = Files.createTempFile(null, null);
Files.write(tempFile, lines, StandardCharsets.UTF_8);
URI uri = tempFile.toUri();
Map<String, ProtectionParameter> passwords = Collections.singletonMap("app1keystore", new PasswordProtection(privateKeyPassword));
KeyStore store = KeyStore.getInstance("DKS");
store.load(new DomainLoadStoreParameter(uri, passwords));
Again, note that passwords are matched specifically to keystores, and not to the aliases inside of the keystores. I've always found it easiest to use "".toCharArray()
as the password everywhere, but the incoherence of password management still nags.
I also don't get why the DomainKeyStore configuration is tied directly to a text format. Ideally, you want to do configuration through plain POJO configuration objects, and then the parser just does the work of going from text to config objects. This gives you the flexibility to parse out in different formats (XML, JSON, natch) while making it easier to do type-safe configuration and unit tests based on sticking config objects together.
Since everything is handled at the KeyStore level, you can initialize the key manager factory with just a plain store and a null password on both "SunX509" and "NewSunX509":
public class DomainKeyStoreBuilderTest {
@Test
public void testMe() throws GeneralSecurityException, IOException {
KeyManagerFactory kmf = KeyManagerFactory.getInstance("NewSunX509"); // or "SunX509"
kmf.init(generateDKSStore(), null);
X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) kmf.getKeyManagers()[0];
X500Principal name = new X500Principal("CN=example.com");
String[] aliases = keyManager.getServerAliases("RSA", new Principal[]{name});
String alias = aliases[0];
PrivateKey privateKey = keyManager.getPrivateKey(alias);
assertThat(privateKey).isNotNull();
}
}
This takes a bit more work to set up, but lets you switch and hide the logic better. However, because you can't pass in a KeyStore.Builder directly to a domain keystore, you also don't have access to alias specific passwords here at all.
Using PKCS11 with a KeyManager
Support for PKCS11 devices in Java is a neat feature, because in theory it means that private key material is never exposed in memory on the server. It's not always the case in practice, but PKCS11 is more secure than keeping private keys on your filesystem.
I haven't seen anyone really go through this end to end, so here's a quick description.
Assuming a Yubikey 4 or similar, you can use the OpenSC integration Yubico recommends. This is popular for signing android releases.
First, install OpenSC:
sudo apt-get install opensc-pkcs11
Then set up the OpenSC file /tmp/pkcs11_java.cfg
:
name = OpenSC-PKCS11
description = SunPKCS11 via OpenSC
library = /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so
slotListIndex = 0
Import the private key store into the Yubikey from PKCS12:
yubico-piv-tool -s 9a -a import-key -a import-cert -i privatekeys.p12 -K PKCS12 -k
Check that you can see it through keytool:
keytool -providerClass sun.security.pkcs11.SunPKCS11 \
-providerArg /tmp/pkcs11_java.cfg \
-keystore NONE \
-storetype PKCS11 \
-list
Once you're able to see it from the command line, you register the provider:
Provider provider = new sun.security.pkcs11.SunPKCS11(new FileInputStream("/tmp/pkcs11_java.cfg"));
Security.addProvider(provider);
with the following in the DKS file:
domain app1 {
keystore app1keystore
keyStoreType="PKCS11"
keystoreURI="NONE"
};
And then you can fake out the callback handler with a password from somewhere else.
public static ProtectionParameter createPKCS11KeyStore(char[] password) {
return new KeyStore.CallbackHandlerProtection(callbacks ->
Arrays.stream(callbacks).map(callback ->
(PasswordCallback) callback).forEach(pc ->
pc.setPassword(password)));
}
Map<String, ProtectionParameter> passwords = new HashMap();
passwords.put("app1keystore", createPKCS11Password(password));
store.load(new DomainLoadStoreParameter(uri, passwords));
If you run into problems, add -Djava.security.debug=sunpkcs11
to see what's going on internally.
Creating your own Key Manager
From time to time, you'll need to create your own key manager. Maybe you'll want to implement SNI, in which case Graham Edgecombe has an SniKeyManager. Or you'll want to set up a debugging proxy, in which case I recommend DebugX509ExtendedKeyManager. Or you may want to set up a simple AliasedX509ExtendedKeyManager. Or you may want to watch the filesystem for changes in the keystore, and reload the private keys with FileWatchingX509ExtendedKeyManager, or choose a certificate based off the alias.
There is no practical way to do custom development in JSSE without access to the underlying source and a debugger.
Here's what you need to set up your environment.
- Download an OpenJDK release from https://adoptopenjdk.net/.
- Clone the source code from AdoptOpenJDK github repository.
- Download IntelliJ IDEA, add the OpenJDK release and set the sources to point to
/jdk/src/share/classes
as this is where all the Sun-specific classes are. - Create a project using the OpenJDK release.
- Create your class extending the
X509ExtendedKeyManager
class. - Write tests against your class.
- Use Ctrl-N to open up Sun-Specific classes and add breakpoints.
- Debug the test, and see what's going on under the hood.
You must extend X509ExtendedKeyManager
, which means you're locked into an abstract base class from the get-go. There's nothing you can do about this.
The JSSE documentation does attempt to point in this direction, saying:
If a key manager is not an instance of the X509ExtendedKeyManager class, then it will not work with the SSLEngine class.
and
For JSSE providers and key manager implementations, the X509ExtendedKeyManager class is highly recommended over the legacy X509KeyManager interface.
The documentation does not mention that if you create your own X509ExtendedKeyManager
, you must override the following methods for your specific implementation:
public class MyX509ExtendedKeyManager extends X509ExtendedKeyManager {
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
return chooseClientAlias(keyType, issuers, null);
}
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
return chooseServerAlias(keyType, issuers, null);
}
}
This is because X509ExtendedKeyManager
, instead of leaving those methods abstract, returns null.
Both the SunX509 implementation and the NewSunX509 implementation override this with their own logic, and it gets called in the normal code flow, so returning null is a useless behavior here, one that trips up everyone.
Conclusion
The key manager is complicated, but it is not magic. Most of the handshake and private key work is done internally in the handshake, and most customization is in choosing what private key entry to pick from what source. When in doubt, look at the source code, and then run it through a debugger and it'll start making sense.
Comments