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.KeyStoreBuilder;
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 or 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, char[] password) 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 = new KeyStoreBuilder().keystore(keystoreFile).storepass(storepass).build();
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}