commit
189e49f6d6
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -26,6 +26,7 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility methods for creating Java trust material from key and certificate files.
|
||||
|
|
@ -50,10 +51,11 @@ final class KeyStoreFactory {
|
|||
static KeyStore create(Path certPath, Path keyPath, String alias) {
|
||||
try {
|
||||
KeyStore keyStore = getKeyStore();
|
||||
X509Certificate[] certificates = CertificateParser.parse(certPath);
|
||||
String certificateText = Files.readString(certPath);
|
||||
List<X509Certificate> certificates = PemCertificateParser.parse(certificateText);
|
||||
PrivateKey privateKey = getPrivateKey(keyPath);
|
||||
try {
|
||||
addCertificates(keyStore, certificates, privateKey, alias);
|
||||
addCertificates(keyStore, certificates.toArray(X509Certificate[]::new), privateKey, alias);
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex);
|
||||
|
|
@ -72,9 +74,10 @@ final class KeyStoreFactory {
|
|||
return keyStore;
|
||||
}
|
||||
|
||||
private static PrivateKey getPrivateKey(Path path) {
|
||||
private static PrivateKey getPrivateKey(Path path) throws IOException {
|
||||
if (path != null && Files.exists(path)) {
|
||||
return PrivateKeyParser.parse(path);
|
||||
String text = Files.readString(path);
|
||||
return PemPrivateKeyParser.parse(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -17,9 +17,6 @@
|
|||
package org.springframework.boot.buildpack.platform.docker.ssl;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
|
@ -30,13 +27,16 @@ import java.util.function.Consumer;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Parser for X.509 certificates in PEM format.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class CertificateParser {
|
||||
final class PemCertificateParser {
|
||||
|
||||
private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+";
|
||||
|
||||
|
|
@ -46,21 +46,23 @@ final class CertificateParser {
|
|||
|
||||
private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private CertificateParser() {
|
||||
private PemCertificateParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load certificates from the specified file paths.
|
||||
* @param paths one or more paths to certificate files
|
||||
* @return certificates parsed from specified file paths
|
||||
* Parse certificates from the specified string.
|
||||
* @param text the text to parse
|
||||
* @return the parsed certificates
|
||||
*/
|
||||
static X509Certificate[] parse(Path... paths) {
|
||||
CertificateFactory factory = getCertificateFactory();
|
||||
List<X509Certificate> certificates = new ArrayList<>();
|
||||
for (Path path : paths) {
|
||||
readCertificates(path, factory, certificates::add);
|
||||
static List<X509Certificate> parse(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return certificates.toArray(new X509Certificate[0]);
|
||||
CertificateFactory factory = getCertificateFactory();
|
||||
List<X509Certificate> certs = new ArrayList<>();
|
||||
readCertificates(text, factory, certs::add);
|
||||
Assert.state(!CollectionUtils.isEmpty(certs), "Missing certificates or unrecognized format");
|
||||
return List.copyOf(certs);
|
||||
}
|
||||
|
||||
private static CertificateFactory getCertificateFactory() {
|
||||
|
|
@ -72,9 +74,8 @@ final class CertificateParser {
|
|||
}
|
||||
}
|
||||
|
||||
private static void readCertificates(Path path, CertificateFactory factory, Consumer<X509Certificate> consumer) {
|
||||
private static void readCertificates(String text, CertificateFactory factory, Consumer<X509Certificate> consumer) {
|
||||
try {
|
||||
String text = Files.readString(path);
|
||||
Matcher matcher = PATTERN.matcher(text);
|
||||
while (matcher.find()) {
|
||||
String encodedText = matcher.group(1);
|
||||
|
|
@ -85,8 +86,8 @@ final class CertificateParser {
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (CertificateException | IOException ex) {
|
||||
throw new IllegalStateException("Error reading certificate from '" + path + "' : " + ex.getMessage(), ex);
|
||||
catch (CertificateException ex) {
|
||||
throw new IllegalStateException("Error reading certificate: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -19,23 +19,33 @@ package org.springframework.boot.buildpack.platform.docker.ssl;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.AlgorithmParameters;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.ssl.PrivateKeyParser.DerElement.TagType;
|
||||
import org.springframework.boot.buildpack.platform.docker.ssl.PrivateKeyParser.DerElement.ValueType;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.ssl.PemPrivateKeyParser.DerElement.TagType;
|
||||
import org.springframework.boot.buildpack.platform.docker.ssl.PemPrivateKeyParser.DerElement.ValueType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +55,7 @@ import org.springframework.util.Assert;
|
|||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
final class PrivateKeyParser {
|
||||
final class PemPrivateKeyParser {
|
||||
|
||||
private static final String PKCS1_RSA_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
|
||||
|
||||
|
|
@ -55,46 +65,59 @@ final class PrivateKeyParser {
|
|||
|
||||
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
|
||||
|
||||
private static final String PKCS8_ENCRYPTED_HEADER = "-+BEGIN\\s+ENCRYPTED\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
|
||||
|
||||
private static final String PKCS8_ENCRYPTED_FOOTER = "-+END\\s+ENCRYPTED\\s+PRIVATE\\s+KEY[^-]*-+";
|
||||
|
||||
private static final String SEC1_EC_HEADER = "-+BEGIN\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
|
||||
|
||||
private static final String SEC1_EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+";
|
||||
|
||||
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
|
||||
|
||||
public static final int BASE64_TEXT_GROUP = 1;
|
||||
|
||||
private static final EncodedOid RSA_ALGORITHM = EncodedOid.OID_1_2_840_113549_1_1_1;
|
||||
|
||||
private static final EncodedOid ELLIPTIC_CURVE_ALGORITHM = EncodedOid.OID_1_2_840_10045_2_1;
|
||||
|
||||
private static final EncodedOid ELLIPTIC_CURVE_384_BIT = EncodedOid.OID_1_3_132_0_34;
|
||||
|
||||
private static final Map<EncodedOid, String> ALGORITHMS;
|
||||
static {
|
||||
Map<EncodedOid, String> algorithms = new HashMap<>();
|
||||
algorithms.put(EncodedOid.OID_1_2_840_113549_1_1_1, "RSA");
|
||||
algorithms.put(EncodedOid.OID_1_2_840_113549_1_1_10, "RSA");
|
||||
algorithms.put(EncodedOid.OID_1_2_840_10040_4_1, "DSA");
|
||||
algorithms.put(EncodedOid.OID_1_3_101_110, "XDH");
|
||||
algorithms.put(EncodedOid.OID_1_3_101_111, "XDH");
|
||||
algorithms.put(EncodedOid.OID_1_3_101_112, "EdDSA");
|
||||
algorithms.put(EncodedOid.OID_1_3_101_113, "EdDSA");
|
||||
algorithms.put(EncodedOid.OID_1_2_840_10045_2_1, "EC");
|
||||
ALGORITHMS = Collections.unmodifiableMap(algorithms);
|
||||
}
|
||||
|
||||
private static final List<PemParser> PEM_PARSERS;
|
||||
static {
|
||||
List<PemParser> parsers = new ArrayList<>();
|
||||
parsers
|
||||
.add(new PemParser(PKCS1_RSA_HEADER, PKCS1_RSA_FOOTER, PrivateKeyParser::createKeySpecForPkcs1Rsa, "RSA"));
|
||||
parsers.add(new PemParser(SEC1_EC_HEADER, SEC1_EC_FOOTER, PrivateKeyParser::createKeySpecForSec1Ec, "EC"));
|
||||
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, PKCS8EncodedKeySpec::new, "RSA", "RSASSA-PSS", "EC",
|
||||
"DSA", "EdDSA", "XDH"));
|
||||
parsers.add(new PemParser(PKCS1_RSA_HEADER, PKCS1_RSA_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs1Rsa,
|
||||
"RSA"));
|
||||
parsers.add(new PemParser(SEC1_EC_HEADER, SEC1_EC_FOOTER, PemPrivateKeyParser::createKeySpecForSec1Ec, "EC"));
|
||||
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs8, "RSA",
|
||||
"RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH"));
|
||||
parsers.add(new PemParser(PKCS8_ENCRYPTED_HEADER, PKCS8_ENCRYPTED_FOOTER,
|
||||
PemPrivateKeyParser::createKeySpecForPkcs8Encrypted, "RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH"));
|
||||
PEM_PARSERS = Collections.unmodifiableList(parsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* ASN.1 encoded object identifier {@literal 1.2.840.113549.1.1.1}.
|
||||
*/
|
||||
private static final int[] RSA_ALGORITHM = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
|
||||
|
||||
/**
|
||||
* ASN.1 encoded object identifier {@literal 1.2.840.10045.2.1}.
|
||||
*/
|
||||
private static final int[] EC_ALGORITHM = { 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01 };
|
||||
|
||||
/**
|
||||
* ASN.1 encoded object identifier {@literal 1.3.132.0.34}.
|
||||
*/
|
||||
private static final int[] EC_PARAMETERS = { 0x2b, 0x81, 0x04, 0x00, 0x22 };
|
||||
|
||||
private PrivateKeyParser() {
|
||||
private PemPrivateKeyParser() {
|
||||
}
|
||||
|
||||
private static PKCS8EncodedKeySpec createKeySpecForPkcs1Rsa(byte[] bytes) {
|
||||
private static PKCS8EncodedKeySpec createKeySpecForPkcs1Rsa(byte[] bytes, String password) {
|
||||
return createKeySpecForAlgorithm(bytes, RSA_ALGORITHM, null);
|
||||
}
|
||||
|
||||
private static PKCS8EncodedKeySpec createKeySpecForSec1Ec(byte[] bytes) {
|
||||
private static PKCS8EncodedKeySpec createKeySpecForSec1Ec(byte[] bytes, String password) {
|
||||
DerElement ecPrivateKey = DerElement.of(bytes);
|
||||
Assert.state(ecPrivateKey.isType(ValueType.ENCODED, TagType.SEQUENCE),
|
||||
"Key spec should be an ASN.1 encoded sequence");
|
||||
|
|
@ -107,37 +130,29 @@ final class PrivateKeyParser {
|
|||
Assert.state(privateKey != null && privateKey.isType(ValueType.PRIMITIVE, TagType.OCTET_STRING),
|
||||
"Key spec should contain private key");
|
||||
DerElement parameters = DerElement.of(ecPrivateKey.getContents());
|
||||
return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, getEcParameters(parameters));
|
||||
return createKeySpecForAlgorithm(bytes, ELLIPTIC_CURVE_ALGORITHM, getEcParameters(parameters));
|
||||
}
|
||||
|
||||
private static int[] getEcParameters(DerElement parameters) {
|
||||
private static EncodedOid getEcParameters(DerElement parameters) {
|
||||
if (parameters == null) {
|
||||
return EC_PARAMETERS;
|
||||
return ELLIPTIC_CURVE_384_BIT;
|
||||
}
|
||||
Assert.state(parameters.isType(ValueType.ENCODED), "Key spec should contain encoded parameters");
|
||||
DerElement contents = DerElement.of(parameters.getContents());
|
||||
Assert.state(contents.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER),
|
||||
Assert.state(contents != null && contents.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER),
|
||||
"Key spec parameters should contain object identifier");
|
||||
return getEcParameters(contents.getContents());
|
||||
return EncodedOid.of(contents);
|
||||
}
|
||||
|
||||
private static int[] getEcParameters(ByteBuffer bytes) {
|
||||
int[] result = new int[bytes.remaining()];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = bytes.get() & 0xFF;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[] algorithm, int[] parameters) {
|
||||
private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, EncodedOid algorithm,
|
||||
EncodedOid parameters) {
|
||||
try {
|
||||
DerEncoder encoder = new DerEncoder();
|
||||
encoder.integer(0x00); // Version 0
|
||||
DerEncoder algorithmIdentifier = new DerEncoder();
|
||||
algorithmIdentifier.objectIdentifier(algorithm);
|
||||
algorithmIdentifier.objectIdentifier(parameters);
|
||||
byte[] byteArray = algorithmIdentifier.toByteArray();
|
||||
encoder.sequence(byteArray);
|
||||
encoder.sequence(algorithmIdentifier.toByteArray());
|
||||
encoder.octetString(bytes);
|
||||
return new PKCS8EncodedKeySpec(encoder.toSequence());
|
||||
}
|
||||
|
|
@ -146,25 +161,59 @@ final class PrivateKeyParser {
|
|||
}
|
||||
}
|
||||
|
||||
private static PKCS8EncodedKeySpec createKeySpecForPkcs8(byte[] bytes, String password) {
|
||||
DerElement ecPrivateKey = DerElement.of(bytes);
|
||||
Assert.state(ecPrivateKey.isType(ValueType.ENCODED, TagType.SEQUENCE),
|
||||
"Key spec should be an ASN.1 encoded sequence");
|
||||
DerElement version = DerElement.of(ecPrivateKey.getContents());
|
||||
Assert.state(version != null && version.isType(ValueType.PRIMITIVE, TagType.INTEGER),
|
||||
"Key spec should start with version");
|
||||
DerElement sequence = DerElement.of(ecPrivateKey.getContents());
|
||||
Assert.state(sequence != null && sequence.isType(ValueType.ENCODED, TagType.SEQUENCE),
|
||||
"Key spec should contain private key");
|
||||
DerElement algorithmId = DerElement.of(sequence.getContents());
|
||||
Assert.state(algorithmId != null && algorithmId.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER),
|
||||
"Key spec container object identifier");
|
||||
String algorithmName = ALGORITHMS.get(EncodedOid.of(algorithmId));
|
||||
return (algorithmName != null) ? new PKCS8EncodedKeySpec(bytes, algorithmName) : new PKCS8EncodedKeySpec(bytes);
|
||||
}
|
||||
|
||||
private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes, String password) {
|
||||
return Pkcs8PrivateKeyDecryptor.decrypt(bytes, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a private key from the specified file paths.
|
||||
* @param path the path to the private key file
|
||||
* @return private key from specified file path
|
||||
* Parse a private key from the specified string.
|
||||
* @param text the text to parse
|
||||
* @return the parsed private key
|
||||
*/
|
||||
static PrivateKey parse(Path path) {
|
||||
static PrivateKey parse(String text) {
|
||||
return parse(text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a private key from the specified string, using the provided password for
|
||||
* decryption if necessary.
|
||||
* @param text the text to parse
|
||||
* @param password the password used to decrypt an encrypted private key
|
||||
* @return the parsed private key
|
||||
*/
|
||||
static PrivateKey parse(String text, String password) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String text = Files.readString(path);
|
||||
for (PemParser pemParser : PEM_PARSERS) {
|
||||
PrivateKey privateKey = pemParser.parse(text);
|
||||
PrivateKey privateKey = pemParser.parse(text, password);
|
||||
if (privateKey != null) {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unrecognized private key format");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Error loading private key file " + path, ex);
|
||||
throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex);
|
||||
}
|
||||
throw new IllegalStateException("Missing private key or unrecognized format");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,20 +223,20 @@ final class PrivateKeyParser {
|
|||
|
||||
private final Pattern pattern;
|
||||
|
||||
private final Function<byte[], PKCS8EncodedKeySpec> keySpecFactory;
|
||||
private final BiFunction<byte[], String, PKCS8EncodedKeySpec> keySpecFactory;
|
||||
|
||||
private final String[] algorithms;
|
||||
|
||||
PemParser(String header, String footer, Function<byte[], PKCS8EncodedKeySpec> keySpecFactory,
|
||||
PemParser(String header, String footer, BiFunction<byte[], String, PKCS8EncodedKeySpec> keySpecFactory,
|
||||
String... algorithms) {
|
||||
this.pattern = Pattern.compile(header + BASE64_TEXT + footer, Pattern.CASE_INSENSITIVE);
|
||||
this.algorithms = algorithms;
|
||||
this.keySpecFactory = keySpecFactory;
|
||||
this.algorithms = algorithms;
|
||||
}
|
||||
|
||||
PrivateKey parse(String text) {
|
||||
PrivateKey parse(String text, String password) {
|
||||
Matcher matcher = this.pattern.matcher(text);
|
||||
return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(1)));
|
||||
return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(BASE64_TEXT_GROUP)), password);
|
||||
}
|
||||
|
||||
private static byte[] decodeBase64(String content) {
|
||||
|
|
@ -195,8 +244,17 @@ final class PrivateKeyParser {
|
|||
return Base64.getDecoder().decode(contentBytes);
|
||||
}
|
||||
|
||||
private PrivateKey parse(byte[] bytes) {
|
||||
PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes);
|
||||
private PrivateKey parse(byte[] bytes, String password) {
|
||||
PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes, password);
|
||||
if (keySpec.getAlgorithm() != null) {
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(keySpec.getAlgorithm());
|
||||
return keyFactory.generatePrivate(keySpec);
|
||||
}
|
||||
catch (InvalidKeySpecException | NoSuchAlgorithmException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
for (String algorithm : this.algorithms) {
|
||||
try {
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
|
||||
|
|
@ -218,9 +276,9 @@ final class PrivateKeyParser {
|
|||
|
||||
private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
void objectIdentifier(int... encodedObjectIdentifier) throws IOException {
|
||||
int code = (encodedObjectIdentifier != null) ? 0x06 : 0x05;
|
||||
codeLengthBytes(code, bytes(encodedObjectIdentifier));
|
||||
void objectIdentifier(EncodedOid encodedOid) throws IOException {
|
||||
int code = (encodedOid != null) ? 0x06 : 0x05;
|
||||
codeLengthBytes(code, (encodedOid != null) ? encodedOid.toByteArray() : null);
|
||||
}
|
||||
|
||||
void integer(int... encodedInteger) throws IOException {
|
||||
|
|
@ -231,10 +289,6 @@ final class PrivateKeyParser {
|
|||
codeLengthBytes(0x04, bytes);
|
||||
}
|
||||
|
||||
void sequence(int... elements) throws IOException {
|
||||
sequence(bytes(elements));
|
||||
}
|
||||
|
||||
void sequence(byte[] bytes) throws IOException {
|
||||
codeLengthBytes(0x30, bytes);
|
||||
}
|
||||
|
|
@ -383,4 +437,102 @@ final class PrivateKeyParser {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Decryptor for PKCS8 encoded private keys.
|
||||
*/
|
||||
static class Pkcs8PrivateKeyDecryptor {
|
||||
|
||||
public static final String PBES2_ALGORITHM = "PBES2";
|
||||
|
||||
static PKCS8EncodedKeySpec decrypt(byte[] bytes, String password) {
|
||||
Assert.notNull(password, "Password is required for an encrypted private key");
|
||||
try {
|
||||
EncryptedPrivateKeyInfo keyInfo = new EncryptedPrivateKeyInfo(bytes);
|
||||
AlgorithmParameters algorithmParameters = keyInfo.getAlgParameters();
|
||||
String encryptionAlgorithm = getEncryptionAlgorithm(algorithmParameters, keyInfo.getAlgName());
|
||||
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptionAlgorithm);
|
||||
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(password.toCharArray()));
|
||||
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameters);
|
||||
return keyInfo.getKeySpec(cipher);
|
||||
}
|
||||
catch (IOException | GeneralSecurityException ex) {
|
||||
throw new IllegalArgumentException("Error decrypting private key", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getEncryptionAlgorithm(AlgorithmParameters algParameters, String algName) {
|
||||
if (algParameters != null && PBES2_ALGORITHM.equals(algName)) {
|
||||
return algParameters.toString();
|
||||
}
|
||||
return algName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* ANS.1 encoded object identifier.
|
||||
*/
|
||||
static final class EncodedOid {
|
||||
|
||||
static final EncodedOid OID_1_2_840_10040_4_1 = EncodedOid.of("2a8648ce380401");
|
||||
static final EncodedOid OID_1_2_840_113549_1_1_1 = EncodedOid.of("2A864886F70D010101");
|
||||
static final EncodedOid OID_1_2_840_113549_1_1_10 = EncodedOid.of("2a864886f70d01010a");
|
||||
static final EncodedOid OID_1_3_101_110 = EncodedOid.of("2b656e");
|
||||
static final EncodedOid OID_1_3_101_111 = EncodedOid.of("2b656f");
|
||||
static final EncodedOid OID_1_3_101_112 = EncodedOid.of("2b6570");
|
||||
static final EncodedOid OID_1_3_101_113 = EncodedOid.of("2b6571");
|
||||
static final EncodedOid OID_1_2_840_10045_2_1 = EncodedOid.of("2a8648ce3d0201");
|
||||
static final EncodedOid OID_1_3_132_0_34 = EncodedOid.of("2b81040022");
|
||||
|
||||
private final byte[] value;
|
||||
|
||||
private EncodedOid(byte[] value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
byte[] toByteArray() {
|
||||
return this.value.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.equals(this.value, ((EncodedOid) obj).value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(this.value);
|
||||
}
|
||||
|
||||
static EncodedOid of(String hexString) {
|
||||
return of(HexFormat.of().parseHex(hexString));
|
||||
}
|
||||
|
||||
static EncodedOid of(DerElement derElement) {
|
||||
return of(derElement.getContents());
|
||||
}
|
||||
|
||||
static EncodedOid of(ByteBuffer byteBuffer) {
|
||||
return of(byteBuffer.array(), byteBuffer.arrayOffset() + byteBuffer.position(), byteBuffer.remaining());
|
||||
}
|
||||
|
||||
static EncodedOid of(byte[] bytes) {
|
||||
return of(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
static EncodedOid of(byte[] bytes, int off, int len) {
|
||||
byte[] value = new byte[len];
|
||||
System.arraycopy(bytes, off, value, 0, len);
|
||||
return new EncodedOid(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.boot.buildpack.platform.docker.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link CertificateParser}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class CertificateParserTests {
|
||||
|
||||
private PemFileWriter fileWriter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.fileWriter = new PemFileWriter();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.fileWriter.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseCertificates() throws IOException {
|
||||
Path caPath = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE);
|
||||
Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE);
|
||||
X509Certificate[] certificates = CertificateParser.parse(caPath, certPath);
|
||||
assertThat(certificates).isNotNull();
|
||||
assertThat(certificates).hasSize(2);
|
||||
assertThat(certificates[0].getType()).isEqualTo("X.509");
|
||||
assertThat(certificates[1].getType()).isEqualTo("X.509");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseCertificateChain() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE);
|
||||
X509Certificate[] certificates = CertificateParser.parse(path);
|
||||
assertThat(certificates).isNotNull();
|
||||
assertThat(certificates).hasSize(2);
|
||||
assertThat(certificates[0].getType()).isEqualTo("X.509");
|
||||
assertThat(certificates[1].getType()).isEqualTo("X.509");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithInvalidPathWillThrowException() throws URISyntaxException {
|
||||
Path path = Paths.get(new URI("file:///bad/path/cert.pem"));
|
||||
assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse(path))
|
||||
.withMessageContaining(path.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.boot.buildpack.platform.docker.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PemCertificateParser}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PemCertificateParserTests {
|
||||
|
||||
private static final String SOURCE = "PemCertificateParser.java";
|
||||
|
||||
@Test
|
||||
void codeShouldMatchSpringBootSslPackage() throws IOException {
|
||||
String buildpackVersion = SslSource.loadBuildpackVersion(SOURCE);
|
||||
String springBootVersion = SslSource.loadSpringBootVersion(SOURCE);
|
||||
assertThat(buildpackVersion).isEqualTo(springBootVersion);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.boot.buildpack.platform.docker.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PemPrivateKeyParser}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PemPrivateKeyParserTests {
|
||||
|
||||
private static final String SOURCE = "PemPrivateKeyParser.java";
|
||||
|
||||
@Test
|
||||
void codeShouldMatchSpringBootSslPackage() throws IOException {
|
||||
String buildpackVersion = SslSource.loadBuildpackVersion(SOURCE);
|
||||
String springBootVersion = SslSource.loadSpringBootVersion(SOURCE);
|
||||
assertThat(buildpackVersion).isEqualTo(springBootVersion);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.boot.buildpack.platform.docker.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.ssl.PrivateKeyParser.DerEncoder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link PrivateKeyParser}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class PrivateKeyParserTests {
|
||||
|
||||
private PemFileWriter fileWriter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.fileWriter = new PemFileWriter();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.fileWriter.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePkcs8RsaKeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PKCS8_PRIVATE_RSA_KEY);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { PemFileWriter.PKCS8_PRIVATE_EC_NIST_P256_KEY, PemFileWriter.PKCS8_PRIVATE_EC_NIST_P384_KEY,
|
||||
PemFileWriter.PKCS8_PRIVATE_EC_PRIME256V1_KEY, PemFileWriter.PKCS8_PRIVATE_EC_SECP256R1_KEY })
|
||||
void parsePkcs8EcKeyFile(String contents) throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", contents);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePkcs8DsaKeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_DSA_KEY);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
assertThat(privateKey.getAlgorithm()).isEqualTo("DSA");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePkcs1RsaKeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_RSA_KEY);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePemEcKeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_EC_KEY);
|
||||
ECPrivateKey privateKey = (ECPrivateKey) PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
|
||||
assertThat(privateKey.getParams().toString()).contains("1.3.132.0.34").doesNotContain("prime256v1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePemEcKeyFilePrime256v1() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_EC_KEY_PRIME_256_V1);
|
||||
ECPrivateKey privateKey = (ECPrivateKey) PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
|
||||
assertThat(privateKey.getParams().toString()).contains("prime256v1").doesNotContain("1.3.132.0.34");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePkcs8Ed25519KeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PKCS8_PRIVATE_EC_ED25519_KEY);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
assertThat(privateKey.getAlgorithm()).isEqualTo("EdDSA");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithNonKeyFileWillThrowException() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("text.pem", "plain text");
|
||||
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path))
|
||||
.withMessageContaining(path.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithInvalidPathWillThrowException() throws URISyntaxException {
|
||||
Path path = Paths.get(new URI("file:///bad/path/key.pem"));
|
||||
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path))
|
||||
.withMessageContaining(path.toString());
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DerEncoderTests {
|
||||
|
||||
@Test
|
||||
void codeLengthBytesShort() throws Exception {
|
||||
DerEncoder encoder = new DerEncoder();
|
||||
encoder.codeLengthBytes(0, new byte[127]);
|
||||
assertThat(encoder.toByteArray()).startsWith(0x0, 0x7F);
|
||||
}
|
||||
|
||||
@Test
|
||||
void codeLengthBytesMedium() throws Exception {
|
||||
DerEncoder encoder = new DerEncoder();
|
||||
encoder.codeLengthBytes(0, new byte[130]);
|
||||
assertThat(encoder.toByteArray()).startsWith(0x0, 0x81, 0x82);
|
||||
}
|
||||
|
||||
@Test
|
||||
void codeLengthBytesLong() throws Exception {
|
||||
DerEncoder encoder = new DerEncoder();
|
||||
encoder.codeLengthBytes(0, new byte[258]);
|
||||
assertThat(encoder.toByteArray()).startsWith(0x0, 0x82, 0x01, 0x02);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.boot.buildpack.platform.docker.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Utility to compare SSL source code.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class SslSource {
|
||||
|
||||
private static final Path BUILDPACK_LOCATION = Path
|
||||
.of("src/main/java/org/springframework/boot/buildpack/platform/docker/ssl");
|
||||
|
||||
private static final Path SPRINGBOOT_LOCATION = Path
|
||||
.of("../../spring-boot/src/main/java/org/springframework/boot/ssl/pem");
|
||||
|
||||
private SslSource() {
|
||||
}
|
||||
|
||||
static String loadBuildpackVersion(String name) throws IOException {
|
||||
return load(BUILDPACK_LOCATION.resolve(name));
|
||||
}
|
||||
|
||||
static String loadSpringBootVersion(String name) throws IOException {
|
||||
return load(SPRINGBOOT_LOCATION.resolve(name));
|
||||
}
|
||||
|
||||
private static String load(Path path) throws IOException {
|
||||
String code = Files.readString(path);
|
||||
int firstBrace = code.indexOf("{");
|
||||
int lastBrace = code.lastIndexOf("}");
|
||||
return code.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue