001/*
002 * Copyright 2024 Björn Kautler
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.KeyStore;
023import java.security.KeyStoreException;
024import java.security.SecureRandom;
025import java.security.UnrecoverableKeyException;
026import java.security.cert.Certificate;
027import java.security.cert.CertificateFactory;
028import java.util.Base64;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033import javax.net.ssl.HttpsURLConnection;
034import javax.net.ssl.KeyManagerFactory;
035import javax.net.ssl.SSLContext;
036
037import net.jsign.DigestAlgorithm;
038
039import static java.nio.charset.StandardCharsets.UTF_8;
040
041/**
042 * Signing service using the Keyfactor SignServer REST API.
043 *
044 * @since 7.0
045 */
046public class SignServerSigningService implements SigningService {
047
048    /** Cache of certificates indexed by alias (worker id or name) */
049    private final Map<String, Certificate[]> certificates = new HashMap<>();
050
051    private final RESTClient client;
052
053    /**
054     * Creates a new SignServer signing service.
055     *
056     * @param endpoint         the SignServer API endpoint (for example <tt>https://example.com/signserver</tt>)
057     * @param credentials      the SignServer credentials
058     */
059    public SignServerSigningService(String endpoint, SignServerCredentials credentials) {
060        this.client = new RESTClient(endpoint)
061                .authentication(conn -> {
062                    if (conn instanceof HttpsURLConnection && credentials.keystore != null) {
063                        try {
064                            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
065                            kmf.init(credentials.keystore.getKeyStore(), ((KeyStore.PasswordProtection) credentials.keystore.getProtectionParameter("")).getPassword());
066
067                            SSLContext context = SSLContext.getInstance("TLS");
068                            context.init(kmf.getKeyManagers(), null, new SecureRandom());
069                            ((HttpsURLConnection) conn).setSSLSocketFactory(context.getSocketFactory());
070                        } catch (GeneralSecurityException e) {
071                            throw new RuntimeException("Unable to load the SignServer client certificate", e);
072                        }
073                    }
074
075                    if (credentials.username != null) {
076                        String httpCredentials = credentials.username + ":" + (credentials.password == null ? "" : credentials.password);
077                        conn.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString(httpCredentials.getBytes(UTF_8)));
078                    }
079                })
080                .errorHandler(response -> (String) response.get("error"));
081    }
082
083    @Override
084    public String getName() {
085        return "SignServer";
086    }
087
088    @Override
089    public List<String> aliases() throws KeyStoreException {
090        return Collections.emptyList();
091    }
092
093    @Override
094    public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
095        if (!certificates.containsKey(alias)) {
096            try {
097                String worker = alias;
098                boolean serverside = false;
099                if (worker.endsWith("|serverside")) {
100                    worker = worker.substring(0, worker.length() - 11);
101                    serverside = true;
102                }
103
104                Map<String, Object> request = new HashMap<>();
105                if (serverside) {
106                    request.put("data", "");
107                    Map<String, String> metadata = new HashMap<>();
108                    metadata.put("USING_CLIENTSUPPLIED_HASH", "false");
109                    request.put("metaData", metadata);
110                } else {
111                    request.put("data", "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=");
112                    request.put("encoding", "BASE64");
113                    Map<String, String> metadata = new HashMap<>();
114                    metadata.put("USING_CLIENTSUPPLIED_HASH", "true");
115                    metadata.put("CLIENTSIDE_HASHDIGESTALGORITHM", "SHA-256");
116                    request.put("metaData", metadata);
117                }
118
119                Map<String, ?> response = client.post("/rest/v1/workers/" + worker + "/process", JsonWriter.format(request));
120                String encodedCertificate = response.get("signerCertificate").toString();
121                byte[] certificateBytes = Base64.getDecoder().decode(encodedCertificate);
122                Certificate certificate = CertificateFactory.getInstance("X.509")
123                        .generateCertificate(new ByteArrayInputStream(certificateBytes));
124                certificates.put(alias, new Certificate[]{certificate});
125            } catch (Exception e) {
126                throw new KeyStoreException("Unable to retrieve the certificate chain '" + alias + "'", e);
127            }
128        }
129
130        return certificates.get(alias);
131    }
132
133    @Override
134    public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
135        try {
136            String algorithm = getCertificateChain(alias)[0].getPublicKey().getAlgorithm();
137            return new SigningServicePrivateKey(alias, algorithm, this);
138        } catch (KeyStoreException e) {
139            throw (UnrecoverableKeyException) new UnrecoverableKeyException().initCause(e);
140        }
141    }
142
143    @Override
144    public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
145        String worker = privateKey.getId();
146        boolean serverside = false;
147        if (worker.endsWith("|serverside")) {
148            worker = worker.substring(0, worker.length() - 11);
149            serverside = true;
150        }
151
152        Map<String, Object> request = new HashMap<>();
153        if (serverside) {
154            request.put("data", Base64.getEncoder().encodeToString(data));
155            Map<String, String> metadata = new HashMap<>();
156            metadata.put("USING_CLIENTSUPPLIED_HASH", "false");
157            request.put("metaData", metadata);
158        } else {
159            DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
160            data = digestAlgorithm.getMessageDigest().digest(data);
161            request.put("data", Base64.getEncoder().encodeToString(data));
162            Map<String, String> metadata = new HashMap<>();
163            metadata.put("USING_CLIENTSUPPLIED_HASH", "true");
164            metadata.put("CLIENTSIDE_HASHDIGESTALGORITHM", digestAlgorithm.id);
165            request.put("metaData", metadata);
166        }
167        request.put("encoding", "BASE64");
168
169        try {
170            Map<String, ?> response = client.post("/rest/v1/workers/" + worker + "/process", JsonWriter.format(request));
171            return Base64.getDecoder().decode((String) response.get("data"));
172        } catch (IOException e) {
173            throw new GeneralSecurityException(e);
174        }
175    }
176}