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