Accept PEM-encoded certificates in SslConfiguration.
We now accept PEM-encoded certificates when configuring SSL settings.
KeyStoreConfiguration keystore = KeyStoreConfiguration
.of(new ClassPathResource("ca.pem")).withStoreType("PEM");
SslConfiguration configuration = SslConfiguration.forTrustStore(keystore);
Closes gh-514.
This commit is contained in:
@@ -18,9 +18,6 @@ package org.springframework.vault.client;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
@@ -176,8 +173,8 @@ public class ClientHttpConnectorFactory {
|
||||
}
|
||||
|
||||
private static org.eclipse.jetty.client.HttpClient getHttpClient(
|
||||
SslConfiguration sslConfiguration) throws KeyStoreException, IOException,
|
||||
NoSuchAlgorithmException, CertificateException {
|
||||
SslConfiguration sslConfiguration)
|
||||
throws IOException, GeneralSecurityException {
|
||||
|
||||
if (hasSslConfiguration(sslConfiguration)) {
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
@@ -61,10 +61,13 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.Netty4ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.support.ClientOptions;
|
||||
import org.springframework.vault.support.PemObject;
|
||||
import org.springframework.vault.support.SslConfiguration;
|
||||
import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration;
|
||||
|
||||
@@ -140,10 +143,7 @@ public class ClientHttpRequestFactoryFactory {
|
||||
return Netty.usingNetty(options, sslConfiguration);
|
||||
}
|
||||
}
|
||||
catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
catch (IOException e) {
|
||||
catch (GeneralSecurityException | IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class ClientHttpRequestFactoryFactory {
|
||||
return new SimpleClientHttpRequestFactory();
|
||||
}
|
||||
|
||||
static SSLContext getSSLContext(SslConfiguration sslConfiguration,
|
||||
private static SSLContext getSSLContext(SslConfiguration sslConfiguration,
|
||||
TrustManager[] trustManagers) throws GeneralSecurityException, IOException {
|
||||
|
||||
KeyConfiguration keyConfiguration = sslConfiguration.getKeyConfiguration();
|
||||
@@ -170,6 +170,7 @@ public class ClientHttpRequestFactoryFactory {
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static TrustManager[] getTrustManagers(SslConfiguration sslConfiguration)
|
||||
throws GeneralSecurityException, IOException {
|
||||
|
||||
@@ -207,18 +208,25 @@ public class ClientHttpRequestFactoryFactory {
|
||||
}
|
||||
|
||||
static KeyStore getKeyStore(KeyStoreConfiguration keyStoreConfiguration)
|
||||
throws KeyStoreException, IOException, NoSuchAlgorithmException,
|
||||
CertificateException {
|
||||
throws IOException, GeneralSecurityException {
|
||||
|
||||
KeyStore keyStore = KeyStore
|
||||
.getInstance(StringUtils.hasText(keyStoreConfiguration.getStoreType())
|
||||
? keyStoreConfiguration.getStoreType()
|
||||
: KeyStore.getDefaultType());
|
||||
KeyStore keyStore = KeyStore.getInstance(getKeyStoreType(keyStoreConfiguration));
|
||||
|
||||
loadKeyStore(keyStoreConfiguration, keyStore);
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
private static String getKeyStoreType(KeyStoreConfiguration keyStoreConfiguration) {
|
||||
|
||||
if (StringUtils.hasText(keyStoreConfiguration.getStoreType())
|
||||
&& !SslConfiguration.PEM_KEYSTORE_TYPE
|
||||
.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) {
|
||||
return keyStoreConfiguration.getStoreType();
|
||||
}
|
||||
|
||||
return KeyStore.getDefaultType();
|
||||
}
|
||||
|
||||
static TrustManagerFactory createTrustManagerFactory(
|
||||
KeyStoreConfiguration keyStoreConfiguration)
|
||||
throws GeneralSecurityException, IOException {
|
||||
@@ -233,13 +241,31 @@ public class ClientHttpRequestFactoryFactory {
|
||||
}
|
||||
|
||||
private static void loadKeyStore(KeyStoreConfiguration keyStoreConfiguration,
|
||||
KeyStore keyStore)
|
||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
||||
KeyStore keyStore) throws IOException, GeneralSecurityException {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Loading keystore from %s",
|
||||
keyStoreConfiguration.getResource()));
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = keyStoreConfiguration.getResource().getInputStream();
|
||||
keyStore.load(inputStream, keyStoreConfiguration.getStorePassword());
|
||||
|
||||
if (SslConfiguration.PEM_KEYSTORE_TYPE
|
||||
.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) {
|
||||
|
||||
keyStore.load(null);
|
||||
loadFromPem(keyStore, inputStream);
|
||||
}
|
||||
else {
|
||||
keyStore.load(inputStream, keyStoreConfiguration.getStorePassword());
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Keystore loaded with %d entries",
|
||||
keyStore.size()));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (inputStream != null) {
|
||||
@@ -248,6 +274,27 @@ public class ClientHttpRequestFactoryFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadFromPem(KeyStore keyStore, InputStream inputStream)
|
||||
throws IOException, KeyStoreException {
|
||||
|
||||
List<PemObject> pemObjects = PemObject
|
||||
.parse(new String(FileCopyUtils.copyToByteArray(inputStream)));
|
||||
|
||||
for (PemObject pemObject : pemObjects) {
|
||||
if (pemObject.isCertificate()) {
|
||||
X509Certificate cert = pemObject.getCertificate();
|
||||
String alias = cert.getSubjectX500Principal().getName();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(
|
||||
String.format("Adding certificate with alias %s", alias));
|
||||
}
|
||||
|
||||
keyStore.setCertificateEntry(alias, cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean hasSslConfiguration(SslConfiguration sslConfiguration) {
|
||||
return sslConfiguration.getTrustStoreConfiguration().isPresent()
|
||||
|| sslConfiguration.getKeyStoreConfiguration().isPresent();
|
||||
|
||||
@@ -102,8 +102,10 @@ import org.springframework.web.client.RestOperations;
|
||||
* <ul>
|
||||
* <li>Keystore resource: {@code vault.ssl.key-store} (optional)</li>
|
||||
* <li>Keystore password: {@code vault.ssl.key-store-password} (optional)</li>
|
||||
* <li>Keystore type: {@code vault.ssl.key-store-type} (since 2.3, optional)</li>
|
||||
* <li>Truststore resource: {@code vault.ssl.trust-store} (optional)</li>
|
||||
* <li>Truststore password: {@code vault.ssl.trust-store-password} (optional)</li>
|
||||
* <li>Truststore type: {@code vault.ssl.trust-store-password} (since 2.3, optional)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Authentication method: {@code vault.authentication} (defaults to {@literal TOKEN},
|
||||
@@ -228,29 +230,34 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration
|
||||
public SslConfiguration sslConfiguration() {
|
||||
|
||||
KeyStoreConfiguration keyStoreConfiguration = getKeyStoreConfiguration(
|
||||
"vault.ssl.key-store", "vault.ssl.key-store-password");
|
||||
"vault.ssl.key-store", "vault.ssl.key-store-password",
|
||||
"vault.ssl.key-store-type");
|
||||
|
||||
KeyStoreConfiguration trustStoreConfiguration = getKeyStoreConfiguration(
|
||||
"vault.ssl.trust-store", "vault.ssl.trust-store-password");
|
||||
"vault.ssl.trust-store", "vault.ssl.trust-store-password",
|
||||
"vault.ssl.trust-store-type");
|
||||
|
||||
return new SslConfiguration(keyStoreConfiguration, trustStoreConfiguration);
|
||||
}
|
||||
|
||||
private KeyStoreConfiguration getKeyStoreConfiguration(String resourceProperty,
|
||||
String passwordProperty) {
|
||||
String passwordProperty, String keystoreTypeProperty) {
|
||||
|
||||
Resource keyStore = getResource(resourceProperty);
|
||||
String keyStorePassword = getProperty(passwordProperty);
|
||||
String keystoreType = getProperty(keystoreTypeProperty,
|
||||
SslConfiguration.PEM_KEYSTORE_TYPE);
|
||||
|
||||
if (keyStore == null) {
|
||||
return KeyStoreConfiguration.unconfigured();
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(keyStorePassword)) {
|
||||
return KeyStoreConfiguration.of(keyStore, keyStorePassword.toCharArray());
|
||||
return KeyStoreConfiguration.of(keyStore, keyStorePassword.toCharArray(),
|
||||
keystoreType);
|
||||
}
|
||||
|
||||
return KeyStoreConfiguration.of(keyStore);
|
||||
return KeyStoreConfiguration.of(keyStore).withStoreType(keystoreType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -91,7 +91,7 @@ public class CertificateBundle extends Certificate {
|
||||
|
||||
try {
|
||||
byte[] bytes = Base64Utils.decodeFromString(getPrivateKey());
|
||||
return KeystoreUtil.getRSAKeySpec(bytes);
|
||||
return KeystoreUtil.getRSAPrivateKeySpec(bytes);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new VaultException("Cannot create KeySpec from private key", e);
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -113,8 +114,7 @@ class KeystoreUtil {
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
static X509Certificate getCertificate(byte[] source)
|
||||
throws CertificateException {
|
||||
static X509Certificate getCertificate(byte[] source) throws CertificateException {
|
||||
|
||||
List<X509Certificate> certificates = getCertificates(CERTIFICATE_FACTORY, source);
|
||||
|
||||
@@ -155,6 +155,75 @@ class KeystoreUtil {
|
||||
return x509Certificates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert PKCS#1 encoded public key into RSAPublicKeySpec.
|
||||
* <p/>
|
||||
* <p/>
|
||||
* The ASN.1 syntax for the public key with CRT is
|
||||
* <p/>
|
||||
*
|
||||
* <pre>
|
||||
* --
|
||||
* -- Representation of RSA public key with information for the CRT algorithm.
|
||||
* --
|
||||
* RSAPublicKey ::= SEQUENCE {
|
||||
* modulus INTEGER, -- n
|
||||
* publicExponent INTEGER, -- e
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Supports PEM objects with a {@code SEQUENCE} and {@code OBJECT IDENTIFIER} header
|
||||
* where the actual key sequence is represented as {@code BIT_STRING} (as of
|
||||
* {@code openssl -pubout} format).
|
||||
*
|
||||
* @param keyBytes PKCS#1 encoded key
|
||||
* @return KeySpec
|
||||
* @since 2.3
|
||||
*/
|
||||
static RSAPublicKeySpec getRSAPublicKeySpec(byte[] keyBytes)
|
||||
throws IOException, IllegalStateException {
|
||||
DerParser parser = new DerParser(keyBytes);
|
||||
|
||||
Asn1Object sequence = parser.read();
|
||||
if (sequence.getType() != DerParser.SEQUENCE) {
|
||||
throw new IllegalStateException("Invalid DER: not a sequence");
|
||||
}
|
||||
|
||||
// Parse inside the sequence
|
||||
parser = sequence.getParser();
|
||||
Asn1Object object = parser.read();
|
||||
|
||||
if (object.type == DerParser.SEQUENCE) {
|
||||
|
||||
Asn1Object read = object.getParser().read();
|
||||
if (!ObjectIdentifiers.RSA.equalsIgnoreCase(read.getString())) {
|
||||
throw new IllegalStateException(
|
||||
"Unsupported Public Key Algorithm. Expected RSA ("
|
||||
+ ObjectIdentifiers.RSA + "), but was: "
|
||||
+ read.getString());
|
||||
}
|
||||
|
||||
Asn1Object bitString = parser.read();
|
||||
if (bitString.getType() != DerParser.BIT_STRING) {
|
||||
throw new IllegalStateException("Invalid DER: not a bit string");
|
||||
}
|
||||
|
||||
parser = new DerParser(bitString.getValue());
|
||||
sequence = parser.read();
|
||||
|
||||
if (sequence.getType() != DerParser.SEQUENCE) {
|
||||
throw new IllegalStateException("Invalid DER: not a sequence");
|
||||
}
|
||||
|
||||
parser = sequence.getParser();
|
||||
}
|
||||
|
||||
BigInteger modulus = parser.read().getInteger();
|
||||
BigInteger publicExp = parser.read().getInteger();
|
||||
|
||||
return new RSAPublicKeySpec(modulus, publicExp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec.
|
||||
* <p/>
|
||||
@@ -183,7 +252,7 @@ class KeystoreUtil {
|
||||
* @param keyBytes PKCS#1 encoded key
|
||||
* @return KeySpec
|
||||
*/
|
||||
static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException {
|
||||
static RSAPrivateCrtKeySpec getRSAPrivateKeySpec(byte[] keyBytes) throws IOException {
|
||||
DerParser parser = new DerParser(keyBytes);
|
||||
|
||||
Asn1Object sequence = parser.read();
|
||||
@@ -208,6 +277,10 @@ class KeystoreUtil {
|
||||
exp1, exp2, crtCoef);
|
||||
}
|
||||
|
||||
private static class ObjectIdentifiers {
|
||||
static final String RSA = "1.2.840.113549.1.1.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare-minimum ASN.1 DER decoder, just having enough functions to decode PKCS#1
|
||||
* private keys. Especially, it doesn't handle explicitly tagged types with an outer
|
||||
@@ -240,6 +313,7 @@ class KeystoreUtil {
|
||||
static final int BIT_STRING = 0x03;
|
||||
static final int OCTET_STRING = 0x04;
|
||||
static final int NULL = 0x05;
|
||||
static final int OID = 0x06;
|
||||
static final int REAL = 0x09;
|
||||
static final int ENUMERATED = 0x0a;
|
||||
|
||||
@@ -297,6 +371,12 @@ class KeystoreUtil {
|
||||
|
||||
int length = getLength();
|
||||
|
||||
if (tag == BIT_STRING) {
|
||||
// Not sure what to do with this one.
|
||||
int padBits = in.read();
|
||||
length--;
|
||||
}
|
||||
|
||||
byte[] value = new byte[length];
|
||||
int n = in.read(value);
|
||||
if (n < length) {
|
||||
@@ -358,6 +438,8 @@ class KeystoreUtil {
|
||||
*/
|
||||
static class Asn1Object {
|
||||
|
||||
private static final long LONG_LIMIT = (Long.MAX_VALUE >> 7) - 0x7f;
|
||||
|
||||
private final int type;
|
||||
private final int length;
|
||||
private final byte[] value;
|
||||
@@ -435,7 +517,8 @@ class KeystoreUtil {
|
||||
BigInteger getInteger() {
|
||||
|
||||
if (type != DerParser.INTEGER) {
|
||||
throw new IllegalStateException("Invalid DER: object is not integer");
|
||||
throw new IllegalStateException(
|
||||
String.format("Invalid DER: object (%d) is not integer.", type));
|
||||
}
|
||||
|
||||
return new BigInteger(value);
|
||||
@@ -474,11 +557,76 @@ class KeystoreUtil {
|
||||
case DerParser.UNIVERSAL_STRING:
|
||||
throw new IOException("Invalid DER: can't handle UCS-4 string");
|
||||
|
||||
case DerParser.OID:
|
||||
return getObjectIdentifier(value);
|
||||
default:
|
||||
throw new IOException("Invalid DER: object is not a string");
|
||||
throw new IOException(
|
||||
String.format("Invalid DER: object (%d) is not a string", type));
|
||||
}
|
||||
|
||||
return new String(value, encoding);
|
||||
}
|
||||
|
||||
private static String getObjectIdentifier(byte bytes[]) {
|
||||
StringBuffer objId = new StringBuffer();
|
||||
long value = 0;
|
||||
BigInteger bigValue = null;
|
||||
boolean first = true;
|
||||
|
||||
for (int i = 0; i != bytes.length; i++) {
|
||||
int b = bytes[i] & 0xff;
|
||||
|
||||
if (value <= LONG_LIMIT) {
|
||||
value += (b & 0x7f);
|
||||
if ((b & 0x80) == 0) // end of number reached
|
||||
{
|
||||
if (first) {
|
||||
if (value < 40) {
|
||||
objId.append('0');
|
||||
}
|
||||
else if (value < 80) {
|
||||
objId.append('1');
|
||||
value -= 40;
|
||||
}
|
||||
else {
|
||||
objId.append('2');
|
||||
value -= 80;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
objId.append('.');
|
||||
objId.append(value);
|
||||
value = 0;
|
||||
}
|
||||
else {
|
||||
value <<= 7;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (bigValue == null) {
|
||||
bigValue = BigInteger.valueOf(value);
|
||||
}
|
||||
bigValue = bigValue.or(BigInteger.valueOf(b & 0x7f));
|
||||
if ((b & 0x80) == 0) {
|
||||
if (first) {
|
||||
objId.append('2');
|
||||
bigValue = bigValue.subtract(BigInteger.valueOf(80));
|
||||
first = false;
|
||||
}
|
||||
|
||||
objId.append('.');
|
||||
objId.append(bigValue);
|
||||
bigValue = null;
|
||||
value = 0;
|
||||
}
|
||||
else {
|
||||
bigValue = bigValue.shiftLeft(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objId.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
package org.springframework.vault.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -33,35 +38,203 @@ import org.springframework.util.Base64Utils;
|
||||
*/
|
||||
public class PemObject {
|
||||
|
||||
private static final Pattern KEY_PATTERN = Pattern
|
||||
private static final Pattern PRIVATE_KEY_PATTERN = Pattern
|
||||
.compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
|
||||
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
|
||||
"-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final byte[] content;
|
||||
private static final Pattern PUBLIC_KEY_PATTERN = Pattern
|
||||
.compile("-+BEGIN\\s+.*PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
|
||||
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
|
||||
"-+END\\s+.*PUBLIC\\s+KEY[^-]*-+", // Footer
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private PemObject(String content) {
|
||||
private static final Pattern CERTIFICATE_PATTERN = Pattern
|
||||
.compile("-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header
|
||||
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
|
||||
"-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern[] PATTERNS = new Pattern[] { PRIVATE_KEY_PATTERN,
|
||||
PUBLIC_KEY_PATTERN, CERTIFICATE_PATTERN };
|
||||
|
||||
private final byte[] content;
|
||||
private final Pattern matchingPattern;
|
||||
|
||||
private PemObject(String content, Pattern matchingPattern) {
|
||||
this.matchingPattern = matchingPattern;
|
||||
|
||||
String sanitized = content.replaceAll("\r", "").replaceAll("\n", "");
|
||||
this.content = Base64Utils.decodeFromString(sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a{@link PemObject} from PEM {@code content} that is enclosed with
|
||||
* {@code -BEGIN PRIVATE KEY-} and {@code -END PRIVATE KEY-}.
|
||||
* Create a {@link PemObject} from PEM {@code content} that is enclosed with
|
||||
* {@code -BEGIN PRIVATE KEY-} and {@code -END PRIVATE KEY-}. This method returns
|
||||
* either the first PEM object ot throws {@link IllegalArgumentException} of no object
|
||||
* could be found.
|
||||
*
|
||||
* @param content the PEM content.
|
||||
* @return the {@link PemObject} from PEM {@code content}.
|
||||
* @throws IllegalArgumentException if no PEM object could be found.
|
||||
*/
|
||||
public static PemObject fromKey(String content) {
|
||||
|
||||
Matcher m = KEY_PATTERN.matcher(content);
|
||||
Matcher m = PRIVATE_KEY_PATTERN.matcher(content);
|
||||
if (!m.find()) {
|
||||
throw new IllegalArgumentException("Could not find a PKCS #8 private key");
|
||||
}
|
||||
|
||||
return new PemObject(m.group(1));
|
||||
return new PemObject(m.group(1), PRIVATE_KEY_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link PemObject} from PEM {@code content} that is enclosed with
|
||||
* {@code -BEGIN PRIVATE KEY-} or {@code -BEGIN PUBLIC KEY-}. This method returns
|
||||
* either the first PEM object ot throws {@link IllegalArgumentException} of no object
|
||||
* could be found.
|
||||
*
|
||||
* @param content the PEM content.
|
||||
* @return the {@link PemObject} from PEM {@code content}.
|
||||
* @throws IllegalArgumentException if no PEM object could be found.
|
||||
* @since 2.3
|
||||
*/
|
||||
public static PemObject parseFirst(String content) {
|
||||
|
||||
List<PemObject> objects = parse(content);
|
||||
|
||||
if (objects.isEmpty()) {
|
||||
throw new IllegalArgumentException("Cannot find PEM object");
|
||||
}
|
||||
|
||||
return objects.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one or more {@link PemObject}s from PEM {@code content}. Accepts
|
||||
* concatenated PEM objects.
|
||||
*
|
||||
* @param content the PEM content.
|
||||
* @return the list of {@link PemObject} from PEM {@code content}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public static List<PemObject> parse(String content) {
|
||||
|
||||
List<PemObject> objects = new ArrayList<>();
|
||||
int index = 0;
|
||||
|
||||
boolean found;
|
||||
|
||||
do {
|
||||
found = false;
|
||||
|
||||
Matcher discoveredMatcher = null;
|
||||
int indexDiscoveredIndex = 0;
|
||||
|
||||
for (Pattern pattern : PATTERNS) {
|
||||
|
||||
Matcher m = pattern.matcher(content);
|
||||
|
||||
if (!m.find(index)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// discover which pattern is the next applicable one.
|
||||
if (indexDiscoveredIndex == 0 || indexDiscoveredIndex > m.start()) {
|
||||
discoveredMatcher = m;
|
||||
indexDiscoveredIndex = m.start();
|
||||
}
|
||||
}
|
||||
|
||||
// extract using the matching pattern.
|
||||
if (discoveredMatcher != null) {
|
||||
found = true;
|
||||
index = discoveredMatcher.end();
|
||||
objects.add(new PemObject(discoveredMatcher.group(1),
|
||||
discoveredMatcher.pattern()));
|
||||
}
|
||||
|
||||
}
|
||||
while (found);
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if the object was identified to contain a private key.
|
||||
* @since 2.3
|
||||
*/
|
||||
public boolean isCertificate() {
|
||||
return this.matchingPattern.equals(CERTIFICATE_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if the object was identified to contain a private key.
|
||||
* @since 2.3
|
||||
*/
|
||||
public boolean isPrivateKey() {
|
||||
return this.matchingPattern.equals(PRIVATE_KEY_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if the object was identified to contain a public key.
|
||||
* @since 2.3
|
||||
*/
|
||||
public boolean isPublicKey() {
|
||||
return this.matchingPattern.equals(PUBLIC_KEY_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link RSAPrivateCrtKeySpec}.
|
||||
*
|
||||
* @return the {@link RSAPrivateCrtKeySpec}.
|
||||
* @deprecated since 2.3. Use {@link #getRSAPrivateKeySpec()} instead that uses an
|
||||
* improved name to indicate what the method is supposed to return.
|
||||
*/
|
||||
@Deprecated
|
||||
public RSAPrivateCrtKeySpec getRSAKeySpec() {
|
||||
return getRSAPrivateKeySpec();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link X509Certificate}.
|
||||
*
|
||||
* @return the {@link X509Certificate}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public X509Certificate getCertificate() {
|
||||
|
||||
if (!isCertificate()) {
|
||||
throw new IllegalStateException("PEM object is not a certificate");
|
||||
}
|
||||
|
||||
try {
|
||||
return KeystoreUtil.getCertificate(this.content);
|
||||
}
|
||||
catch (CertificateException e) {
|
||||
throw new IllegalStateException("Cannot obtain Certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link RSAPrivateCrtKeySpec}.
|
||||
*
|
||||
* @return the {@link RSAPrivateCrtKeySpec}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public RSAPrivateCrtKeySpec getRSAPrivateKeySpec() {
|
||||
|
||||
if (!isPrivateKey()) {
|
||||
throw new IllegalStateException("PEM object is not a private key");
|
||||
}
|
||||
|
||||
try {
|
||||
return KeystoreUtil.getRSAPrivateKeySpec(this.content);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot obtain PrivateKey", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,13 +242,17 @@ public class PemObject {
|
||||
*
|
||||
* @return the {@link RSAPrivateCrtKeySpec}.
|
||||
*/
|
||||
public RSAPrivateCrtKeySpec getRSAKeySpec() {
|
||||
public RSAPublicKeySpec getRSAPublicKeySpec() {
|
||||
|
||||
if (!isPublicKey()) {
|
||||
throw new IllegalStateException("PEM object is not a public key");
|
||||
}
|
||||
|
||||
try {
|
||||
return KeystoreUtil.getRSAKeySpec(this.content);
|
||||
return KeystoreUtil.getRSAPublicKeySpec(this.content);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalArgumentException("Cannot obtain PrivateKey", e);
|
||||
throw new IllegalStateException("Cannot obtain PrivateKey", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,19 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class SslConfiguration {
|
||||
|
||||
private static final String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType();
|
||||
/**
|
||||
* Constant for PEM-based keystore type.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public static final String PEM_KEYSTORE_TYPE = "PEM";
|
||||
|
||||
/**
|
||||
* Constant for system-default keystore type.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public static final String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType();
|
||||
|
||||
private final KeyStoreConfiguration keyStoreConfiguration;
|
||||
|
||||
@@ -515,8 +527,23 @@ public class SslConfiguration {
|
||||
*/
|
||||
public static KeyStoreConfiguration of(Resource resource,
|
||||
@Nullable char[] storePassword) {
|
||||
return new KeyStoreConfiguration(resource, storePassword,
|
||||
DEFAULT_KEYSTORE_TYPE);
|
||||
return of(resource, storePassword, DEFAULT_KEYSTORE_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link KeyStoreConfiguration} given {@link Resource},
|
||||
* {@code storePassword}, and {@code keyStoreType}.
|
||||
*
|
||||
* @param resource resource referencing the key store, must not be {@literal null}
|
||||
* .
|
||||
* @param storePassword key store password, may be {@literal null}.
|
||||
* @param keyStoreType key store type, must not be {@literal null}.
|
||||
* @return the {@link KeyStoreConfiguration} for {@code resource}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public static KeyStoreConfiguration of(Resource resource,
|
||||
@Nullable char[] storePassword, String keyStoreType) {
|
||||
return new KeyStoreConfiguration(resource, storePassword, keyStoreType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -560,6 +587,21 @@ public class SslConfiguration {
|
||||
public String getStoreType() {
|
||||
return storeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link KeyStoreConfiguration} by applying all values from this
|
||||
* object and the given {@code storeType}.
|
||||
*
|
||||
* @param storeType must not be {@literal null}.
|
||||
* @return a new {@link KeyStoreConfiguration}.
|
||||
* @since 2.3
|
||||
*/
|
||||
public KeyStoreConfiguration withStoreType(String storeType) {
|
||||
|
||||
Assert.notNull(storeType, "Key store type must not be null");
|
||||
return new KeyStoreConfiguration(this.resource, this.storePassword,
|
||||
storeType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
*/
|
||||
package org.springframework.vault.client;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
@@ -29,6 +32,7 @@ import org.springframework.vault.client.ClientHttpRequestFactoryFactory.HttpComp
|
||||
import org.springframework.vault.client.ClientHttpRequestFactoryFactory.Netty;
|
||||
import org.springframework.vault.client.ClientHttpRequestFactoryFactory.OkHttp3;
|
||||
import org.springframework.vault.support.ClientOptions;
|
||||
import org.springframework.vault.support.SslConfiguration;
|
||||
import org.springframework.vault.util.Settings;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@@ -59,6 +63,27 @@ class ClientHttpRequestFactoryFactoryIntegrationTests {
|
||||
((DisposableBean) factory).destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpComponentsClientUsingPemShouldWork() throws Exception {
|
||||
|
||||
File caCertificate = new File(Settings.findWorkDir(), "ca/certs/ca.cert.pem");
|
||||
SslConfiguration sslConfiguration = SslConfiguration
|
||||
.forTrustStore(SslConfiguration.KeyStoreConfiguration
|
||||
.of(new FileSystemResource(caCertificate))
|
||||
.withStoreType(SslConfiguration.PEM_KEYSTORE_TYPE));
|
||||
|
||||
ClientHttpRequestFactory factory = HttpComponents
|
||||
.usingHttpComponents(new ClientOptions(), sslConfiguration);
|
||||
RestTemplate template = new RestTemplate(factory);
|
||||
|
||||
String response = request(template);
|
||||
|
||||
assertThat(factory).isInstanceOf(HttpComponentsClientHttpRequestFactory.class);
|
||||
assertThat(response).isNotNull().contains("initialized");
|
||||
|
||||
((DisposableBean) factory).destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nettyClientShouldWork() throws Exception {
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2020 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.vault.support;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.vault.util.Settings;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PemObject}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class PemObjectUnitTests {
|
||||
|
||||
final File workdir = Settings.findWorkDir();
|
||||
final File privateDir = new File(workdir, "ca/private");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
assertThat(privateDir).exists().isDirectoryContaining(
|
||||
file -> file.getName().equalsIgnoreCase("localhost.public.key.pem"));
|
||||
assertThat(privateDir).exists().isDirectoryContaining(
|
||||
file -> file.getName().equalsIgnoreCase("localhost.decrypted.key.pem"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecodePublicKey() throws IOException {
|
||||
|
||||
String content = new String(FileCopyUtils
|
||||
.copyToByteArray(new File(privateDir, "localhost.public.key.pem")));
|
||||
|
||||
PemObject pemObject = PemObject.parseFirst(content);
|
||||
assertThat(pemObject.isPrivateKey()).isFalse();
|
||||
assertThat(pemObject.isPublicKey()).isTrue();
|
||||
assertThat(pemObject.getRSAPublicKeySpec()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecodePrivateKey() throws IOException {
|
||||
|
||||
String content = new String(FileCopyUtils
|
||||
.copyToByteArray(new File(privateDir, "localhost.decrypted.key.pem")));
|
||||
|
||||
PemObject pemObject = PemObject.parseFirst(content);
|
||||
assertThat(pemObject.isPrivateKey()).isTrue();
|
||||
assertThat(pemObject.isPublicKey()).isFalse();
|
||||
assertThat(pemObject.getRSAPrivateKeySpec()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecodeConcatenatedPEMContent() throws IOException {
|
||||
|
||||
String content1 = new String(FileCopyUtils
|
||||
.copyToByteArray(new File(privateDir, "localhost.public.key.pem")));
|
||||
String content2 = new String(FileCopyUtils
|
||||
.copyToByteArray(new File(privateDir, "localhost.decrypted.key.pem")));
|
||||
|
||||
List<PemObject> pemObjects = PemObject.parse(content1 + content2);
|
||||
|
||||
assertThat(pemObjects).hasSize(2);
|
||||
assertThat(pemObjects.get(0).isPublicKey()).isTrue();
|
||||
assertThat(pemObjects.get(1).isPrivateKey()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void keysShouldMatch() throws IOException {
|
||||
|
||||
PemObject publicKey = PemObject.parseFirst(new String(FileCopyUtils
|
||||
.copyToByteArray(new File(privateDir, "localhost.public.key.pem"))));
|
||||
|
||||
PemObject privateKey = PemObject.parseFirst(new String(FileCopyUtils
|
||||
.copyToByteArray(new File(privateDir, "localhost.decrypted.key.pem"))));
|
||||
|
||||
RSAPublicKeySpec publicSpec = publicKey.getRSAPublicKeySpec();
|
||||
RSAPrivateCrtKeySpec privateKeySpec = privateKey.getRSAPrivateKeySpec();
|
||||
|
||||
assertThat(publicSpec.getModulus()).isEqualTo(privateKeySpec.getModulus());
|
||||
assertThat(publicSpec.getPublicExponent())
|
||||
.isEqualTo(privateKeySpec.getPublicExponent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecodeX509Certificate() throws IOException {
|
||||
|
||||
String content = new String(
|
||||
FileCopyUtils.copyToByteArray(new File(workdir, "ca/certs/ca.cert.pem")));
|
||||
|
||||
PemObject pemObject = PemObject.parseFirst(content);
|
||||
|
||||
assertThat(pemObject.isCertificate()).isTrue();
|
||||
assertThat(pemObject.getCertificate().getSubjectDN().getName())
|
||||
.contains("O=spring-cloud-vault-config");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,4 +65,15 @@ class SslConfigurationUnitTests {
|
||||
assertThat(tsConfig.getTrustStoreConfiguration()).isSameAs(keystore);
|
||||
assertThat(tsConfig.getKeyStoreConfiguration().isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreatePemConfiguration() {
|
||||
|
||||
KeyStoreConfiguration keystore = KeyStoreConfiguration
|
||||
.of(new ClassPathResource("certificate.json")).withStoreType("PEM");
|
||||
SslConfiguration configuration = SslConfiguration.forTrustStore(keystore);
|
||||
|
||||
assertThat(configuration.getTrustStoreConfiguration().getStoreType())
|
||||
.isEqualTo("PEM");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[[new-features]]
|
||||
== New & Noteworthy
|
||||
|
||||
[[new-features.2-3-0]]
|
||||
=== What's new in Spring Vault 2.3
|
||||
|
||||
* Support for PEM-encoded certificates for keystore and truststore usage.
|
||||
|
||||
[[new-features.2-2-0]]
|
||||
=== What's new in Spring Vault 2.2
|
||||
|
||||
@@ -9,7 +14,8 @@
|
||||
* Add support for Jetty as reactive HttpClient.
|
||||
* `LifecycleAwareSessionManager` and `ReactiveLifecycleAwareSessionManager` emit now ``AuthenticationEvent``s.
|
||||
* <<vault.authentication.pcf>>.
|
||||
* Deprecation of `AppIdAuthentication`. Use `AppRoleAuthentication` instead as recommended by HashiCorp Vault.
|
||||
* Deprecation of `AppIdAuthentication`.
|
||||
Use `AppRoleAuthentication` instead as recommended by HashiCorp Vault.
|
||||
* `CubbyholeAuthentication` and wrapped `AppRoleAuthentication` now use `sys/wrapping/unwrap` endpoints by default.
|
||||
* Kotlin Coroutines support for `ReactiveVaultOperations`.
|
||||
|
||||
|
||||
@@ -127,6 +127,18 @@ SslConfiguration.forKeyStore(new FileSystemResource("keystore.jks"), <4>
|
||||
<4> Configuring only key store settings with providing a key-configuration.
|
||||
====
|
||||
|
||||
Please note that providing `SslConfiguration` can be only
|
||||
applied when either Apache Http Components or the OkHttp client
|
||||
is on your class-path.
|
||||
Please note that providing `SslConfiguration` can be only applied when either Apache Http Components or the OkHttp client is on your class-path.
|
||||
|
||||
The SSL configuration supports also PEM-encoded certificates as alternative to a Java Key Store.
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
KeyStoreConfiguration keystore = KeyStoreConfiguration
|
||||
.of(new ClassPathResource("ca.pem")).withStoreType("PEM");
|
||||
SslConfiguration configuration = SslConfiguration.forTrustStore(keystore);
|
||||
----
|
||||
====
|
||||
|
||||
PEM files may contain one or more certificates (blocks of `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`).
|
||||
Certificates added to the underlying `KeyStore` use the full subject name as alias.
|
||||
|
||||
@@ -172,8 +172,10 @@ vault.token=00000000-0000-0000-0000-000000000000
|
||||
* SSL Configuration
|
||||
** Keystore resource: `vault.ssl.key-store` (optional)
|
||||
** Keystore password: `vault.ssl.key-store-password` (optional)
|
||||
** Keystore type: `vault.ssl.key-store-type` (optional, typically `jks`, supports also `pem`)
|
||||
** Truststore resource: `vault.ssl.trust-store` (optional)
|
||||
** Truststore password: `vault.ssl.trust-store-password` (optional)
|
||||
** Truststore type: `vault.ssl.trust-store-type` (optional, typically `jks`, supports also `pem`)
|
||||
* Authentication method: `vault.authentication` (defaults to `TOKEN`, supported authentication methods are: `TOKEN`, `APPID`, `APPROLE`, `AWS_EC2`, `AZURE`, `CERT`, `CUBBYHOLE`, `KUBERNETES`)
|
||||
|
||||
**Authentication-specific property keys**
|
||||
|
||||
@@ -62,6 +62,10 @@ openssl rsa -in ${CA_DIR}/private/localhost.key.pem \
|
||||
-out ${CA_DIR}/private/localhost.decrypted.key.pem \
|
||||
-passin pass:changeit
|
||||
|
||||
openssl rsa -in ${CA_DIR}/private/localhost.key.pem \
|
||||
-pubout -out ${CA_DIR}/private/localhost.public.key.pem \
|
||||
-passin pass:changeit
|
||||
|
||||
chmod 400 ${CA_DIR}/private/localhost.key.pem
|
||||
chmod 400 ${CA_DIR}/private/localhost.decrypted.key.pem
|
||||
|
||||
@@ -82,7 +86,6 @@ openssl ca -config ${DIR}/openssl.cnf \
|
||||
-in ${CA_DIR}/csr/localhost.csr.pem \
|
||||
-out ${CA_DIR}/certs/localhost.cert.pem
|
||||
|
||||
|
||||
echo "[INFO] Generating client auth private key"
|
||||
openssl genrsa -aes256 \
|
||||
-passout pass:changeit \
|
||||
|
||||
Reference in New Issue
Block a user