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}