001/* 002 * Copyright 2024 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.Base64; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034 035import net.jsign.DigestAlgorithm; 036 037/** 038 * Signing service using the Azure Trusted Signing API. 039 * 040 * @since 7.0 041 */ 042public class AzureTrustedSigningService implements SigningService { 043 044 /** Cache of certificate chains indexed by alias */ 045 private final Map<String, Certificate[]> certificates = new HashMap<>(); 046 047 private final RESTClient client; 048 049 /** Timeout in seconds for the signing operation */ 050 private long timeout = 60; 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 public AzureTrustedSigningService(String endpoint, String token) { 070 if (!endpoint.startsWith("http")) { 071 endpoint = "https://" + endpoint; 072 } 073 client = new RESTClient(endpoint) 074 .authentication(conn -> conn.setRequestProperty("Authorization", "Bearer " + token)) 075 .errorHandler(response -> { 076 if (response.containsKey("errorDetail")) { 077 Map error = (Map) response.get("errorDetail"); 078 return error.get("code") + " - " + error.get("message"); 079 } else { 080 String errors = JsonWriter.format(response.get("errors")); 081 return response.get("status") + " - " + response.get("title") + ": " + errors; 082 } 083 }); 084 } 085 086 void setTimeout(int timeout) { 087 this.timeout = timeout; 088 } 089 090 @Override 091 public String getName() { 092 return "TrustedSigning"; 093 } 094 095 @Override 096 public List<String> aliases() throws KeyStoreException { 097 return Collections.emptyList(); 098 } 099 100 @Override 101 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 102 if (!certificates.containsKey(alias)) { 103 try { 104 String account = alias.substring(0, alias.indexOf('/')); 105 String profile = alias.substring(alias.indexOf('/') + 1); 106 SignStatus status = sign(account, profile, "RS256", new byte[32]); 107 certificates.put(alias, status.getCertificateChain().toArray(new Certificate[0])); 108 } catch (Exception e) { 109 throw new KeyStoreException("Unable to retrieve the certificate chain '" + alias + "'", e); 110 } 111 } 112 113 return certificates.get(alias); 114 } 115 116 @Override 117 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 118 return new SigningServicePrivateKey(alias, "RSA", this); 119 } 120 121 @Override 122 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 123 String alg = algorithmMapping.get(algorithm); 124 if (alg == null) { 125 throw new InvalidAlgorithmParameterException("Unsupported signing algorithm: " + algorithm); 126 } 127 128 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 129 data = digestAlgorithm.getMessageDigest().digest(data); 130 131 String alias = privateKey.getId(); 132 String account = alias.substring(0, alias.indexOf('/')); 133 String profile = alias.substring(alias.indexOf('/') + 1); 134 try { 135 SignStatus status = sign(account, profile, alg, data); 136 return status.signature; 137 } catch (IOException e) { 138 throw new GeneralSecurityException(e); 139 } 140 } 141 142 private SignStatus sign(String account, String profile, String algorithm, byte[] data) throws IOException { 143 Map<String, Object> request = new HashMap<>(); 144 request.put("signatureAlgorithm", algorithm); 145 request.put("digest", Base64.getEncoder().encodeToString(data)); 146 147 Map<String, ?> response = client.post("/codesigningaccounts/" + account + "/certificateprofiles/" + profile + "/sign?api-version=2022-06-15-preview", JsonWriter.format(request)); 148 149 String operationId = (String) response.get("operationId"); 150 151 // poll until the operation is completed 152 long startTime = System.currentTimeMillis(); 153 int i = 0; 154 while (System.currentTimeMillis() - startTime < timeout * 1000) { 155 try { 156 Thread.sleep(Math.min(1000, 50 + 10 * i++)); 157 } catch (InterruptedException e) { 158 break; 159 } 160 response = client.get("/codesigningaccounts/" + account + "/certificateprofiles/" + profile + "/sign/" + operationId + "?api-version=2022-06-15-preview"); 161 String status = (String) response.get("status"); 162 if ("InProgress".equals(status)) { 163 continue; 164 } 165 if ("Succeeded".equals(status)) { 166 break; 167 } 168 169 throw new IOException("Signing operation " + operationId + " failed: " + status); 170 } 171 172 if (!"Succeeded".equals(response.get("status"))) { 173 throw new IOException("Signing operation " + operationId + " timed out"); 174 } 175 176 SignStatus status = new SignStatus(); 177 status.signature = Base64.getDecoder().decode((String) response.get("signature")); 178 status.signingCertificate = new String(Base64.getDecoder().decode((String) response.get("signingCertificate"))); 179 180 return status; 181 } 182 183 private static class SignStatus { 184 public byte[] signature; 185 public String signingCertificate; 186 187 public Collection<? extends Certificate> getCertificateChain() throws CertificateException { 188 byte[] cerbin = Base64.getMimeDecoder().decode(signingCertificate); 189 190 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 191 return certificateFactory.generateCertificates(new ByteArrayInputStream(cerbin)); 192 } 193 } 194}