From 5c25eddc48b15fd4b1167f2f82b7d51430fb76b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kautler?= Date: Sun, 10 Nov 2024 18:31:58 +0100 Subject: [PATCH] Add SignServer support (#252) --- .../src/main/java/net/jsign/KeyStoreType.java | 30 ++++ .../net/jsign/jca/SignServerCredentials.java | 73 ++++++++++ .../jsign/jca/SignServerSigningService.java | 133 ++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 jsign-crypto/src/main/java/net/jsign/jca/SignServerCredentials.java create mode 100644 jsign-crypto/src/main/java/net/jsign/jca/SignServerSigningService.java diff --git a/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java b/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java index 1d8e9d21..81023876 100644 --- a/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java +++ b/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java @@ -48,6 +48,8 @@ import net.jsign.jca.OracleCloudCredentials; import net.jsign.jca.OracleCloudSigningService; import net.jsign.jca.PIVCardSigningService; +import net.jsign.jca.SignServerCredentials; +import net.jsign.jca.SignServerSigningService; import net.jsign.jca.SigningServiceJcaProvider; /** @@ -543,6 +545,34 @@ Provider getProvider(KeyStoreBuilder params) { GaraSignCredentials credentials = new GaraSignCredentials(username, password, certificate, params.keypass()); return new SigningServiceJcaProvider(new GaraSignSigningService(params.keystore(), credentials)); } + }, + + SIGNSERVER(false, false, false) { + @Override + void validate(KeyStoreBuilder params) { + if (params.storepass() != null && params.storepass().split("\\|").length > 2) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SignServer username/password or the path to the keystore containing the TLS client certificate: |, "); + } + } + + @Override + Provider getProvider(KeyStoreBuilder params) { + String username = null; + String password = null; + String certificate = null; + if (params.storepass() != null) { + String[] elements = params.storepass().split("\\|"); + if (elements.length == 1) { + certificate = elements[0]; + } else if (elements.length == 2) { + username = elements[0]; + password = elements[1]; + } + } + + SignServerCredentials credentials = new SignServerCredentials(username, password, certificate, params.keypass()); + return new SigningServiceJcaProvider(new SignServerSigningService(params.keystore(), credentials)); + } }; diff --git a/jsign-crypto/src/main/java/net/jsign/jca/SignServerCredentials.java b/jsign-crypto/src/main/java/net/jsign/jca/SignServerCredentials.java new file mode 100644 index 00000000..06c30512 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/jca/SignServerCredentials.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Björn Kautler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.jsign.jca; + +import net.jsign.KeyStoreBuilder; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import java.net.HttpURLConnection; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Credentials for the SignServer REST interface. + * + * @since 7.0 + */ +public class SignServerCredentials { + + public String username; + public String password; + public KeyStore.Builder keystore; + + public SignServerCredentials(String username, String password, String keystore, String storepass) { + this(username, password, keystore == null ? null : new KeyStoreBuilder().keystore(keystore).storepass(storepass).builder()); + } + + public SignServerCredentials(String username, String password, KeyStore.Builder keystore) { + this.username = username; + this.password = password; + this.keystore = keystore; + } + + void addAuthentication(HttpURLConnection conn) { + if (conn instanceof HttpsURLConnection && keystore != null) { + try { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keystore.getKeyStore(), ((KeyStore.PasswordProtection) keystore.getProtectionParameter("")).getPassword()); + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(kmf.getKeyManagers(), null, new SecureRandom()); + ((HttpsURLConnection) conn).setSSLSocketFactory(context.getSocketFactory()); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Unable to load the SignServer client certificate", e); + } + } + + if (username != null) { + conn.setRequestProperty( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString((username + ":" + (password == null ? "" : password)).getBytes(UTF_8))); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/jca/SignServerSigningService.java b/jsign-crypto/src/main/java/net/jsign/jca/SignServerSigningService.java new file mode 100644 index 00000000..5a4d0053 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/jca/SignServerSigningService.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024 Björn Kautler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.jsign.jca; + +import net.jsign.DigestAlgorithm; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; + +/** + * Signing service using the SignServer REST interface. + * + * @since 7.0 + */ +public class SignServerSigningService implements SigningService { + /** Cache of certificates indexed by id or alias */ + private final Map certificates = new HashMap<>(); + + private final RESTClient client; + + /** + * Creates a new SignServer signing service. + * + * @param endpoint the SignServer API endpoint (for example https://signserver.company.com/signserver/) + * @param credentials the SignServer credentials + */ + public SignServerSigningService(String endpoint, SignServerCredentials credentials) { + this.client = new RESTClient( + requireNonNull(endpoint, "You need to provide the SignServer endpoint URL as keystore parameter") + + (endpoint.endsWith("/") ? "" : "/")) + .authentication(credentials::addAuthentication) + .errorHandler(response -> response.get("error").toString()); + } + + @Override + public String getName() { + return "SignServer"; + } + + @Override + public List aliases() { + return emptyList(); + } + + @Override + public Certificate[] getCertificateChain(String alias) throws KeyStoreException { + if (!certificates.containsKey(alias)) { + try { + Map response = client.post(getResourcePath(alias), "{\"data\":\"\"}"); + String encodedCertificate = response.get("signerCertificate").toString(); + byte[] certificateBytes = Base64.getDecoder().decode(encodedCertificate); + Certificate certificate = CertificateFactory + .getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(certificateBytes)); + certificates.put(alias, new Certificate[]{certificate}); + } catch (IOException | CertificateException e) { + throw new KeyStoreException(e); + } + } + + return certificates.get(alias); + } + + @Override + public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { + try { + String algorithm = getCertificateChain(alias)[0].getPublicKey().getAlgorithm(); + return new SigningServicePrivateKey(alias, algorithm, this); + } catch (KeyStoreException e) { + throw (UnrecoverableKeyException) new UnrecoverableKeyException().initCause(e); + } + } + + @Override + public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { + DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); + data = digestAlgorithm.getMessageDigest().digest(data); + + Map request = new HashMap<>(); + request.put("data", Base64.getEncoder().encodeToString(data)); + request.put("encoding", "BASE64"); + Map metaData = new HashMap<>(); + metaData.put("USING_CLIENTSUPPLIED_HASH", true); + metaData.put("CLIENTSIDE_HASHDIGESTALGORITHM", digestAlgorithm.id); + request.put("metaData", metaData); + + try { + Map response = client.post(getResourcePath(privateKey.getId()), JsonWriter.format(request)); + String value = response.get("data").toString(); + return Base64.getDecoder().decode(value); + } catch (IOException e) { + throw new GeneralSecurityException(e); + } + } + + private String getResourcePath(String alias) { + return "rest/v1/workers/" + alias + "/process"; + } +}