SignServerSigningService.java
/*
* 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 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.CertificateFactory;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import net.jsign.DigestAlgorithm;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Signing service using the Keyfactor SignServer REST API.
*
* @since 7.0
*/
public class SignServerSigningService implements SigningService {
/** Cache of certificates indexed by alias (worker id or name) */
private final Map<String, Certificate[]> certificates = new HashMap<>();
private final RESTClient client;
/**
* Creates a new SignServer signing service.
*
* @param endpoint the SignServer API endpoint (for example <tt>https://example.com/signserver</tt>)
* @param credentials the SignServer credentials
*/
public SignServerSigningService(String endpoint, SignServerCredentials credentials) {
this.client = new RESTClient(endpoint)
.authentication(conn -> {
if (conn instanceof HttpsURLConnection && credentials.keystore != null) {
try {
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(credentials.keystore.getKeyStore(), ((KeyStore.PasswordProtection) credentials.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 (credentials.username != null) {
String httpCredentials = credentials.username + ":" + (credentials.password == null ? "" : credentials.password);
conn.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString(httpCredentials.getBytes(UTF_8)));
}
})
.errorHandler(response -> (String) response.get("error"));
}
@Override
public String getName() {
return "SignServer";
}
@Override
public List<String> aliases() throws KeyStoreException {
return Collections.emptyList();
}
@Override
public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
if (!certificates.containsKey(alias)) {
try {
String worker = alias;
boolean serverside = false;
if (worker.endsWith("|serverside")) {
worker = worker.substring(0, worker.length() - 11);
serverside = true;
}
Map<String, Object> request = new HashMap<>();
if (serverside) {
request.put("data", "");
Map<String, String> metadata = new HashMap<>();
metadata.put("USING_CLIENTSUPPLIED_HASH", "false");
request.put("metaData", metadata);
} else {
request.put("data", "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=");
request.put("encoding", "BASE64");
Map<String, String> metadata = new HashMap<>();
metadata.put("USING_CLIENTSUPPLIED_HASH", "true");
metadata.put("CLIENTSIDE_HASHDIGESTALGORITHM", "SHA-256");
request.put("metaData", metadata);
}
Map<String, ?> response = client.post("/rest/v1/workers/" + worker + "/process", JsonWriter.format(request));
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 (Exception e) {
throw new KeyStoreException("Unable to retrieve the certificate chain '" + alias + "'", 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 {
String worker = privateKey.getId();
boolean serverside = false;
if (worker.endsWith("|serverside")) {
worker = worker.substring(0, worker.length() - 11);
serverside = true;
}
Map<String, Object> request = new HashMap<>();
if (serverside) {
request.put("data", Base64.getEncoder().encodeToString(data));
Map<String, String> metadata = new HashMap<>();
metadata.put("USING_CLIENTSUPPLIED_HASH", "false");
request.put("metaData", metadata);
} else {
DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
data = digestAlgorithm.getMessageDigest().digest(data);
request.put("data", Base64.getEncoder().encodeToString(data));
Map<String, String> metadata = new HashMap<>();
metadata.put("USING_CLIENTSUPPLIED_HASH", "true");
metadata.put("CLIENTSIDE_HASHDIGESTALGORITHM", digestAlgorithm.id);
request.put("metaData", metadata);
}
request.put("encoding", "BASE64");
try {
Map<String, ?> response = client.post("/rest/v1/workers/" + worker + "/process", JsonWriter.format(request));
return Base64.getDecoder().decode((String) response.get("data"));
} catch (IOException e) {
throw new GeneralSecurityException(e);
}
}
}