Skip to content

Commit

Permalink
Added support for Google Cloud KMS via HashiCorp Vault (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
MariaMerkel authored and ebourg committed May 11, 2023
1 parent 214413c commit c592716
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 5 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Jsign is free to use and licensed under the [Apache License version 2.0](https:/
* Keystores supported:
* PKCS#12, JKS and JCEKS files
* PKCS#11 hardware tokens ([YubiKey](https://www.yubico.com), [Nitrokey](https://www.nitrokey.com), etc)
* Cloud key management systems ([AWS KMS](https://aws.amazon.com/kms/), [Azure Key Vault](https://azure.microsoft.com/services/key-vault/), [DigiCert ONE](https://one.digicert.com), [Google Cloud KMS](https://cloud.google.com/security-key-management), [SSL.com eSigner](https://www.ssl.com/esigner/))
* Cloud key management systems ([AWS KMS](https://aws.amazon.com/kms/), [Azure Key Vault](https://azure.microsoft.com/services/key-vault/), [DigiCert ONE](https://one.digicert.com), [Google Cloud KMS](https://cloud.google.com/security-key-management), [SSL.com eSigner](https://www.ssl.com/esigner/), [HashiCorp Vault](https://www.vaultproject.io/))
* Private key formats: PVK and PEM (PKCS#1 and PKCS#8), encrypted or not
* Certificates: PKCS#7 in PEM and DER format
* Build tools integration (Maven, Gradle, Ant)
Expand All @@ -46,6 +46,7 @@ See https://ebourg.github.io/jsign for more information.
* Nitrokey support has been improved with automatic PKCS#11 configuration using the new `NITROKEY` storetype
* Smart cards are now supported with the new `OPENSC` storetype
* OpenPGP cards are now supported with the new `OPENPGP` storetype
* Google Cloud KMS via HashiCorp Vault is now supported with the new `HASHICORPVAULT` storetype (contributed by Maria Merkel)
* The Maven plugin can now use passwords defined in the Maven settings.xml file
* The "X.509 Certificate for PIV Authentication" on a Yubikey (slot 9a) is now automatically detected
* SHA-1 signing with Azure Key Vault is now possible (contributed by Andrij Abyzov)
Expand Down
19 changes: 17 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ <h3>Features</h3>
<ul>
<li>PKCS#12, JKS and JCEKS files</li>
<li>PKCS#11 hardware tokens (<a href="https://www.yubico.com">YubiKey</a>, <a href="https://www.nitrokey.com">Nitrokey</a>, etc)</li>
<li>Cloud key management systems (<a href="https://aws.amazon.com/kms/">AWS KMS</a>, <a href="https://azure.microsoft.com/services/key-vault/">Azure Key Vault</a>, <a href="https://one.digicert.com">DigiCert ONE</a>, <a href="https://cloud.google.com/security-key-management">Google Cloud KMS</a>, <a href="https://www.ssl.com/esigner/">SSL.com eSigner</a>)</li>
<li>Cloud key management systems (<a href="https://aws.amazon.com/kms/">AWS KMS</a>, <a href="https://azure.microsoft.com/services/key-vault/">Azure Key Vault</a>, <a href="https://one.digicert.com">DigiCert ONE</a>, <a href="https://cloud.google.com/security-key-management">Google Cloud KMS</a>, <a href="https://www.ssl.com/esigner/">SSL.com eSigner</a>, <a href="https://www.vaultproject.io">HashiCorp Vault</a>)</li>
</ul>
<li>Private key formats: PVK and PEM (PKCS#1 and PKCS#8), encrypted or not</li>
<li>Certificates: PKCS#7 in PEM and DER format</li>
Expand Down Expand Up @@ -180,6 +180,7 @@ <h4 class="mobile-only">Attributes</h4>
<li><code>DIGICERTONE</code>: DigiCert ONE Secure Software Manager</li>
<li><code>ESIGNER</code>: SSL.com eSigner</li>
<li><code>GOOGLECLOUD</code>: Google Cloud KMS</li>
<li><code>HASHICORPVAULT</code>: Google Cloud KMS via HashiCorp Vault</li>
</ul>
</td>
<td class="required">No, automatically detected for file based keystores.</td>
Expand Down Expand Up @@ -448,6 +449,7 @@ <h3>Command Line Tool</h3>
- DIGICERTONE: DigiCert ONE Secure Software Manager
- ESIGNER: SSL.com eSigner
- GOOGLECLOUD: Google Cloud KMS
- HASHICORPVAULT: Google Cloud KMS via HashiCorp Vault
-a,--alias &lt;NAME> The alias of the certificate used for signing in the keystore.
--keypass &lt;PASSWORD> The password of the private key. When using a keystore,
this parameter can be omitted if the keystore shares the
Expand Down Expand Up @@ -600,6 +602,19 @@ <h4 id="example-googlecloud">Example using Google Cloud KMS:</h4>
--alias test --certfile full-chain.pem application.exe
</pre>

<h4 id="example-hashicorpvault">Example using Google Cloud KMS via HashiCorp Vault:</h4>

<p>Google Cloud KMS stores only the private key, the certificate must be provided separately. The keystore parameter
references the URL of the HashiCorp Vault secrets engine, consisting of the Vault server URL, the API version v1 and
the secrets engine path. The alias specifies the name of the key in Vault and the key version in Google Cloud separated
by a colon character.</p>

<pre>
jsign --storetype HASHICORPVAULT --storepass &lt;vault-token&gt; \
--keystore https://vault.example.com/v1/gcpkms \
--alias test:1 --certfile full-chain.pem application.exe
</pre>


<h3>API</h3>

Expand Down Expand Up @@ -649,7 +664,7 @@ <h3>Credits</h3>
PVK parsing is based on the <a href="https://web.archive.org/web/20170810033553/http://www.drh-consultancy.demon.co.uk/pvk.html">pvktool</a> by Stephen N Henson.<br>
MSI signing was possible thanks to the work done by the <a href="https://github.com/mtrojnar/osslsigncode">osslsigncode</a> and <a href="https://poi.apache.org/">Apache POI</a> projects.</p>

<p>Jsign includes contributions from Emmanuel Bourg, Florent Daigniere, Michael Szediwy, Michael Peterson, Markus Kilås, Erwin Tratar, Björn Kautler and Joseph Lee.</p>
<p>Jsign includes contributions from Emmanuel Bourg, Florent Daigniere, Michael Szediwy, Michael Peterson, Markus Kilås, Erwin Tratar, Björn Kautler, Joseph Lee and Maria Merkel.</p>

<h3>Contact</h3>

Expand Down
3 changes: 2 additions & 1 deletion jsign-cli/src/main/java/net/jsign/JsignCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ public static void main(String... args) {
+ "- AZUREKEYVAULT: Azure Key Vault key management system\n"
+ "- DIGICERTONE: DigiCert ONE Secure Software Manager\n"
+ "- ESIGNER: SSL.com eSigner\n"
+ "- GOOGLECLOUD: Google Cloud KMS\n").build());
+ "- GOOGLECLOUD: Google Cloud KMS\n"
+ "- HASHICORPVAULT: Google Cloud KMS via HashiCorp Vault\n").build());
options.addOption(Option.builder("a").hasArg().longOpt(PARAM_ALIAS).argName("NAME").desc("The alias of the certificate used for signing in the keystore.").build());
options.addOption(Option.builder().hasArg().longOpt(PARAM_KEYPASS).argName("PASSWORD").desc("The password of the private key. When using a keystore, this parameter can be omitted if the keystore shares the same password.").build());
options.addOption(Option.builder().hasArg().longOpt(PARAM_KEYFILE).argName("FILE").desc("The file containing the private key. PEM and PVK files are supported. ").type(File.class).build());
Expand Down
28 changes: 28 additions & 0 deletions jsign-core/src/main/java/net/jsign/KeyStoreType.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import net.jsign.jca.DigiCertOneSigningService;
import net.jsign.jca.ESignerSigningService;
import net.jsign.jca.GoogleCloudSigningService;
import net.jsign.jca.HashiCorpVaultSigningService;
import net.jsign.jca.OpenPGPCardSigningService;
import net.jsign.jca.SigningServiceJcaProvider;

Expand Down Expand Up @@ -327,6 +328,33 @@ Provider getProvider(KeyStoreBuilder params) {
}
}));
}
},

/** HashiCorp Vault secrets engine (GCP only) */
HASHICORPVAULT(false, false, false) {
@Override
void validate(KeyStoreBuilder params) {
if (params.keystore() == null) {
throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the HashiCorp Vault secrets engine URL");
}
if (params.storepass() == null) {
throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the HashiCorp Vault token");
}
if (params.certfile() == null) {
throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set");
}
}

@Override
Provider getProvider(KeyStoreBuilder params) {
return new SigningServiceJcaProvider(new HashiCorpVaultSigningService(params.keystore(), params.storepass(), alias -> {
try {
return CertificateUtils.loadCertificateChain(params.certfile());
} catch (IOException | CertificateException e) {
throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e);
}
}));
}
};


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Copyright 2023 Maria Merkel
*
* 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 net.jsign.jca;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import com.cedarsoftware.util.io.JsonWriter;

import net.jsign.DigestAlgorithm;

/**
* Signing service using the HashiCorp Vault API.
*
* @see <a href="https://developer.hashicorp.com/vault/api-docs/secret/gcpkms">HashiCorp Vault API - Google Cloud KMS Secrets Engine</a>
* @since 5.0
*/
public class HashiCorpVaultSigningService implements SigningService {

private final Function<String, Certificate[]> certificateStore;

/** Cache of private keys indexed by id */
private final Map<String, SigningServicePrivateKey> keys = new HashMap<>();

private final RESTClient client;

/**
* Creates a new HashiCorp Vault signing service.
*
* @param engineURL the URL of the HashiCorp Vault secrets engine
* @param token the HashiCorp Vault token
* @param certificateStore provides the certificate chain for the keys
*/
public HashiCorpVaultSigningService(String engineURL, String token, Function<String, Certificate[]> certificateStore) {
this.certificateStore = certificateStore;
this.client = new RESTClient(engineURL.endsWith("/") ? engineURL : engineURL + "/", conn -> conn.setRequestProperty("Authorization", "Bearer " + token));
}

@Override
public String getName() {
return "HashiCorpVault";
}

/**
* Returns the list of key names available in the secrets engine.
*
* NOTE: This will return the key name only, not the key name and version.
* HashiCorp Vault does not provide a function to retrieve the key version.
* The key version will need to be appended to the key name when using the key.
*
* @return list of key names
*/
@Override
public List<String> aliases() throws KeyStoreException {
List<String> aliases;

try {
Map<String, ?> response = client.get("keys?list=true");
String[] keys = ((Map<String, String[]>) response.get("data")).get("keys");
aliases = Arrays.asList(keys);
} catch (IOException e) {
throw new KeyStoreException(e);
}

return aliases;
}

@Override
public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
return certificateStore.apply(alias);
}

@Override
public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
if (keys.containsKey(alias)) {
return keys.get(alias);
}

if (!alias.contains(":")) {
throw new UnrecoverableKeyException("Unable to fetch HashiCorp Vault Google Cloud private key '" + alias + "' (missing key version)");
}

String algorithm;

try {
Map<String, ?> response = client.get("keys/" + alias.substring(0, alias.indexOf(":")));
algorithm = ((Map<String, String>) response.get("data")).get("algorithm");
} catch (IOException e) {
throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch HashiCorp Vault Google Cloud private key '" + alias + "'").initCause(e);
}

algorithm = algorithm.substring(0, algorithm.indexOf("_")).toUpperCase();

SigningServicePrivateKey key = new SigningServicePrivateKey(alias, algorithm);
keys.put(alias, key);
return key;
}

@Override
public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
data = digestAlgorithm.getMessageDigest().digest(data);

String alias = privateKey.getId();
String keyName = alias.substring(0, alias.indexOf(":"));
String keyVersion = alias.substring(alias.indexOf(":") + 1);

Map<String, Object> request = new HashMap<>();
request.put("key_version", keyVersion);
request.put("digest", Base64.getEncoder().encodeToString(data));

try {
Map<String, Object> args = new HashMap<>();
args.put(JsonWriter.TYPE, "false");
Map<String, ?> response = client.post("sign/" + keyName, JsonWriter.objectToJson(request, args));
String signature = ((Map<String, String>) response.get("data")).get("signature");

return Base64.getDecoder().decode(signature);
} catch (IOException e) {
throw new GeneralSecurityException(e);
}
}
}
35 changes: 35 additions & 0 deletions jsign-core/src/test/java/net/jsign/KeyStoreBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,41 @@ public void testBuildGoogleCloud() throws Exception {
assertNotNull("keystore", keystore);
}

@Test
public void testBuildHashiCorpVault() throws Exception {
KeyStoreBuilder builder = new KeyStoreBuilder().storetype(HASHICORPVAULT);

try {
builder.build();
fail("Exception not thrown");
} catch (IllegalArgumentException e) {
assertEquals("message", "keystore parameter must specify the HashiCorp Vault secrets engine URL", e.getMessage());
}

builder.keystore("https://vault.example.com:8200/v1/gcpkms/");

try {
builder.build();
fail("Exception not thrown");
} catch (IllegalArgumentException e) {
assertEquals("message", "storepass parameter must specify the HashiCorp Vault token", e.getMessage());
}

builder.storepass("0123456789ABCDEF");

try {
builder.build();
fail("Exception not thrown");
} catch (IllegalArgumentException e) {
assertEquals("message", "certfile parameter must be set", e.getMessage());
}

builder.certfile("keystores/jsign-test-certificate.pem");

KeyStore keystore = builder.build();
assertNotNull("keystore", keystore);
}

@Test
public void testBuildJKS() throws Exception {
KeyStoreBuilder builder = new KeyStoreBuilder().storetype(JKS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ _jsign()
return 0
;;
--storetype)
COMPREPLY=( $( compgen -W 'JKS JCEKS PKCS12 PKCS11 AWS AZUREKEYVAULT DIGICERTONE ESIGNER GOOGLECLOUD YUBIKEY NITROKEY OPENPGP OPENSC' -- "$cur" ) )
COMPREPLY=( $( compgen -W 'JKS JCEKS PKCS12 PKCS11 AWS AZUREKEYVAULT DIGICERTONE ESIGNER GOOGLECLOUD HASHICORPVAULT YUBIKEY NITROKEY OPENPGP OPENSC' -- "$cur" ) )
return 0
;;
--storepass|-a|--alias|--keypass|-t|--tsaurl|-r|--tsretries|-w|--tsretrywait|-n|--name|-u|--url|-e|--encoding)
Expand Down
13 changes: 13 additions & 0 deletions jsign/src/deb/data/usr/share/man/man1/jsign.1
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ The type of the keystore:
.br
- GOOGLECLOUD : Google Cloud KMS
.br
- HASHICORPVAULT : Google Cloud KMS via HashiCorp Vault
.br
This option is not required for file based keystores (JKS, JCEKS and PKCS12).

.TP
Expand Down Expand Up @@ -292,6 +294,17 @@ jsign --storetype GOOGLECLOUD --storepass <api-access-token> \\
--keystore projects/first-rain-123/locations/global/keyRings/mykeyring \\
--alias test --certfile full-chain.pem application.exe

Signing with Google Cloud KMS via HashiCorp Vault:

Google Cloud KMS stores only the private key, the certificate must be provided separately. The keystore parameter
references the URL of the HashiCorp Vault secrets engine, consisting of the Vault server URL, the API version v1
and the secrets engine path. The alias specifies the name of the key in Vault and the key version in Google Cloud
separated by a colon character.

jsign --storetype HASHICORPVAULT --storepass <vault-token> \\
--keystore https://vault.example.com/v1/gcpkms \\
--alias test:1 --certfile full-chain.pem application.exe


.SH REPORTING BUGS
Bugs and suggestions can be reported to the project home page: https://ebourg.github.io/jsign
Expand Down

0 comments on commit c592716

Please sign in to comment.