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.ByteArrayInputStream; 020import java.io.IOException; 021import java.security.GeneralSecurityException; 022import java.security.InvalidAlgorithmParameterException; 023import java.security.KeyStoreException; 024import java.security.UnrecoverableKeyException; 025import java.security.cert.Certificate; 026import java.security.cert.CertificateException; 027import java.security.cert.CertificateFactory; 028import java.util.ArrayList; 029import java.util.Base64; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033 034import org.bouncycastle.asn1.DERNull; 035import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 036import org.bouncycastle.asn1.x509.DigestInfo; 037 038import net.jsign.DigestAlgorithm; 039 040/** 041 * Signing service using the Azure KeyVault API. 042 * 043 * @since 4.0 044 * @see <a href="https://docs.microsoft.com/en-us/rest/api/keyvault/">Azure Key Vault REST API reference</a> 045 */ 046public class AzureKeyVaultSigningService implements SigningService { 047 048 /** Cache of certificates indexed by alias */ 049 private final Map<String, Map<String, ?>> certificates = new HashMap<>(); 050 051 private final RESTClient client; 052 053 /** 054 * Mapping between Java and Azure signing algorithms. 055 * @see <a href="https://docs.microsoft.com/en-us/rest/api/keyvault/sign/sign#jsonwebkeysignaturealgorithm">Key Vault API - JonWebKeySignatureAlgorithm</a> 056 */ 057 private final Map<String, String> algorithmMapping = new HashMap<>(); 058 { 059 algorithmMapping.put("SHA1withRSA", "RSNULL"); 060 algorithmMapping.put("SHA256withRSA", "RS256"); 061 algorithmMapping.put("SHA384withRSA", "RS384"); 062 algorithmMapping.put("SHA512withRSA", "RS512"); 063 algorithmMapping.put("SHA256withECDSA", "ES256"); 064 algorithmMapping.put("SHA384withECDSA", "ES384"); 065 algorithmMapping.put("SHA512withECDSA", "ES512"); 066 algorithmMapping.put("SHA256withRSA/PSS", "PS256"); 067 algorithmMapping.put("SHA384withRSA/PSS", "PS384"); 068 algorithmMapping.put("SHA512withRSA/PSS", "PS512"); 069 } 070 071 /** 072 * Creates a new Azure Key Vault signing service. 073 * 074 * @param vault the name of the key vault, either the short name (e.g. <tt>myvault</tt>), 075 * or the full URL (e.g. <tt>https://myvault.vault.azure.net</tt>). 076 * @param token the Azure API access token 077 */ 078 public AzureKeyVaultSigningService(String vault, String token) { 079 if (!vault.startsWith("http")) { 080 vault = "https://" + vault + ".vault.azure.net"; 081 } 082 this.client = new RESTClient(vault) 083 .authentication(conn -> conn.setRequestProperty("Authorization", "Bearer " + token)) 084 .errorHandler(response -> { 085 Map error = (Map) response.get("error"); 086 return error.get("code") + ": " + error.get("message"); 087 }); 088 } 089 090 @Override 091 public String getName() { 092 return "AzureKeyVault"; 093 } 094 095 /** 096 * Returns the certificate details 097 * 098 * @param alias the alias of the certificate 099 */ 100 private Map<String, ?> getCertificateInfo(String alias) throws IOException { 101 if (!certificates.containsKey(alias)) { 102 Map<String, ?> response = client.get("/certificates/" + alias + "?api-version=7.2"); 103 certificates.put(alias, response); 104 } 105 106 return certificates.get(alias); 107 } 108 109 @Override 110 public List<String> aliases() throws KeyStoreException { 111 List<String> aliases = new ArrayList<>(); 112 113 try { 114 Map<String, ?> response = client.get("/certificates?api-version=7.2"); 115 Object[] certificates = (Object[]) response.get("value"); 116 for (Object certificate : certificates) { 117 String id = (String) ((Map) certificate).get("id"); 118 aliases.add(id.substring(id.lastIndexOf('/') + 1)); 119 } 120 } catch (IOException e) { 121 // return an empty list when called from the jarsigner JDK tool, because jarsigner fetches the aliases 122 // even if unnecessary for signing and this requires extra permissions on the Azure account (see #219) 123 if (!isCalledByJarSigner(e.getStackTrace())) { 124 throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate aliases", e); 125 } 126 } 127 128 return aliases; 129 } 130 131 /** 132 * Checks the stacktrace and tells if the calling class is the jarsigner tool. 133 */ 134 private boolean isCalledByJarSigner(StackTraceElement[] trace) { 135 for (StackTraceElement element : trace) { 136 if (element.getClassName().contains("jarsigner")) { 137 return true; 138 } 139 } 140 return false; 141 } 142 143 @Override 144 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 145 try { 146 Map<String, ?> response = getCertificateInfo(alias); 147 String pem = (String) response.get("cer"); 148 149 Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(pem))); 150 return new Certificate[]{certificate}; 151 } catch (IOException | CertificateException e) { 152 if (e.getMessage() != null && e.getMessage().contains("was not found in this key vault")) { 153 return null; 154 } else { 155 throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate '" + alias + "'", e); 156 } 157 } 158 } 159 160 @Override 161 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 162 try { 163 Map<String, ?> response = getCertificateInfo(alias); 164 String kid = (String) response.get("kid"); 165 Map policy = (Map) response.get("policy"); 166 Map keyprops = (Map) policy.get("key_props"); 167 String algorithm = ((String) keyprops.get("kty")).replace("-HSM", ""); 168 169 return new SigningServicePrivateKey(kid, algorithm, this); 170 } catch (IOException e) { 171 throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch Azure Key Vault private key for the certificate '" + alias + "'").initCause(e); 172 } 173 } 174 175 @Override 176 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 177 String alg = algorithmMapping.get(algorithm); 178 if (alg == null) { 179 throw new InvalidAlgorithmParameterException("Unsupported signing algorithm: " + algorithm); 180 } 181 182 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 183 data = digestAlgorithm.getMessageDigest().digest(data); 184 185 if (alg.equals("RSNULL")) { 186 AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); 187 DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, data); 188 try { 189 data = digestInfo.getEncoded("DER"); 190 } catch (IOException e) { 191 throw new GeneralSecurityException(e); 192 } 193 } 194 195 Map<String, String> request = new HashMap<>(); 196 request.put("alg", alg); 197 request.put("value", Base64.getEncoder().encodeToString(data)); 198 199 try { 200 Map<String, ?> response = client.post(privateKey.getId() + "/sign?api-version=7.2", JsonWriter.format(request)); 201 String value = (String) response.get("value"); 202 203 return Base64.getUrlDecoder().decode(value); 204 } catch (IOException e) { 205 throw new GeneralSecurityException(e); 206 } 207 } 208}