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