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.KeyStoreException; 023import java.security.UnrecoverableKeyException; 024import java.security.cert.Certificate; 025import java.security.cert.CertificateException; 026import java.security.cert.CertificateFactory; 027import java.util.ArrayList; 028import java.util.Base64; 029import java.util.LinkedHashMap; 030import java.util.List; 031import java.util.Map; 032 033import net.jsign.DigestAlgorithm; 034 035/** 036 * Signing service using the Garantir Remote Signing REST Service API. 037 * 038 * @since 7.0 039 */ 040public class GaraSignSigningService implements SigningService { 041 042 /** Cache of certificates indexed by alias */ 043 private final Map<String, Map<String, ?>> certificates = new LinkedHashMap<>(); 044 045 /** The API endpoint of the Garantir Remote Signing service */ 046 private final String endpoint; 047 048 private final RESTClient client; 049 050 /** The credentials to authenticate with the service */ 051 private final GaraSignCredentials credentials; 052 053 /** Timeout in seconds for the signing operation */ 054 private long timeout = 60 * 60; // one hour 055 056 /** 057 * Creates a new GaraSign signing service. 058 * 059 * @param endpoint the GaraSign API endpoint (for example <tt>https://demo.garantir.io/CodeSigningRestService/</tt>) 060 * @param credentials the GaraSign credentials 061 */ 062 public GaraSignSigningService(String endpoint, GaraSignCredentials credentials) { 063 this.endpoint = endpoint != null ? endpoint : "https://garasign.com:8443/CodeSigningRestService/"; 064 this.credentials = credentials; 065 this.client = new RESTClient(endpoint); 066 } 067 068 void setTimeout(int timeout) { 069 this.timeout = timeout; 070 } 071 072 @Override 073 public String getName() { 074 return "GaraSign"; 075 } 076 077 private void loadKeyStore() throws KeyStoreException { 078 if (certificates.isEmpty()) { 079 try { 080 Map<String, String> params = new LinkedHashMap<>(); 081 params.put("api_version", "1.0"); 082 params.put("session_token", credentials.getSessionToken(endpoint)); 083 084 Map<String, ?> response = client.post("/keystore", params); 085 String status = (String) response.get("status"); 086 if (!"SUCCESS".equals(status)) { 087 throw new KeyStoreException("Unable to retrieve the GaraSign keystore: " + response.get("message")); 088 } 089 090 Object[] keys = (Object[]) response.get("keys"); 091 for (Object key : keys) { 092 String name = (String) ((Map) key).get("name"); 093 certificates.put(name, (Map<String, ?>) key); 094 } 095 } catch (IOException e) { 096 throw new KeyStoreException("Unable to retrieve the GaraSign keystore", e); 097 } 098 } 099 } 100 101 @Override 102 public List<String> aliases() throws KeyStoreException { 103 loadKeyStore(); 104 return new ArrayList<>(certificates.keySet()); 105 } 106 107 @Override 108 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 109 loadKeyStore(); 110 111 Map<String, ?> key = certificates.get(alias); 112 if (key == null) { 113 throw new KeyStoreException("Unable to retrieve GaraSign certificate '" + alias + "'"); 114 } 115 116 Object[] certChain = (Object[]) key.get("certChain"); 117 Certificate[] chain = new Certificate[certChain.length]; 118 119 for (int i = 0; i < certChain.length; i++) { 120 byte[] data = decode((Object[]) certChain[i]); 121 122 try { 123 chain[i] = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(data)); 124 } catch (CertificateException e) { 125 throw new KeyStoreException(e); 126 } 127 } 128 129 return chain; 130 } 131 132 private String getAlgorithm(String alias) throws KeyStoreException { 133 loadKeyStore(); 134 Map<String, ?> key = certificates.get(alias); 135 if (key == null) { 136 return null; 137 } 138 139 return (String) key.get("algorithm"); 140 } 141 142 @Override 143 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 144 try { 145 String algorithm = getAlgorithm(alias); 146 if (algorithm == null) { 147 throw new UnrecoverableKeyException("Unable to fetch GaraSign private key for the certificate '" + alias + "'"); 148 } 149 return new SigningServicePrivateKey(alias, algorithm, this); 150 } catch (KeyStoreException e) { 151 throw (UnrecoverableKeyException) new UnrecoverableKeyException().initCause(e); 152 } 153 } 154 155 @Override 156 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 157 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 158 data = digestAlgorithm.getMessageDigest().digest(data); 159 160 try { 161 Map<String, String> params = new LinkedHashMap<>(); 162 params.put("api_version", "1.0"); 163 params.put("session_token", credentials.getSessionToken(endpoint)); 164 params.put("key_name", privateKey.getId()); 165 params.put("signature_scheme", algorithm); 166 params.put("data_to_sign", Base64.getEncoder().encodeToString(data)); 167 168 Map<String, ?> response = client.post("/sign", params); 169 170 String status = (String) response.get("status"); 171 if ("FAILURE".equals(status)) { 172 throw new IOException("Signing operation failed: " + response.get("message")); 173 } 174 175 String requestId = (String) response.get("requestId"); 176 177 params.put("request_id", requestId); 178 params.remove("key_name"); 179 params.remove("signature_scheme"); 180 params.remove("data_to_sign"); 181 182 String message = null; 183 if ("IN_PROGRESS".equals(status)) { 184 // poll until the operation is completed 185 long startTime = System.currentTimeMillis(); 186 int i = 0; 187 while (System.currentTimeMillis() - startTime < timeout * 1000) { 188 try { 189 Thread.sleep(Math.min(1000, 100 + 100 * i++)); 190 } catch (InterruptedException e) { 191 break; 192 } 193 response = client.post("/sign", params); 194 status = (String) response.get("status"); 195 if ("IN_PROGRESS".equals(status)) { 196 // display the message once if the operation is pending for more than 3 seconds 197 if (System.currentTimeMillis() - startTime > 3000 && response.get("message") != null && !response.get("message").equals(message)) { 198 message = (String) response.get("message"); 199 } 200 continue; 201 } 202 if ("SUCCESS".equals(status)) { 203 break; 204 } 205 206 throw new IOException("Signing operation " + requestId + " failed: " + response.get("message")); 207 } 208 } 209 210 if (!"SUCCESS".equals(response.get("status"))) { 211 throw new IOException("Signing operation " + requestId + " timed out"); 212 } 213 214 return decode((Object[]) response.get("signature")); 215 216 } catch (IOException e) { 217 throw new GeneralSecurityException(e); 218 } 219 } 220 221 private byte[] decode(Object[] array) { 222 byte[] data = new byte[array.length]; 223 for (int i = 0; i < array.length; i++) { 224 data[i] = ((Number) array[i]).byteValue(); 225 } 226 return data; 227 } 228}