Skip to content

Commit 6c1103c

Browse files
Support PEM formatted elliptic-curve TLS keys
Apply the changes from e0c79ce to the `PrivateKeyParser` used for web server SSL configuration. See gh-32646
1 parent f76248d commit 6c1103c

File tree

3 files changed

+184
-63
lines changed

3 files changed

+184
-63
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java

Lines changed: 170 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import java.security.GeneralSecurityException;
2525
import java.security.KeyFactory;
2626
import java.security.PrivateKey;
27-
import java.security.spec.InvalidKeySpecException;
2827
import java.security.spec.PKCS8EncodedKeySpec;
28+
import java.util.ArrayList;
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.function.Function;
2932
import java.util.regex.Matcher;
3033
import java.util.regex.Pattern;
3134

@@ -45,21 +48,68 @@ final class PrivateKeyParser {
4548

4649
private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+";
4750

51+
private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
52+
4853
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
4954

50-
private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
55+
private static final String EC_HEADER = "-+BEGIN\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
56+
57+
private static final String EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+";
5158

5259
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
5360

54-
private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_HEADER + BASE64_TEXT + PKCS1_FOOTER,
55-
Pattern.CASE_INSENSITIVE);
61+
private static final List<PemParser> PEM_PARSERS;
62+
static {
63+
List<PemParser> parsers = new ArrayList<>();
64+
parsers.add(new PemParser(PKCS1_HEADER, PKCS1_FOOTER, "RSA", PrivateKeyParser::createKeySpecForPkcs1));
65+
parsers.add(new PemParser(EC_HEADER, EC_FOOTER, "EC", PrivateKeyParser::createKeySpecForEc));
66+
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, "RSA", PKCS8EncodedKeySpec::new));
67+
PEM_PARSERS = Collections.unmodifiableList(parsers);
68+
}
69+
70+
/**
71+
* ASN.1 encoded object identifier {@literal 1.2.840.113549.1.1.1}.
72+
*/
73+
private static final int[] RSA_ALGORITHM = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
5674

57-
private static final Pattern PKCS8_KEY_PATTERN = Pattern.compile(PKCS8_HEADER + BASE64_TEXT + PKCS8_FOOTER,
58-
Pattern.CASE_INSENSITIVE);
75+
/**
76+
* ASN.1 encoded object identifier {@literal 1.2.840.10045.2.1}.
77+
*/
78+
private static final int[] EC_ALGORITHM = { 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01 };
79+
80+
/**
81+
* ASN.1 encoded object identifier {@literal 1.3.132.0.34}.
82+
*/
83+
private static final int[] EC_PARAMETERS = { 0x2b, 0x81, 0x04, 0x00, 0x22 };
5984

6085
private PrivateKeyParser() {
6186
}
6287

88+
private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes) {
89+
return createKeySpecForAlgorithm(bytes, RSA_ALGORITHM, null);
90+
}
91+
92+
private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes) {
93+
return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, EC_PARAMETERS);
94+
}
95+
96+
private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[] algorithm, int[] parameters) {
97+
try {
98+
DerEncoder encoder = new DerEncoder();
99+
encoder.integer(0x00); // Version 0
100+
DerEncoder algorithmIdentifier = new DerEncoder();
101+
algorithmIdentifier.objectIdentifier(algorithm);
102+
algorithmIdentifier.objectIdentifier(parameters);
103+
byte[] byteArray = algorithmIdentifier.toByteArray();
104+
encoder.sequence(byteArray);
105+
encoder.octetString(bytes);
106+
return new PKCS8EncodedKeySpec(encoder.toSequence());
107+
}
108+
catch (IOException ex) {
109+
throw new IllegalStateException(ex);
110+
}
111+
}
112+
63113
/**
64114
* Load a private key from the specified resource.
65115
* @param resource the private key to parse
@@ -68,82 +118,139 @@ private PrivateKeyParser() {
68118
static PrivateKey parse(String resource) {
69119
try {
70120
String text = readText(resource);
71-
Matcher matcher = PKCS1_PATTERN.matcher(text);
72-
if (matcher.find()) {
73-
return parsePkcs1(decodeBase64(matcher.group(1)));
121+
for (PemParser pemParser : PEM_PARSERS) {
122+
PrivateKey privateKey = pemParser.parse(text);
123+
if (privateKey != null) {
124+
return privateKey;
125+
}
74126
}
75-
matcher = PKCS8_KEY_PATTERN.matcher(text);
76-
if (matcher.find()) {
77-
return parsePkcs8(decodeBase64(matcher.group(1)));
78-
}
79-
throw new IllegalStateException("Unrecognized private key format in " + resource);
127+
throw new IllegalStateException("Unrecognized private key format");
80128
}
81-
catch (GeneralSecurityException | IOException ex) {
129+
catch (Exception ex) {
82130
throw new IllegalStateException("Error loading private key file " + resource, ex);
83131
}
84132
}
85133

86-
private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException {
87-
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes);
88-
return parsePkcs8(pkcs8Bytes);
134+
private static String readText(String resource) throws IOException {
135+
URL url = ResourceUtils.getURL(resource);
136+
try (Reader reader = new InputStreamReader(url.openStream())) {
137+
return FileCopyUtils.copyToString(reader);
138+
}
89139
}
90140

91-
private static byte[] convertPkcs1ToPkcs8(byte[] pkcs1) {
92-
try {
93-
ByteArrayOutputStream result = new ByteArrayOutputStream();
94-
int pkcs1Length = pkcs1.length;
95-
int totalLength = pkcs1Length + 22;
96-
// Sequence + total length
97-
result.write(bytes(0x30, 0x82));
98-
result.write((totalLength >> 8) & 0xff);
99-
result.write(totalLength & 0xff);
100-
// Integer (0)
101-
result.write(bytes(0x02, 0x01, 0x00));
102-
// Sequence: 1.2.840.113549.1.1.1, NULL
103-
result.write(
104-
bytes(0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00));
105-
// Octet string + length
106-
result.write(bytes(0x04, 0x82));
107-
result.write((pkcs1Length >> 8) & 0xff);
108-
result.write(pkcs1Length & 0xff);
109-
// PKCS1
110-
result.write(pkcs1);
111-
return result.toByteArray();
141+
/**
142+
* Parser for a specific PEM format.
143+
*/
144+
private static class PemParser {
145+
146+
private final Pattern pattern;
147+
148+
private final String algorithm;
149+
150+
private final Function<byte[], PKCS8EncodedKeySpec> keySpecFactory;
151+
152+
PemParser(String header, String footer, String algorithm,
153+
Function<byte[], PKCS8EncodedKeySpec> keySpecFactory) {
154+
this.pattern = Pattern.compile(header + BASE64_TEXT + footer, Pattern.CASE_INSENSITIVE);
155+
this.algorithm = algorithm;
156+
this.keySpecFactory = keySpecFactory;
112157
}
113-
catch (IOException ex) {
114-
throw new IllegalStateException(ex);
158+
159+
PrivateKey parse(String text) {
160+
Matcher matcher = this.pattern.matcher(text);
161+
return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(1)));
162+
}
163+
164+
private static byte[] decodeBase64(String content) {
165+
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
166+
return Base64Utils.decode(contentBytes);
115167
}
116-
}
117168

118-
private static byte[] bytes(int... elements) {
119-
byte[] result = new byte[elements.length];
120-
for (int i = 0; i < elements.length; i++) {
121-
result[i] = (byte) elements[i];
169+
private PrivateKey parse(byte[] bytes) {
170+
try {
171+
PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes);
172+
KeyFactory keyFactory = KeyFactory.getInstance(this.algorithm);
173+
return keyFactory.generatePrivate(keySpec);
174+
}
175+
catch (GeneralSecurityException ex) {
176+
throw new IllegalArgumentException("Unexpected key format", ex);
177+
}
122178
}
123-
return result;
179+
124180
}
125181

126-
private static PrivateKey parsePkcs8(byte[] privateKeyBytes) throws GeneralSecurityException {
127-
try {
128-
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
129-
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
130-
return keyFactory.generatePrivate(keySpec);
182+
/**
183+
* Simple ASN.1 DER encoder.
184+
*/
185+
static class DerEncoder {
186+
187+
private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
188+
189+
void objectIdentifier(int... encodedObjectIdentifier) throws IOException {
190+
int code = (encodedObjectIdentifier != null) ? 0x06 : 0x05;
191+
codeLengthBytes(code, bytes(encodedObjectIdentifier));
131192
}
132-
catch (InvalidKeySpecException ex) {
133-
throw new IllegalArgumentException("Unexpected key format", ex);
193+
194+
void integer(int... encodedInteger) throws IOException {
195+
codeLengthBytes(0x02, bytes(encodedInteger));
134196
}
135-
}
136197

137-
private static String readText(String resource) throws IOException {
138-
URL url = ResourceUtils.getURL(resource);
139-
try (Reader reader = new InputStreamReader(url.openStream())) {
140-
return FileCopyUtils.copyToString(reader);
198+
void octetString(byte[] bytes) throws IOException {
199+
codeLengthBytes(0x04, bytes);
200+
}
201+
202+
void sequence(int... elements) throws IOException {
203+
sequence(bytes(elements));
204+
}
205+
206+
void sequence(byte[] bytes) throws IOException {
207+
codeLengthBytes(0x30, bytes);
208+
}
209+
210+
void codeLengthBytes(int code, byte[] bytes) throws IOException {
211+
this.stream.write(code);
212+
int length = (bytes != null) ? bytes.length : 0;
213+
if (length <= 127) {
214+
this.stream.write(length & 0xFF);
215+
}
216+
else {
217+
ByteArrayOutputStream lengthStream = new ByteArrayOutputStream();
218+
while (length != 0) {
219+
lengthStream.write(length & 0xFF);
220+
length = length >> 8;
221+
}
222+
byte[] lengthBytes = lengthStream.toByteArray();
223+
this.stream.write(0x80 | lengthBytes.length);
224+
for (int i = lengthBytes.length - 1; i >= 0; i--) {
225+
this.stream.write(lengthBytes[i]);
226+
}
227+
}
228+
if (bytes != null) {
229+
this.stream.write(bytes);
230+
}
231+
}
232+
233+
private static byte[] bytes(int... elements) {
234+
if (elements == null) {
235+
return null;
236+
}
237+
byte[] result = new byte[elements.length];
238+
for (int i = 0; i < elements.length; i++) {
239+
result[i] = (byte) elements[i];
240+
}
241+
return result;
242+
}
243+
244+
byte[] toSequence() throws IOException {
245+
DerEncoder sequenceEncoder = new DerEncoder();
246+
sequenceEncoder.sequence(toByteArray());
247+
return sequenceEncoder.toByteArray();
248+
}
249+
250+
byte[] toByteArray() {
251+
return this.stream.toByteArray();
141252
}
142-
}
143253

144-
private static byte[] decodeBase64(String content) {
145-
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
146-
return Base64Utils.decode(contentBytes);
147254
}
148255

149256
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ void parsePkcs8KeyFile() {
3535
PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-key.pem");
3636
assertThat(privateKey).isNotNull();
3737
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
38+
assertThat(privateKey.getAlgorithm()).isEqualTo("RSA");
39+
}
40+
41+
@Test
42+
void parsePkcs8KeyFileWithEcdsa() {
43+
PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-ec-key.pem");
44+
assertThat(privateKey).isNotNull();
45+
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
46+
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
3847
}
3948

4049
@Test
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MHcCAQEEIBEZhSR+d8kwL5L/K0f/eNBm4RfzyyA1jfg+dV1/8WvqoAoGCCqGSM49
3+
AwEHoUQDQgAEBbfdBTSUWuui7O2R+W9mDPjAHjgdBJsjrjnvkjnq8f/k4U/OqvjK
4+
qnHEZwYgdaF2WqYdqBYMns0n+tSMgBoonQ==
5+
-----END EC PRIVATE KEY-----

0 commit comments

Comments
 (0)