PDF Cert validation (#2394)

* verifyCerts

* cert info

* Hardening suggestions for Stirling-PDF / certValidate (#2395)

* Protect `readLine()` against DoS

* Switch order of literals to prevent NullPointerException

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

* some basic html excaping and translation fixing

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
Co-authored-by: a <a>
This commit is contained in:
Anthony Stirling 2024-12-05 15:56:22 +00:00 committed by GitHub
parent 0e3865618d
commit cce9f74eb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 26669 additions and 23 deletions

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.*;
@ -13,7 +12,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@ -32,35 +30,21 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@ -163,7 +147,7 @@ public class SecurityConfiguration {
http.sessionManagement(
sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
@ -287,7 +271,6 @@ public class SecurityConfiguration {
relyingPartyRegistrations())
.authenticationManager(
new ProviderManager(authenticationProvider))
.successHandler(
new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService,
@ -452,7 +435,7 @@ public class SecurityConfiguration {
.clientName("OIDC")
.build());
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
@ -506,7 +489,7 @@ public class SecurityConfiguration {
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}

View File

@ -147,7 +147,7 @@ public class StampController {
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
.replaceFirst("[.][^.]+$", "")
+ "_stamped.pdf");
}
@ -191,7 +191,7 @@ public class StampController {
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) {
FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os);
font = PDType0Font.load(document, tempFile);
} finally {
@ -339,4 +339,4 @@ public class StampController {
private float calculateTextCapHeight(PDFont font, float fontSize) {
return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
}
}
}

View File

@ -0,0 +1,168 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.util.Store;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
import stirling.software.SPDF.service.CertificateValidationService;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class ValidateSignatureController {
private final CustomPDDocumentFactory pdfDocumentFactory;
private final CertificateValidationService certValidationService;
@Autowired
public ValidateSignatureController(
CustomPDDocumentFactory pdfDocumentFactory,
CertificateValidationService certValidationService) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.certValidationService = certValidationService;
}
@Operation(
summary = "Validate PDF Digital Signature",
description =
"Validates the digital signatures in a PDF file against default or custom certificates. Input:PDF Output:JSON Type:SISO")
@PostMapping(value = "/validate-signature")
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
@ModelAttribute SignatureValidationRequest request) throws IOException {
List<SignatureValidationResult> results = new ArrayList<>();
MultipartFile file = request.getFileInput();
// Load custom certificate if provided
X509Certificate customCert = null;
if (request.getCertFile() != null && !request.getCertFile().isEmpty()) {
try (ByteArrayInputStream certStream =
new ByteArrayInputStream(request.getCertFile().getBytes())) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
customCert = (X509Certificate) cf.generateCertificate(certStream);
} catch (CertificateException e) {
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
}
}
try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
List<PDSignature> signatures = document.getSignatureDictionaries();
for (PDSignature sig : signatures) {
SignatureValidationResult result = new SignatureValidationResult();
try {
byte[] signedContent = sig.getSignedContent(file.getInputStream());
byte[] signatureBytes = sig.getContents(file.getInputStream());
CMSProcessable content = new CMSProcessableByteArray(signedContent);
CMSSignedData signedData = new CMSSignedData(content, signatureBytes);
Store<X509CertificateHolder> certStore = signedData.getCertificates();
SignerInformationStore signerStore = signedData.getSignerInfos();
for (SignerInformation signer : signerStore.getSigners()) {
X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
result.setValid(isValid);
// Additional validations
result.setChainValid(customCert != null
? certValidationService.validateCertificateChainWithCustomCert(cert, customCert)
: certValidationService.validateCertificateChain(cert));
result.setTrustValid(customCert != null
? certValidationService.validateTrustWithCustomCert(cert, customCert)
: certValidationService.validateTrustStore(cert));
result.setNotRevoked(!certValidationService.isRevoked(cert));
result.setNotExpired(!cert.getNotAfter().before(new Date()));
// Set basic signature info
result.setSignerName(sig.getName());
result.setSignatureDate(sig.getSignDate().getTime().toString());
result.setReason(sig.getReason());
result.setLocation(sig.getLocation());
// Set new certificate details
result.setIssuerDN(cert.getIssuerX500Principal().getName());
result.setSubjectDN(cert.getSubjectX500Principal().getName());
result.setSerialNumber(cert.getSerialNumber().toString(16)); // Hex format
result.setValidFrom(cert.getNotBefore().toString());
result.setValidUntil(cert.getNotAfter().toString());
result.setSignatureAlgorithm(cert.getSigAlgName());
// Get key size (if possible)
try {
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
} catch (Exception e) {
// If not RSA or error, set to 0
result.setKeySize(0);
}
result.setVersion(String.valueOf(cert.getVersion()));
// Set key usage
List<String> keyUsages = new ArrayList<>();
boolean[] keyUsageFlags = cert.getKeyUsage();
if (keyUsageFlags != null) {
String[] keyUsageLabels = {
"Digital Signature", "Non-Repudiation", "Key Encipherment",
"Data Encipherment", "Key Agreement", "Certificate Signing",
"CRL Signing", "Encipher Only", "Decipher Only"
};
for (int i = 0; i < keyUsageFlags.length; i++) {
if (keyUsageFlags[i]) {
keyUsages.add(keyUsageLabels[i]);
}
}
}
result.setKeyUsages(keyUsages);
// Check if self-signed
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
}
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage("Signature validation failed: " + e.getMessage());
}
results.add(result);
}
}
return ResponseEntity.ok(results);
}
}

View File

@ -53,6 +53,13 @@ public class SecurityWebController {
return "security/cert-sign";
}
@GetMapping("/validate-signature")
@Hidden
public String certSignVerifyForm(Model model) {
model.addAttribute("currentPage", "validate-signature");
return "security/validate-signature";
}
@GetMapping("/remove-cert-sign")
@Hidden
public String certUnSignForm(Model model) {

View File

@ -0,0 +1,17 @@
package stirling.software.SPDF.model.api.security;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class SignatureValidationRequest extends PDFFile {
@Schema(description = "(Optional) file to compare PDF cert signatures against x.509 format")
private MultipartFile certFile;
}

View File

@ -0,0 +1,31 @@
package stirling.software.SPDF.model.api.security;
import java.util.List;
import lombok.Data;
@Data
public class SignatureValidationResult {
private boolean valid;
private String signerName;
private String signatureDate;
private String reason;
private String location;
private String errorMessage;
private boolean chainValid;
private boolean trustValid;
private boolean notExpired;
private boolean notRevoked;
private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date
private String signatureAlgorithm;// Algorithm used for signing
private int keySize; // Key size in bits
private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed
}

View File

@ -0,0 +1,157 @@
package stirling.software.SPDF.service;
import io.github.pixee.security.BoundedLineReader;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
@Service
public class CertificateValidationService {
private KeyStore trustStore;
@PostConstruct
private void initializeTrustStore() throws Exception {
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
loadMozillaCertificates();
}
private void loadMozillaCertificates() throws Exception {
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder certData = new StringBuilder();
boolean inCert = false;
int certCount = 0;
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
inCert = true;
certData = new StringBuilder();
continue;
}
if (inCert) {
if ("END".equals(line)) {
inCert = false;
byte[] certBytes = parseOctalData(certData.toString());
if (certBytes != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert =
(X509Certificate)
cf.generateCertificate(
new ByteArrayInputStream(certBytes));
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
}
} else {
certData.append(line).append("\n");
}
}
}
}
}
private byte[] parseOctalData(String data) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] tokens = data.split("\\\\");
for (String token : tokens) {
token = token.trim();
if (!token.isEmpty()) {
baos.write(Integer.parseInt(token, 8));
}
}
return baos.toByteArray();
} catch (Exception e) {
return null;
}
}
public boolean validateCertificateChain(X509Certificate cert) {
try {
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<X509Certificate> certList = Arrays.asList(cert);
CertPath certPath = cf.generateCertPath(certList);
Set<TrustAnchor> anchors = new HashSet<>();
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) {
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
}
}
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
validator.validate(certPath, params);
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustStore(X509Certificate cert) {
try {
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
return true;
}
}
return false;
} catch (KeyStoreException e) {
return false;
}
}
public boolean isRevoked(X509Certificate cert) {
try {
cert.checkValidity();
return false;
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
return true;
}
}
public boolean validateCertificateChainWithCustomCert(
X509Certificate cert, X509Certificate customCert) {
try {
cert.verify(customCert.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
try {
// Compare the issuer of the signature certificate with the custom certificate
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
} catch (Exception e) {
return false;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -512,6 +512,10 @@ home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color
replace-color.title=Advanced Colour options
replace-color.header=Replace-Invert Color PDF
@ -1275,3 +1279,39 @@ releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@ -154,6 +154,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('validate-signature','verified','home.validateSignature.title','home.validateSignature.desc','validateSignature.tags','security')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
</div>

View File

@ -215,6 +215,9 @@
<div
th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', toolIcon='workspace_premium', tags=#{certSign.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='validate-signature', cardTitle=#{home.validateSignature.title}, cardText=#{home.validateSignature.desc}, cardLink='validate-signature', toolIcon='verified', tags=#{validateSignature.tags}, toolGroup='security')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-cert-sign', cardTitle=#{home.removeCertSign.title}, cardText=#{home.removeCertSign.desc}, cardLink='remove-cert-sign', toolIcon='remove_moderator', tags=#{removeCertSign.tags}, toolGroup='security')}">
</div>

View File

@ -0,0 +1,265 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{validateSignature.title}, header=#{validateSignature.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon security">verified</span>
<span class="tool-header-text" th:text="#{validateSignature.header}"></span>
</div>
<form id="pdfForm" th:action="@{'api/v1/security/validate-signature'}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label th:text="#{validateSignature.selectPDF}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
</div>
<div class="mb-3">
<label th:text="#{validateSignature.selectCustomCert}" ></label>
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, remoteCall='false', accept='.cer,.crt,.pem')}"></div>
</div>
<div class="mb-3 text-left">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{validateSignature.submit}"></button>
</div>
</form>
<!-- Results section -->
<div id="results" style="display: none;">
<h4 th:text="#{validateSignature.results}"></h4>
<div id="signatures-list"></div>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script th:inline="javascript">
const translations = {
signature: /*[[#{validateSignature.signature}]]*/,
signatureInfo: /*[[#{validateSignature.signature.info}]]*/,
certInfo: /*[[#{validateSignature.cert.info}]]*/,
signer: /*[[#{validateSignature.signer}]]*/,
date: /*[[#{validateSignature.date}]]*/,
reason: /*[[#{validateSignature.reason}]]*/,
location: /*[[#{validateSignature.location}]]*/,
noSignatures: /*[[#{validateSignature.noSignatures}]]*/,
statusValid: /*[[#{validateSignature.status.valid}]]*/,
statusInvalid: /*[[#{validateSignature.status.invalid}]]*/,
mathValid: /*[[#{validateSignature.signature.mathValid}]]*/,
chainInvalid: /*[[#{validateSignature.chain.invalid}]]*/,
trustInvalid: /*[[#{validateSignature.trust.invalid}]]*/,
certExpired: /*[[#{validateSignature.cert.expired}]]*/,
certRevoked: /*[[#{validateSignature.cert.revoked}]]*/,
certIssuer: /*[[#{validateSignature.cert.issuer}]]*/,
certSubject: /*[[#{validateSignature.cert.subject}]]*/,
certSerialNumber: /*[[#{validateSignature.cert.serialNumber}]]*/,
certValidFrom: /*[[#{validateSignature.cert.validFrom}]]*/,
certValidUntil: /*[[#{validateSignature.cert.validUntil}]]*/,
certAlgorithm: /*[[#{validateSignature.cert.algorithm}]]*/,
certKeySize: /*[[#{validateSignature.cert.keySize}]]*/,
certBits: /*[[#{validateSignature.cert.bits}]]*/,
certVersion: /*[[#{validateSignature.cert.version}]]*/,
certKeyUsage: /*[[#{validateSignature.cert.keyUsage}]]*/,
certSelfSigned: /*[[#{validateSignature.cert.selfSigned}]]*/,
yes: /*[[#{yes}]]*/,
no: /*[[#{no}]]*/,
selectPDF: /*[[#{validateSignature.selectPDF}]]*/
};
function escapeHtml(unsafe) {
return unsafe
?.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;") || 'N/A';
}
document.querySelector('#pdfForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('fileInput-input');
const certInput = document.getElementById('certFile-input');
if (!fileInput.files.length) {
alert(escapeHtml(translations.selectPDF));
return;
}
const results = [];
for (const file of fileInput.files) {
const formData = new FormData();
formData.append('fileInput', file);
if (certInput.files.length > 0) {
formData.append('certFile', certInput.files[0]);
}
try {
const response = await fetch(e.target.action, {
method: 'POST',
body: formData
});
const fileResults = await response.json();
fileResults.forEach(result => {
result.fileName = file.name;
});
results.push(...fileResults);
} catch (error) {
results.push({
fileName: file.name,
valid: false,
errorMessage: `${escapeHtml(translations.statusInvalid)}: ${escapeHtml(error.message)}`
});
}
}
displayResults(results);
});
function displayResults(results) {
const resultDiv = document.getElementById('results');
const listDiv = document.getElementById('signatures-list');
listDiv.innerHTML = '';
resultDiv.style.display = 'block';
if (!results || results.length === 0) {
listDiv.innerHTML = `<div class="alert alert-warning">${escapeHtml(translations.noSignatures)}</div>`;
return;
}
results.forEach((result, index) => {
const signatureDiv = document.createElement('div');
signatureDiv.className = 'card mb-3';
let validationClass = 'alert-danger';
let validationIssues = [];
if (!result.valid) {
validationIssues.push(`${escapeHtml(translations.statusInvalid)}: ${escapeHtml(result.errorMessage || '')}`);
} else {
const isFullyValid = result.valid &&
result.chainValid &&
result.trustValid &&
result.notExpired &&
result.notRevoked;
if (isFullyValid) {
validationClass = 'alert-success';
validationIssues.push(escapeHtml(translations.statusValid));
} else {
validationClass = 'alert-warning';
validationIssues.push(escapeHtml(translations.mathValid));
if (!result.chainValid) {
validationIssues.push(escapeHtml(translations.chainInvalid));
}
if (!result.trustValid) {
validationIssues.push(escapeHtml(translations.trustInvalid));
}
if (!result.notExpired) {
validationIssues.push(escapeHtml(translations.certExpired));
}
if (result.trustValid && result.chainValid && !result.notRevoked) {
validationIssues.push(escapeHtml(translations.certRevoked));
}
}
}
let statusMessage = validationIssues[0];
if (validationIssues.length > 1) {
statusMessage += '<ul class="mb-0 mt-2">';
for (let i = 1; i < validationIssues.length; i++) {
statusMessage += `<li>${validationIssues[i]}</li>`;
}
statusMessage += '</ul>';
}
let content = `
<div class="card-body">
${results.length > 1 ? `<h4 class="mb-3">${escapeHtml(translations.signature)} ${index + 1}</h4>` : ''}
<div class="alert ${validationClass}">
${statusMessage}
</div>
<div class="card-text">
<h5>${escapeHtml(translations.signatureInfo)}</h5>
<table class="table table-borderless">
<tr>
<td><strong>${escapeHtml(translations.signer)}:</strong></td>
<td>${escapeHtml(result.signerName)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.date)}:</strong></td>
<td>${escapeHtml(result.signatureDate)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.reason)}:</strong></td>
<td>${escapeHtml(result.reason)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.location)}:</strong></td>
<td>${escapeHtml(result.location)}</td>
</tr>
</table>
<h5>${escapeHtml(translations.certInfo)}</h5>
<table class="table table-borderless">
<tr>
<td><strong>${escapeHtml(translations.certIssuer)}:</strong></td>
<td>${escapeHtml(result.issuerDN)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certSubject)}:</strong></td>
<td>${escapeHtml(result.subjectDN)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certSerialNumber)}:</strong></td>
<td>${escapeHtml(result.serialNumber)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certValidFrom)}:</strong></td>
<td>${escapeHtml(result.validFrom)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certValidUntil)}:</strong></td>
<td>${escapeHtml(result.validUntil)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certAlgorithm)}:</strong></td>
<td>${escapeHtml(result.signatureAlgorithm)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certKeySize)}:</strong></td>
<td>${result.keySize ? escapeHtml(result.keySize) + ' ' + escapeHtml(translations.certBits) : 'N/A'}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certVersion)}:</strong></td>
<td>${escapeHtml(result.version)}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certKeyUsage)}:</strong></td>
<td>${result.keyUsages ? result.keyUsages.map(usage => escapeHtml(usage)).join(', ') : 'N/A'}</td>
</tr>
<tr>
<td><strong>${escapeHtml(translations.certSelfSigned)}:</strong></td>
<td>${result.selfSigned ? escapeHtml(translations.yes) : escapeHtml(translations.no)}</td>
</tr>
</table>
</div>
</div>
`;
signatureDiv.innerHTML = content;
listDiv.appendChild(signatureDiv);
});
}
</script>
</body>
</html>