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.MessageDigest; 025import java.security.UnrecoverableKeyException; 026import java.security.cert.Certificate; 027import java.security.cert.CertificateException; 028import java.security.cert.CertificateFactory; 029import java.util.ArrayList; 030import java.util.Base64; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034 035import com.cedarsoftware.util.io.JsonWriter; 036 037import net.jsign.DigestAlgorithm; 038 039/** 040 * Signing service using the Azure KeyVault API. 041 * 042 * @since 4.0 043 * @see <a href="https://docs.microsoft.com/en-us/rest/api/keyvault/">Azure Key Vault REST API reference</a> 044 */ 045public class AzureKeyVaultSigningService implements SigningService { 046 047 /** Cache of certificates indexed by alias */ 048 private final Map<String, Map<String, ?>> certificates = new HashMap<>(); 049 050 private final RESTClient client; 051 052 /** 053 * Mapping between Java and Azure signing algorithms. 054 * @see <a href="https://docs.microsoft.com/en-us/rest/api/keyvault/sign/sign#jsonwebkeysignaturealgorithm">Key Vault API - JonWebKeySignatureAlgorithm</a> 055 */ 056 private final Map<String, String> algorithmMapping = new HashMap<>(); 057 { 058 algorithmMapping.put("SHA256withRSA", "RS256"); 059 algorithmMapping.put("SHA384withRSA", "RS384"); 060 algorithmMapping.put("SHA512withRSA", "RS512"); 061 algorithmMapping.put("SHA256withECDSA", "ES256"); 062 algorithmMapping.put("SHA384withECDSA", "ES384"); 063 algorithmMapping.put("SHA512withECDSA", "ES512"); 064 algorithmMapping.put("SHA256withRSA/PSS", "PS256"); 065 algorithmMapping.put("SHA384withRSA/PSS", "PS384"); 066 algorithmMapping.put("SHA512withRSA/PSS", "PS512"); 067 } 068 069 /** 070 * Creates a new Azure Key Vault signing service. 071 * 072 * @param vault the name of the key vault, either the short name (e.g. <tt>myvault</tt>), 073 * or the full URL (e.g. <tt>https://myvault.vault.azure.net</tt>). 074 * @param token the Azure API access token 075 */ 076 public AzureKeyVaultSigningService(String vault, String token) { 077 if (!vault.startsWith("http")) { 078 vault = "https://" + vault + ".vault.azure.net"; 079 } 080 this.client = new RESTClient(vault, conn -> conn.setRequestProperty("Authorization", "Bearer " + token)); 081 } 082 083 @Override 084 public String getName() { 085 return "AzureKeyVault"; 086 } 087 088 /** 089 * Returns the certificate details 090 * 091 * @param alias the alias of the certificate 092 */ 093 private Map<String, ?> getCertificateInfo(String alias) throws IOException { 094 if (!certificates.containsKey(alias)) { 095 Map<String, ?> response = client.get("/certificates/" + alias + "?api-version=7.2"); 096 certificates.put(alias, response); 097 } 098 099 return certificates.get(alias); 100 } 101 102 @Override 103 public List<String> aliases() throws KeyStoreException { 104 List<String> aliases = new ArrayList<>(); 105 106 try { 107 Map<String, ?> response = client.get("/certificates?api-version=7.2"); 108 Object[] certificates = (Object[]) response.get("value"); 109 for (Object certificate : certificates) { 110 String id = (String) ((Map) certificate).get("id"); 111 aliases.add(id.substring(id.lastIndexOf('/') + 1)); 112 } 113 } catch (IOException e) { 114 throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate aliases", e); 115 } 116 117 return aliases; 118 } 119 120 @Override 121 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 122 try { 123 Map<String, ?> response = getCertificateInfo(alias); 124 String pem = (String) response.get("cer"); 125 126 Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(pem))); 127 return new Certificate[]{certificate}; 128 } catch (IOException | CertificateException e) { 129 throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate '" + alias + "'", e); 130 } 131 } 132 133 @Override 134 public SigningServicePrivateKey getPrivateKey(String alias) throws UnrecoverableKeyException { 135 try { 136 Map<String, ?> response = getCertificateInfo(alias); 137 String kid = (String) response.get("kid"); 138 Map policy = (Map) response.get("policy"); 139 Map keyprops = (Map) policy.get("key_props"); 140 String algorithm = ((String) keyprops.get("kty")).replace("-HSM", ""); 141 142 return new SigningServicePrivateKey(kid, algorithm); 143 } catch (IOException e) { 144 throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch Azure Key Vault private key for the certificate '" + alias + "'").initCause(e); 145 } 146 } 147 148 @Override 149 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 150 String alg = algorithmMapping.get(algorithm); 151 if (alg == null) { 152 throw new InvalidAlgorithmParameterException("Unsupported signing algorithm: " + algorithm); 153 } 154 155 MessageDigest digest = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))).getMessageDigest(); 156 data = digest.digest(data); 157 158 Map<String, String> request = new HashMap<>(); 159 request.put("alg", alg); 160 request.put("value", Base64.getEncoder().encodeToString(data)); 161 162 try { 163 Map<String, Object> args = new HashMap<>(); 164 args.put(JsonWriter.TYPE, "false"); 165 Map<String, ?> response = client.post(privateKey.getId() + "/sign?api-version=7.2", JsonWriter.objectToJson(request, args)); 166 String value = (String) response.get("value"); 167 168 return Base64.getUrlDecoder().decode(value); 169 } catch (IOException e) { 170 throw new GeneralSecurityException(e); 171 } 172 } 173}