Skip to content

Commit 5cd5cfc

Browse files
committed
Add SignServer support (#252)
1 parent 3ddf1a0 commit 5cd5cfc

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed

jsign-crypto/src/main/java/net/jsign/KeyStoreType.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
import net.jsign.jca.OracleCloudCredentials;
4949
import net.jsign.jca.OracleCloudSigningService;
5050
import net.jsign.jca.PIVCardSigningService;
51+
import net.jsign.jca.SignServerCredentials;
52+
import net.jsign.jca.SignServerSigningService;
5153
import net.jsign.jca.SigningServiceJcaProvider;
5254

5355
/**
@@ -543,6 +545,34 @@ Provider getProvider(KeyStoreBuilder params) {
543545
GaraSignCredentials credentials = new GaraSignCredentials(username, password, certificate, params.keypass());
544546
return new SigningServiceJcaProvider(new GaraSignSigningService(params.keystore(), credentials));
545547
}
548+
},
549+
550+
SIGNSERVER(false, false, false) {
551+
@Override
552+
void validate(KeyStoreBuilder params) {
553+
if (params.storepass() != null && params.storepass().split("\\|").length > 2) {
554+
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>");
555+
}
556+
}
557+
558+
@Override
559+
Provider getProvider(KeyStoreBuilder params) {
560+
String username = null;
561+
String password = null;
562+
String certificate = null;
563+
if (params.storepass() != null) {
564+
String[] elements = params.storepass().split("\\|");
565+
if (elements.length == 1) {
566+
certificate = elements[0];
567+
} else if (elements.length == 2) {
568+
username = elements[0];
569+
password = elements[1];
570+
}
571+
}
572+
573+
SignServerCredentials credentials = new SignServerCredentials(username, password, certificate, params.keypass());
574+
return new SigningServiceJcaProvider(new SignServerSigningService(params.keystore(), credentials));
575+
}
546576
};
547577

548578

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2024 Björn Kautler
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.jsign.jca;
18+
19+
import net.jsign.KeyStoreBuilder;
20+
21+
import javax.net.ssl.HttpsURLConnection;
22+
import javax.net.ssl.KeyManagerFactory;
23+
import javax.net.ssl.SSLContext;
24+
import java.net.HttpURLConnection;
25+
import java.security.GeneralSecurityException;
26+
import java.security.KeyStore;
27+
import java.security.SecureRandom;
28+
import java.util.Base64;
29+
30+
import static java.nio.charset.StandardCharsets.UTF_8;
31+
32+
/**
33+
* Credentials for the SignServer REST interface.
34+
*
35+
* @since 7.0
36+
*/
37+
public class SignServerCredentials {
38+
39+
public String username;
40+
public String password;
41+
public KeyStore.Builder keystore;
42+
public String sessionToken;
43+
44+
public SignServerCredentials(String username, String password, String keystore, String storepass) {
45+
this(username, password, keystore == null ? null : new KeyStoreBuilder().keystore(keystore).storepass(storepass).builder());
46+
}
47+
48+
public SignServerCredentials(String username, String password, KeyStore.Builder keystore) {
49+
this.username = username;
50+
this.password = password;
51+
this.keystore = keystore;
52+
}
53+
54+
void addAuthentication(HttpURLConnection conn) {
55+
if (conn instanceof HttpsURLConnection && keystore != null) {
56+
try {
57+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
58+
kmf.init(keystore.getKeyStore(), ((KeyStore.PasswordProtection) keystore.getProtectionParameter("")).getPassword());
59+
60+
SSLContext context = SSLContext.getInstance("TLS");
61+
context.init(kmf.getKeyManagers(), null, new SecureRandom());
62+
((HttpsURLConnection) conn).setSSLSocketFactory(context.getSocketFactory());
63+
} catch (GeneralSecurityException e) {
64+
throw new RuntimeException("Unable to load the SignServer client certificate", e);
65+
}
66+
}
67+
68+
if (username != null) {
69+
conn.setRequestProperty(
70+
"Authorization",
71+
"Basic " + Base64.getEncoder().encodeToString((username + ":" + (password == null ? "" : password)).getBytes(UTF_8)));
72+
}
73+
}
74+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2024 Björn Kautler
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.jsign.jca;
18+
19+
import net.jsign.DigestAlgorithm;
20+
21+
import javax.net.ssl.HttpsURLConnection;
22+
import javax.net.ssl.KeyManagerFactory;
23+
import javax.net.ssl.SSLContext;
24+
import java.io.ByteArrayInputStream;
25+
import java.io.IOException;
26+
import java.security.GeneralSecurityException;
27+
import java.security.KeyStore;
28+
import java.security.KeyStoreException;
29+
import java.security.SecureRandom;
30+
import java.security.UnrecoverableKeyException;
31+
import java.security.cert.Certificate;
32+
import java.security.cert.CertificateException;
33+
import java.security.cert.CertificateFactory;
34+
import java.util.Base64;
35+
import java.util.HashMap;
36+
import java.util.LinkedHashMap;
37+
import java.util.List;
38+
import java.util.Map;
39+
40+
import static java.nio.charset.StandardCharsets.UTF_8;
41+
import static java.util.Collections.emptyList;
42+
import static java.util.Objects.requireNonNull;
43+
44+
/**
45+
* Signing service using the SignServer REST interface.
46+
*
47+
* @since 7.0
48+
*/
49+
public class SignServerSigningService implements SigningService {
50+
/** Cache of certificates indexed by id or alias */
51+
private final Map<String, Certificate[]> certificates = new HashMap<>();
52+
53+
private final RESTClient client;
54+
55+
/**
56+
* Creates a new SignServer signing service.
57+
*
58+
* @param endpoint the SignServer API endpoint (for example <tt>https://signserver.company.com/signserver/</tt>)
59+
* @param credentials the SignServer credentials
60+
*/
61+
public SignServerSigningService(String endpoint, SignServerCredentials credentials) {
62+
this.client = new RESTClient(
63+
requireNonNull(endpoint, "You need to provide the SignServer endpoint URL as keystore parameter")
64+
+ (endpoint.endsWith("/") ? "" : "/"))
65+
.authentication(credentials::addAuthentication)
66+
.errorHandler(response -> response.get("error").toString());
67+
}
68+
69+
@Override
70+
public String getName() {
71+
return "SignServer";
72+
}
73+
74+
@Override
75+
public List<String> aliases() {
76+
return emptyList();
77+
}
78+
79+
@Override
80+
public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
81+
if (!certificates.containsKey(alias)) {
82+
try {
83+
Map<String, ?> response = client.post(getResourcePath(alias), "{\"data\":\"\"}");
84+
String encodedCertificate = response.get("signerCertificate").toString();
85+
byte[] certificateBytes = Base64.getDecoder().decode(encodedCertificate);
86+
Certificate certificate = CertificateFactory
87+
.getInstance("X.509")
88+
.generateCertificate(new ByteArrayInputStream(certificateBytes));
89+
certificates.put(alias, new Certificate[]{certificate});
90+
} catch (IOException | CertificateException e) {
91+
throw new KeyStoreException(e);
92+
}
93+
}
94+
95+
return certificates.get(alias);
96+
}
97+
98+
@Override
99+
public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
100+
try {
101+
String algorithm = getCertificateChain(alias)[0].getPublicKey().getAlgorithm();
102+
return new SigningServicePrivateKey(alias, algorithm, this);
103+
} catch (KeyStoreException e) {
104+
throw (UnrecoverableKeyException) new UnrecoverableKeyException().initCause(e);
105+
}
106+
}
107+
108+
@Override
109+
public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
110+
DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
111+
data = digestAlgorithm.getMessageDigest().digest(data);
112+
113+
Map<String, Object> request = new HashMap<>();
114+
request.put("data", Base64.getEncoder().encodeToString(data));
115+
request.put("encoding", "BASE64");
116+
Map<String, Object> metaData = new HashMap<>();
117+
metaData.put("USING_CLIENTSUPPLIED_HASH", true);
118+
metaData.put("CLIENTSIDE_HASHDIGESTALGORITHM", digestAlgorithm.id);
119+
request.put("metaData", metaData);
120+
121+
try {
122+
Map<String, ?> response = client.post(getResourcePath(privateKey.getId()), JsonWriter.format(request));
123+
String value = response.get("data").toString();
124+
return Base64.getDecoder().decode(value);
125+
} catch (IOException e) {
126+
throw new GeneralSecurityException(e);
127+
}
128+
}
129+
130+
private String getResourcePath(String alias) {
131+
return "rest/v1/workers/" + alias + "/process";
132+
}
133+
}

0 commit comments

Comments
 (0)