add file share to the top workbench bar and add shared signing (#6715)

This commit is contained in:
EthanHealy01
2026-06-30 22:14:49 +01:00
committed by GitHub
parent 276eb8f2a7
commit 7ab30d2629
65 changed files with 3539 additions and 7975 deletions
@@ -404,18 +404,16 @@ public class WorkflowParticipantController {
certSubmission.put("reason", request.getReason());
certSubmission.put("showLogo", request.getShowLogo());
// Store certificate files as base64
// Store the certificate keystores encrypted at rest.
if (request.getP12File() != null && !request.getP12File().isEmpty()) {
certSubmission.put(
"p12Keystore",
java.util.Base64.getEncoder()
.encodeToString(request.getP12File().getBytes()));
metadataEncryptionService.encryptBytes(request.getP12File().getBytes()));
}
if (request.getJksFile() != null && !request.getJksFile().isEmpty()) {
certSubmission.put(
"jksKeystore",
java.util.Base64.getEncoder()
.encodeToString(request.getJksFile().getBytes()));
metadataEncryptionService.encryptBytes(request.getJksFile().getBytes()));
}
metadata.put("certificateSubmission", certSubmission);
@@ -32,6 +32,7 @@ public class SignDocumentRequest {
private String password;
private MultipartFile privateKeyFile;
private MultipartFile certFile;
private MultipartFile jksFile;
// Signature metadata (participant can override owner defaults)
private String reason; // Participant's reason for signing
@@ -96,6 +96,28 @@ public class MetadataEncryptionService {
}
}
/**
* Encodes raw bytes to Base64 and encrypts them for at-rest storage (e.g. keystore files held
* in JSONB metadata). Returns {@code null} for {@code null} input.
*/
public String encryptBytes(byte[] data) {
if (data == null) {
return null;
}
return encrypt(Base64.getEncoder().encodeToString(data));
}
/**
* Reverses {@link #encryptBytes}. Also accepts legacy values stored as plain Base64 before
* encryption was introduced — {@link #decrypt} returns those unchanged, so they still decode.
*/
public byte[] decryptBytes(String stored) {
if (stored == null) {
return null;
}
return Base64.getDecoder().decode(decrypt(stored));
}
// ── Internals ───────────────────────────────────────────────────────────
private SecretKeySpec deriveKey() throws Exception {
@@ -1039,17 +1039,18 @@ public class SigningFinalizationService {
metadataEncryptionService.decrypt(submission.getPassword()));
}
// Decode base64 keystore bytes
// Decrypt + decode keystore bytes (supports both legacy plaintext base64 and
// encrypted values).
var certNode = node.get("certificateSubmission");
if (certNode.has("p12Keystore")) {
submission.setP12Keystore(
java.util.Base64.getDecoder()
.decode(certNode.get("p12Keystore").asText()));
metadataEncryptionService.decryptBytes(
certNode.get("p12Keystore").asText()));
}
if (certNode.has("jksKeystore")) {
submission.setJksKeystore(
java.util.Base64.getDecoder()
.decode(certNode.get("jksKeystore").asText()));
metadataEncryptionService.decryptBytes(
certNode.get("jksKeystore").asText()));
}
return submission;
}
@@ -1,6 +1,13 @@
package stirling.software.proprietary.workflow.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
@@ -8,6 +15,16 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -705,24 +722,70 @@ public class WorkflowSessionService {
}
}
// 2. Store certificate submission data
Map<String, Object> certSubmission = new HashMap<>();
certSubmission.put("certType", request.getCertType());
certSubmission.put("password", metadataEncryptionService.encrypt(request.getPassword()));
// Store keystore files as base64 if provided
if (request.getP12File() != null && !request.getP12File().isEmpty()) {
// Validate an uploaded JKS keystore too (same early rejection as P12/PFX).
if ("JKS".equalsIgnoreCase(request.getCertType())
&& request.getJksFile() != null
&& !request.getJksFile().isEmpty()) {
try {
byte[] keystoreBytes = request.getP12File().getBytes();
String base64Keystore = java.util.Base64.getEncoder().encodeToString(keystoreBytes);
certSubmission.put("p12Keystore", base64Keystore);
certificateSubmissionValidator.validateAndExtractInfo(
request.getJksFile().getBytes(), "JKS", request.getPassword());
} catch (ResponseStatusException e) {
throw e;
} catch (IOException e) {
log.error("Failed to read P12 keystore file", e);
log.error("Failed to read JKS keystore file for validation", e);
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "Failed to process certificate file");
}
}
// 2. Store certificate submission data
Map<String, Object> certSubmission = new HashMap<>();
certSubmission.put("password", metadataEncryptionService.encrypt(request.getPassword()));
if ("PEM".equalsIgnoreCase(request.getCertType())) {
// PEM uploads are a separate private key + certificate, not a keystore. Convert them to
// a PKCS12 keystore here so finalization signs via the standard PKCS12 path.
byte[] p12 =
buildPkcs12FromPem(
request.getPrivateKeyFile(),
request.getCertFile(),
request.getPassword());
// Give PEM the same early validation (expiry, key recovery, test-sign) as uploaded
// PKCS12/JKS keystores, so an expired or unusable cert is rejected now rather than at
// finalization.
certificateSubmissionValidator.validateAndExtractInfo(
p12, "PKCS12", request.getPassword());
certSubmission.put("certType", "PKCS12");
// Store the keystore encrypted at rest.
certSubmission.put("p12Keystore", metadataEncryptionService.encryptBytes(p12));
} else {
certSubmission.put("certType", request.getCertType());
// Encrypt the uploaded keystore at rest: PKCS12/PFX → p12Keystore, JKS → jksKeystore.
if (request.getP12File() != null && !request.getP12File().isEmpty()) {
try {
certSubmission.put(
"p12Keystore",
metadataEncryptionService.encryptBytes(
request.getP12File().getBytes()));
} catch (IOException e) {
log.error("Failed to read P12 keystore file", e);
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "Failed to process certificate file");
}
} else if (request.getJksFile() != null && !request.getJksFile().isEmpty()) {
try {
certSubmission.put(
"jksKeystore",
metadataEncryptionService.encryptBytes(
request.getJksFile().getBytes()));
} catch (IOException e) {
log.error("Failed to read JKS keystore file", e);
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "Failed to process certificate file");
}
}
}
// Note: Signature appearance settings (showSignature, pageNumber, location, reason,
// showLogo)
// may already be in metadata if owner configured them when adding participant.
@@ -837,6 +900,69 @@ public class WorkflowSessionService {
"User is not a participant in this session"));
}
/**
* Converts an uploaded PEM private key + certificate into a PKCS12 keystore (protected with the
* supplied password) so finalization can sign via the standard PKCS12 path.
*/
private byte[] buildPkcs12FromPem(
MultipartFile privateKeyFile, MultipartFile certFile, String password) {
if (privateKeyFile == null
|| privateKeyFile.isEmpty()
|| certFile == null
|| certFile.isEmpty()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"PEM signing requires both a private key file and a certificate file");
}
char[] pw = password != null ? password.toCharArray() : new char[0];
try {
PrivateKey privateKey = readPemPrivateKey(privateKeyFile.getBytes(), pw);
Certificate cert =
CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null, null);
keyStore.setKeyEntry("alias", privateKey, pw, new Certificate[] {cert});
ByteArrayOutputStream out = new ByteArrayOutputStream();
keyStore.store(out, pw);
return out.toByteArray();
} catch (ResponseStatusException e) {
throw e;
} catch (Exception e) {
log.error("Failed to build keystore from PEM certificate", e);
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Failed to read PEM certificate — check the key/certificate files and password");
}
}
/** Reads a PEM private key (PKCS8/PKCS1, optionally password-encrypted). */
private PrivateKey readPemPrivateKey(byte[] pemBytes, char[] password) throws Exception {
try (PEMParser pemParser =
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
Object pemObject = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKeyInfo keyInfo;
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo encrypted) {
InputDecryptorProvider decryptor =
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password);
keyInfo = encrypted.decryptPrivateKeyInfo(decryptor);
} else if (pemObject instanceof PEMEncryptedKeyPair encryptedKeyPair) {
PEMDecryptorProvider decryptor =
new JcePEMDecryptorProviderBuilder().build(password);
keyInfo = encryptedKeyPair.decryptKeyPair(decryptor).getPrivateKeyInfo();
} else if (pemObject instanceof PEMKeyPair keyPair) {
keyInfo = keyPair.getPrivateKeyInfo();
} else if (pemObject instanceof PrivateKeyInfo info) {
keyInfo = info;
} else {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "Unsupported PEM private key format");
}
return converter.getPrivateKey(keyInfo);
}
}
/** Helper class to wrap byte array as MultipartFile. */
private static class ByteArrayMultipartFile implements MultipartFile {
private final byte[] content;