001/* 002 * Copyright 2021 Emmanuel Bourg 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 net.jsign.DigestAlgorithm; 032 033/** 034 * Signing service using the Google Cloud Key Management API. 035 * 036 * <p>The key alias can take one of the following forms:</p> 037 * <ul> 038 * <li>The absolute path of the key with the exact version specified: 039 * <tt>projects/first-rain-123/locations/global/keyRings/mykeyring/cryptoKeys/mykey/cryptoKeyVersions/2</tt></li> 040 * <li>The absolute path of the key without the version specified, the first version enabled will be used: 041 * <tt>projects/first-rain-123/locations/global/keyRings/mykeyring/cryptoKeys/mykey</tt></li> 042 * <li>The path of the key relatively to the keyring with the version specified: <tt>mykey/cryptoKeyVersions/2</tt></li> 043 * <li>The path of the key relatively to the keyring without the version specified: <tt>mykey</tt></li> 044 * </ul> 045 * 046 * @since 4.0 047 * @see <a href="https://cloud.google.com/kms/docs/reference/rest">Cloud Key Management Service (KMS) API</a> 048 */ 049public class GoogleCloudSigningService implements SigningService { 050 051 /** The name of the keyring */ 052 private final String keyring; 053 054 /** Source for the certificates */ 055 private final Function<String, Certificate[]> certificateStore; 056 057 /** Cache of private keys indexed by id */ 058 private final Map<String, SigningServicePrivateKey> keys = new HashMap<>(); 059 060 private final RESTClient client; 061 062 /** 063 * Creates a new Google Cloud signing service. 064 * 065 * @param keyring the path of the keyring (for example <tt>projects/first-rain-123/locations/global/keyRings/mykeyring</tt>) 066 * @param token the Google Cloud API access token 067 * @param certificateStore provides the certificate chain for the keys 068 */ 069 public GoogleCloudSigningService(String keyring, String token, Function<String, Certificate[]> certificateStore) { 070 this("https://cloudkms.googleapis.com/v1/", keyring, token, certificateStore); 071 } 072 073 GoogleCloudSigningService(String endpoint, String keyring, String token, Function<String, Certificate[]> certificateStore) { 074 this.keyring = keyring; 075 this.certificateStore = certificateStore; 076 this.client = new RESTClient(endpoint) 077 .authentication(conn -> conn.setRequestProperty("Authorization", "Bearer " + token)) 078 .errorHandler(response -> { 079 StringBuilder message = new StringBuilder(); 080 if (response.get("error") instanceof Map) { 081 Map error = (Map) response.get("error"); 082 if (error.get("code") != null) { 083 message.append(error.get("code")); 084 } 085 if (error.get("status") != null) { 086 if (message.length() > 0) { 087 message.append(" - "); 088 } 089 message.append(error.get("status")); 090 } 091 if (error.get("message") != null) { 092 if (message.length() > 0) { 093 message.append(": "); 094 } 095 message.append(error.get("message")); 096 } 097 } 098 return message.toString(); 099 }); 100 } 101 102 @Override 103 public String getName() { 104 return "GoogleCloud"; 105 } 106 107 @Override 108 public List<String> aliases() throws KeyStoreException { 109 List<String> aliases = new ArrayList<>(); 110 111 try { 112 Map<String, ?> response = client.get(keyring + "/cryptoKeys"); 113 Object[] cryptoKeys = (Object[]) response.get("cryptoKeys"); 114 for (Object cryptoKey : cryptoKeys) { 115 String name = (String) ((Map) cryptoKey).get("name"); 116 aliases.add(name.substring(name.lastIndexOf("/") + 1)); 117 } 118 } catch (IOException e) { 119 throw new KeyStoreException(e); 120 } 121 122 return aliases; 123 } 124 125 @Override 126 public Certificate[] getCertificateChain(String alias) { 127 return certificateStore.apply(alias); 128 } 129 130 @Override 131 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 132 // check if the alias is absolute or relative to the keyring 133 if (!alias.startsWith("projects/")) { 134 alias = keyring + "/cryptoKeys/" + alias; 135 } 136 137 if (keys.containsKey(alias)) { 138 return keys.get(alias); 139 } 140 141 String algorithm; 142 143 try { 144 if (alias.contains("cryptoKeyVersions")) { 145 // full key with version specified 146 if (alias.contains(":")) { 147 // syntax with the algorithm appended to the alias 148 algorithm = alias.substring(alias.indexOf(':') + 1) + "_SIGN"; 149 alias = alias.substring(0, alias.indexOf(':')); 150 } else { 151 Certificate[] chain = getCertificateChain(alias); 152 if (chain != null && chain.length > 0) { 153 Certificate certificate = chain[0]; 154 algorithm = certificate.getPublicKey().getAlgorithm() + "_SIGN"; 155 } else { 156 Map<String, ?> response = client.get(alias); 157 algorithm = (String) response.get("algorithm"); 158 } 159 } 160 } else { 161 // key version not specified, find the most recent 162 Map<String, ?> response = client.get(alias + "/cryptoKeyVersions?filter=state%3DENABLED"); 163 Object[] cryptoKeyVersions = (Object[]) response.get("cryptoKeyVersions"); 164 if (cryptoKeyVersions == null || cryptoKeyVersions.length == 0) { 165 throw new UnrecoverableKeyException("Unable to fetch Google Cloud private key '" + alias + "', no version found"); 166 } 167 168 Map<String, ?> cryptoKeyVersion = (Map) cryptoKeyVersions[cryptoKeyVersions.length - 1]; 169 alias = (String) cryptoKeyVersion.get("name"); 170 algorithm = (String) cryptoKeyVersion.get("algorithm"); 171 } 172 } catch (IOException e) { 173 throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch Google Cloud private key '" + alias + "'").initCause(e); 174 } 175 176 algorithm = algorithm.substring(0, algorithm.indexOf("_")); // RSA_SIGN_PKCS1_2048_SHA256 -> RSA 177 178 SigningServicePrivateKey key = new SigningServicePrivateKey(alias, algorithm, this); 179 keys.put(alias, key); 180 keys.put(alias.substring(0, alias.indexOf("/cryptoKeyVersions")), key); // cache without the version 181 keys.put(alias + ":" + algorithm, key); // cache with the algorithm appended 182 return key; 183 } 184 185 @Override 186 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 187 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 188 data = digestAlgorithm.getMessageDigest().digest(data); 189 190 Map<String, String> digest = new HashMap<>(); 191 digest.put(digestAlgorithm.name().toLowerCase(), Base64.getEncoder().encodeToString(data)); 192 Map<String, Object> request = new HashMap<>(); 193 request.put("digest", digest); 194 195 try { 196 Map<String, ?> response = client.post(privateKey.getId() + ":asymmetricSign", JsonWriter.format(request)); 197 String signature = (String) response.get("signature"); 198 199 return Base64.getDecoder().decode(signature); 200 } catch (IOException e) { 201 throw new GeneralSecurityException(e); 202 } 203 } 204}