Fix desktop cert store support

This commit is contained in:
Anthony Stirling
2026-01-06 20:59:49 +00:00
parent dfaa43a24a
commit 7a7445ad85
6 changed files with 86 additions and 43 deletions
@@ -10,7 +10,6 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -20,7 +19,6 @@ import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
@@ -32,6 +30,7 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
@@ -47,6 +46,8 @@ import stirling.software.SPDF.model.Dependency;
import stirling.software.SPDF.model.SignatureFile;
import stirling.software.SPDF.model.api.security.CertStoreEntriesRequest;
import stirling.software.SPDF.service.SharedSignatureService;
import stirling.software.SPDF.util.DesktopModeUtils;
import stirling.software.SPDF.util.Pkcs11ProviderLoader;
import stirling.software.common.annotations.api.UiDataApi;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.configuration.RuntimePathConfig;
@@ -218,8 +219,7 @@ public class UIDataController {
})
@Operation(summary = "Get available certificates from OS-backed stores")
public ResponseEntity<CertificateStoreEntriesData> getCertificateStoreEntries(
@org.springframework.web.bind.annotation.ModelAttribute CertStoreEntriesRequest request)
throws Exception {
@ModelAttribute CertStoreEntriesRequest request) throws Exception {
String certType = request.getCertType();
MultipartFile pkcs11ConfigFile = request.getPkcs11ConfigFile();
String password = request.getPassword();
@@ -270,9 +270,7 @@ public class UIDataController {
}
private void ensureDesktopMode(String certType) {
boolean isDesktopMode =
Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"));
if (!isDesktopMode) {
if (!DesktopModeUtils.isDesktopMode()) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
@@ -299,7 +297,7 @@ public class UIDataController {
"Invalid argument: {0}",
"PKCS11 configuration file is required");
}
Provider pkcs11Provider = loadPkcs11Provider(pkcs11ConfigFile);
Provider pkcs11Provider = Pkcs11ProviderLoader.loadProvider(pkcs11ConfigFile);
KeyStore pkcs11Store = KeyStore.getInstance("PKCS11", pkcs11Provider);
pkcs11Store.load(null, password != null ? password.toCharArray() : null);
return pkcs11Store;
@@ -311,21 +309,6 @@ public class UIDataController {
}
}
private Provider loadPkcs11Provider(MultipartFile configFile) throws IOException {
Provider baseProvider = Security.getProvider("SunPKCS11");
if (baseProvider == null) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"SunPKCS11 provider is not available in this JVM");
}
java.io.File tempFile = java.io.File.createTempFile("spdf-pkcs11", ".cfg");
FileUtils.copyInputStreamToFile(configFile.getInputStream(), tempFile);
Provider provider = baseProvider.configure(tempFile.getAbsolutePath());
Security.addProvider(provider);
return provider;
}
private CertificateStoreEntry buildCertificateEntry(String alias, X509Certificate certificate) {
String displayName = getCertificateDisplayName(certificate);
return new CertificateStoreEntry(
@@ -73,6 +73,8 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.util.DesktopModeUtils;
import stirling.software.SPDF.util.Pkcs11ProviderLoader;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.ServerCertificateServiceInterface;
@@ -260,7 +262,7 @@ public class CertSignController {
pkcs11ConfigFile,
"PKCS11 configuration",
"PKCS11 configuration file is required");
Provider pkcs11Provider = loadPkcs11Provider(pkcs11ConfigFile);
Provider pkcs11Provider = Pkcs11ProviderLoader.loadProvider(pkcs11ConfigFile);
ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
ks.load(null, password != null ? password.toCharArray() : null);
aliasRequired = true;
@@ -312,9 +314,7 @@ public class CertSignController {
}
private void ensureDesktopMode(String certType) {
boolean isDesktopMode =
Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"));
if (!isDesktopMode) {
if (!DesktopModeUtils.isDesktopMode()) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
@@ -367,21 +367,6 @@ public class CertSignController {
return new KeyStoreSelection(filtered, entryPassword);
}
private Provider loadPkcs11Provider(MultipartFile configFile) throws IOException {
Provider baseProvider = Security.getProvider("SunPKCS11");
if (baseProvider == null) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"SunPKCS11 provider is not available in this JVM");
}
File tempFile = File.createTempFile("spdf-pkcs11", ".cfg");
FileUtils.copyInputStreamToFile(configFile.getInputStream(), tempFile);
Provider provider = baseProvider.configure(tempFile.getAbsolutePath());
Security.addProvider(provider);
return provider;
}
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
throws IOException, OperatorCreationException, PKCSException {
try (PEMParser pemParser =
@@ -0,0 +1,14 @@
package stirling.software.SPDF.util;
public final class DesktopModeUtils {
private DesktopModeUtils() {}
public static boolean isDesktopMode() {
return Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))
|| Boolean.parseBoolean(
System.getenv().getOrDefault("STIRLING_PDF_TAURI_MODE", "false"))
|| Boolean.parseBoolean(System.getenv().getOrDefault("STIRLING_DESKTOP", "false"))
|| Boolean.parseBoolean(System.getenv().getOrDefault("VITE_DESKTOP", "false"));
}
}
@@ -0,0 +1,44 @@
package stirling.software.SPDF.util;
import java.io.File;
import java.io.IOException;
import java.security.Provider;
import java.security.Security;
import org.apache.commons.io.FileUtils;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.util.ExceptionUtils;
public final class Pkcs11ProviderLoader {
private Pkcs11ProviderLoader() {}
public static Provider loadProvider(MultipartFile configFile) throws IOException {
Provider baseProvider = Security.getProvider("SunPKCS11");
if (baseProvider == null) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"SunPKCS11 provider is not available in this JVM");
}
File tempFile = File.createTempFile("spdf-pkcs11", ".cfg");
tempFile.deleteOnExit();
try {
FileUtils.copyInputStreamToFile(configFile.getInputStream(), tempFile);
Provider provider = baseProvider.configure(tempFile.getAbsolutePath());
Provider existingProvider = Security.getProvider(provider.getName());
if (existingProvider != null) {
return existingProvider;
}
Security.addProvider(provider);
return provider;
} catch (IOException e) {
if (tempFile.exists()) {
tempFile.delete();
}
throw e;
}
}
}
@@ -2655,6 +2655,7 @@ chooseJksFile = "Choose JKS File"
chooseP12File = "Choose PKCS12 File"
choosePfxFile = "Choose PFX File"
choosePrivateKey = "Choose Private Key File"
choosePkcs11Config = "Choose PKCS#11 Config File"
location = "Location"
logoTitle = "Logo"
name = "Name"
@@ -2663,8 +2664,18 @@ pageNumber = "Page Number"
password = "Certificate Password"
passwordOptional = "Leave empty if no password"
reason = "Reason"
refreshStoreCertificates = "Refresh"
selectStoreCertificate = "Select a certificate"
serverCertMessage = "Using server certificate - no files or password required"
showLogo = "Show Logo"
storeCertificateExpires = "Expires"
storeCertificateIssuer = "Issuer"
storeCertificateSubject = "Subject"
storeCertificates = "Available Certificates"
storeCertificatesError = "Failed to load certificates"
loadingStoreCertificates = "Loading certificates..."
pin = "Token PIN / Password"
pinOptional = "Leave empty if not required"
[certSign.signMode]
stepTitle = "Sign Mode"
@@ -2693,6 +2704,11 @@ text = "Need recipient <b>Trusted</b> status? <b>Manual</b>. Need a fast, tamper
[certSign.certTypeStep]
stepTitle = "Certificate Format"
[certSign.certType]
windowsStore = "Windows Store"
macKeychain = "macOS Keychain"
pkcs11 = "PKCS#11"
[certSign.certFiles]
stepTitle = "Certificate Files"
+2 -1
View File
@@ -1,4 +1,5 @@
export const isDesktopMode = (): boolean =>
import.meta.env.MODE === 'desktop'
|| import.meta.env.VITE_DESKTOP === 'true'
|| import.meta.env.STIRLING_DESKTOP === 'true';
|| import.meta.env.STIRLING_DESKTOP === 'true'
|| import.meta.env.STIRLING_PDF_TAURI_MODE === 'true';