Skip to content

Commit

Permalink
SignPath support
Browse files Browse the repository at this point in the history
  • Loading branch information
ebourg committed Feb 7, 2025
1 parent b9216fc commit d5f7f2e
Show file tree
Hide file tree
Showing 19 changed files with 633 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Jsign is free to use and licensed under the [Apache License version 2.0](https:/
* [HashiCorp Vault](https://www.vaultproject.io/)
* [Keyfactor SignServer](https://www.signserver.org)
* [Oracle Cloud KMS](https://www.oracle.com/security/cloud-security/key-management/)
* [SignPath](https://signpath.io)
* [SSL.com eSigner](https://www.ssl.com/esigner/)
* Private key formats: PVK and PEM (PKCS#1 and PKCS#8), encrypted or not
* Certificates: PKCS#7 in PEM and DER format
Expand All @@ -55,6 +56,7 @@ See https://ebourg.github.io/jsign for more information.

#### Version 7.1 (in development)

* New signing service: SignPath
* The "Unsupported file" error when using the Ant task has been fixed

#### Version 7.0 (2025-01-16)
Expand Down
20 changes: 20 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ <h3 id="features">Features</h3>
<li><a href="https://www.vaultproject.io">HashiCorp Vault</a></li>
<li><a href="https://www.signserver.org">Keyfactor SignServer</a></li>
<li><a href="https://www.oracle.com/security/cloud-security/key-management/">Oracle Cloud KMS</a></li>
<li><a href="https://signpath.io">SignPath</a></li>
<li><a href="https://www.ssl.com/esigner/">SSL.com eSigner</a></li>
</ul>
</li>
Expand Down Expand Up @@ -229,6 +230,7 @@ <h4 id="attributes" class="mobile-only">Attributes</h4>
<li><code>GOOGLECLOUD</code>: Google Cloud KMS</li>
<li><code>HASHICORPVAULT</code>: HashiCorp Vault</li>
<li><code>ORACLECLOUD</code>: Oracle Cloud Key Management Service</li>
<li><code>SIGNPATH</code>: SignPath</li>
<li><code>SIGNSERVER</code>: Keyfactor SignServer</li>
<li><code>TRUSTEDSIGNING</code>: Azure Trusted Signing</li>
</ul>
Expand Down Expand Up @@ -552,6 +554,7 @@ <h3 id="cli">Command Line Tool</h3>
- GOOGLECLOUD: Google Cloud KMS
- HASHICORPVAULT: HashiCorp Vault
- ORACLECLOUD: Oracle Cloud Key Management Service
- SIGNPATH: SignPath
- SIGNSERVER: Keyfactor SignServer
- TRUSTEDSIGNING: Azure Trusted Signing
-a,--alias &lt;NAME> The alias of the certificate used for signing in the keystore
Expand Down Expand Up @@ -978,6 +981,23 @@ <h4 id="example-oraclecloud">Signing with Oracle Cloud Key Management Service</h
<li><code>OCI_CLI_PASS_PHRASE</code>: The pass phrase of the private key</li>
</ul>


<h4 id="example-signpath">Signing with SignPath</h4>

<p>Signing with <a href="https://signpath.io">SignPath</a> requires an account entitled to use the code signing gateway
and a signing policy configured for hash signing. The <code>keystore</code> parameter specifies the organization
identifier, and the <code>storepass</code> parameter the API access token. The <code>alias</code> parameter is the
concatenation of the project slug and the signing policy slug, separated by a slash character.</p>

<pre>
jsign --storetype SIGNPATH \
--kesytore &lt;organizationId&gt; \
--storepass &lt;accessToken&gt; \
--alias &lt;projectSlug&gt;/&lt;signingPolicySlug&gt; \
application.exe
</pre>


<br>

<h4 id="tagging">Tagging</h4>
Expand Down
1 change: 1 addition & 0 deletions jsign-cli/src/main/java/net/jsign/JsignCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public static void main(String... args) {
+ "- GOOGLECLOUD: Google Cloud KMS\n"
+ "- HASHICORPVAULT: HashiCorp Vault\n"
+ "- ORACLECLOUD: Oracle Cloud Key Management Service\n"
+ "- SIGNPATH: SignPath\n"
+ "- SIGNSERVER: Keyfactor SignServer\n"
+ "- TRUSTEDSIGNING: Azure Trusted Signing\n").build());
options.addOption(Option.builder("a").hasArg().longOpt(PARAM_ALIAS).argName("NAME").desc("The alias of the certificate used for signing in the keystore").build());
Expand Down
26 changes: 26 additions & 0 deletions jsign-core/src/test/java/net/jsign/SignerHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

import static net.jsign.DigestAlgorithm.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;

public class SignerHelperTest {

Expand Down Expand Up @@ -339,6 +340,31 @@ public void testTrustedSigning() throws Exception {
SignatureAssert.assertTimestamped("Invalid timestamp", signable.getSignatures().get(0));
}

@Test
public void testSignPath() throws Exception {
String organization = System.getenv("SIGNPATH_ORGANIZATION_ID");
String accessToken = System.getenv("SIGNPATH_API_TOKEN");
assumeNotNull("SIGNPATH_ORGANIZATION_ID environment variable not defined", organization);
assumeNotNull("SIGNPATH_API_TOKEN environment variable not defined", accessToken);

File sourceFile = new File("target/test-classes/wineyes.exe");
File targetFile = new File("target/test-classes/wineyes-signed-with-signpath.exe");

FileUtils.copyFile(sourceFile, targetFile);

SignerHelper helper = new SignerHelper("option")
.storetype("SIGNPATH")
.keystore(organization)
.storepass(accessToken)
.alias("jsign/rsa-2048")
.alg("SHA-256");

helper.sign(targetFile);

Signable signable = Signable.of(targetFile);
SignatureAssert.assertSigned(signable, SHA256);
}

@Test
public void testPIV() throws Exception {
PIVCardTest.assumeCardPresent();
Expand Down
24 changes: 23 additions & 1 deletion jsign-crypto/src/main/java/net/jsign/KeyStoreType.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import net.jsign.jca.OracleCloudCredentials;
import net.jsign.jca.OracleCloudSigningService;
import net.jsign.jca.PIVCardSigningService;
import net.jsign.jca.SignPathSigningService;
import net.jsign.jca.SignServerCredentials;
import net.jsign.jca.SignServerSigningService;
import net.jsign.jca.SigningServiceJcaProvider;
Expand Down Expand Up @@ -602,8 +603,29 @@ Provider getProvider(KeyStoreBuilder params) {
SignServerCredentials credentials = new SignServerCredentials(username, password, certificate, params.keypass());
return new SigningServiceJcaProvider(new SignServerSigningService(params.keystore(), credentials));
}
};
},

/**
* SignPath. The keystore parameter specifies the organization, and the storepass parameter the API access token.
* The alias parameter is the concatenation of the project slug and the signing policy slug, separated by a slash
* character (e.g. <code>myproject/mypolicy</code>).
*/
SIGNPATH(false, false) {
@Override
void validate(KeyStoreBuilder params) {
if (params.keystore() == null) {
throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the SignPath organization id (e.g. eacd4b78-6038-4450-9eec-4acd1c7ba6f1)");
}
if (params.storepass() == null) {
throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SignPath API access token");
}
}

@Override
Provider getProvider(KeyStoreBuilder params) {
return new SigningServiceJcaProvider(new SignPathSigningService(params.keystore(), params.storepass()));
}
};

/** Tells if the keystore is contained in a local file */
private final boolean fileBased;
Expand Down
166 changes: 166 additions & 0 deletions jsign-crypto/src/main/java/net/jsign/jca/SignPathSigningService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Copyright 2025 Emmanuel Bourg
*
* 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.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import net.jsign.DigestAlgorithm;

/**
* Signing service using the SignPath REST API.
*
* @since 7.1
* @see <a href="https://about.signpath.io/documentation/crypto-providers/rest-api">SignPath REST API</a>
*/
public class SignPathSigningService implements SigningService {

/** Cache of certificates indexed by alias */
private final Map<String, Map<String, ?>> certificates = new HashMap<>();

private final RESTClient client;

/**
* Create a new SignPath signing service.
*
* @param organizationId the organization ID
* @param token the API access token
*/
public SignPathSigningService(String organizationId, String token) {
this("https://app.signpath.io/API/v1", organizationId, token);
}

SignPathSigningService(String endpoint, String organizationId, String token) {
this.client = new RESTClient(endpoint + "/" + organizationId)
.authentication(conn -> conn.setRequestProperty("Authorization", "Bearer " + token))
.errorHandler(response -> response.get("status") + " - " + response.get("title") + " - " + JsonWriter.format(response.get("errors")));
}

@Override
public String getName() {
return "SignPath";
}

private void loadKeyStore() throws KeyStoreException {
if (certificates.isEmpty()) {
try {
Map<String, ?> response = client.get("/Cryptoki/MySigningPolicies");
Object[] policies = (Object[]) response.get("signingPolicies");
for (Object policy : policies) {
String alias = ((Map) policy).get("projectSlug") + "/" + ((Map) policy).get("signingPolicySlug");
certificates.put(alias, ((Map) policy));
}
} catch (IOException e) {
throw new KeyStoreException("Unable to retrieve the SignPath signing policies", e);
}
}
}

@Override
public List<String> aliases() throws KeyStoreException {
loadKeyStore();
return new ArrayList<>(certificates.keySet());
}

@Override
public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
loadKeyStore();

Map<String, ?> policy = certificates.get(alias);
if (policy == null) {
throw new KeyStoreException("Unable to retrieve SignPath signing policy '" + alias + "'");
}

byte[] certificateBytes = Base64.getDecoder().decode((String) policy.get("certificateBytes"));
Certificate certificate;
try {
certificate = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certificateBytes));
} catch (CertificateException e) {
throw new KeyStoreException(e);
}

return new Certificate[] { certificate };
}

private String getAlgorithm(String alias) throws KeyStoreException {
loadKeyStore();
Map<String, ?> policy = certificates.get(alias);
if (policy == null) {
return null;
}

String keyType = (String) policy.get("keyType");

return keyType != null ? keyType.toUpperCase() : null;
}

@Override
public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
try {
String algorithm = getAlgorithm(alias);
if (algorithm == null) {
throw new UnrecoverableKeyException("Unable to initialize the SignPath private key for the certificate '" + alias + "'");
}
return new SigningServicePrivateKey(alias, algorithm, this);
} catch (KeyStoreException e) {
throw (UnrecoverableKeyException) new UnrecoverableKeyException(e.getMessage()).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);

String[] slugs = privateKey.getId().split("/");
String project = slugs[0];
String signingPolicy = slugs[1];

Map<String, String> artifact = new LinkedHashMap<>();
artifact.put("SignatureAlgorithm", "RsaPkcs1");
artifact.put("RsaHashAlgorithm", digestAlgorithm.oid.toString());
artifact.put("Base64EncodedHash", Base64.getEncoder().encodeToString(data));

Map<String, Object> request = new LinkedHashMap<>();
request.put("ProjectSlug", project);
request.put("SigningPolicySlug", signingPolicy);
request.put("IsFastSigningRequest", "true");
request.put("Artifact", JsonWriter.format(artifact).getBytes(StandardCharsets.UTF_8));

try {
Map<String, ?> response = client.post("/SigningRequests", request, true);
String signature = (String) response.get("Signature");

return Base64.getDecoder().decode(signature);
} catch (IOException e) {
throw new GeneralSecurityException(e);
}
}
}
18 changes: 18 additions & 0 deletions jsign-crypto/src/test/java/net/jsign/KeyStoreBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,24 @@ public void testBuildGaraSign() throws Exception {
assertNotNull("keystore", keystore);
}

@Test
public void testBuildSignPath() throws Exception {
KeyStoreBuilder builder = new KeyStoreBuilder().storetype(SIGNPATH);

Exception e = assertThrows(IllegalArgumentException.class, builder::build);
assertEquals("message", "keystore parameter must specify the SignPath organization id (e.g. eacd4b78-6038-4450-9eec-4acd1c7ba6f1)", e.getMessage());

builder.keystore("eacd4b78-6038-4450-9eec-4acd1c7ba6f1");

e = assertThrows(IllegalArgumentException.class, builder::build);
assertEquals("message", "storepass parameter must specify the SignPath API access token", e.getMessage());

builder.storepass("AIk/65sl23lA1nVV/pgSqk96SvHFsSw3xitmp5Qhr+F/");

KeyStore keystore = builder.build();
assertNotNull("keystore", keystore);
}

@Test
public void testBuildSignServer() throws Exception {
KeyStoreBuilder builder = new KeyStoreBuilder().storetype(SIGNSERVER);
Expand Down
Loading

0 comments on commit d5f7f2e

Please sign in to comment.