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.IOException;
021import java.nio.ByteBuffer;
022import java.security.GeneralSecurityException;
023import java.security.KeyStoreException;
024import java.security.MessageDigest;
025import java.security.UnrecoverableKeyException;
026import java.security.cert.Certificate;
027import java.security.cert.CertificateException;
028import java.security.cert.CertificateFactory;
029import java.util.ArrayList;
030import java.util.Base64;
031import java.util.HashMap;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.stream.Collectors;
036import java.util.stream.Stream;
037import javax.crypto.Mac;
038import javax.crypto.spec.SecretKeySpec;
039
040import com.cedarsoftware.util.io.JsonWriter;
041import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
042
043import net.jsign.DigestAlgorithm;
044
045/**
046 * SSL.com eSigner signing service.
047 *
048 * @see <a href="https://www.ssl.com/guide/integration-guide-testing-remote-signing-with-esigner-csc-api/">Integration Guide to Testing Remote Signing with eSigner CSC API</a>
049 * @see <a href="https://www.ssl.com/guide/esigner-demo-credentials-and-certificates/">eSigner Demo Credentials and Certificates</a>
050 * @see <a href="https://cloudsignatureconsortium.org/wp-content/uploads/2020/05/CSC_API_V0_0.1.7.9.pdf">CSC API specifications (version 0.1.7.9)</a>
051 * @since 4.1
052 */
053public class ESignerSigningService implements SigningService {
054
055    /** Cache of certificates indexed by alias */
056    private final Map<String, Map<String, ?>> certificates = new HashMap<>();
057
058    private final RESTClient client;
059
060    public ESignerSigningService(String endpoint, String username, String password) throws IOException {
061        this(endpoint, getAccessToken(endpoint.contains("-try.ssl.com") ? "https://oauth-sandbox.ssl.com" : "https://login.ssl.com",
062                endpoint.contains("-try.ssl.com") ? "qOUeZCCzSqgA93acB3LYq6lBNjgZdiOxQc-KayC3UMw" : "kaXTRACNijSWsFdRKg_KAfD3fqrBlzMbWs6TwWHwAn8",
063                username, password));
064    }
065
066    public ESignerSigningService(String endpoint, String accessToken) {
067        client = new RESTClient(endpoint, conn -> conn.setRequestProperty("Authorization", "Bearer " + accessToken));
068    }
069
070    private static String getAccessToken(String endpoint, String clientId, String username, String password) throws IOException {
071        Map<String, String> request = new LinkedHashMap<>();
072        request.put("client_id", clientId);
073        request.put("grant_type", "password");
074        request.put("username", username);
075        request.put("password", password);
076
077        RESTClient client = new RESTClient(endpoint);
078        Map<String, ?> response = client.post("/oauth2/token", JsonWriter.objectToJson(request));
079        return (String) response.get("access_token");
080    }
081
082    @Override
083    public String getName() {
084        return "ESIGNER";
085    }
086
087    @Override
088    public List<String> aliases() throws KeyStoreException {
089        try {
090            Map<String, String> request = new HashMap<>();
091            request.put("clientData", "EVCS");
092            Map<String, ?> response = client.post("/csc/v0/credentials/list", JsonWriter.objectToJson(request));
093            Object[] credentials = (Object[]) response.get("credentialIDs");
094            return Stream.of(credentials).map(Object::toString).collect(Collectors.toList());
095        } catch (IOException e) {
096            throw new KeyStoreException("Unable to retrieve SSL.com certificate aliases", e);
097        }
098    }
099
100    /**
101     * Returns the certificate details
102     *
103     * @param alias the alias of the certificate
104     */
105    private Map<String, ?> getCertificateInfo(String alias) throws IOException {
106        if (!certificates.containsKey(alias)) {
107            Map<String, String> request = new HashMap<>();
108            request.put("credentialID", alias);
109            request.put("certificates", "chain");
110            Map<String, ?> response = client.post("/csc/v0/credentials/info", JsonWriter.objectToJson(request));
111            certificates.put(alias, (Map) response.get("cert"));
112        }
113
114        return certificates.get(alias);
115    }
116
117    @Override
118    public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
119        try {
120            Map<String, ?> cert = getCertificateInfo(alias);
121            Object[] encodedChain = (Object[]) cert.get("certificates");
122
123            List<Certificate> chain = new ArrayList<>();
124            for (Object encodedCertificate : encodedChain) {
125                chain.add(CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(encodedCertificate.toString()))));
126            }
127            return chain.toArray(new Certificate[0]);
128        } catch (IOException | CertificateException e) {
129            throw new KeyStoreException("Unable to retrieve SSL.com certificate '" + alias + "'", e);
130        }
131    }
132
133    @Override
134    public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
135        try {
136            Certificate[] chain = getCertificateChain(alias);
137            String algorithm = chain[0].getPublicKey().getAlgorithm();
138            SigningServicePrivateKey key = new SigningServicePrivateKey(alias, algorithm);
139            if (password != null) {
140                key.getProperties().put("totpsecret", new String(password));
141            }
142            return key;
143        } catch (KeyStoreException e) {
144            throw (UnrecoverableKeyException) new UnrecoverableKeyException().initCause(e);
145        }
146    }
147
148    @Override
149    public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
150        MessageDigest digest = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))).getMessageDigest();
151        data = digest.digest(data);
152        String hash = Base64.getEncoder().encodeToString(data);
153
154        Map<String, Object>  request = new LinkedHashMap<>();
155        request.put("credentialID", privateKey.getId());
156        request.put("SAD", getSignatureActivationData(privateKey, hash));
157        request.put("hash", new String[] { hash });
158        request.put("signAlgo", new DefaultSignatureAlgorithmIdentifierFinder().find(algorithm).getAlgorithm().getId());
159
160        Map<String, Object> args = new HashMap<>();
161        args.put(JsonWriter.TYPE, "false");
162        try {
163            Map<String, ?> response = client.post("/csc/v0/signatures/signHash", JsonWriter.objectToJson(request, args));
164            Object[] signatures = (Object[]) response.get("signatures");
165
166            return Base64.getDecoder().decode(signatures[0].toString());
167        } catch (IOException e) {
168            throw new GeneralSecurityException(e);
169        }
170    }
171
172    private String getSignatureActivationData(SigningServicePrivateKey privateKey, String hash) throws GeneralSecurityException {
173        Map<String, Object> request = new LinkedHashMap<>();
174        request.put("credentialID", privateKey.getId());
175        request.put("numSignatures", 1);
176        request.put("hash", new String[] { hash });
177
178        String totpsecret = (String) privateKey.getProperties().get("totpsecret");
179        if (totpsecret != null) {
180            request.put("OTP", generateOTP(totpsecret));
181        }
182
183        try {
184            Map<String, Object> args = new HashMap<>();
185            args.put(JsonWriter.TYPE, "false");
186            Map<String, ?> response = client.post("/csc/v0/credentials/authorize", JsonWriter.objectToJson(request, args));
187            return (String) response.get("SAD");
188        } catch (IOException e) {
189            throw new GeneralSecurityException("Couldn't get signing authorization for SSL.com certificate " + privateKey.getId(), e);
190        }
191    }
192
193    private String generateOTP(String secret) throws GeneralSecurityException {
194        Mac mac = Mac.getInstance("HmacSHA1");
195
196        byte[] counter = new byte[8];
197        ByteBuffer.wrap(counter).putLong(System.currentTimeMillis() / 30000);
198
199        mac.init(new SecretKeySpec(Base64.getDecoder().decode(secret), "RAW"));
200        mac.update(counter);
201        ByteBuffer hash = ByteBuffer.wrap(mac.doFinal());
202
203        int offset = hash.get(hash.capacity() - 1) & 0x0F;
204        long value = (hash.getInt(offset) & 0x7FFFFFFF) % 1000000;
205
206        return String.format("%06d", value);
207    }
208}