001/* 002 * Copyright 2023 Maria Merkel 003 * Copyright 2024 Eatay Mizrachi 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package net.jsign.jca; 019 020import java.io.IOException; 021import java.security.GeneralSecurityException; 022import java.security.KeyStoreException; 023import java.security.UnrecoverableKeyException; 024import java.security.cert.Certificate; 025import java.util.ArrayList; 026import java.util.Base64; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.function.Function; 031 032import net.jsign.DigestAlgorithm; 033 034/** 035 * Signing service using the HashiCorp Vault API. It supports the Google Cloud KMS and Transit secrets engines. 036 * 037 * @see <a href="https://developer.hashicorp.com/vault/api-docs/secret/gcpkms">HashiCorp Vault API - Google Cloud KMS Secrets Engine</a> 038 * @see <a href="https://developer.hashicorp.com/vault/api-docs/secret/transit">HashiCorp Vault API - Transit 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 /** The type of secret engine */ 051 private VaultEngine engine; 052 053 private enum VaultEngine { GCPKMS, TRANSIT } 054 055 /** 056 * Creates a new HashiCorp Vault signing service. 057 * 058 * @param engineURL the URL of the HashiCorp Vault secrets engine 059 * @param token the HashiCorp Vault token 060 * @param certificateStore provides the certificate chain for the keys 061 */ 062 public HashiCorpVaultSigningService(String engineURL, String token, Function<String, Certificate[]> certificateStore) { 063 this.certificateStore = certificateStore; 064 this.client = new RESTClient(engineURL.endsWith("/") ? engineURL : engineURL + "/") 065 .authentication(conn -> conn.setRequestProperty("Authorization", "Bearer " + token)); 066 } 067 068 @Override 069 public String getName() { 070 return "HashiCorpVault"; 071 } 072 073 /** 074 * Returns the list of key names available in the secrets engine. 075 * 076 * NOTE: This will return the key name only, not the key name and version. 077 * HashiCorp Vault does not provide a function to retrieve the key version. 078 * The key version will need to be appended to the key name when using the key. 079 * 080 * @return list of key names 081 */ 082 @Override 083 public List<String> aliases() throws KeyStoreException { 084 List<String> aliases = new ArrayList<>(); 085 086 try { 087 Map<String, ?> response = client.get("keys?list=true"); 088 Object[] keys = ((Map<String, Object[]>) response.get("data")).get("keys"); 089 for (Object key : keys) { 090 aliases.add((String) key); 091 } 092 } catch (IOException e) { 093 throw new KeyStoreException(e); 094 } 095 096 return aliases; 097 } 098 099 @Override 100 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 101 return certificateStore.apply(alias); 102 } 103 104 @Override 105 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 106 if (keys.containsKey(alias)) { 107 return keys.get(alias); 108 } 109 110 if (!alias.contains(":")) { 111 throw new UnrecoverableKeyException("Unable to fetch HashiCorp Vault private key '" + alias + "' (missing key version)"); 112 } 113 114 String algorithm; 115 116 try { 117 Map<String, ?> response = client.get("keys/" + alias.substring(0, alias.indexOf(":"))); 118 Map<String, String> data = (Map<String, String>) response.get("data"); 119 120 if (data.containsKey("algorithm")) { 121 engine = VaultEngine.GCPKMS; 122 // GCPKMS key type format : 'rsa_sign_pkcs1_<BITS>_<DIGEST_ALGORITHM>' 123 algorithm = data.get("algorithm"); 124 algorithm = algorithm.substring(0, algorithm.indexOf("_")).toUpperCase(); 125 } else if (data.containsKey("type")) { 126 engine = VaultEngine.TRANSIT; 127 // Transit key type format : 'rsa-<BITS>' 128 algorithm = data.get("type"); 129 algorithm = algorithm.substring(0, algorithm.indexOf("-")).toUpperCase(); 130 } else { 131 throw new UnrecoverableKeyException("Unsupported secret engine"); 132 } 133 } catch (IOException e) { 134 throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch HashiCorp Vault private key '" + alias + "'").initCause(e); 135 } 136 137 SigningServicePrivateKey key = new SigningServicePrivateKey(alias, algorithm, this); 138 keys.put(alias, key); 139 return key; 140 } 141 142 @Override 143 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 144 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 145 data = digestAlgorithm.getMessageDigest().digest(data); 146 147 String alias = privateKey.getId(); 148 String keyName = alias.substring(0, alias.indexOf(":")); 149 String keyVersion = alias.substring(alias.indexOf(":") + 1); 150 151 Map<String, Object> request = new HashMap<>(); 152 request.put("key_version", keyVersion); 153 154 if (engine == VaultEngine.GCPKMS) { 155 request.put("digest", Base64.getEncoder().encodeToString(data)); 156 } else { 157 request.put("input", Base64.getEncoder().encodeToString(data)); 158 request.put("prehashed", true); 159 request.put("hash_algorithm", getHashAlgorithm(digestAlgorithm)); 160 161 if ("RSA".equals(privateKey.getAlgorithm())) { 162 // RSA signatures in HashiCorp Vault Transit use RSA-PSS by default 163 request.put("signature_algorithm", "pkcs1v15"); 164 } 165 } 166 167 try { 168 Map<String, ?> response = client.post("sign/" + keyName, JsonWriter.format(request)); 169 170 Map<String, String> response_data = (Map<String, String>) response.get("data"); 171 172 String signature; 173 if (engine == VaultEngine.GCPKMS) { 174 // Google Cloud KMS signature format: '<BASE64>' 175 signature = response_data.get("signature"); 176 } else { 177 // Transit signature format: 'vault:v<KEY_VERSION>:<BASE64>' 178 String[] fields = response_data.get("signature").split(":"); 179 signature = fields[2]; 180 } 181 182 return Base64.getDecoder().decode(signature); 183 } catch (IOException e) { 184 throw new GeneralSecurityException(e); 185 } 186 } 187 188 private String getHashAlgorithm(DigestAlgorithm digestAlgorithm) { 189 switch (digestAlgorithm) { 190 case SHA1: 191 return "sha1"; 192 case SHA256: 193 return "sha2-256"; 194 case SHA384: 195 return "sha2-384"; 196 case SHA512: 197 return "sha2-512"; 198 default: 199 throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm); 200 } 201 } 202}