001/** 002 * Copyright 2023 Maria Merkel 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.jca; 018 019import java.io.IOException; 020import java.security.GeneralSecurityException; 021import java.security.KeyStoreException; 022import java.security.UnrecoverableKeyException; 023import java.security.cert.Certificate; 024import java.util.ArrayList; 025import java.util.Base64; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.function.Function; 030 031import com.cedarsoftware.util.io.JsonWriter; 032 033import net.jsign.DigestAlgorithm; 034 035/** 036 * Signing service using the HashiCorp Vault API. It supports the Google Cloud KMS secrets engine only. 037 * 038 * @see <a href="https://developer.hashicorp.com/vault/api-docs/secret/gcpkms">HashiCorp Vault API - Google Cloud KMS Secrets Engine</a> 039 * @since 5.0 040 */ 041public class HashiCorpVaultSigningService implements SigningService { 042 043 private final Function<String, Certificate[]> certificateStore; 044 045 /** Cache of private keys indexed by id */ 046 private final Map<String, SigningServicePrivateKey> keys = new HashMap<>(); 047 048 private final RESTClient client; 049 050 /** 051 * Creates a new HashiCorp Vault signing service. 052 * 053 * @param engineURL the URL of the HashiCorp Vault secrets engine 054 * @param token the HashiCorp Vault token 055 * @param certificateStore provides the certificate chain for the keys 056 */ 057 public HashiCorpVaultSigningService(String engineURL, String token, Function<String, Certificate[]> certificateStore) { 058 this.certificateStore = certificateStore; 059 this.client = new RESTClient(engineURL.endsWith("/") ? engineURL : engineURL + "/", conn -> conn.setRequestProperty("Authorization", "Bearer " + token)); 060 } 061 062 @Override 063 public String getName() { 064 return "HashiCorpVault"; 065 } 066 067 /** 068 * Returns the list of key names available in the secrets engine. 069 * 070 * NOTE: This will return the key name only, not the key name and version. 071 * HashiCorp Vault does not provide a function to retrieve the key version. 072 * The key version will need to be appended to the key name when using the key. 073 * 074 * @return list of key names 075 */ 076 @Override 077 public List<String> aliases() throws KeyStoreException { 078 List<String> aliases = new ArrayList<>(); 079 080 try { 081 Map<String, ?> response = client.get("keys?list=true"); 082 Object[] keys = ((Map<String, Object[]>) response.get("data")).get("keys"); 083 for (Object key : keys) { 084 aliases.add((String) key); 085 } 086 } catch (IOException e) { 087 throw new KeyStoreException(e); 088 } 089 090 return aliases; 091 } 092 093 @Override 094 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 095 return certificateStore.apply(alias); 096 } 097 098 @Override 099 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 100 if (keys.containsKey(alias)) { 101 return keys.get(alias); 102 } 103 104 if (!alias.contains(":")) { 105 throw new UnrecoverableKeyException("Unable to fetch HashiCorp Vault Google Cloud private key '" + alias + "' (missing key version)"); 106 } 107 108 String algorithm; 109 110 try { 111 Map<String, ?> response = client.get("keys/" + alias.substring(0, alias.indexOf(":"))); 112 algorithm = ((Map<String, String>) response.get("data")).get("algorithm"); 113 } catch (IOException e) { 114 throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch HashiCorp Vault Google Cloud private key '" + alias + "'").initCause(e); 115 } 116 117 algorithm = algorithm.substring(0, algorithm.indexOf("_")).toUpperCase(); 118 119 SigningServicePrivateKey key = new SigningServicePrivateKey(alias, algorithm); 120 keys.put(alias, key); 121 return key; 122 } 123 124 @Override 125 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 126 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 127 data = digestAlgorithm.getMessageDigest().digest(data); 128 129 String alias = privateKey.getId(); 130 String keyName = alias.substring(0, alias.indexOf(":")); 131 String keyVersion = alias.substring(alias.indexOf(":") + 1); 132 133 Map<String, Object> request = new HashMap<>(); 134 request.put("key_version", keyVersion); 135 request.put("digest", Base64.getEncoder().encodeToString(data)); 136 137 try { 138 Map<String, Object> args = new HashMap<>(); 139 args.put(JsonWriter.TYPE, "false"); 140 Map<String, ?> response = client.post("sign/" + keyName, JsonWriter.objectToJson(request, args)); 141 String signature = ((Map<String, String>) response.get("data")).get("signature"); 142 143 return Base64.getDecoder().decode(signature); 144 } catch (IOException e) { 145 throw new GeneralSecurityException(e); 146 } 147 } 148}