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.File; 021import java.io.IOException; 022import java.security.GeneralSecurityException; 023import java.security.KeyStore; 024import java.security.KeyStoreException; 025import java.security.SecureRandom; 026import java.security.UnrecoverableKeyException; 027import java.security.cert.Certificate; 028import java.security.cert.CertificateException; 029import java.security.cert.CertificateFactory; 030import java.util.ArrayList; 031import java.util.Base64; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.regex.Pattern; 036import javax.net.ssl.HttpsURLConnection; 037import javax.net.ssl.KeyManager; 038import javax.net.ssl.KeyManagerFactory; 039import javax.net.ssl.SSLContext; 040import javax.net.ssl.X509KeyManager; 041 042import net.jsign.DigestAlgorithm; 043import net.jsign.KeyStoreBuilder; 044 045/** 046 * DigiCert ONE signing service. 047 * 048 * @since 4.0 049 * @see <a href="https://one.digicert.com/signingmanager/docs/swagger-ui/index.html">Software Trust Manager REST API</a> 050 */ 051public class DigiCertOneSigningService implements SigningService { 052 053 /** Cache of certificates indexed by id and alias */ 054 private final Map<String, Map<String, ?>> certificates = new HashMap<>(); 055 056 private final RESTClient client; 057 058 /** Pattern of a certificate or key identifier */ 059 private static final Pattern ID_PATTERN = Pattern.compile("[0-9a-f\\-]+"); 060 061 /** 062 * Creates a new DigiCert ONE signing service. 063 * 064 * @param apiKey the DigiCert ONE API access token 065 * @param keystore the keystore holding the client certificate to authenticate with the server 066 * @param storepass the password of the keystore 067 */ 068 public DigiCertOneSigningService(String apiKey, File keystore, String storepass) { 069 this(apiKey, (X509KeyManager) getKeyManager(keystore, storepass)); 070 } 071 072 /** 073 * Creates a new DigiCert ONE signing service. 074 * 075 * @param endpoint the URL of the DigiCert ONE host 076 * @param apiKey the DigiCert ONE API access token 077 * @param keystore the keystore holding the client certificate to authenticate with the server 078 * @param storepass the password of the keystore 079 */ 080 public DigiCertOneSigningService(String endpoint, String apiKey, File keystore, String storepass) { 081 this(endpoint, apiKey, (X509KeyManager) getKeyManager(keystore, storepass)); 082 } 083 084 /** 085 * Creates a new DigiCert ONE signing service. 086 * 087 * @param apiKey the DigiCert ONE API access token 088 * @param keyManager the key manager to authenticate the client with the server 089 */ 090 public DigiCertOneSigningService(String apiKey, X509KeyManager keyManager) { 091 this(null, apiKey, keyManager); 092 } 093 094 DigiCertOneSigningService(String endpoint, String apiKey, X509KeyManager keyManager) { 095 if (endpoint == null) { 096 endpoint = "https://clientauth.one.digicert.com"; 097 } 098 this.client = new RESTClient(endpoint + "/signingmanager/api/v1/") 099 .authentication(conn -> { 100 conn.setRequestProperty("x-api-key", apiKey); 101 try { 102 SSLContext context = SSLContext.getInstance("TLS"); 103 context.init(new KeyManager[]{keyManager}, null, new SecureRandom()); 104 if (conn instanceof HttpsURLConnection) { 105 ((HttpsURLConnection) conn).setSSLSocketFactory(context.getSocketFactory()); 106 } 107 } catch (GeneralSecurityException e) { 108 throw new RuntimeException("Unable to load the DigiCert ONE client certificate", e); 109 } 110 }) 111 .errorHandler(response -> { 112 Object error = response.get("error"); 113 if (error instanceof Map) { 114 return ((Map) error).get("status") + ": " + ((Map) error).get("message"); 115 } else if (error instanceof String) { 116 return (String) error; 117 } else { 118 return JsonWriter.format(response); 119 } 120 }); 121 } 122 123 @Override 124 public String getName() { 125 return "DigiCertONE"; 126 } 127 128 /** 129 * Returns the certificate details 130 * 131 * @param alias the id or alias of the certificate 132 */ 133 private Map<String, ?> getCertificateInfo(String alias) throws IOException { 134 if (!certificates.containsKey(alias)) { 135 Map<String, ?> response = client.get("certificates?" + (isIdentifier(alias) ? "id" : "alias") + "=" + alias); 136 for (Object item : (Object[]) response.get("items")) { 137 Map<String, ?> certificate = (Map<String, ?>) item; 138 certificates.put((String) certificate.get("id"), certificate); 139 certificates.put((String) certificate.get("alias"), certificate); 140 } 141 } 142 143 return certificates.get(alias); 144 } 145 146 private boolean isIdentifier(String id) { 147 return ID_PATTERN.matcher(id).matches(); 148 } 149 150 @Override 151 public List<String> aliases() throws KeyStoreException { 152 List<String> aliases = new ArrayList<>(); 153 154 try { 155 Map<String, ?> response = client.get("certificates?limit=100&certificate_status=ACTIVE"); 156 for (Object item : (Object[]) response.get("items")) { 157 Map<String, ?> certificate = (Map<String, ?>) item; 158 certificates.put((String) certificate.get("id"), certificate); 159 certificates.put((String) certificate.get("alias"), certificate); 160 161 aliases.add((String) certificate.get("alias")); 162 } 163 } catch (IOException e) { 164 throw new KeyStoreException("Unable to retrieve DigiCert ONE certificate aliases", e); 165 } 166 167 return aliases; 168 } 169 170 @Override 171 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 172 try { 173 Map<String, ?> response = getCertificateInfo(alias); 174 if (response == null) { 175 throw new KeyStoreException("Unable to retrieve DigiCert ONE certificate '" + alias + "'"); 176 } 177 178 List<String> encodedChain = new ArrayList<>(); 179 encodedChain.add((String) response.get("cert")); 180 181 if (response.get("chain") != null) { 182 for (Object certificate : (Object[]) response.get("chain")) { 183 encodedChain.add(((Map<String, String>) certificate).get("blob")); 184 } 185 } 186 187 List<Certificate> chain = new ArrayList<>(); 188 for (String encodedCertificate : encodedChain) { 189 chain.add(CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(encodedCertificate)))); 190 } 191 return chain.toArray(new Certificate[0]); 192 } catch (IOException | CertificateException e) { 193 throw new KeyStoreException("Unable to retrieve DigiCert ONE certificate '" + alias + "'", e); 194 } 195 } 196 197 @Override 198 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 199 try { 200 Map<String, ?> certificate = getCertificateInfo(alias); 201 Map<String, Object> keypair = (Map<String, Object>) certificate.get("keypair"); 202 String keyId = (String) keypair.get("id"); 203 204 Map<String, ?> response = client.get("/keypairs/" + keyId); 205 String algorithm = (String) response.get("key_alg"); 206 207 SigningServicePrivateKey key = new SigningServicePrivateKey(keyId, algorithm, this); 208 key.getProperties().put("account", response.get("account")); 209 return key; 210 } catch (IOException e) { 211 throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch DigiCert ONE private key for the certificate '" + alias + "'").initCause(e); 212 } 213 } 214 215 @Override 216 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 217 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 218 data = digestAlgorithm.getMessageDigest().digest(data); 219 220 Map<String, Object> request = new HashMap<>(); 221 request.put("account", privateKey.getProperties().get("account")); 222 request.put("sig_alg", algorithm); 223 request.put("hash", Base64.getEncoder().encodeToString(data)); 224 225 try { 226 Map<String, ?> response = client.post("keypairs/" + privateKey.getId() + "/sign", JsonWriter.format(request)); 227 String value = (String) response.get("signature"); 228 229 return Base64.getDecoder().decode(value); 230 } catch (IOException e) { 231 throw new GeneralSecurityException(e); 232 } 233 } 234 235 static KeyManager getKeyManager(File keystoreFile, String storepass) { 236 try { 237 KeyStore keystore = new KeyStoreBuilder().keystore(keystoreFile).storepass(storepass).build(); 238 239 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 240 kmf.init(keystore, storepass.toCharArray()); 241 242 return kmf.getKeyManagers()[0]; 243 } catch (Exception e) { 244 throw new RuntimeException("Failed to load the client certificate for DigiCert ONE", e); 245 } 246 } 247}