Add support to generate certificates using the PKI backend.

Fixes gh-11.
This commit is contained in:
Mark Paluch
2016-09-26 21:11:58 +02:00
parent a929a08d16
commit 7af121f237
18 changed files with 1612 additions and 7 deletions

View File

@@ -72,6 +72,19 @@ public interface VaultOperations {
*/
VaultTransitOperations opsForTransit(String path);
/**
* @return the operations interface to interact with the Vault PKI backend.
*/
VaultPkiOperations opsForPki();
/**
* Returns {@link VaultPkiOperations} if the PKI backend is mounted on a different path than {@code pki}.
*
* @param path the mount path
* @return the operations interface to interact with the Vault PKI backend.
*/
VaultPkiOperations opsForPki(String path);
/**
* Read from a secret backend. Reading data using this method is suitable for secret backends that do not require a
* request body.

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2016 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
*
* http://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.core;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.support.CertificateBundle;
import org.springframework.vault.support.VaultCertificateRequest;
import org.springframework.vault.support.VaultCertificateResponse;
/**
* Interface that specifies PKI backend-related operations.
* <p>
* The PKI secret backend for Vault generates X.509 certificates dynamically based on configured roles. This means
* services can get certificates needed for both client and server authentication without going through the usual manual
* process of generating a private key and CSR, submitting to a CA, and waiting for a verification and signing process
* to complete. Vault's built-in authentication and authorization mechanisms provide the verification functionality.
*
* @author Mark Paluch
* @see <a href=
* "https://www.vaultproject.io/docs/secrets/pki/index.html">https://www.vaultproject.io/docs/secrets/pki/index.html</a>
*/
public interface VaultPkiOperations {
/**
* Requests a certificate bundle (private key and certificate) from Vault's PKI backend given a {@code roleName} and
* {@link VaultCertificateRequest}. The issuing CA certificate is returned as well, so that only the root CA need be
* in a client's trust store. Certificates use DER format and are base64 encoded.
*
* @param roleName must not be empty or {@literal null}.
* @param certificateRequest must not be {@literal null}.
* @return the {@link VaultCertificateResponse} containing a {@link CertificateBundle}.
* @see <a href="https://www.vaultproject.io/docs/secrets/pki/index.html#pki-issue">POST /pki/issue/[role name]</a>
*/
VaultCertificateResponse issueCertificate(String roleName, VaultCertificateRequest certificateRequest)
throws VaultException;
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2016 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
*
* http://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.core;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.client.VaultResponseEntity;
import org.springframework.vault.support.VaultCertificateRequest;
import org.springframework.vault.support.VaultCertificateResponse;
/**
* Default implementation of {@link VaultPkiOperations}.
*
* @author Mark Paluch
*/
public class VaultPkiTemplate implements VaultPkiOperations {
private final VaultOperations vaultOperations;
private final String path;
/**
* Create a new {@link VaultPkiTemplate} given {@link VaultPkiOperations} and the mount {@code path}.
*
* @param vaultOperations must not be {@literal null}.
* @param path must not be empty or {@literal null}.
*/
public VaultPkiTemplate(VaultOperations vaultOperations, String path) {
Assert.notNull(vaultOperations, "VaultOperations must not be null");
Assert.hasText(path, "Path must not be empty");
this.vaultOperations = vaultOperations;
this.path = path;
}
@Override
public VaultCertificateResponse issueCertificate(final String roleName, VaultCertificateRequest certificateRequest)
throws VaultException {
Assert.hasText(roleName, "Role name must not be empty");
Assert.notNull(certificateRequest, "Certificate request must not be null");
final Map<String, Object> request = new HashMap<String, Object>();
request.put("common_name", certificateRequest.getCommonName());
if (!certificateRequest.getAltNames().isEmpty()) {
request.put("alt_names", StringUtils.collectionToDelimitedString(certificateRequest.getAltNames(), ","));
}
if (!certificateRequest.getIpSubjectAltNames().isEmpty()) {
request.put("ip_sans", StringUtils.collectionToDelimitedString(certificateRequest.getIpSubjectAltNames(), ","));
}
if (certificateRequest.getTtl() != null) {
request.put("ttl", certificateRequest.getTtl());
}
request.put("format", "der");
if (certificateRequest.isExcludeCommonNameFromSubjectAltNames()) {
request.put("exclude_cn_from_sans", true);
}
VaultResponseEntity<VaultCertificateResponse> entity = vaultOperations
.doWithVault(new VaultOperations.SessionCallback<VaultResponseEntity<VaultCertificateResponse>>() {
@Override
public VaultResponseEntity<VaultCertificateResponse> doWithVault(VaultOperations.VaultSession session) {
return session.postForEntity(String.format("%s/issue/%s", path, roleName), request,
VaultCertificateResponse.class);
}
});
if (entity.isSuccessful() && entity.hasBody()) {
return entity.getBody();
}
throw new VaultException(buildExceptionMessage(entity));
}
private static String buildExceptionMessage(VaultResponseEntity<?> response) {
if (StringUtils.hasText(response.getMessage())) {
return String.format("Status %s URI %s: %s", response.getStatusCode(), response.getUri(), response.getMessage());
}
return String.format("Status %s URI %s", response.getStatusCode(), response.getUri());
}
}

View File

@@ -25,8 +25,7 @@ import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultUnsealStatus;
/**
* Interface that specified a basic set of Vault operations, implemented by {@link VaultTemplate}. Request errors are
* wrapped within {@link VaultException}.
* Interface that specifies a basic set of administrative Vault operations.
*
* @author Mark Paluch
*/

View File

@@ -139,6 +139,16 @@ public class VaultTemplate implements InitializingBean, VaultOperations {
return new VaultTransitTemplate(this, path);
}
@Override
public VaultPkiOperations opsForPki() {
return opsForPki("pki");
}
@Override
public VaultPkiOperations opsForPki(String path) {
return new VaultPkiTemplate(this, path);
}
@Override
public <T> T doWithVault(ClientCallback<T> clientCallback) {

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2016 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
*
* http://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;
class Base64 {
private Base64() {}
public static byte[] decode(String in) {
// Ignore trailing '=' padding and whitespace from the input.
int limit = in.length();
for (; limit > 0; limit--) {
char c = in.charAt(limit - 1);
if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') {
break;
}
}
// If the input includes whitespace, this output array will be longer than necessary.
byte[] out = new byte[(int) (limit * 6L / 8L)];
int outCount = 0;
int inCount = 0;
int word = 0;
for (int pos = 0; pos < limit; pos++) {
char c = in.charAt(pos);
int bits;
if (c >= 'A' && c <= 'Z') {
// char ASCII value
// A 65 0
// Z 90 25 (ASCII - 65)
bits = c - 65;
} else if (c >= 'a' && c <= 'z') {
// char ASCII value
// a 97 26
// z 122 51 (ASCII - 71)
bits = c - 71;
} else if (c >= '0' && c <= '9') {
// char ASCII value
// 0 48 52
// 9 57 61 (ASCII + 4)
bits = c + 4;
} else if (c == '+' || c == '-') {
bits = 62;
} else if (c == '/' || c == '_') {
bits = 63;
} else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
continue;
} else {
return null;
}
// Append this char's 6 bits to the word.
word = (word << 6) | (byte) bits;
// For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes.
inCount++;
if (inCount % 4 == 0) {
out[outCount++] = (byte) (word >> 16);
out[outCount++] = (byte) (word >> 8);
out[outCount++] = (byte) word;
}
}
int lastWordChars = inCount % 4;
if (lastWordChars == 1) {
// We read 1 char followed by "===". But 6 bits is a truncated byte! Fail.
return null;
} else if (lastWordChars == 2) {
// We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits.
word = word << 12;
out[outCount++] = (byte) (word >> 16);
} else if (lastWordChars == 3) {
// We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits.
word = word << 6;
out[outCount++] = (byte) (word >> 16);
out[outCount++] = (byte) (word >> 8);
}
// If we sized our out array perfectly, we're done.
if (outCount == out.length)
return out;
// Copy the decoded bytes to a new, right-sized array.
byte[] prefix = new byte[outCount];
System.arraycopy(out, 0, prefix, 0, outCount);
return prefix;
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2016 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
*
* http://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.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.KeySpec;
import org.springframework.util.Assert;
import org.springframework.vault.client.VaultException;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Value object representing a certificate bundle consisting of a private key, the certificate and the issuer
* certificate. Certificate and keys can be either DER or PEM encoded. DER-encoded certificates can be converted to a
* {@link KeySpec} and {@link X509Certificate}.
*
* @author Mark Paluch
* @see #getPrivateKeySpec()
* @see #getX509Certificate()
* @see #getIssuingCaCertificate()
*/
public class CertificateBundle {
private final String serialNumber;
private final String certificate;
private final String issuingCaCertificate;
private final String privateKey;
private CertificateBundle(@JsonProperty("serial_number") String serialNumber,
@JsonProperty("certificate") String certificate, @JsonProperty("issuing_ca") String issuingCaCertificate,
@JsonProperty("private_key") String privateKey) {
this.serialNumber = serialNumber;
this.certificate = certificate;
this.issuingCaCertificate = issuingCaCertificate;
this.privateKey = privateKey;
}
/**
* Create a {@link CertificateBundle} given a private key with certificates and the serial number.
*
* @param serialNumber must not be empty or {@literal null}.
* @param certificate must not be empty or {@literal null}.
* @param issuingCaCertificate must not be empty or {@literal null}.
* @param privateKey must not be empty or {@literal null}.
* @return the {@link CertificateBundle}
*/
public static CertificateBundle of(String serialNumber, String certificate, String issuingCaCertificate,
String privateKey) {
Assert.hasText(serialNumber, "Serial number must not be empty");
Assert.hasText(certificate, "Certificate must not be empty");
Assert.hasText(issuingCaCertificate, "Issuing CA certificate must not be empty");
Assert.hasText(privateKey, "Private key must not be empty");
return new CertificateBundle(serialNumber, certificate, issuingCaCertificate, privateKey);
}
/**
* @return the serial number.
*/
public String getSerialNumber() {
return this.serialNumber;
}
/**
* @return encoded certificate (PEM or DER-encoded).
*/
public String getCertificate() {
return this.certificate;
}
/**
* @return encoded certificate of the issuing CA (PEM or DER-encoded).
*/
public String getIssuingCaCertificate() {
return this.issuingCaCertificate;
}
/**
* @return the private key (decrypted form, PEM or DER-encoded)
*/
public String getPrivateKey() {
return this.privateKey;
}
/**
* Retrieve the private key as {@link KeySpec}. Only supported if private key is DER-encoded.
*
* @return the private {@link KeySpec}. {@link java.security.KeyFactory} can generate a
* {@link java.security.PrivateKey} from this {@link KeySpec}.
*/
public KeySpec getPrivateKeySpec() {
try {
byte[] bytes = Base64.decode(getPrivateKey());
return KeystoreUtil.getRSAKeySpec(bytes);
} catch (IOException e) {
throw new VaultException("Cannot create KeySpec from private key", e);
}
}
/**
* Retrieve the certificate as {@link X509Certificate}. Only supported if certificate is DER-encoded.
*
* @return the {@link X509Certificate}.
*/
public X509Certificate getX509Certificate() {
try {
byte[] bytes = Base64.decode(getCertificate());
return KeystoreUtil.getCertificate(bytes);
} catch (IOException e) {
throw new VaultException("Cannot create Certificate from certificate", e);
} catch (CertificateException e) {
throw new VaultException("Cannot create Certificate from certificate", e);
}
}
/**
* Retrieve the issuing CA certificate as {@link X509Certificate}. Only supported if certificate is DER-encoded.
*
* @return the issuing CA {@link X509Certificate}.
*/
public X509Certificate getX509IssuerCertificate() {
try {
byte[] bytes = Base64.decode(getIssuingCaCertificate());
return KeystoreUtil.getCertificate(bytes);
} catch (IOException e) {
throw new VaultException("Cannot create Certificate from issuing CA certificate", e);
} catch (CertificateException e) {
throw new VaultException("Cannot create Certificate from issuing CA certificate", e);
}
}
/**
* Create a {@link KeyStore} from this {@link CertificateBundle} containing the private key and certificate chain.
* Only supported if certificate and private key are DER-encoded.
*
* @param keyAlias the key alias to use.
* @return the {@link KeyStore} containing the private key and certificate chain.
*/
public KeyStore createKeyStore(String keyAlias) {
Assert.hasText(keyAlias, "Key alias must not be empty");
try {
return KeystoreUtil.createKeyStore(keyAlias, getPrivateKeySpec(), getX509Certificate(),
getX509IssuerCertificate());
} catch (GeneralSecurityException e) {
throw new VaultException("Cannot create KeyStore", e);
} catch (IOException e) {
throw new VaultException("Cannot create KeyStore", e);
}
}
}

View File

@@ -0,0 +1,433 @@
/*
* Copyright 2016 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
*
* http://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.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Keystore utility to create a {@link KeyStore} containing a {@link CertificateBundle} with the certificate chain and
* its private key.
*
* @author Mark Paluch
*/
class KeystoreUtil {
/**
* Creates a {@link KeyStore} containing the {@link KeySpec} and {@link X509Certificate certificates} using the given
* {@code keyAlias}.
*
* @param keyAlias
* @param certificates
* @return
* @throws GeneralSecurityException
* @throws IOException
*/
static KeyStore createKeyStore(String keyAlias, KeySpec privateKeySpec, X509Certificate... certificates)
throws GeneralSecurityException, IOException {
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(privateKeySpec);
KeyStore keyStore = createKeyStore();
List<X509Certificate> certChain = new ArrayList<X509Certificate>();
Collections.addAll(certChain, certificates);
keyStore.setKeyEntry(keyAlias, privateKey, new char[0],
certChain.toArray(new java.security.cert.Certificate[certChain.size()]));
return keyStore;
}
static X509Certificate getCertificate(byte[] source) throws CertificateException, IOException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
List<X509Certificate> certificates = getCertificates(certificateFactory, source);
if (certificates.isEmpty()) {
return null;
}
return certificates.get(0);
}
/**
* Creates an empty {@link KeyStore}.
*
* @return
* @throws GeneralSecurityException
* @throws IOException
*/
private static KeyStore createKeyStore() throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, new char[0]);
return keyStore;
}
private static List<X509Certificate> getCertificates(CertificateFactory cf, byte[] source)
throws CertificateException, IOException {
List<X509Certificate> x509Certificates = new ArrayList<X509Certificate>();
ByteArrayInputStream bis = new ByteArrayInputStream(source);
while (bis.available() > 0) {
java.security.cert.Certificate cert = cf.generateCertificate(bis);
if (cert instanceof X509Certificate) {
x509Certificates.add((X509Certificate) cert);
}
}
return x509Certificates;
}
/**
* Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec.
* <p/>
* <p/>
* The ASN.1 syntax for the private key with CRT is
* <p/>
*
* <pre>
* --
* -- Representation of RSA private key with information for the CRT algorithm.
* --
* RSAPrivateKey ::= SEQUENCE {
* version Version,
* modulus INTEGER, -- n
* publicExponent INTEGER, -- e
* privateExponent INTEGER, -- d
* prime1 INTEGER, -- p
* prime2 INTEGER, -- q
* exponent1 INTEGER, -- d mod (p-1)
* exponent2 INTEGER, -- d mod (q-1)
* coefficient INTEGER, -- (inverse of q) mod p
* otherPrimeInfos OtherPrimeInfos OPTIONAL
* }
* </pre>
*
* @param keyBytes PKCS#1 encoded key
* @return KeySpec
* @throws IOException
*/
static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException {
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();
parser.read(); // Skip version
BigInteger modulus = parser.read().getInteger();
BigInteger publicExp = parser.read().getInteger();
BigInteger privateExp = parser.read().getInteger();
BigInteger prime1 = parser.read().getInteger();
BigInteger prime2 = parser.read().getInteger();
BigInteger exp1 = parser.read().getInteger();
BigInteger exp2 = parser.read().getInteger();
BigInteger crtCoef = parser.read().getInteger();
return new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef);
}
/**
* 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 tag.
* <p/>
* <p/>
* This parser can only handle one layer. To parse nested constructs, get a new parser for each layer using
* <code>Asn1Object.getParser()</code>.
* <p/>
* <p/>
* There are many DER decoders in JRE but using them will tie this program to a specific JCE/JVM.
*/
private static class DerParser {
// Classes
public final static int UNIVERSAL = 0x00;
public final static int APPLICATION = 0x40;
public final static int CONTEXT = 0x80;
public final static int PRIVATE = 0xC0;
// Constructed Flag
public final static int CONSTRUCTED = 0x20;
// Tag and data types
public final static int ANY = 0x00;
public final static int BOOLEAN = 0x01;
public final static int INTEGER = 0x02;
public final static int BIT_STRING = 0x03;
public final static int OCTET_STRING = 0x04;
public final static int NULL = 0x05;
public final static int REAL = 0x09;
public final static int ENUMERATED = 0x0a;
public final static int SEQUENCE = 0x10;
public final static int SET = 0x11;
public final static int NUMERIC_STRING = 0x12;
public final static int PRINTABLE_STRING = 0x13;
public final static int VIDEOTEX_STRING = 0x15;
public final static int IA5_STRING = 0x16;
public final static int GRAPHIC_STRING = 0x19;
public final static int ISO646_STRING = 0x1A;
public final static int GENERAL_STRING = 0x1B;
public final static int UTF8_STRING = 0x0C;
public final static int UNIVERSAL_STRING = 0x1C;
public final static int BMP_STRING = 0x1E;
public final static int UTC_TIME = 0x17;
protected InputStream in;
/**
* Create a new DER decoder from an input stream.
*
* @param in The DER encoded stream
*/
public DerParser(InputStream in) {
this.in = in;
}
/**
* Create a new DER decoder from a byte array.
*
* @param bytes The encoded bytes
*/
public DerParser(byte[] bytes) {
this(new ByteArrayInputStream(bytes));
}
/**
* Read next object. If it's constructed, the value holds encoded content and it should be parsed by a new parser
* from <code>Asn1Object.getParser</code>.
*
* @return A object
* @throws IOException
*/
public Asn1Object read() throws IOException {
int tag = in.read();
if (tag == -1) {
throw new IllegalStateException("Invalid DER: stream too short, missing tag");
}
int length = getLength();
byte[] value = new byte[length];
int n = in.read(value);
if (n < length) {
throw new IllegalStateException("Invalid DER: stream too short, missing value");
}
return new Asn1Object(tag, length, value);
}
/**
* Decode the length of the field. Can only support length encoding up to 4 octets.
* <p/>
* <p/>
* In BER/DER encoding, length can be encoded in 2 forms,
* <ul>
* <li>Short form. One octet. Bit 8 has value "0" and bits 7-1 give the length.
* <li>Long form. Two to 127 octets (only 4 is supported here). Bit 8 of first octet has value "1" and bits 7-1 give
* the number of additional length octets. Second and following octets give the length, base 256, most significant
* digit first.
* </ul>
*
* @return The length as integer
* @throws IOException
*/
private int getLength() throws IOException {
int i = in.read();
if (i == -1) {
throw new IllegalStateException("Invalid DER: length missing");
}
// A single byte short length
if ((i & ~0x7F) == 0) {
return i;
}
int num = i & 0x7F;
// We can't handle length longer than 4 bytes
if (i >= 0xFF || num > 4) {
throw new IllegalStateException("Invalid DER: length field too big (" + i + ")");
}
byte[] bytes = new byte[num];
int n = in.read(bytes);
if (n < num) {
throw new IllegalStateException("Invalid DER: length too short");
}
return new BigInteger(1, bytes).intValue();
}
}
/**
* An ASN.1 TLV. The object is not parsed. It can only handle integers and strings.
*/
private static class Asn1Object {
private final int type;
private final int length;
private final byte[] value;
private final int tag;
/**
* Construct a ASN.1 TLV. The TLV could be either a constructed or primitive entity.
* <p/>
* <p/>
* The first byte in DER encoding is made of following fields,
*
* <pre>
* -------------------------------------------------
* |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
* -------------------------------------------------
* | Class | CF | + Type |
* -------------------------------------------------
* </pre>
* <ul>
* <li>Class: Universal, Application, Context or Private
* <li>CF: Constructed flag. If 1, the field is constructed.
* <li>Type: This is actually called tag in ASN.1. It indicates data type (Integer, String) or a construct
* (sequence, choice, set).
* </ul>
*
* @param tag Tag or Identifier
* @param length Length of the field
* @param value Encoded octet string for the field.
*/
public Asn1Object(int tag, int length, byte[] value) {
this.tag = tag;
this.type = tag & 0x1F;
this.length = length;
this.value = value;
}
public int getType() {
return type;
}
public int getLength() {
return length;
}
public byte[] getValue() {
return value;
}
public boolean isConstructed() {
return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED;
}
/**
* For constructed field, return a parser for its content.
*
* @return A parser for the construct.
* @throws IOException
*/
public DerParser getParser() throws IOException {
if (!isConstructed()) {
throw new IllegalStateException("Invalid DER: can't parse primitive entity");
}
return new DerParser(value);
}
/**
* Get the value as integer
*
* @return BigInteger
*/
public BigInteger getInteger() {
if (type != DerParser.INTEGER) {
throw new IllegalStateException("Invalid DER: object is not integer");
}
return new BigInteger(value);
}
/**
* Get value as string. Most strings are treated as ISO-8859-1.
*
* @return Java string
* @throws IOException
*/
public String getString() throws IOException {
String encoding;
switch (type) {
// Not all are ISO-8859-1 but it's the closest thing
case DerParser.NUMERIC_STRING:
case DerParser.PRINTABLE_STRING:
case DerParser.VIDEOTEX_STRING:
case DerParser.IA5_STRING:
case DerParser.GRAPHIC_STRING:
case DerParser.ISO646_STRING:
case DerParser.GENERAL_STRING:
encoding = "ISO-8859-1";
break;
case DerParser.BMP_STRING:
encoding = "UTF-16BE";
break;
case DerParser.UTF8_STRING:
encoding = "UTF-8";
break;
case DerParser.UNIVERSAL_STRING:
throw new IOException("Invalid DER: can't handle UCS-4 string");
default:
throw new IOException("Invalid DER: object is not a string");
}
return new String(value, encoding);
}
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright 2016 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
*
* http://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.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.util.Assert;
/**
* Request for a Certificate.
*
* @author Mark Paluch
*/
public class VaultCertificateRequest {
/**
* The CN of the certificate. Should match the host name.
*/
private final String commonName;
/**
* Alternate CN names for additional host names.
*/
private final List<String> altNames;
/**
* Requested IP Subject Alternative Names.
*/
private final List<String> ipSubjectAltNames;
/**
* Requested Time To Live
*/
private final Integer ttl;
/**
* If {@literal true}, the given common name will not be included in DNS or Email Subject Alternate Names (as
* appropriate). Useful if the CN is not a hostname or email address, but is instead some human-readable identifier.
*/
private final boolean excludeCommonNameFromSubjectAltNames;
VaultCertificateRequest(String commonName, List<String> altNames, List<String> ipSubjectAltNames, Integer ttl,
Boolean excludeCommonNameFromSubjectAltNames) {
this.commonName = commonName;
this.altNames = altNames;
this.ipSubjectAltNames = ipSubjectAltNames;
this.ttl = ttl;
this.excludeCommonNameFromSubjectAltNames = excludeCommonNameFromSubjectAltNames != null
&& excludeCommonNameFromSubjectAltNames.booleanValue();
}
/**
* @return a new {@link VaultCertificateRequestBuilder}.
*/
public static VaultCertificateRequestBuilder builder() {
return new VaultCertificateRequestBuilder();
}
/**
* Creates a new {@link VaultCertificateRequest} given a {@code commonName}.
*
* @param commonName must not be empty or {@literal null}.
* @return the created {@link VaultCertificateRequest}.
*/
public static VaultCertificateRequest create(String commonName) {
return builder().commonName(commonName).build();
}
public String getCommonName() {
return commonName;
}
public List<String> getAltNames() {
return altNames;
}
public List<String> getIpSubjectAltNames() {
return ipSubjectAltNames;
}
public Integer getTtl() {
return ttl;
}
public boolean isExcludeCommonNameFromSubjectAltNames() {
return excludeCommonNameFromSubjectAltNames;
}
public static class VaultCertificateRequestBuilder {
private String commonName;
private List<String> altNames = new ArrayList<String>();
private List<String> ipSubjectAltNames = new ArrayList<String>();
private Integer ttl;
private Boolean excludeCommonNameFromSubjectAltNames;
VaultCertificateRequestBuilder() {}
/**
* Configure the common name.
*
* @param commonName must not be empty or {@literal null}.
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder commonName(String commonName) {
Assert.hasText(commonName, "Common name must not be empty");
this.commonName = commonName;
return this;
}
/**
* Configures alternative names. Replaces previously configured alt names
*
* @param altNames must not be {@literal null}.
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder altNames(Iterable<String> altNames) {
Assert.notNull(altNames, "Alt names must not be null");
this.altNames = toList(altNames);
return this;
}
/**
* Add an alternative name.
*
* @param altName must not be empty or {@literal null}.
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder withAltName(String altName) {
Assert.hasText(altName, "Alt name must not be empty");
this.altNames.add(altName);
return this;
}
/**
* Configures IP subject alternative names. Replaces previously configured IP subject alt names.
*
* @param ipSubjectAltNames must not be {@literal null}.
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder ipSubjectAltNames(Iterable<String> ipSubjectAltNames) {
Assert.notNull(ipSubjectAltNames, "IP subject alt names must not be null");
this.ipSubjectAltNames = toList(ipSubjectAltNames);
return this;
}
/**
* Add an IP subject alternative name.
*
* @param ipSubjectAltName must not be empty or {@literal null}.
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder withIpSubjectAltName(String ipSubjectAltName) {
Assert.hasText(ipSubjectAltName, "IP subject alt name must not be empty");
this.ipSubjectAltNames.add(ipSubjectAltName);
return this;
}
/**
* Configures a TTL.
*
* @param ttl the TTL, must be a positive number.
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder ttl(int ttl) {
Assert.isTrue(ttl > 0, "TTL must be greater 0");
this.ttl = ttl;
return this;
}
/**
* Configures a TTL.
*
* @param ttl the TTL, must be a positive number.
* @param timeUnit must not be {@literal null}
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder ttl(long ttl, TimeUnit timeUnit) {
Assert.isTrue(ttl > 0, "TTL must be greater 0");
Assert.notNull(timeUnit, "TimeUnit must be greater 0");
this.ttl = (int) timeUnit.toSeconds(ttl);
return this;
}
/**
* The given common name will not be included in DNS or Email Subject Alternate Names (as appropriate). Useful if
* the CN is not a hostname or email address, but is instead some human-readable identifier.
*
* @return {@code this} {@link VaultCertificateRequestBuilder}.
*/
public VaultCertificateRequestBuilder excludeCommonNameFromSubjectAltNames() {
this.excludeCommonNameFromSubjectAltNames = true;
return this;
}
/**
* Builds a new {@link VaultCertificateRequest} instance. Requires {@link #commonName(String)} to be configured.
*
* @return a new {@link VaultCertificateRequest}.
*/
public VaultCertificateRequest build() {
Assert.hasText(commonName, "Common name must not be empty");
List<String> altNames;
switch (this.altNames.size()) {
case 0:
altNames = java.util.Collections.emptyList();
break;
case 1:
altNames = java.util.Collections.singletonList(this.altNames.get(0));
break;
default:
altNames = java.util.Collections.unmodifiableList(new ArrayList<String>(this.altNames));
}
List<String> ipSubjectAltNames;
switch (this.ipSubjectAltNames.size()) {
case 0:
ipSubjectAltNames = java.util.Collections.emptyList();
break;
case 1:
ipSubjectAltNames = java.util.Collections.singletonList(this.ipSubjectAltNames.get(0));
break;
default:
ipSubjectAltNames = java.util.Collections.unmodifiableList(new ArrayList<String>(this.ipSubjectAltNames));
}
return new VaultCertificateRequest(commonName, altNames, ipSubjectAltNames, ttl,
excludeCommonNameFromSubjectAltNames);
}
private static <E> List<E> toList(Iterable<E> iter) {
List<E> list = new ArrayList<E>();
for (E item : iter) {
list.add(item);
}
return list;
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2016 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
*
* http://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;
/**
* Value object to bind Vault HTTP PKI issue certificate API responses.
*
* @author Mark Paluch
*/
public class VaultCertificateResponse extends VaultResponseSupport<CertificateBundle> {}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2016 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
*
* http://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.core;
import static org.springframework.vault.util.Settings.*;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.assertj.core.util.Files;
import org.junit.Before;
import org.junit.Test;
import org.springframework.vault.util.IntegrationTestSupport;
/**
* Integration test to request certificates from the Vault PKI backend.
*
* @author Mark Paluch
*/
public class PkiSecretIntegrationTests extends IntegrationTestSupport {
/**
* Initialize the pki secret backend.
*/
@Before
public void setUp() {
if (!prepare().hasSecret("pki")) {
prepare().mountSecret("pki");
}
File workDir = findWorkDir(new File(System.getProperty("user.dir")));
String cert = Files.contentOf(new File(workDir, "ca/certs/intermediate.cert.pem"), StandardCharsets.US_ASCII);
String key = Files.contentOf(new File(workDir, "ca/private/intermediate.decrypted.key.pem"),
StandardCharsets.US_ASCII);
Map<String, String> pembundle = Collections.singletonMap("pem_bundle", cert + key);
VaultOperations vaultOperations = prepare().getVaultOperations();
vaultOperations.write("pki/config/ca", pembundle);
Map<String, String> role = new HashMap<String, String>();
role.put("allowed_domains", "localhost,example.com");
role.put("allow_subdomains", "true");
role.put("allow_localhost", "true");
role.put("allow_ip_sans", "true");
role.put("max_ttl", "72h");
vaultOperations.write("pki/roles/test", role);
}
@Test
public void shouldCreateCertificateCorrectly() {
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2016 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
*
* http://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.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.vault.util.Settings.*;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.assertj.core.util.Files;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.client.VaultException;
import org.springframework.vault.support.CertificateBundle;
import org.springframework.vault.support.VaultCertificateRequest;
import org.springframework.vault.support.VaultCertificateResponse;
import org.springframework.vault.util.IntegrationTestSupport;
/**
* Integration tests for {@link VaultPkiTemplate} through {@link VaultPkiOperations}.
*
* @author Mark Paluch
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
public class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport {
@Autowired private VaultOperations vaultOperations;
private VaultPkiOperations pkiOperations;
@Before
public void before() throws Exception {
pkiOperations = vaultOperations.opsForPki();
if (!prepare().hasSecret("pki")) {
prepare().mountSecret("pki");
}
File workDir = findWorkDir(new File(System.getProperty("user.dir")));
String cert = Files.contentOf(new File(workDir, "ca/certs/intermediate.cert.pem"), "US-ASCII");
String key = Files.contentOf(new File(workDir, "ca/private/intermediate.decrypted.key.pem"), "US-ASCII");
Map<String, String> pembundle = Collections.singletonMap("pem_bundle", cert + key);
vaultOperations.write("pki/config/ca", pembundle);
Map<String, String> role = new HashMap<String, String>();
role.put("allowed_domains", "localhost,example.com");
role.put("allow_subdomains", "true");
role.put("allow_localhost", "true");
role.put("allow_ip_sans", "true");
role.put("max_ttl", "72h");
vaultOperations.write("pki/roles/testrole", role);
}
@Test
public void issueCertificateShouldCreateCertificate() {
VaultCertificateRequest request = VaultCertificateRequest.create("hello.example.com");
VaultCertificateResponse certificateResponse = pkiOperations.issueCertificate("testrole", request);
CertificateBundle data = certificateResponse.getData();
assertThat(data.getPrivateKey()).isNotEmpty();
assertThat(data.getCertificate()).isNotEmpty();
assertThat(data.getIssuingCaCertificate()).isNotEmpty();
assertThat(data.getSerialNumber()).isNotEmpty();
assertThat(data.getX509Certificate().getSubjectX500Principal().getName()).isEqualTo("CN=hello.example.com");
}
@Test(expected = VaultException.class)
public void issueCertificateFail() {
VaultCertificateRequest request = VaultCertificateRequest.create("not.supported");
pkiOperations.issueCertificate("testrole", request);
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2016 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
*
* http://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 static org.assertj.core.api.Assertions.*;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Unit tests for {@link CertificateBundle}.
*
* @author Mark Paluch
*/
public class CertificateBundleUnitTests {
CertificateBundle certificateBundle;
@SuppressWarnings("unchecked")
@Before
public void before() throws Exception {
Map<String, String> data = new ObjectMapper().readValue(getClass().getResource("/certificate.json"), Map.class);
certificateBundle = CertificateBundle.of(data.get("serial_number"), data.get("certificate"), data.get("issuing_ca"),
data.get("private_key"));
}
@Test
public void getPrivateKeySpecShouldCreatePrivateKey() throws Exception {
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(certificateBundle.getPrivateKeySpec());
assertThat(privateKey.getAlgorithm()).isEqualTo("RSA");
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
}
@Test
public void getX509CertificateShouldReturnCertificate() throws Exception {
X509Certificate x509Certificate = certificateBundle.getX509Certificate();
assertThat(x509Certificate.getSubjectDN().getName()).isEqualTo("CN=hello.example.com");
}
@Test
public void getX509IssuerCertificateShouldReturnCertificate() throws Exception {
X509Certificate x509Certificate = certificateBundle.getX509IssuerCertificate();
assertThat(x509Certificate.getSubjectDN().getName()).startsWith("CN=Intermediate CA Certificate");
}
@Test
public void getAsKeystore() throws Exception {
KeyStore keyStore = certificateBundle.createKeyStore("mykey");
assertThat(keyStore.size()).isEqualTo(1);
assertThat(keyStore.getCertificateChain("mykey")).hasSize(2);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2016 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
*
* http://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 static org.assertj.core.api.Assertions.*;
import org.junit.Test;
/**
* Unit tests for {@link VaultCertificateRequest}.
*
* @author Mark Paluch
*/
public class VaultCertificateRequestUnitTests {
@Test(expected = IllegalArgumentException.class)
public void shouldRejectUnconfiguredBuilder() throws Exception {
VaultCertificateRequest.builder().build();
}
@Test
public void shouldBuildRequestWithCommonName() throws Exception {
VaultCertificateRequest request = VaultCertificateRequest.builder().commonName("hello.com").build();
assertThat(request.getCommonName()).isEqualTo("hello.com");
}
@Test
public void shouldBuildFullyConfiguredRequest() throws Exception {
VaultCertificateRequest request = VaultCertificateRequest.builder() //
.commonName("hello.com") //
.withAltName("alt") //
.withIpSubjectAltName("127.0.0.1") //
.excludeCommonNameFromSubjectAltNames() //
.build();
assertThat(request.getCommonName()).isEqualTo("hello.com");
assertThat(request.getAltNames()).hasSize(1).contains("alt");
assertThat(request.getIpSubjectAltNames()).hasSize(1).contains("127.0.0.1");
assertThat(request.isExcludeCommonNameFromSubjectAltNames()).isTrue();
assertThat(request.getCommonName()).isEqualTo("hello.com");
}
}

View File

@@ -145,7 +145,7 @@ public class PrepareVault {
public boolean hasSecret(String secretBackend) {
Assert.hasText(secretBackend, "SecretBackend must not be empty");
return adminOperations.getMounts().containsKey(secretBackend);
return adminOperations.getMounts().containsKey(secretBackend + "/");
}
public VaultOperations getVaultOperations() {

View File

@@ -0,0 +1,7 @@
{
"serial_number": "11:34:10:87:5e:be:bc:0b:22:7b:18:c5:80:77:7a:dc:47:52:b4:53",
"certificate": "MIIDpDCCAoygAwIBAgIUETQQh16+vAsiexjFgHd63EdStFMwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCTk4xEDAOBgNVBAgMB1Vua25vd24xIjAgBgNVBAoMGXNwcmluZy1jbG91ZC12YXVsdC1jb25maWcxJDAiBgNVBAMMG0ludGVybWVkaWF0ZSBDQSBDZXJ0aWZpY2F0ZTAeFw0xNjA5MjUxODIxMzlaFw0xNjA5MjgxODIyMDlaMBwxGjAYBgNVBAMTEWhlbGxvLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuc4sOQoLFiC6pwFWuFsG592xfP0fYTTTe6p/+5b6OjdhNGlYb1VpLczdBNuJXtf6YjsN38h/R3w8FBOMiIDwmbJ7fD4xcu7AWpTG0WlW6hs2gi9aOS0x2UoL/9B86mTd6y6St5fAhOtNyYvVy5fu29y++qKOpFNoN67gggXc2Rq5MRXmAoXwmsmdXhczdiGYpsaRqL3aXZ582zXgXzH419i0wc5QRoB+QfoxPclTgzkOiMP88bY8RdvTbhAe5dv4ws4W2OQYC7yQ3814duuZKb06vyzBb3j4GBTiX32K1H97Qx4jcFGD/DlsF+7N6EY1pxUm96qg/k/q7rZnikNcBwIDAQABo4GQMIGNMA4GA1UdDwEB/wQEAwIDqDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFGCl7M1plNMrWXzalhTXGbhsE3FnMB8GA1UdIwQYMBaAFHjYUnRFNIYGPS2OMuwJKGXP1g8QMBwGA1UdEQQVMBOCEWhlbGxvLmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBDshpRSurHQNpB7Bc+wbsNEVjASg8wK4UAWQIzyVBQv7b6wauErFsbtYNM7dYBy0+ABVuSs+kRP/n5EPOvQ/aCQcSg9ghVOB1ZNiUAsPotjfxYP9I7Lo2BUI2ghonyoOaBj/vL7y+7gbG6EoYo+TYO6ccdRg121qA6SzAKGpX5W2ppoiVpgVLMO5oLG2IHl7uu2iEkE5E32qceKwggSzkXiITiNIEVW9iHnT+U76vdFmVtpnKNi/0oPw5p6+f0p7G7x7788st3xEk5HCvqNTF1nxRnPL0/wabvkH1s5EzoLKwcd5vgQS7Y0N7g9docIl/aoWNBtRKv9iqLHpUVV1Jb",
"issuing_ca": "MIIDuTCCAqGgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwbjELMAkGA1UEBhMCTk4xEDAOBgNVBAgMB1Vua25vd24xEDAOBgNVBAcMB1Vua25vd24xIjAgBgNVBAoMGXNwcmluZy1jbG91ZC12YXVsdC1jb25maWcxFzAVBgNVBAMMDkNBIENlcnRpZmljYXRlMB4XDTE2MDkyNTE4MjAyNFoXDTI2MDkyMzE4MjAyNFowaTELMAkGA1UEBhMCTk4xEDAOBgNVBAgMB1Vua25vd24xIjAgBgNVBAoMGXNwcmluZy1jbG91ZC12YXVsdC1jb25maWcxJDAiBgNVBAMMG0ludGVybWVkaWF0ZSBDQSBDZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJu47f0402Brk0xKcRM2mNXGsVdT59Yxt2jhu0Jtsmc03sUZ97rrrG70JsovUNzmK+vfTgJWt/W2aHzKoYPUpBgcFhaOTQye28QE+rqFGplsknM9PWXaeyC22d3src8R5Mg0UmQsWZaY7lat5upgNnvvGzpbLv7n5XiVD2k75zp/1o2TOUVcb1reT2hlB2/Vse6552e1bfLhn6VPKZNt+zkZ6itx2l5lzHtFBlVa8eOuhLEoZjIFl7LyeoLWqq+j3D3L+wTmkXAl1NeGHkAHVwAcR30gvJob7XHC42kM8at0oaFtGiriaiUMKCcYhNDHY1bVSQJNCg80QVk/ZDP3NI0CAwEAAaNmMGQwHQYDVR0OBBYEFHjYUnRFNIYGPS2OMuwJKGXP1g8QMB8GA1UdIwQYMBaAFMBZt5wTqO7CgiHEYfaN/JKweWBZMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IBAQCPEwM66l+DivQRUw0RQ4menGLR67sM88MTgtxJ5E3NAz9J7r0cZT7KV6pAk7sOJoqiU6rRkdr3VPLvdHloPrFlAdmEh1kTBQZmhv2qg9D69VdTrezkfI3aCn2EBmpNbyLgL9nodi2pBSOclI6ANQShNHwqkc6lPH7K53RsJ6Oj0VJrJjMVssn3IJODB4U/0R3jBkV1mDjyV60LC29uXTLpdM6aYDXRjsSlA8X5GNsTHCj7xG0Pgwd/c6esE5hhakQlGbszLWheJi6/un7qn7y+83on7wYOArp4D0olQHx9q6hAOHxNm32eXVw4PiNL24C6BnLb5k1eyh3Jn0oEuhEL",
"private_key": "MIIEowIBAAKCAQEAuc4sOQoLFiC6pwFWuFsG592xfP0fYTTTe6p/+5b6OjdhNGlYb1VpLczdBNuJXtf6YjsN38h/R3w8FBOMiIDwmbJ7fD4xcu7AWpTG0WlW6hs2gi9aOS0x2UoL/9B86mTd6y6St5fAhOtNyYvVy5fu29y++qKOpFNoN67gggXc2Rq5MRXmAoXwmsmdXhczdiGYpsaRqL3aXZ582zXgXzH419i0wc5QRoB+QfoxPclTgzkOiMP88bY8RdvTbhAe5dv4ws4W2OQYC7yQ3814duuZKb06vyzBb3j4GBTiX32K1H97Qx4jcFGD/DlsF+7N6EY1pxUm96qg/k/q7rZnikNcBwIDAQABAoIBAEbvSzrLdsgP3rJ7dFwVhGFo5aXODPjTbhm8Uff+/7DSC64yuaxncywlFgwQtDUdR13RJc7VgKIN7/HKNNC4SSKkXThllgNG2vC5891m1eOyV/EDmvJRKsLF3ibftZDWyVJMCX9F1B3qp1vWotyDOJF/iPmurb5WiWcYwGVjT5kiT8YNQo3FrOSGdyI3LIHtEJCHuPShLIaFEzDYe6iAo/iO0TfOqMKRz6kByAj76bCFwMlZC6dsjt4jUz7SkGf4YbGq35uBdWMyg+VkZ6T6EnfBFuUuXAEbmbJX6PvbzH1BHHQxHfGojSKp6zvHlY9qeNm0TYME/6zFL1qq7fFHNeECgYEA6OpcUDdnZC7mU1Nzly+AbhUSUEjSRwgKsiMe+iytuUyDjbFWmzo3/oae9pAiWDBpof4yVyga/7wK78RjK6EgCAW2mLnJJndJ/HbmfuX/Qf0LdqO9W7qRaioDIyB9ZmMcTKfND9mPBddJCrVnlu530p4DGNOObNWO3pN7cq8/DbUCgYEAzDiDOj/CedFk3lBlfvXyIsbTI+dJKhKQIcBxI38MKa1GErzQRJu0+WuyebgMX0NtY8rD2ugDX3ZvWHJ8tHXMQNpuSU0OP0DLEuA+OJ7xlbvSUSkCmbsMMlCbEJyiVWGSuNh8pGbuYQ2URlCIQ+DMn8+D104y2ZGuyx6s7sFX+EsCgYB6I4PbMOKP/6QaqBVif69qirQ606UpSIdLWO2CpXFITV5jLE7NQd9hIrkzdOv9sQ2DvGgrmkVmO/bEOwkCNRE2A8D4H9zixgVPVg6TFwsfUJH1WfIPqytcL8HLZts7mNebPxdiq2S73GRaMDytQkolcWUQXMhAPFEaWN6QimOXTQKBgHNhXmrziQnQTz/3hlCD3kjdsn7vdnvJvxsCoile511Io3WErdqFamsc+NoiLQNJki02aw9r1DQEMwZysJg4kaz2fCb33RvJnLJOdXkB/BYP5tSp6ek3L73hoqpxBJpepUnALjOoX/f/dw1/BG3tPU7xLFjhuoMH9wyViTjfjbcfAoGBAI8hJE2GH/oqcnVcOOKISsMHthFANNIkmRLJh5/Npq+OENMjXRhO1Ns6BALNGjqp+lnrFmVLDQXy1bWZ2tFpM5nGLjx7QbI+uNEhVRRXbx0MeKmU1U3reiY3oNSUXgRNi9wznrJ7eKUd8ViQTEFKfIdIvjatWlazggIp/MLGn1va"
}

View File

@@ -46,6 +46,7 @@ openssl rsa -in ${CA_DIR}/private/localhost.key.pem \
-passin pass:changeit
chmod 400 ${CA_DIR}/private/localhost.key.pem
chmod 400 ${CA_DIR}/private/localhost.decrypted.key.pem
echo "[INFO] Generating server certificate request"
openssl req -config ${DIR}/openssl.cnf \
@@ -96,10 +97,35 @@ openssl pkcs12 -export -clcerts \
-passout pass:changeit \
-out ${CA_DIR}/client.p12
KEYTOOL=${JAVA_HOME}/bin/keytool
${KEYTOOL} -importcert -keystore ${KEYSTORE_FILE} -file ${CA_DIR}/certs/ca.cert.pem -noprompt -storepass changeit
${KEYTOOL} -importkeystore \
${JAVA_HOME}/bin/keytool -importcert -keystore ${KEYSTORE_FILE} -file ${CA_DIR}/certs/ca.cert.pem -noprompt -storepass changeit
${JAVA_HOME}/bin/keytool -importkeystore \
-srckeystore ${CA_DIR}/client.p12 -srcstoretype PKCS12 -srcstorepass changeit\
-destkeystore ${CLIENT_CERT_KEYSTORE} -deststoretype JKS \
-noprompt -storepass changeit
echo "[INFO] Generating intermediate CA private key"
# Less bits = less secure = faster to generate
openssl genrsa -passout pass:changeit -aes256 -out ${CA_DIR}/private/intermediate.key.pem 2048
openssl rsa -in ${CA_DIR}/private/intermediate.key.pem \
-out ${CA_DIR}/private/intermediate.decrypted.key.pem \
-passin pass:changeit
chmod 400 ${CA_DIR}/private/intermediate.key.pem
chmod 400 ${CA_DIR}/private/intermediate.decrypted.key.pem
echo "[INFO] Generating intermediate certificate"
openssl req -config ${DIR}/intermediate.cnf \
-key ${CA_DIR}/private/intermediate.key.pem \
-new -sha256 \
-out ${CA_DIR}/csr/intermediate.csr.pem \
-passin pass:changeit \
-subj "/C=NN/ST=Unknown/L=Unknown/O=spring-cloud-vault-config/CN=Intermediate CA Certificate"
echo "[INFO] Signing intermediate certificate request"
openssl ca -config ${DIR}/openssl.cnf \
-days 3650 -notext -md sha256 -extensions v3_intermediate_ca \
-passin pass:changeit \
-batch \
-in ${CA_DIR}/csr/intermediate.csr.pem \
-out ${CA_DIR}/certs/intermediate.cert.pem

View File

@@ -0,0 +1,68 @@
[ ca ]
# `man ca`
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = work/ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
# The intermediate key and root certificate.
private_key = $dir/private/intermediate.key.pem
certificate = $dir/certs/intermediate.cert.pem
# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/intermediate.crl.pem
crl_extensions = crl_ext
default_crl_days = 30
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
# Optionally, specify some defaults.
countryName_default = NN
stateOrProvinceName_default = Vault Test
localityName_default =
0.organizationName_default = spring-cloud-vault-config
#organizationalUnitName_default =
#emailAddress_default = info@spring-cloud-vault-config.dummy