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:
Mark Paluch
2020-03-25 16:32:07 +01:00
parent 8b6f12f9db
commit 4fe87aec43
14 changed files with 645 additions and 49 deletions

View File

@@ -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)) {

View File

@@ -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();

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
/**

View File

@@ -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 {

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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**

View File

@@ -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 \