-
-
Notifications
You must be signed in to change notification settings - Fork 113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SignServer support #252
Comments
I'm open to adding SignServer support to Jsign. I did try to install it and see how it works but I couldn't figure out how to use it and eventually gave up. If someone wants to implement this I'll review and integrate the PR. |
Initially getting up-and-running is not exactly trivial it seems, yeah. I got the SignServer CE set up using the Docker image. This requires client certificate authentication in the browser to get to the Administration page. This tutorial then shows how to get the SignServer up and running: https://youtu.be/wMqPWKi3ukE?si=ns567leubHKRpCcK Actually, it basically is doing docker run -it --rm -p 80:8080 -p 443:8443 -h localhost -v /path/to/ManagementCA.pem:/mnt/external/secrets/tls/cas/Managem
entCA.crt --name signserver keyfactor/signserver-ce and then opening Now you are in the admin site and can add token workers and signer workers like with
Or whatever signer is necessary for usage with JSign, that is yet to be found out. |
This is hopefully what you need to do for signing:
Depending on what signer actually is needed and with what data and with the described setup it does not need authentication, but that would of course also be nice if supported. |
Thank you for the details. I guess the authentication is performed with the same client certificate? Do you know if the signing key is selectable, or if only one key is assigned to a given worker? |
In the setup I showed, no authentication is necessary.
As the property in the plain signer worker is called Btw. you might also need to adjust some other properties. What I showed was just very basic setup to get it working at all. |
You probably should then also send But all this is just guesswork right now. |
Maybe you also need the CMS Signer instead of the Plain Signer, or can support both. 🤷♂️ |
I started trying to get it running here at least as a start as we now need to use SignServer. AuthenticodeSigner(KeyStore.getInstance("DIGICERTONE", SigningServiceJcaProvider(DigiCertOneSigningService("foo", File("bar.p12"), "baz"))), "alias", null).sign(signable) But I right away get
:-/ |
You should used the KeyStoreBuilder class instead:
|
Ah, ok, thanks, I tried it with |
Ah, I was missing the |
This gives a first progress, though the verification in class SignServerSigningService : SigningService {
private val client = RESTClient("http://localhost/signserver/rest/v1/workers/PlainSigner/")
private val certificateChain by lazy {
client
.post("process", """{"data":""}""")["signerCertificate"]
.let { Base64.getDecoder().decode("$it") }
.inputStream()
.use {
CertificateFactory
.getInstance("X.509")
.generateCertificate(it)
}
.let { arrayOf(it) }
}
override fun getName() = "SignServer"
override fun aliases(): List<String?> {
throw KeyStoreException("Unable to retrieve SignServer certificate aliases")
}
override fun getCertificateChain(alias: String) = certificateChain
override fun getPrivateKey(alias: String, password: CharArray?) = SigningServicePrivateKey(null, "RSA", this)
override fun sign(privateKey: SigningServicePrivateKey, algorithm: String, data: ByteArray?): ByteArray {
val digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.lowercase().indexOf("with")))
val digest = Base64.getEncoder().encodeToString(digestAlgorithm.messageDigest.digest(data))
return client
.post("process", """{"data":"$digest"}""")["data"]
.let { Base64.getDecoder().decode("$it") }
}
} |
Yeah, big step forward.
and changing the signing service to class SignServerSigningService : SigningService {
private val client = RESTClient("http://localhost/signserver/rest/v1/workers/PlainSigner/")
private val certificateChain by lazy {
client
.post("process", """{"data":""}""")["signerCertificate"]
.let { Base64.getDecoder().decode("$it") }
.inputStream()
.use {
CertificateFactory
.getInstance("X.509")
.generateCertificate(it)
}
.let { arrayOf(it) }
}
override fun getName() = "SignServer"
override fun aliases(): List<String?> {
throw KeyStoreException("Unable to retrieve SignServer certificate aliases")
}
override fun getCertificateChain(alias: String) = certificateChain
override fun getPrivateKey(alias: String, password: CharArray?) = SigningServicePrivateKey(null, "RSA", this)
override fun sign(privateKey: SigningServicePrivateKey, algorithm: String, data: ByteArray?): ByteArray {
val digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.lowercase().indexOf("with")))
val digest = Base64.getEncoder().encodeToString(digestAlgorithm.messageDigest.digest(data))
return client
.post(
"process",
"""
{
"data": "$digest",
"encoding": "BASE64",
"metaData": {
"USING_CLIENTSUPPLIED_HASH": "true",
"CLIENTSIDE_HASHDIGESTALGORITHM": "SHA256"
}
}
""".trimIndent()
)["data"].let { Base64.getDecoder().decode("$it") }
}
} produce a valid signature. 🎉 It might not yet work properly if you have a certificate that is signed by an intermediary CA, as at least in this test so far only the certificate itself was returned, not the whole chain as far as I can tell, but maybe there can something be done. And of course various authentication mechanisms are missing and properly having the URL and so on configurable etc. But maybe this now can get you up to speed to add proper support for everything. :-) |
Small correction to make class SignServerSigningService : SigningService {
private val client = RESTClient("http://localhost/signserver/rest/v1/workers/PlainSigner/")
private val certificateChain by lazy {
client
.post("process", """{"data":""}""")["signerCertificate"]
.let { Base64.getDecoder().decode("$it") }
.inputStream()
.use {
CertificateFactory
.getInstance("X.509")
.generateCertificate(it)
}
.let { arrayOf(it) }
}
override fun getName() = "SignServer"
override fun aliases() = listOf<String>()
override fun getCertificateChain(alias: String) = certificateChain
override fun getPrivateKey(alias: String, password: CharArray?) = SigningServicePrivateKey(null, "RSA", this)
override fun sign(privateKey: SigningServicePrivateKey, algorithm: String, data: ByteArray?): ByteArray {
val digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.lowercase().indexOf("with")))
val digest = Base64.getEncoder().encodeToString(digestAlgorithm.messageDigest.digest(data))
return client
.post(
"process",
"""
{
"data": "$digest",
"encoding": "BASE64",
"metaData": {
"USING_CLIENTSUPPLIED_HASH": "true",
"CLIENTSIDE_HASHDIGESTALGORITHM": "${digestAlgorithm.id}"
}
}
""".trimIndent()
)["data"].let { Base64.getDecoder().decode("$it") }
}
} |
The URL and private key algorithm of course need to be made configurable in a proper implementation :-D |
Nice, if you can rewrite it in Java I'd be happy to merge it. I got a look at the authentication mechanisms supported by SignServer, I think at least client certificate and username/password authentication could be supported, and maybe JWT too but that's an enterprise feature. There could be 3 supported syntaxes for the
The GaraSign signing service already supports the dual certificate and username/password authentications, so its code could probably be copied. |
Instead of hardcoding the URL you can use the |
Rewriting in Java would be trivial, but yeah, as I said, this is just a successful test, not ready to be merged. It misses ways to authenticate that SS supports, it misses configurability, and so on. |
These are the authorizers supported by SS: https://docs.keyfactor.com/signserver/latest/authorizers |
What is the output of the API call to |
Just checked, it's only the id and the name. The request could have been used to get the list of PlainSigners, and the name of the worker could have been mapped to a key alias. |
I didn't try that, as the openapi spec says that it requires admin role |
I wrote the Java implementation with the authentication support, I'll push it shortly. |
Oh, great, without the authentication, and not tested yet what I have now in Java is: /**
* 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<>();
/** The API endpoint of the SignServer REST interface */
private final String endpoint;
private final RESTClient client;
/** The credentials to authenticate with the interface */
private final SignServerCredentials credentials;
/**
* 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.endpoint = requireNonNull(endpoint);
this.credentials = credentials;
this.client = new RESTClient(endpoint + (endpoint.endsWith("/") ? "" : "/"));
}
@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";
}
} |
Thanks, that's pretty close, I'll merge the differences. |
Great. :-) Also still untested, but I guess this is about how it will be with authentication: /**
* 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<>();
/** The API endpoint of the SignServer REST interface */
private final String endpoint;
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.endpoint = requireNonNull(endpoint);
this.client = credentials.buildRESTClient(endpoint);
}
@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";
}
} with /**
* 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, 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;
}
RESTClient buildRESTClient(String endpoint) {
return new RESTClient(endpoint + (endpoint.endsWith("/") ? "" : "/"))
.authentication(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)));
}
})
.errorHandler(response -> response.get("error").toString());
}
} :-) |
Could you open a pull request please? I'll merge it and apply my changes over it. |
Once I tried whether it actually works, I will. |
I'll take care of the unit tests |
...and the documentation |
Ok, there you have it at #258 :-) |
I've merged the PR with some syntax changes and added the tests and the documentation. Thank you for the help! |
Great, some remarks and questions:
It can be installed on-premise, but you can also have it as paid cloud service. (You also (only) listed it as Cloud service above that sentence)
If authentication is necessary, it also works without authentication (for example authenticated by IP, by simply being in the same network, and so on).
Why did you make the license header comments JavaDoc comments? Have you any idea when 7.0 will be released? :-) |
I've always used that format for the license headers and never seen any issue. What IDE complains about it?
When it's ready :) |
Well, it is just wrong. :-) JavaDoc comments are for commenting Java elements that then land in the generated JavaDoc documentation or are displayed in the IDE. Also, the IntellliJ IDEA complains that - which for JavaDoc comments is correct - that you should add Also all other tools that might investigate or process the sources sees the wrong type of comment and thus might handle them incorrectly.
Yeah, thanks, I did not ask for the platitude I also use in my FOSS projects if someone asks when a version is release, I also just asked whether you do have an idea when it will be, a simple "no" would have been sufficient. ;-P |
Good point, I got caught by that feature sometimes. I'll change the header format then.
Sorry but I don't know, I'm a bit short on time currently. I'd like to finish the signature verification first but I'll probably postpone that feature to the 8.0 release. |
In the list of supported Cloud key management systems, I miss the possibility to use an on-premise SignServer instance.
While an on-premise SignServer instance is not exactly a "Cloud key management system",
it would be really nice if Jsign could support using an own SignServer instance.
Would it be feasible that such support is added?
If it is already possible and just not documented, maybe it would make sense to add the support for on-premise SignServer instances to the documentation.
Actually, it seems SignServer also provides a Cloud offering, so supporting it would match the "Cloud key management system" category and just needs a way to use an own URL, but it seems this is already supported using
--keystore
option it seems from the other cloud support options.The text was updated successfully, but these errors were encountered: