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}