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