Skip to content

Commit ac7beb9

Browse files
committed
Support PEM encoded certificates and EC for private keys.
We now support PEM, PEM-bundle and DER-encoded certificate bundles along with RSA and EC key types. Keys can be provided either in their plain representation or wrapped within a PKCS8 container. Original pull request: gh-688. Closes gh-678 Closes gh-683
1 parent 35ad898 commit ac7beb9

24 files changed

+1422
-657
lines changed

spring-vault-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@
260260
<scope>test</scope>
261261
</dependency>
262262

263+
<dependency>
264+
<groupId>org.junit.jupiter</groupId>
265+
<artifactId>junit-jupiter-params</artifactId>
266+
<scope>test</scope>
267+
</dependency>
268+
263269
<dependency>
264270
<groupId>org.junit.jupiter</groupId>
265271
<artifactId>junit-jupiter-engine</artifactId>

spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiOperations.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public interface VaultPkiOperations {
4444
* Requests a certificate bundle (private key and certificate) from Vault's PKI
4545
* backend given a {@code roleName} and {@link VaultCertificateRequest}. The issuing
4646
* CA certificate is returned as well, so that only the root CA need be in a client's
47-
* trust store. Certificates use DER format and are base64 encoded.
47+
* trust store.
4848
* @param roleName must not be empty or {@literal null}.
4949
* @param certificateRequest must not be {@literal null}.
5050
* @return the {@link VaultCertificateResponse} containing a {@link CertificateBundle}
@@ -59,8 +59,7 @@ VaultCertificateResponse issueCertificate(String roleName, VaultCertificateReque
5959
/**
6060
* Signs a CSR using Vault's PKI backend given a {@code roleName}, {@code csr} and
6161
* {@link VaultCertificateRequest}. The issuing CA certificate is returned as well, so
62-
* that only the root CA need be in a client's trust store. Certificates use DER
63-
* format and are base64 encoded.
62+
* that only the root CA need be in a client's trust store.
6463
* @param roleName must not be empty or {@literal null}.
6564
* @param csr must not be empty or {@literal null}.
6665
* @param certificateRequest must not be {@literal null}.

spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public VaultSignCertificateRequestResponse signCertificateRequest(String roleNam
8888
private <T> T requestCertificate(String roleName, String requestPath, Map<String, Object> request,
8989
Class<T> responseType) {
9090

91-
request.put("format", "der");
91+
request.putIfAbsent("format", "der");
9292

9393
T response = this.vaultOperations.doWithSession(restOperations -> {
9494

spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.security.KeyStore;
2121
import java.security.cert.CertificateException;
2222
import java.security.cert.X509Certificate;
23+
import java.util.ArrayList;
24+
import java.util.List;
2325

2426
import com.fasterxml.jackson.annotation.JsonProperty;
2527

@@ -29,8 +31,8 @@
2931

3032
/**
3133
* Value object representing a certificate consisting of the certificate and the issuer
32-
* certificate. Certificate and keys can be either DER or PEM encoded. DER-encoded
33-
* certificates can be converted to a {@link X509Certificate}.
34+
* certificate. Certificate and keys can be either DER or PEM (including PEM bundle) encoded.
35+
* Certificates can be obtained as {@link X509Certificate}.
3436
*
3537
* @author Mark Paluch
3638
* @since 2.0
@@ -92,40 +94,39 @@ public String getIssuingCaCertificate() {
9294
}
9395

9496
/**
95-
* Retrieve the certificate as {@link X509Certificate}. Only supported if certificate
96-
* is DER-encoded.
97+
* Retrieve the certificate as {@link X509Certificate}.
9798
* @return the {@link X509Certificate}.
99+
* @throws IllegalStateException if there is no X.509 certificate available.
98100
*/
99101
public X509Certificate getX509Certificate() {
100-
101-
try {
102-
byte[] bytes = Base64Utils.decodeFromString(getCertificate());
103-
return KeystoreUtil.getCertificate(bytes);
104-
}
105-
catch (CertificateException e) {
106-
throw new VaultException("Cannot create Certificate from certificate", e);
107-
}
102+
return doGetCertificate(getCertificate());
108103
}
109104

110105
/**
111-
* Retrieve the issuing CA certificate as {@link X509Certificate}. Only supported if
112-
* certificate is DER-encoded.
106+
* Retrieve the issuing CA certificate as {@link X509Certificate}.
113107
* @return the issuing CA {@link X509Certificate}.
114108
*/
115109
public X509Certificate getX509IssuerCertificate() {
110+
return doGetCertificate(getIssuingCaCertificate());
111+
}
112+
113+
private X509Certificate doGetCertificate(String cert) {
116114

117115
try {
118-
byte[] bytes = Base64Utils.decodeFromString(getIssuingCaCertificate());
119-
return KeystoreUtil.getCertificate(bytes);
116+
List<X509Certificate> certificates = getCertificates(cert);
117+
if (certificates.isEmpty()) {
118+
throw new IllegalStateException("No certificate found");
119+
}
120+
return certificates.get(0);
120121
}
121122
catch (CertificateException e) {
122-
throw new VaultException("Cannot create Certificate from issuing CA certificate", e);
123+
throw new VaultException("Cannot create Certificate from certificate", e);
123124
}
124125
}
125126

126127
/**
127128
* Create a trust store as {@link KeyStore} from this {@link Certificate} containing
128-
* the certificate chain. Only supported if certificate is DER-encoded.
129+
* the certificate chain.
129130
* @return the {@link KeyStore} containing the private key and certificate chain.
130131
*/
131132
public KeyStore createTrustStore() {
@@ -138,4 +139,26 @@ public KeyStore createTrustStore() {
138139
}
139140
}
140141

142+
static List<X509Certificate> getCertificates(String certificates) throws CertificateException {
143+
144+
Assert.hasText(certificates, "Certificates must not be empty");
145+
146+
List<X509Certificate> result = new ArrayList<>(1);
147+
if (PemObject.isPemEncoded(certificates)) {
148+
149+
List<PemObject> pemObjects = PemObject.parse(certificates);
150+
151+
for (PemObject pemObject : pemObjects) {
152+
if (pemObject.isCertificate()) {
153+
result.add(pemObject.getCertificate());
154+
}
155+
}
156+
}
157+
else {
158+
result.addAll(KeystoreUtil.getCertificates(Base64Utils.decodeFromString(certificates)));
159+
}
160+
161+
return result;
162+
}
163+
141164
}

spring-vault-core/src/main/java/org/springframework/vault/support/CertificateBundle.java

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import java.util.ArrayList;
2525
import java.util.Collections;
2626
import java.util.List;
27+
import java.util.Locale;
2728

29+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2830
import com.fasterxml.jackson.annotation.JsonProperty;
2931

3032
import org.springframework.lang.Nullable;
@@ -35,15 +37,19 @@
3537
/**
3638
* Value object representing a certificate bundle consisting of a private key, the
3739
* certificate and the issuer certificate. Certificate and keys can be either DER or PEM
38-
* encoded. DER-encoded certificates can be converted to a {@link KeySpec} and
39-
* {@link X509Certificate}.
40+
* encoded. RSA and Elliptic Curve keys and certificates can be converted to a
41+
* {@link KeySpec} respective {@link X509Certificate} object. Supports creation of
42+
* {@link #createKeyStore(String) key stores} that contain the key and the certificate
43+
* chain.
4044
*
4145
* @author Mark Paluch
4246
* @author Alex Bremora
4347
* @see #getPrivateKeySpec()
4448
* @see #getX509Certificate()
4549
* @see #getIssuingCaCertificate()
50+
* @see PemObject
4651
*/
52+
@JsonIgnoreProperties(ignoreUnknown = true)
4753
public class CertificateBundle extends Certificate {
4854

4955
private final String privateKey;
@@ -135,26 +141,39 @@ public String getPrivateKeyType() {
135141
}
136142

137143
/**
138-
* Retrieve the private key as {@link KeySpec}. Only supported if private key is
139-
* DER-encoded.
144+
* @return the required private key type, can be {@literal null}.
145+
* @since 2.4
146+
* @throws IllegalStateException if the private key type is {@literal null}
147+
*/
148+
public String getRequiredPrivateKeyType() {
149+
150+
String privateKeyType = getPrivateKeyType();
151+
152+
if (privateKeyType == null) {
153+
throw new IllegalStateException("Private key type is not set");
154+
}
155+
156+
return privateKeyType;
157+
}
158+
159+
/**
160+
* Retrieve the private key as {@link KeySpec}.
140161
* @return the private {@link KeySpec}. {@link java.security.KeyFactory} can generate
141162
* a {@link java.security.PrivateKey} from this {@link KeySpec}.
142163
*/
143164
public KeySpec getPrivateKeySpec() {
144165

145166
try {
146-
byte[] bytes = Base64Utils.decodeFromString(getPrivateKey());
147-
return KeystoreUtil.getRSAPrivateKeySpec(bytes);
167+
return getPrivateKey(getPrivateKey(), getRequiredPrivateKeyType());
148168
}
149-
catch (IOException e) {
169+
catch (IOException | GeneralSecurityException e) {
150170
throw new VaultException("Cannot create KeySpec from private key", e);
151171
}
152172
}
153173

154174
/**
155175
* Create a {@link KeyStore} from this {@link CertificateBundle} containing the
156-
* private key and certificate chain. Only supported if certificate and private key
157-
* are DER-encoded.
176+
* private key and certificate chain.
158177
* @param keyAlias the key alias to use.
159178
* @return the {@link KeyStore} containing the private key and certificate chain.
160179
*/
@@ -164,8 +183,7 @@ public KeyStore createKeyStore(String keyAlias) {
164183

165184
/**
166185
* Create a {@link KeyStore} from this {@link CertificateBundle} containing the
167-
* private key and certificate chain. Only supported if certificate and private key
168-
* are DER-encoded.
186+
* private key and certificate chain.
169187
* @param keyAlias the key alias to use.
170188
* @param includeCaChain whether to include the certificate authority chain instead of
171189
* just the issuer certificate.
@@ -197,8 +215,7 @@ public KeyStore createKeyStore(String keyAlias, boolean includeCaChain) {
197215
}
198216

199217
/**
200-
* Retrieve the issuing CA certificates as list of {@link X509Certificate}. Only
201-
* supported if certificates are DER-encoded.
218+
* Retrieve the issuing CA certificates as list of {@link X509Certificate}.
202219
* @return the issuing CA {@link X509Certificate}.
203220
* @since 2.3.3
204221
*/
@@ -208,8 +225,7 @@ public List<X509Certificate> getX509IssuerCertificates() {
208225

209226
for (String data : caChain) {
210227
try {
211-
byte[] bytes = Base64Utils.decodeFromString(data);
212-
certificates.add(KeystoreUtil.getCertificate(bytes));
228+
certificates.addAll(getCertificates(data));
213229
}
214230
catch (CertificateException e) {
215231
throw new VaultException("Cannot create Certificate from issuing CA certificate", e);
@@ -219,4 +235,41 @@ public List<X509Certificate> getX509IssuerCertificates() {
219235
return certificates;
220236
}
221237

238+
private static KeySpec getPrivateKey(String privateKey, String keyType)
239+
throws GeneralSecurityException, IOException {
240+
241+
Assert.hasText(privateKey, "Private key must not be empty");
242+
Assert.hasText(keyType, "Private key type must not be empty");
243+
244+
if (PemObject.isPemEncoded(privateKey)) {
245+
246+
List<PemObject> pemObjects = PemObject.parse(privateKey);
247+
248+
for (PemObject pemObject : pemObjects) {
249+
250+
if (pemObject.isPrivateKey()) {
251+
return getPrivateKey(pemObject.getContent(), keyType);
252+
}
253+
}
254+
255+
throw new IllegalArgumentException("No private key found in PEM-encoded key spec");
256+
}
257+
258+
return getPrivateKey(Base64Utils.decodeFromString(privateKey), keyType);
259+
}
260+
261+
private static KeySpec getPrivateKey(byte[] privateKey, String keyType)
262+
throws GeneralSecurityException, IOException {
263+
264+
switch (keyType.toLowerCase(Locale.ROOT)) {
265+
case "rsa":
266+
return KeyFactories.RSA_PRIVATE.getKey(privateKey);
267+
case "ec":
268+
return KeyFactories.EC.getKey(privateKey);
269+
}
270+
271+
throw new IllegalArgumentException(
272+
String.format("Key type %s not supported. Supported types are: rsa, ec.", keyType));
273+
}
274+
222275
}

0 commit comments

Comments
 (0)