001/* 002 * Copyright 2025 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign.jca; 018 019import java.io.ByteArrayInputStream; 020import java.io.IOException; 021import java.nio.charset.StandardCharsets; 022import java.security.GeneralSecurityException; 023import java.security.KeyStoreException; 024import java.security.UnrecoverableKeyException; 025import java.security.cert.Certificate; 026import java.security.cert.CertificateException; 027import java.security.cert.CertificateFactory; 028import java.util.ArrayList; 029import java.util.Base64; 030import java.util.HashMap; 031import java.util.LinkedHashMap; 032import java.util.List; 033import java.util.Map; 034 035import net.jsign.DigestAlgorithm; 036 037/** 038 * Signing service using the SignPath REST API. 039 * 040 * @since 7.1 041 * @see <a href="https://about.signpath.io/documentation/crypto-providers/rest-api">SignPath REST API</a> 042 */ 043public class SignPathSigningService implements SigningService { 044 045 /** Cache of certificates indexed by alias */ 046 private final Map<String, Map<String, ?>> certificates = new HashMap<>(); 047 048 private final RESTClient client; 049 050 /** 051 * Create a new SignPath signing service. 052 * 053 * @param organizationId the organization ID 054 * @param token the API access token 055 */ 056 public SignPathSigningService(String organizationId, String token) { 057 this("https://app.signpath.io/API/v1", organizationId, token); 058 } 059 060 SignPathSigningService(String endpoint, String organizationId, String token) { 061 this.client = new RESTClient(endpoint + "/" + organizationId) 062 .authentication(conn -> conn.setRequestProperty("Authorization", "Bearer " + token)) 063 .errorHandler(response -> response.get("status") + " - " + response.get("title") + " - " + JsonWriter.format(response.get("errors"))); 064 } 065 066 @Override 067 public String getName() { 068 return "SignPath"; 069 } 070 071 private void loadKeyStore() throws KeyStoreException { 072 if (certificates.isEmpty()) { 073 try { 074 Map<String, ?> response = client.get("/Cryptoki/MySigningPolicies"); 075 Object[] policies = (Object[]) response.get("signingPolicies"); 076 for (Object policy : policies) { 077 String alias = ((Map) policy).get("projectSlug") + "/" + ((Map) policy).get("signingPolicySlug"); 078 certificates.put(alias, ((Map) policy)); 079 } 080 } catch (IOException e) { 081 throw new KeyStoreException("Unable to retrieve the SignPath signing policies", e); 082 } 083 } 084 } 085 086 @Override 087 public List<String> aliases() throws KeyStoreException { 088 loadKeyStore(); 089 return new ArrayList<>(certificates.keySet()); 090 } 091 092 @Override 093 public Certificate[] getCertificateChain(String alias) throws KeyStoreException { 094 loadKeyStore(); 095 096 Map<String, ?> policy = certificates.get(alias); 097 if (policy == null) { 098 throw new KeyStoreException("Unable to retrieve SignPath signing policy '" + alias + "'"); 099 } 100 101 byte[] certificateBytes = Base64.getDecoder().decode((String) policy.get("certificateBytes")); 102 Certificate certificate; 103 try { 104 certificate = CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(certificateBytes)); 105 } catch (CertificateException e) { 106 throw new KeyStoreException(e); 107 } 108 109 return new Certificate[] { certificate }; 110 } 111 112 private String getAlgorithm(String alias) throws KeyStoreException { 113 loadKeyStore(); 114 Map<String, ?> policy = certificates.get(alias); 115 if (policy == null) { 116 return null; 117 } 118 119 String keyType = (String) policy.get("keyType"); 120 121 return keyType != null ? keyType.toUpperCase() : null; 122 } 123 124 @Override 125 public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException { 126 try { 127 String algorithm = getAlgorithm(alias); 128 if (algorithm == null) { 129 throw new UnrecoverableKeyException("Unable to initialize the SignPath private key for the certificate '" + alias + "'"); 130 } 131 return new SigningServicePrivateKey(alias, algorithm, this); 132 } catch (KeyStoreException e) { 133 throw (UnrecoverableKeyException) new UnrecoverableKeyException(e.getMessage()).initCause(e); 134 } 135 } 136 137 @Override 138 public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException { 139 DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with"))); 140 data = digestAlgorithm.getMessageDigest().digest(data); 141 142 String[] slugs = privateKey.getId().split("/"); 143 String project = slugs[0]; 144 String signingPolicy = slugs[1]; 145 146 Map<String, String> artifact = new LinkedHashMap<>(); 147 artifact.put("SignatureAlgorithm", "RsaPkcs1"); 148 artifact.put("RsaHashAlgorithm", digestAlgorithm.oid.toString()); 149 artifact.put("Base64EncodedHash", Base64.getEncoder().encodeToString(data)); 150 151 Map<String, Object> request = new LinkedHashMap<>(); 152 request.put("ProjectSlug", project); 153 request.put("SigningPolicySlug", signingPolicy); 154 request.put("IsFastSigningRequest", "true"); 155 request.put("Artifact", JsonWriter.format(artifact).getBytes(StandardCharsets.UTF_8)); 156 157 try { 158 Map<String, ?> response = client.post("/SigningRequests", request, true); 159 String signature = (String) response.get("Signature"); 160 161 return Base64.getDecoder().decode(signature); 162 } catch (IOException e) { 163 throw new GeneralSecurityException(e); 164 } 165 } 166}