Skip to content

Commit cfe31c0

Browse files
authored
Add support for credentials of type certificate (#247)
1 parent ce96be0 commit cfe31c0

File tree

4 files changed

+249
-0
lines changed

4 files changed

+249
-0
lines changed

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ To use a different type add a tag called `type` with one of the below values:
259259
- `sshUserPrivateKey` - SSH Private key
260260
- add a tag `username` for the username of the credential
261261
- (optional) add a tag `username-is-secret` and set it to true to hide the username in the build logs
262+
- (optional) add a tag `passphrase-id` that points to the secret name in the vault that has the passphrase that should be used with the ssh keys
263+
- `certificate` - a certificate as secret
264+
- (optional) add tag `password-id` that points to the secret name in the vault that has the password of the certificate.
262265

263266
#### Secret String
264267

@@ -401,6 +404,70 @@ az keyvault secret set --tags type=sshUserPrivateKey username=my-username passph
401404

402405
If the passphrase can not be found in the vault, the secret will not load and a warning will be logged.
403406

407+
#### Certificate
408+
409+
It is possible to load certificates from the vault.
410+
The certificate needs to be stored in a vault-secret **base64-encoded** and **without whitespace**.
411+
In addition to the secret containing the keystore info (the certificate), another vault-secret is needed to store the password of the keystore.
412+
413+
1. If the certificate is protected by a password, Create the password secret:
414+
415+
```bash
416+
az keyvault secret set \
417+
--vault-name my-vault \
418+
--name secret-containing-keystore-password \
419+
--value my-keystore-password
420+
```
421+
422+
2. Store the (**base64-encoded** and **without whitespace**) keystore with the password tag:
423+
424+
if the certificate is protected by a password:
425+
426+
```bash
427+
az keyvault secret set \
428+
--tags type=certificate password-id=secret-containing-keystore-password \
429+
--vault-name my-vault \
430+
--name secret-containing-base64encoded-keystore \
431+
--value {base64-encoded-keystore}
432+
```
433+
434+
if the certificate has no password ( a zero length password )
435+
436+
```bash
437+
az keyvault secret set \
438+
--tags type=certificate \
439+
--vault-name my-vault \
440+
--name secret-containing-base64encoded-keystore \
441+
--value {base64-encoded-keystore}
442+
```
443+
444+
If the tag `password-id` is set but the password can not be found in the vault, the secret will not load and a warning will be logged.
445+
If the keystore cannot be decoded, or cannot be loaded the pipeline using the certificate-credentialsId will throw an error.
446+
447+
448+
Scripted pipeline:
449+
450+
```groovy
451+
node {
452+
withCredentials([
453+
certificate(
454+
credentialsId: certificateCredentialsId,
455+
keystoreVariable: 'PATH_TO_KEYSTORE',
456+
passwordVariable: 'PASSWORD'
457+
)
458+
]) {
459+
sh(
460+
script: """
461+
curl -X POST \
462+
--cert-type P12 \
463+
--cert "\$PATH_TO_KEYSTORE":\$PASSWORD \
464+
${url}
465+
"""
466+
)
467+
}
468+
}
469+
```
470+
404471
#### Secret Labels
405472

406473
You can filter which secrets are visible to the credentials provider.

src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import jenkins.model.Jenkins;
3939
import org.acegisecurity.Authentication;
4040
import org.apache.commons.lang3.StringUtils;
41+
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.certificate.AzureCertificateCredentials;
4142
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.secretfile.AzureSecretFileCredentials;
4243
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.sshuserprivatekey.AzureSSHUserPrivateKeyCredentials;
4344
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string.AzureSecretStringCredentials;
@@ -229,6 +230,27 @@ private static Collection<IdCredentials> fetchCredentials() {
229230
credentials.add(cred);
230231
break;
231232
}
233+
case "certificate": {
234+
String passwordId = tags.get("password-id");
235+
Supplier<Secret> password = () -> Secret.fromString("");
236+
if (StringUtils.isNotBlank(passwordId)) {
237+
try {
238+
password = new KeyVaultSecretRetriever(client, keyVaultURL + "secrets/" + passwordId);
239+
} catch (Exception e) {
240+
LOG.log(Level.WARNING, "Could not find password with ID " + passwordId + " in KeyVault.");
241+
continue;
242+
}
243+
}
244+
AzureCertificateCredentials cred = new AzureCertificateCredentials(
245+
scope,
246+
jenkinsID,
247+
description,
248+
password,
249+
new KeyVaultSecretRetriever(client, id)
250+
);
251+
credentials.add(cred);
252+
break;
253+
}
232254
default: {
233255
throw new IllegalStateException("Unknown type: " + type);
234256
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.certificate;
2+
3+
import com.cloudbees.plugins.credentials.CredentialsProvider;
4+
import com.cloudbees.plugins.credentials.CredentialsScope;
5+
import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;
6+
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
7+
import com.cloudbees.plugins.credentials.impl.Messages;
8+
import edu.umd.cs.findbugs.annotations.CheckForNull;
9+
import edu.umd.cs.findbugs.annotations.NonNull;
10+
import hudson.Extension;
11+
import hudson.Util;
12+
import hudson.util.Secret;
13+
import java.io.ByteArrayInputStream;
14+
import java.security.KeyStore;
15+
import java.util.Base64;
16+
import java.util.Objects;
17+
import java.util.function.Supplier;
18+
import java.util.logging.Level;
19+
import java.util.logging.Logger;
20+
import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider;
21+
import org.jvnet.localizer.ResourceBundleHolder;
22+
23+
public class AzureCertificateCredentials extends BaseStandardCredentials implements StandardCertificateCredentials {
24+
private static final Logger LOG = Logger.getLogger(AzureCertificateCredentials.class.getName());
25+
26+
private final Supplier<Secret> keyStoreSource;
27+
private final Supplier<Secret> password;
28+
29+
public AzureCertificateCredentials(
30+
CredentialsScope scope,
31+
String id,
32+
String description,
33+
Supplier<Secret> password,
34+
Supplier<Secret> keyStoreSource
35+
) {
36+
super(scope, id, description);
37+
38+
Objects.requireNonNull(keyStoreSource);
39+
this.password = password;
40+
this.keyStoreSource = keyStoreSource;
41+
}
42+
43+
/**
44+
* Helper to convert a {@link Secret} password into a {@code char[]}
45+
*
46+
* @param password the password.
47+
* @return a {@code char[]} containing the password or {@code null}
48+
*/
49+
@CheckForNull
50+
private static char[] toCharArray(@NonNull Secret password) {
51+
String plainText = Util.fixEmpty(password.getPlainText());
52+
return plainText == null ? null : plainText.toCharArray();
53+
}
54+
55+
@NonNull
56+
@Override
57+
public KeyStore getKeyStore()
58+
{
59+
KeyStore keyStore;
60+
61+
try {
62+
keyStore = KeyStore.getInstance("PKCS12");
63+
} catch (java.security.KeyStoreException e) {
64+
throw new IllegalStateException("PKCS12 is a keystore type per the JLS spec", e);
65+
}
66+
67+
ByteArrayInputStream keyStoreByteInputStream;
68+
69+
try{
70+
keyStoreByteInputStream = new ByteArrayInputStream(
71+
Base64.getDecoder().decode(
72+
Secret.toString(keyStoreSource.get())
73+
)
74+
);
75+
} catch (java.lang.IllegalArgumentException e) {
76+
LOG.log(Level.WARNING, "Error decoding Keystore. A base64 encoded certificate is expected. Secret ID:" + getId() + ". " + e.getMessage(), e);
77+
throw new IllegalStateException("Cannot decode keystore", e);
78+
}
79+
80+
81+
try {
82+
keyStore.load(keyStoreByteInputStream, toCharArray(getPassword()));
83+
}
84+
catch(
85+
java.security.cert.CertificateException |
86+
java.security.NoSuchAlgorithmException |
87+
java.io.IOException e
88+
){
89+
LOG.log(Level.WARNING, "Error loading Keystore . Secret ID:" + getId() + ". " + e.getMessage(), e);
90+
throw new IllegalStateException("Error loading Keystore.", e);
91+
}
92+
return keyStore;
93+
}
94+
95+
@NonNull
96+
public Secret getKeyStoreSecret() {
97+
return keyStoreSource.get();
98+
}
99+
100+
@NonNull
101+
@Override
102+
public Secret getPassword() {
103+
return password.get();
104+
}
105+
106+
107+
@Extension
108+
@SuppressWarnings("unused")
109+
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
110+
@Override
111+
@NonNull
112+
public String getDisplayName() {
113+
return ResourceBundleHolder.get(Messages.class).format("CertificateCredentialsImpl.DisplayName");
114+
}
115+
116+
@Override
117+
public String getIconClassName() {
118+
return "icon-application-certificate";
119+
}
120+
121+
@Override
122+
public boolean isApplicable(CredentialsProvider provider) {
123+
return provider instanceof AzureCredentialsProvider;
124+
}
125+
}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.certificate;
2+
3+
import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker;
4+
import hudson.Extension;
5+
import hudson.util.Secret;
6+
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.Snapshot;
7+
8+
@Extension
9+
@SuppressWarnings("unused")
10+
public class AzureCertificateCredentialsSnapshotTaker extends CredentialsSnapshotTaker<AzureCertificateCredentials> {
11+
@Override
12+
public Class<AzureCertificateCredentials> type() {
13+
return AzureCertificateCredentials.class;
14+
}
15+
16+
@Override
17+
public AzureCertificateCredentials snapshot(AzureCertificateCredentials credential) {
18+
SecretSnapshot keyStoreSnapshot = new SecretSnapshot(credential.getKeyStoreSecret());
19+
SecretSnapshot passwordSnapshot = new SecretSnapshot(credential.getPassword());
20+
return new AzureCertificateCredentials(
21+
credential.getScope(),
22+
credential.getId(),
23+
credential.getDescription(),
24+
passwordSnapshot,
25+
keyStoreSnapshot
26+
);
27+
}
28+
29+
private static class SecretSnapshot extends Snapshot<Secret> {
30+
SecretSnapshot(Secret value) {
31+
super(value);
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)