Import a private key into a Java Key Store
OpenSSL and Java never quite seem to get along. OpenSSL, in addition to being the primary library used for SSL functionality in open source as well as commercial software products, is also a set of tools used to create all of the peripheral SSL-related artifacts such as X.509 certificates. Java, however, doesn't use OpenSSL and hasn't since the first release of the Java Cryptography Extensions (JCE) — Java has had its own native implementation of SSL since JDK 1.3. This can cause some interoperability problems for people who use both out of necessity.
One such interoperability headache surrounds key stores. Sun's cryptography
implementation includes a thoroughly integrated public/private key management infrastructure
called the Java Key Store (JKS), along with its own file format. The Java Virtual Machine knows
how to read these key stores and extract public/private keypairs from them for code signing as
well as key negotiation (e.g. SSL handshaking) purposes. In the ideal (from the Java user's
perspective) scenario, a keypair would be generated using Java's keytool
, the
corresponding certificate exported and signed by a certificate authority, and then re-imported
into the key store once signed. To create a private key and its corresponding public-key
certificate using Java tools, you would do something like:
$ keytool -genkeypair -keyalg rsa -keysize 2048 -alias jdavies -keystore jdavieskeys.jks -dname "CN=Joshua Davies"
$ keytool -certreq -alias jdavies -keystore jdavieskeys.jks > jdaviescert.csr
(get the CSR signed by a CA)
$ keytool -import -alias jdavies -file jdaviescert.pem -keystore jdavieskeys.jks
OpenSSL's artifacts, by contrast, are discrete. If you want to create a keypair using OpenSSL,
you get a key file and a CSR in two separate files; it's up to you to keep track of which private
key file is associated with which signed certificate. Generating a certificate in OpenSSL is
something like:
$ openssl req -newkey rsa:2048 -keyout jdavies.key -out jdavies.csr -subj "/CN=Joshua Davies"
Since there's a real monetary cost associated with each of the CA signatures, there's always interest in
using certificates in a JKS file for OpenSSL-related purposes and vice versa. For example,
Sun made it deliberately hard to get a private key out of a JKS file, but if you want to use
a certificate exported from a JKS file anywhere outside of Java, you'll need to convert the JKS
file into a PKCS #12 file (I talk more about this process
here).
Conversely, if you have a key/certificate pair that you generated using OpenSSL tools and you
want to use it in a Java-based implementation, the keytool
utility that comes
with Java doesn't offer much help. You could, if you wanted, invert the private key export
process I described above: convert the JKS file to PKCS #12 and import the keys that way, but
that strikes me as quite a hassle. As it turns out, it's not really that much extra work to
develop a utility that can import OpenSSL-formatted private keys into a Java Key Store while
still preserving the JKS file format.
First, it's worth examining what, exactly, an OpenSSL-formatted private key looks like. By
default, the jdavies.key
that would be produced by example 2, above, would be a
Base-64 encoded representation of an encrypted message. Because it's Base64 encoded, you can
open it up and view it in a standard text editor, but it's not very interesting to look at
(unless you're into that sort of thing):
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQItmQ4gm2gJy0CAggA
MBQGCCqGSIb3DQMHBAg9NJTnjKg6aASCBMhp0b5nXyEp4j8ekugAVAdDQqXgHytD
Tgd30CZLkXCO0lZ8dtu394DX+v54GOD5U7esl6/aUL6KQdd23wMeo2uIWVf0bbiT
MmTjtgy58ziV8pVnzTU7yo+PVJpBK7jh4QZjpdKLT3Y2GIQjaF/uQAgabVJpJmx/
hI7KjJ8mXctTcJKvxtqnBpG22La3F2b1YKUMXPbH5iiuK/YKyAg2FdPTYtCh6SyD
...
0SwrD3aBShifkNYno7H/rkgn3aWDpHlS5DSUe8+a9OPDjDJsjCDYC9sCYlANupb6
Vh8=
-----END ENCRYPTED PRIVATE KEY-----
OpenSSL does, however, come with a utility for viewing and even decrypting RSA private keys.
$ openssl rsa -in localhost_rsa.key -noout -text
Enter pass phrase for localhost_rsa.key:
Private-Key: (2048 bit)
modulus:
00:b7:2f:a9:8d:70:79:69:0e:09:7c:37:34:73:f6:
...
publicExponent: 65537 (0x10001)
privateExponent:
6f:17:38:7b:cd:f9:d2:fb:f0:44:a4:35:eb:1d:39:
...
prime1:
00:ee:6d:32:92:1f:fe:54:d2:f0:fc:5b:fb:ff:ef:
...
prime2:
00:c4:b0:26:65:8e:ba:44:25:9f:de:8f:95:95:2a:
...
exponent1:
36:4f:09:45:df:a3:bf:0e:8d:75:ee:3d:e8:7e:5b:
...
exponent2:
31:f4:5f:3c:29:fc:ea:f3:f7:5f:aa:6e:1e:5d:93:
...
coefficient:
00:dd:da:f6:00:13:b3:8b:2e:64:94:37:b4:f1:33:
...
Thus, the trick to importing these into a Java Key Store is to parse the ASN.1 structure of the
private key file and present it to the JCE API for import. ASN.1 parsing, in general, is a hard
problem, but in this case I can minimize it by recognizing that there are only two ASN.1 tags in
use in this format: the STRUCT tag 0x30
and the INTEGER tag 0x02
.
If you're not familiar with ASN.1, (in which case you're in good company: most people aren't, as it's a very obsolete format only found in OpenSSL anymore), it's a data interchange format, somewhat similar in concept to XML as it's typically used these days. However, unlike the very verbose XML, ASN.1 is designed to be almost hyper-efficient in its use of space. As such, every ASN.1 data element is represented by a one-byte tag indicating what type the data is, a one-byte length identifier, followed by that many bytes of data, followed by another tag. Data elements can be grouped under a STRUCT tag but it's worth noting that none of the data elements are named inside an ASN.1-encoded file: the caller has to know what each element represents.
You may have spotted a crippling limitation in the description of ASN.1 elements as I presented
them above, though: if an ASN.1 element is prefixed by a tag indicating its type and a single byte
indicating its length, then a data element can never be longer than 255 bytes. So, although a
single-byte tag and a single-byte length is the default, ASN.1 does include a provision for longer
data elements. If the high-order bit in the length is set, then the length byte doesn't indicate
the length of the data element, but instead the 7 lower-order bits indicate the length of the
length. (This implies also that if the data is longer than 127 bytes, it has to use this extended
length encoding). In other words, 0x82
tells me that the next two bytes encode the
length of the data element (which starts immediately after those two bytes). This extended-length
encoding turns out to be the most complex part of parsing a PKCS #1 formatted private key.
There's a LOT more to ASN.1 than I've presented here, but this is enough to parse an RSA private key file.
Example 5, below, is the hexadecimal representation of the first part of a PKCS #1-formatted RSA private key:
30 82 04 A3 02 01 00 02 82 01 01 00 B7 2F A9 8D 70 79 69 0E 09 7C 37 34 73 F6 58 36 B7 B9 5B 5B
E7 69 73 07 7E 3A E1 E5 BA 38 31 B0 D1 A0 08 A9 8B 64 3B 32 D2 BC A6 A3 20 E5 6B DA 5F 0A AC 66
88 D2 38 ED 8C 69 EB 16 9B B3 BC EB A6 4A 15 F0 37 80 F1 F1 0F DF 7F BA 2E B8 8E 98 6D 1B 7F 53
4D 3F 8E F0 6D DF 57 6C 47 4B 6D 77 4A 60 81 F0 9B 48 8A AD 7F EA 36 0A 82 EE E2 83 19 E0 44 3E
FA 7D 2E 69 86 7E C4 64 32 05 E0 E4 EC DC E3 56 BD 9A 1C AA 94 51 8D 22 2E 8D D4 50 59 74 1C C3
16 72 B7 C2 AF EC 1B 98 D2 30 AC 88 67 99 D2 5E 0C 91 3C C3 7E AB 2A EC 8B 2F F2 67 FD C5 D3 BC
9B C9 1E 69 1B 27 38 C9 90 30 17 46 0B A1 2C 1D 4E 52 74 98 11 F1 E4 DF 24 40 94 E1 52 C3 4C 93
9D 80 25 73 B3 9A 3C D1 89 33 59 1F 95 60 8E 16 E1 E6 A9 58 9D D9 1D 9A 44 A2 72 25 16 E4 3A 58
4A 26 3A 9C E1 10 72 AB 19 06 0C A1 02 03 01 00 01 02 82 01 00 6F 17 38 7B CD F9 D2 FB F0 44 A4
...
The first byte is 0x30
which you may recall I mentioned is the ASN.1 tag for a
STRUCT: indicating that the data element is itself a grouping of other ASN.1 data elements. This is followed by the byte
0x82
. Since the high-order bit of 0x82
is set, this tells me that
the following 2 bytes encode the length of the structure. These two bytes are 0x04A3
,
which is the big-endian hexadecimal representation of the decimal number 1,187: the next 1,187
bytes are the struct itself. This means also that the following byte, the fifth, must be an ASN.1 tag
indicating the first data element of the (unnamed, you may recall) structure. And it is:
0x02
is the ASN.1 tag for an integer. This is followed by the byte 0x01, indicating
that this integer is one byte long. The next single byte, then, is the data value: 0, the version
identifier.
The very next byte must again be a valid ASN.1 tag — and it's 0x02
, another
integer (recall that this structure is easy to parse because it's all integers). This time, though,
the length byte is 0x82
again, indicating that the length of the integer is encoded
in the following two bytes: 0x0101
, or 257 decimal. This is followed by a 257-byte
value, in big-endian format. If you're inclined to fastidiously count all 257 bytes here, you'll
see that this is followed by the sequence: 02 03 01 00 01
. These 5 bytes together
are the ASN.1 encoding of a 3-byte
integer whose decimal value is 65,537; if you peek back at example 4, you'll see that this is
the correct public exponent that OpenSSL reported. This value is followed by 02 82 01 00
: another 256 byte
integer. You can probably guess at this point that the first 257 byte integer was the modulus
and the second is the private exponent.
Example 6 shows the breakdown of tags, lengths, extended lengths and actual values of these first few parts of an ASN.1-encoded private key.
30 82 04 A3 02 01 00 02 82 01 01 00 B7 2F A9 8D 70 79 69 0E 09 7C 37 34 73 F6 58 36 B7 B9 5B 5B
E7 69 73 07 7E 3A E1 E5 BA 38 31 B0 D1 A0 08 A9 8B 64 3B 32 D2 BC A6 A3 20 E5 6B DA 5F 0A AC 66
88 D2 38 ED 8C 69 EB 16 9B B3 BC EB A6 4A 15 F0 37 80 F1 F1 0F DF 7F BA 2E B8 8E 98 6D 1B 7F 53
4D 3F 8E F0 6D DF 57 6C 47 4B 6D 77 4A 60 81 F0 9B 48 8A AD 7F EA 36 0A 82 EE E2 83 19 E0 44 3E
FA 7D 2E 69 86 7E C4 64 32 05 E0 E4 EC DC E3 56 BD 9A 1C AA 94 51 8D 22 2E 8D D4 50 59 74 1C C3
16 72 B7 C2 AF EC 1B 98 D2 30 AC 88 67 99 D2 5E 0C 91 3C C3 7E AB 2A EC 8B 2F F2 67 FD C5 D3 BC
9B C9 1E 69 1B 27 38 C9 90 30 17 46 0B A1 2C 1D 4E 52 74 98 11 F1 E4 DF 24 40 94 E1 52 C3 4C 93
9D 80 25 73 B3 9A 3C D1 89 33 59 1F 95 60 8E 16 E1 E6 A9 58 9D D9 1D 9A 44 A2 72 25 16 E4 3A 58
4A 26 3A 9C E1 10 72 AB 19 06 0C A1 02 03 01 00 01 02 82 01 00 6F 17 38 7B CD F9 D2 FB F0 44 A4
...
Since all I need out of this file is the list of the integers in order, I can almost parse it as in example 7.
private static void ASN1Parse(byte[] b, List<BigInteger> integers) {
int pos = 0;
while (pos < b.length) {
byte tag = b[pos++];
int length = b[pos++];
byte[] contents = new byte[length];
System.arraycopy(b, pos, contents, 0, length);
pos += length;
if (tag == 0x30) { // sequence
ASN1Parse(contents, integers);
} else if (tag == 0x02) { // Integer
BigInteger i = new BigInteger(contents);
integers.add(i);
}
}
}
The idea behind example 7 is that I take as input a "blob" of bytes b
like the one
in example 5 and every time I successfully parse a properly-formatted integer, I append it to the out parameter
integers
. It's up to the caller to recognize that integers[1]
is the
modulus, for example.
So I first read the tag, I then read the length, and then I slurp all of the data represented
by the length into the byte array contents
. If the tag was an integer, I give
the contents directly to Java's BigInteger
constructor which was designed for just
this sort of thing, and append that BigInteger
to the integers list. If, on the other hand, the tag was a
struct, I just recursively call the parser again with the contents which, by the structure of
ASN.1, must itself be a valid ASN.1-formatted byte array. A full-featured ASN.1 parser would, of course,
deal with quite a few other tags, but since I know I have an RSA key, I don't have to worry about
them in this case.
However, this doesn't quite work — it doesn't take into account extended-length tags which,
if you recall from example 5, my very first tag is. Handling extended-length tags in Java turns
out to be a bit of a pain because Java is just a tad byte-hostile. java.lang.Integer
doesn't have a byte-array constructor analogous to the char
array constructor in
java.lang.String
. BigInteger
does (but see below), but ugh... a whole
BigInteger
just to decode a couple of bytes? You would expect something like this
to work:
if ((length & 0x80) !=0) {
int extLen = 0;
for (int i = 0; i < (length & 0x7F); i++) {
extLen = (extLen << 8) | b[pos++];
}
length = extLen;
}
0x04 0xA3
, this loop would construct the
final integer as:
0x00 0x00 0x00 0x04
<< 8
0x00 0x00 0x04 0x00 |
0xA3
-------------------
0x00 0x00 0x04 0xA3
But it doesn't, because 0xA3
is treated as a signed, two's-complement byte, and then
promoted to an integer, preserving the sign bit. So what happens instead is:
0x00 0x00 0x00 0x04
<< 8
0x00 0x00 0x04 0x00 |
0xFF 0xFF 0xFF 0xA3
-------------------
0xFF 0xFF 0xFF 0xA3
Which is actually the two's complement representation of decimal integer -93. What I want is some way to force
Java to not interpret this byte as a signed byte. Java doesn't have an unsigned byte type,
though. There is a sneaky trick to make this work, though: AND
ing each byte with
0xFF
wipes out the signed upper bytes and makes this work as it's supposed to:
if ((length & 0x80) !=0) {
int extLen = 0;
for (int i = 0; i < (length & 0x7F); i++) {
extLen = (extLen << 8) | (b[pos++] & 0xFF);
}
length = extLen;
}
I mentioned that you can do this with BigInteger
s as well. If you
do, you have to watch out for the high order bit there — BigInteger assumes that if the
high-order bit is set, then the resulting integer is negative. Instead, you have to use the
two-args constructor that takes a sign indicator, followed by the byte array.
This is, incidentally, why the RSA public exponent shown in
example 5 is 257, rather than 256, bytes long in the encoded file: 256 bytes = 2,048 bits, which is the length of
the key. The extra byte is the 0-pad that stops Java from recognizing this as a negative number.
That's why I didn't have to be tricky when I created the BigInteger
in
example 7; the encoder was tricky on my behalf.
Once you have parsed the integers, it's easy to use the JCE API to construct an RSA private key
and associate it with a certificate — this is all boilerplate JCE. Example 10 is a complete
example that mimics the JDK's keytool
. I've also added support for PKCS #8 (since
it's part of the JDK) and DSA keys, since they're simple to add once you have basic ASN.1
support.
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.math.BigInteger;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.NoSuchAlgorithmException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateException;
class KeyImportException extends Exception {
public KeyImportException(String msg) {
super(msg);
}
}
/**
* Import an existing key into a keystore. This requires both the private key file and
* the corresponding certificate file.
*/
public class KeyImport {
// Base64 decoding helper
private static final int invCodes[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 64, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
};
private static byte[] base64Decode(String input) {
if (input.length() % 4 != 0) {
throw new IllegalArgumentException("Invalid base64 input");
}
byte decoded[] = new byte[((input.length() * 3) / 4) -
(input.indexOf('=') > 0 ? (input.length() - input.indexOf('=')) : 0)];
char[] inChars = input.toCharArray();
int j = 0;
int b[] = new int[4];
for (int i = 0; i < inChars.length; i += 4) {
b[0] = invCodes[inChars[i]];
b[1] = invCodes[inChars[i + 1]];
b[2] = invCodes[inChars[i + 2]];
b[3] = invCodes[inChars[i + 3]];
decoded[j++] = (byte) ((b[0] << 2) | (b[1] >> 4));
if (b[2] < 64) {
decoded[j++] = (byte) ((b[1] << 4) | (b[2] >> 2));
if (b[3] < 64) {
decoded[j++] = (byte) ((b[2] << 6) | b[3]);
}
}
}
return decoded;
}
/**
* Bare-bones ASN.1 parser that can only deal with a structure that contains integers
* (as I expect for the RSA private key format given in PKCS #1 and RFC 3447).
* @param b the bytes to be parsed as ASN.1 DER
* @param integers an output array to which all integers encountered during the parse
* will be appended in the order they're encountered. It's up to the caller to determine
* which is which.
*/
private static void ASN1Parse(byte[] b, List<BigInteger> integers)
throws KeyImportException {
int pos = 0;
while (pos < b.length) {
byte tag = b[pos++];
int length = b[pos++];
if ((length & 0x80) != 0) {
int extLen = 0;
for (int i = 0; i < (length & 0x7F); i++) {
extLen = (extLen << 8) | (b[pos++] & 0xFF);
}
length = extLen;
}
byte[] contents = new byte[length];
System.arraycopy(b, pos, contents, 0, length);
pos += length;
if (tag == 0x30) { // sequence
ASN1Parse(contents, integers);
} else if (tag == 0x02) { // Integer
BigInteger i = new BigInteger(contents);
integers.add(i);
} else {
throw new KeyImportException("Unsupported ASN.1 tag " + tag + " encountered. Is this a " +
"valid RSA key?");
}
}
}
/**
* Read a PKCS #8, Base64-encrypted file as a Key instance.
* If the file is encrypted, decrypt it via:
* openssl rsa -in keyfilename -out decryptedkeyfilename
* TODO deal with an encrypted private key internally
*/
private static PrivateKey readPrivateKeyFile(String keyFileName)
throws IOException,
NoSuchAlgorithmException,
InvalidKeySpecException,
KeyImportException {
BufferedReader in = new BufferedReader(new FileReader(keyFileName));
try {
String line;
boolean readingKey = false;
boolean pkcs8Format = false;
boolean rsaFormat = false;
boolean dsaFormat = false;
StringBuffer base64EncodedKey = new StringBuffer();
while ((line = in.readLine()) != null) {
if (readingKey) {
if (line.trim().equals("-----END RSA PRIVATE KEY-----")) { // PKCS #1
readingKey = false;
} else if (line.trim().equals("-----END DSA PRIVATE KEY-----")) {
readingKey = false;
} else if (line.trim().equals("-----END PRIVATE KEY-----")) { // PKCS #8
readingKey = false;
} else {
base64EncodedKey.append(line.trim());
}
} else if (line.trim().equals("-----BEGIN RSA PRIVATE KEY-----")) {
readingKey = true;
rsaFormat = true;
} else if (line.trim().equals("-----BEGIN DSA PRIVATE KEY-----")) {
readingKey = true;
dsaFormat = true;
} else if (line.trim().equals("-----BEGIN PRIVATE KEY-----")) {
readingKey = true;
pkcs8Format = true;
}
}
if (base64EncodedKey.length() == 0) {
throw new IOException("File '" + keyFileName +
"' did not contain an unencrypted private key");
}
byte[] bytes = base64Decode(base64EncodedKey.toString());
KeyFactory kf = null;
KeySpec spec = null;
if (pkcs8Format) {
kf = KeyFactory.getInstance("RSA");
spec = new PKCS8EncodedKeySpec(bytes);
} else if (rsaFormat) {
// PKCS#1 format
kf = KeyFactory.getInstance("RSA");
List<BigInteger> rsaIntegers = new ArrayList<BigInteger>();
ASN1Parse(bytes, rsaIntegers);
if (rsaIntegers.size() < 8) {
throw new KeyImportException("'" + keyFileName +
"' does not appear to be a properly formatted RSA key");
}
BigInteger publicExponent = rsaIntegers.get(2);
BigInteger privateExponent = rsaIntegers.get(3);
BigInteger modulus = rsaIntegers.get(1);
BigInteger primeP = rsaIntegers.get(4);
BigInteger primeQ = rsaIntegers.get(5);
BigInteger primeExponentP = rsaIntegers.get(6);
BigInteger primeExponentQ = rsaIntegers.get(7);
BigInteger crtCoefficient = rsaIntegers.get(8);
//spec = new RSAPrivateKeySpec(modulus, privateExponent);
spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent,
primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient);
} else if (dsaFormat) {
kf = KeyFactory.getInstance("DSA");
List<BigInteger> dsaIntegers = new ArrayList<BigInteger>();
ASN1Parse(bytes, dsaIntegers);
if (dsaIntegers.size() < 5) {
throw new KeyImportException("'" + keyFileName +
"' does not appear to be a properly formatted DSA key");
}
BigInteger privateExponent = dsaIntegers.get(1);
BigInteger publicExponent = dsaIntegers.get(2);
BigInteger P = dsaIntegers.get(3);
BigInteger Q = dsaIntegers.get(4);
BigInteger G = dsaIntegers.get(5);
spec = new DSAPrivateKeySpec(privateExponent, P, Q, G);
}
return kf.generatePrivate(spec);
} finally {
in.close();
}
}
/**
* Support for parsing command line parameters of the form "-id value"
*/
static class Parameter {
String flag;
boolean required;
String description;
String defaultValue;
public Parameter(String flag, boolean required, String description, String defaultValue) {
this.flag = flag;
this.required = required;
this.description = description;
this.defaultValue = defaultValue;
}
public boolean equals(Object o) {
return (o instanceof Parameter) && (this.flag.equals(((Parameter) o).flag));
}
}
private static String KEY_FILE = "-keyFile";
private static String ALIAS = "-alias";
private static String CERT_FILE = "-certificateFile";
private static String KEY_STORE = "-keystore";
private static String KEY_STORE_PASSWORD = "-keystorePassword";
private static String KEY_STORE_TYPE = "-keystoreType";
private static String KEY_PASSWORD = "-keyPassword";
private static List<Parameter> paramDesc = Arrays.asList(
new Parameter[] {
new Parameter(KEY_FILE, true, "Name of file containing a private key in PEM or DER form", null),
new Parameter(ALIAS, true, "The alias that this key should be imported as", null),
new Parameter(CERT_FILE, true, "Name of file containing the certificate that corresponds to the key named by '-keyFile'", null),
new Parameter(KEY_STORE, false, "Name of the keystore to import the private key into.", "~/.keystore"),
new Parameter(KEY_STORE_PASSWORD, false, "Keystore password", "changeit"),
new Parameter(KEY_STORE_TYPE, false, "Type of keystore; must be JKS or PKCS12", "JKS"),
// If this password is different than the key store password, Tomcat (at least) chokes
// on it with: java.security.UnrecoverableKeyException: Cannot recover key
new Parameter(KEY_PASSWORD, false, "The password to protect the imported key with", "changeit")
});
private static void usage() {
for (Parameter param : paramDesc) {
System.out.println(param.flag + "\t" + (param.required ? "required" : "optional") + "\t" +
param.description + "\t" +
(param.defaultValue != null ? ("default '" + param.defaultValue + "'") : ""));
}
}
public static void main(String[] args) throws IOException,
KeyStoreException,
NoSuchAlgorithmException,
CertificateException,
InvalidKeySpecException,
KeyImportException {
Map<String, String> parsedArgs = new HashMap<String, String>();
for (Parameter param : paramDesc) {
if (param.defaultValue != null) {
parsedArgs.put(param.flag, param.defaultValue);
}
}
for (int i = 0; i < args.length; i += 2) {
parsedArgs.put(args[i], args[i + 1]);
}
boolean invalidParameters = false;
for (Parameter param : paramDesc) {
if (param.required && parsedArgs.get(param.flag) == null) {
System.err.println("Missing required parameter " + param.flag);
invalidParameters = true;
}
}
for (String key : parsedArgs.keySet()) {
if (!paramDesc.contains(new Parameter(key, false, null, null))) {
System.err.println("Invalid parameter '" + key + "'");
invalidParameters = true;
}
}
if (invalidParameters) {
usage();
System.exit(0);
}
KeyStore ks = KeyStore.getInstance(parsedArgs.get(KEY_STORE_TYPE));
InputStream keyStoreIn = new FileInputStream(parsedArgs.get(KEY_STORE));
try {
ks.load(keyStoreIn, parsedArgs.get(KEY_STORE_PASSWORD).toCharArray());
} finally {
keyStoreIn.close();
}
Certificate cert;
CertificateFactory fact = CertificateFactory.getInstance("X.509");
FileInputStream certIn = new FileInputStream(parsedArgs.get(CERT_FILE));
try {
cert = fact.generateCertificate(certIn);
} finally {
certIn.close();
}
PrivateKey privateKey = readPrivateKeyFile(parsedArgs.get(KEY_FILE));
ks.setKeyEntry(parsedArgs.get(ALIAS), privateKey,
parsedArgs.get(KEY_PASSWORD).toCharArray(), new Certificate[] {cert});
OutputStream keyStoreOut = new FileOutputStream(parsedArgs.get(KEY_STORE));
try {
ks.store(keyStoreOut, parsedArgs.get(KEY_STORE_PASSWORD).toCharArray());
} finally {
keyStoreOut.close();
}
}
}
This does still require that you decrypt the key before importing it as in:
As it turns out, tackling in-place decryption of an RSA key requires a more sophisticated ASN.1
parser than the one in example 10. I'll come back to this topic in my next post.
$ openssl rsa -in encrypted.key -out decrypted.key