SignPathSigningService.java
/*
* 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);
}
}
}