Skip to content
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

Closed
Vampire opened this issue Oct 4, 2024 · 38 comments · Fixed by #258
Closed

SignServer support #252

Vampire opened this issue Oct 4, 2024 · 38 comments · Fixed by #258

Comments

@Vampire
Copy link
Contributor

Vampire commented Oct 4, 2024

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.

@ebourg
Copy link
Owner

ebourg commented Oct 4, 2024

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.

@Vampire
Copy link
Contributor Author

Vampire commented Oct 30, 2024

Initially getting up-and-running is not exactly trivial it seems, yeah.
I got it managed to.
Maybe you can get it set up with this and could have a look again? :-)

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 can be generated using the sister product EJBCA.
This short tutorial shows how to get EJBCA up and running: https://youtu.be/x-kEqPrz1g0?si=W5U85I5z8DRITQMu
This short tutorial shows how to issue a client certificate for authentication and to get the CA certificate: https://youtu.be/wMD1GgSF-JE?si=qe3xcZkrXANjTpOT
But you can use the CA cert and client cert I just issued for testing and thus skip that step for the next two years: client-auth.zip

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 https://localhost/signserver/adminweb, authenticating with the installed certificate for client authentication.

Now you are in the admin site and can add token workers and signer workers like with

  • Add...
    • From Template
    • keystore-crypto.properties
    • Uncomment "WORKERGENID1.KEYSTOREPASSWORD=foo123"
    • Apply
  • Add...
    • From Template
    • plainsigner.properties
    • Apply

Or whatever signer is necessary for usage with JSign, that is yet to be found out.

@Vampire
Copy link
Contributor Author

Vampire commented Oct 30, 2024

This is hopefully what you need to do for signing:

$ http -v --verify false POST https://localhost/signserver/rest/v1/workers/PlainSigner/process data=asdf
POST /signserver/rest/v1/workers/PlainSigner/process HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 16
Content-Type: application/json
Host: localhost
User-Agent: HTTPie/1.0.3

{
    "data": "asdf"
}

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 1704
Content-Security-Policy: default-src https:
Content-Type: application/json
Date: Wed, 30 Oct 2024 20:22:55 GMT
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1

{
    "archiveId": "a9d8a242d3af5fee56f0e9f77e7cacaa39388bc8",
    "data": "Owx+ET8Ms+e9w063a5r7AaWSeyTnz1L+PF3uxaPrPKHhTqQAoE2Bv0pv2WNkVdkqoR7VoxJzPq854JqiNhtcqz1VIJ6z5KScTaiL9mdBzsoZVp42mBlPJBV4+jFcHvw6S38o6EMfykTBg6ysyueMauk34iK09UHeD0VCU2EI88oL+Vm07gRGp4vTKW5d7oIBw+pUiwTn1OySptNwD/IRsMsVJL1j7We1qhoVNjyBEs61EyKAv8pnt8agvb1pgyaDixjgutefmlL9tHyTLJOu9s1rS4bhV9tPzzyCsPAkvpbpFxj/pDxu8ytvrDDf1WhU5v8jl7IT6wgTsockqR11kA==",
    "metaData": {},
    "requestId": "1155026644",
    "signerCertificate": "MIIDlzCCAn+gAwIBAgIIOdPbElQbJhcwDQYJKoZIhvcNAQELBQAwTDEWMBQGA1UEAwwNRFNTIFN1YiBDQSAxMTEQMA4GA1UECwwHVGVzdGluZzETMBEGA1UECgwKU2lnblNlcnZlcjELMAkGA1UEBhMCU0UwHhcNMTYwMzAzMDgyNTA0WhcNMzYwMjI3MDgyNTA0WjBKMRQwEgYDVQQDDAtzaWduZXIwMDAwMzEQMA4GA1UECwwHVGVzdGluZzETMBEGA1UECgwKU2lnblNlcnZlcjELMAkGA1UEBhMCU0UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCyFKqwwiHS2o4PO3zovqC+jGuIELnja1iAlg/hyrRp28mF6BOGVaKE6ZzbQIMmmICRz+EeqXN1W8gyCEh6T2qN3QvXTAF9mrrUI3hG4Xn/Davgsln8saRE0zt45yy47dPq5YofYJWWIdW/6qssiX+ApcPqthCQfkgraUSagS/Reqy0WT/A2lwKh147GB9+MxhheskQIPaKQasOpI7vGfzey+GnkHPsfU21irS2nC8uzv6hd0G6hNYUEmJtIh9/5WebMoMiGFq1sydTtZp7pJilfPyxrAkHXEwMUEEMcVlE/ISCoKMttnLMUT/F00cHesU4D2yNl6gcSjpMj4Q/iF+hAgMBAAGjfzB9MB0GA1UdDgQWBBQ2/WY3Ln7tdUmDrTyvtvSZwBg8YzAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFBxgQUremK3l1gOK6GaCqX6w8gKHMA4GA1UdDwEB/wQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwDQYJKoZIhvcNAQELBQADggEBAIUh6kkCMc0Fs6U+Sw6Ns0Yd28Fb5SM//nE6mq3mf1SD4lAyChVrFvlqMZJaqeJlkVeHc9E+KCE5bX1r2iGC8rnE9DuItI0pKMrgFt4cbSbDwgovnTrkiIhuqP2pjdhmrHtlLqZBR8e16c4xGSn6XWKJ8vPzx2AJl7MY3sY3Z4aPckBFNjG1lzH1inq5WM/+WaLghOQQngaXeU+SWpoAM7cUjB8Uyjf2Qr2GerI4AZZJMuC6BuvMdFMyXX78l7c9qmvK9Bre+SFKdtcMAgnglLzu0lyPHPwYL0R+pwc5dFOJipafxeqeHGpkZTXMsdMn6f1USRznlGbRWru68/XOOFU="
}

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.

@ebourg
Copy link
Owner

ebourg commented Oct 30, 2024

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?

@Vampire
Copy link
Contributor Author

Vampire commented Oct 30, 2024

I guess the authentication is performed with the same client certificate?

In the setup I showed, no authentication is necessary.
Which authentication is necessary can be configured per worker.
If you do that setup and look through the properties you will for example see WORKERGENID1.AUTHTYPE=NOAUTH which could for example be any of these: https://docs.keyfactor.com/signserver/latest/authorizers, including client certificate authorization Basic Auth authorization and some others.

Do you know if the signing key is selectable, or if only one key is assigned to a given worker?

As the property in the plain signer worker is called WORKERGENID1.DEFAULTKEY, I guess you can also specify a key to use, but I don't know for sure.

Btw. you might also need to adjust some other properties.
I guess for the JSign integration the "client-side hashing" will be used, so that JSign calculates the digest, and then just sends this to SignServer, getting the signature to be embedded back: https://docs.keyfactor.com/signserver/latest/client-side-hashing / https://docs.keyfactor.com/signserver/latest/client-side-hashing-1
So you might for example need to set WORKERGENID1.CLIENTSIDEHASHING to true for the signer too.

What I showed was just very basic setup to get it working at all.

@Vampire
Copy link
Contributor Author

Vampire commented Oct 30, 2024

You probably should then also send USING_CLIENTSUPPLIED_HASH in the metadata, so that if client-side hashing is disallowed but override is allowed it is selected.

But all this is just guesswork right now.

@Vampire
Copy link
Contributor Author

Vampire commented Oct 30, 2024

@Vampire
Copy link
Contributor Author

Vampire commented Oct 30, 2024

Maybe you also need the CMS Signer instead of the Plain Signer, or can support both. 🤷‍♂️

@Vampire
Copy link
Contributor Author

Vampire commented Nov 5, 2024

I started trying to get it running here at least as a start as we now need to use SignServer.
But I don't really get very far.
I started with trying to do

AuthenticodeSigner(KeyStore.getInstance("DIGICERTONE", SigningServiceJcaProvider(DigiCertOneSigningService("foo", File("bar.p12"), "baz"))), "alias", null).sign(signable)

But I right away get

Caused by: java.security.KeyStoreException: Uninitialized keystore
	at java.base/java.security.KeyStore.getCertificateChain(KeyStore.java:1080)
	at net.jsign.AuthenticodeSigner.<init>(AuthenticodeSigner.java:143)
	...

:-/
Do you have any hint what I do wrongly?

@ebourg
Copy link
Owner

ebourg commented Nov 5, 2024

You should used the KeyStoreBuilder class instead:

KeyStore keystore = new KeyStoreBuilder().storetype("DIGICERTONE")
                                         .storepass("<api-key>|/path/to/Certificate_pkcs12.p12|<password>")
                                         .build();
AuthenticodeSigner signer = new AuthenticodeSigner(keystore, "alias", null);

@Vampire
Copy link
Contributor Author

Vampire commented Nov 5, 2024

Ah, ok, thanks, I tried it with KeyStore.getInstance as the KeyStoreBuilder does not support custom store types but only the ones from the enum. With that snippet I now get "No certificate found in the keystore with the alias" which is expected, thanks.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 5, 2024

Ah, I was missing the ks.load(null, null) which initializes it.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 5, 2024

This gives a first progress, though the verification in AuthenticodeSigner#verify fails as probably the signer is not yet configured correctly, or the data not sent in the correct format or whatever:

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") }
    }
}

@Vampire
Copy link
Contributor Author

Vampire commented Nov 6, 2024

Yeah, big step forward.
Adding to the plain signer configuration the properties

ALLOW_CLIENTSIDEHASHING_OVERRIDE=true
SIGNATUREALGORITHM=NONEwithRSA

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. :-)

@Vampire
Copy link
Contributor Author

Vampire commented Nov 7, 2024

Small correction to make aliases() return an empty list, otherwise it fails when using it with jarsigner for example as this always requests the aliases, but an empty alias list is working fine:

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") }
    }
}

@Vampire
Copy link
Contributor Author

Vampire commented Nov 7, 2024

The URL and private key algorithm of course need to be made configurable in a proper implementation :-D

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

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 storepass parameter:

  • for username/password authentication:
    --storepass "<username>|<password>"
  • for client certificate authentication:
    --storepass "/path/to/client-certificate.p12" --keypass <client-certificate-password>
  • for JWT:
    --storepass "<api-key>"

The GaraSign signing service already supports the dual certificate and username/password authentications, so its code could probably be copied.

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

class SignServerSigningService : SigningService {
    private val client = RESTClient("http://localhost/signserver/rest/v1/workers/PlainSigner/")

Instead of hardcoding the URL you can use the --keystore parameter to let the user specify it.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 8, 2024

Nice, if you can rewrite it in Java I'd be happy to merge it.

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.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 8, 2024

These are the authorizers supported by SS: https://docs.keyfactor.com/signserver/latest/authorizers

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

What is the output of the API call to /signserver/rest/v1/workers/ on your instance? Does it list only the name of the workers or also the type?

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

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.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 8, 2024

I didn't try that, as the openapi spec says that it requires admin role

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

I wrote the Java implementation with the authentication support, I'll push it shortly.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 8, 2024

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";
    }
}

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

Thanks, that's pretty close, I'll merge the differences.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 8, 2024

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());
    }
}

:-)

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

Could you open a pull request please? I'll merge it and apply my changes over it.

@Vampire
Copy link
Contributor Author

Vampire commented Nov 8, 2024

Once I tried whether it actually works, I will.
I cannot promise adding tests though.

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

I'll take care of the unit tests

@ebourg
Copy link
Owner

ebourg commented Nov 8, 2024

...and the documentation

Vampire added a commit to Vampire/jsign that referenced this issue Nov 10, 2024
@Vampire
Copy link
Contributor Author

Vampire commented Nov 10, 2024

Ok, there you have it at #258 :-)
Username/Password auth I successfully tested.
Client Cert auth I probably configured something wrongly. :-/

ebourg pushed a commit that referenced this issue Nov 13, 2024
ebourg added a commit that referenced this issue Nov 13, 2024
@ebourg
Copy link
Owner

ebourg commented Nov 13, 2024

I've merged the PR with some syntax changes and added the tests and the documentation. Thank you for the help!

@Vampire
Copy link
Contributor Author

Vampire commented Nov 13, 2024

Great, some remarks and questions:

SignServer is an on-premises open source signing service

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)

The authentication is performed by specifying the username/password or the TLS client certificate

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?
That's not really appropriate.
Besides causing wrong highlighting and sometimes also wrong formatting, they should not be JavaDoc comments as they do not contribute JavaDoc, they are just comments in the source file and should thus be simple block comments.


Have you any idea when 7.0 will be released? :-)

@ebourg
Copy link
Owner

ebourg commented Nov 13, 2024

Why did you make the license header comments JavaDoc comments?
That's not really appropriate.
Besides causing wrong highlighting and sometimes also wrong formatting, they should not be JavaDoc comments as they do not contribute JavaDoc, they are just comments in the source file and should thus be simple block comments.

I've always used that format for the license headers and never seen any issue. What IDE complains about it?

Have you any idea when 7.0 will be released? :-)

When it's ready :)

@Vampire
Copy link
Contributor Author

Vampire commented Nov 14, 2024

I've always used that format for the license headers and never seen any issue. What IDE complains about it?

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.
Doing those license headers as JavaDoc comments are just stray JavaDoc comments that are ignored when generating documentation as they are not documenting any valid Java element.

Also, the IntellliJ IDEA complains that - which for JavaDoc comments is correct - that you should add <p> between two paragraphs or otherwise in the generated documentation - if it would be used - the text is joined. And reformatting the file also adds those <p> tags and removes the spaces in front of the http.

idea64_qbrWvYtZQz

Also all other tools that might investigate or process the sources sees the wrong type of comment and thus might handle them incorrectly.

When it's ready :)

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

@ebourg
Copy link
Owner

ebourg commented Nov 14, 2024

reformatting the file also adds those

tags and removes the spaces in front of the http.

Good point, I got caught by that feature sometimes. I'll change the header format then.

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

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.

ebourg added a commit that referenced this issue Nov 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants