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}