Skip to content

Commit

Permalink
Add SignServer support (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire committed Nov 10, 2024
1 parent 3ddf1a0 commit 5cd5cfc
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 0 deletions.
30 changes: 30 additions & 0 deletions jsign-crypto/src/main/java/net/jsign/KeyStoreType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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: <username>|<password>, <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));
}
};


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 String sessionToken;

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)));
}
}
}
133 changes: 133 additions & 0 deletions jsign-crypto/src/main/java/net/jsign/jca/SignServerSigningService.java
Original file line number Diff line number Diff line change
@@ -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<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://signserver.company.com/signserver/</tt>)
* @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<String> aliases() {
return emptyList();
}

@Override
public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
if (!certificates.containsKey(alias)) {
try {
Map<String, ?> 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<String, Object> request = new HashMap<>();
request.put("data", Base64.getEncoder().encodeToString(data));
request.put("encoding", "BASE64");
Map<String, Object> metaData = new HashMap<>();
metaData.put("USING_CLIENTSUPPLIED_HASH", true);
metaData.put("CLIENTSIDE_HASHDIGESTALGORITHM", digestAlgorithm.id);
request.put("metaData", metaData);

try {
Map<String, ?> 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";
}
}

0 comments on commit 5cd5cfc

Please sign in to comment.