mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-11-01 01:21:18 +01:00 
			
		
		
		
	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:
		
							parent
							
								
									0e3865618d
								
							
						
					
					
						commit
						cce9f74eb9
					
				@ -1,6 +1,5 @@
 | 
				
			|||||||
package stirling.software.SPDF.config.security;
 | 
					package stirling.software.SPDF.config.security;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.security.cert.X509Certificate;
 | 
					import java.security.cert.X509Certificate;
 | 
				
			||||||
import java.util.*;
 | 
					import java.util.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,7 +12,6 @@ import org.springframework.context.annotation.Configuration;
 | 
				
			|||||||
import org.springframework.context.annotation.DependsOn;
 | 
					import org.springframework.context.annotation.DependsOn;
 | 
				
			||||||
import org.springframework.context.annotation.Lazy;
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
import org.springframework.core.io.Resource;
 | 
					import org.springframework.core.io.Resource;
 | 
				
			||||||
import org.springframework.security.authentication.AuthenticationProvider;
 | 
					 | 
				
			||||||
import org.springframework.security.authentication.ProviderManager;
 | 
					import org.springframework.security.authentication.ProviderManager;
 | 
				
			||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 | 
					import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 | 
				
			||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 | 
					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.oauth2.core.user.OAuth2UserAuthority;
 | 
				
			||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
 | 
					import org.springframework.security.saml2.core.Saml2X509Credential;
 | 
				
			||||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
 | 
					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.authentication.OpenSaml4AuthenticationProvider;
 | 
				
			||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
 | 
					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.RelyingPartyRegistration;
 | 
				
			||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
 | 
					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.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.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
 | 
				
			||||||
import org.springframework.security.web.SecurityFilterChain;
 | 
					import org.springframework.security.web.SecurityFilterChain;
 | 
				
			||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 | 
					import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 | 
				
			||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
 | 
					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.CookieCsrfTokenRepository;
 | 
				
			||||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
 | 
					import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
 | 
				
			||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
 | 
					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.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.HttpServletRequest;
 | 
				
			||||||
import jakarta.servlet.http.HttpServletResponse;
 | 
					 | 
				
			||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
 | 
					import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
 | 
				
			||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
 | 
					import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
 | 
				
			||||||
@ -163,7 +147,7 @@ public class SecurityConfiguration {
 | 
				
			|||||||
            http.sessionManagement(
 | 
					            http.sessionManagement(
 | 
				
			||||||
                    sessionManagement ->
 | 
					                    sessionManagement ->
 | 
				
			||||||
                            sessionManagement
 | 
					                            sessionManagement
 | 
				
			||||||
                            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
 | 
					                                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
 | 
				
			||||||
                                    .maximumSessions(10)
 | 
					                                    .maximumSessions(10)
 | 
				
			||||||
                                    .maxSessionsPreventsLogin(false)
 | 
					                                    .maxSessionsPreventsLogin(false)
 | 
				
			||||||
                                    .sessionRegistry(sessionRegistry)
 | 
					                                    .sessionRegistry(sessionRegistry)
 | 
				
			||||||
@ -287,7 +271,6 @@ public class SecurityConfiguration {
 | 
				
			|||||||
                                                        relyingPartyRegistrations())
 | 
					                                                        relyingPartyRegistrations())
 | 
				
			||||||
                                                .authenticationManager(
 | 
					                                                .authenticationManager(
 | 
				
			||||||
                                                        new ProviderManager(authenticationProvider))
 | 
					                                                        new ProviderManager(authenticationProvider))
 | 
				
			||||||
                                     
 | 
					 | 
				
			||||||
                                                .successHandler(
 | 
					                                                .successHandler(
 | 
				
			||||||
                                                        new CustomSaml2AuthenticationSuccessHandler(
 | 
					                                                        new CustomSaml2AuthenticationSuccessHandler(
 | 
				
			||||||
                                                                loginAttemptService,
 | 
					                                                                loginAttemptService,
 | 
				
			||||||
@ -452,7 +435,7 @@ public class SecurityConfiguration {
 | 
				
			|||||||
                        .clientName("OIDC")
 | 
					                        .clientName("OIDC")
 | 
				
			||||||
                        .build());
 | 
					                        .build());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    @Bean
 | 
					    @Bean
 | 
				
			||||||
    @ConditionalOnProperty(
 | 
					    @ConditionalOnProperty(
 | 
				
			||||||
            name = "security.saml2.enabled",
 | 
					            name = "security.saml2.enabled",
 | 
				
			||||||
@ -506,7 +489,7 @@ public class SecurityConfiguration {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    AuthnRequest authnRequest = customizer.getAuthnRequest();
 | 
					                    AuthnRequest authnRequest = customizer.getAuthnRequest();
 | 
				
			||||||
                    log.debug("AuthnRequest ID: {}", authnRequest.getID());
 | 
					                    log.debug("AuthnRequest ID: {}", authnRequest.getID());
 | 
				
			||||||
                    
 | 
					
 | 
				
			||||||
                    if (authnRequest.getID() == null) {
 | 
					                    if (authnRequest.getID() == null) {
 | 
				
			||||||
                        authnRequest.setID("ARQ" + UUID.randomUUID().toString());
 | 
					                        authnRequest.setID("ARQ" + UUID.randomUUID().toString());
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
				
			|||||||
@ -147,7 +147,7 @@ public class StampController {
 | 
				
			|||||||
        return WebResponseUtils.pdfDocToWebResponse(
 | 
					        return WebResponseUtils.pdfDocToWebResponse(
 | 
				
			||||||
                document,
 | 
					                document,
 | 
				
			||||||
                Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
 | 
					                Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
 | 
				
			||||||
                        .replaceFirst("[.][^.]+$", "")
 | 
					                                .replaceFirst("[.][^.]+$", "")
 | 
				
			||||||
                        + "_stamped.pdf");
 | 
					                        + "_stamped.pdf");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -191,7 +191,7 @@ public class StampController {
 | 
				
			|||||||
            String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
 | 
					            String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
 | 
				
			||||||
            File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
 | 
					            File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
 | 
				
			||||||
            try (InputStream is = classPathResource.getInputStream();
 | 
					            try (InputStream is = classPathResource.getInputStream();
 | 
				
			||||||
                 FileOutputStream os = new FileOutputStream(tempFile)) {
 | 
					                    FileOutputStream os = new FileOutputStream(tempFile)) {
 | 
				
			||||||
                IOUtils.copy(is, os);
 | 
					                IOUtils.copy(is, os);
 | 
				
			||||||
                font = PDType0Font.load(document, tempFile);
 | 
					                font = PDType0Font.load(document, tempFile);
 | 
				
			||||||
            } finally {
 | 
					            } finally {
 | 
				
			||||||
@ -339,4 +339,4 @@ public class StampController {
 | 
				
			|||||||
    private float calculateTextCapHeight(PDFont font, float fontSize) {
 | 
					    private float calculateTextCapHeight(PDFont font, float fontSize) {
 | 
				
			||||||
        return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
 | 
					        return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -53,6 +53,13 @@ public class SecurityWebController {
 | 
				
			|||||||
        return "security/cert-sign";
 | 
					        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")
 | 
					    @GetMapping("/remove-cert-sign")
 | 
				
			||||||
    @Hidden
 | 
					    @Hidden
 | 
				
			||||||
    public String certUnSignForm(Model model) {
 | 
					    public String certUnSignForm(Model model) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25972
									
								
								src/main/resources/certdata.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25972
									
								
								src/main/resources/certdata.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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.
 | 
					home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
 | 
				
			||||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
 | 
					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-invert-color
 | 
				
			||||||
replace-color.title=Advanced Colour options
 | 
					replace-color.title=Advanced Colour options
 | 
				
			||||||
replace-color.header=Replace-Invert Color PDF
 | 
					replace-color.header=Replace-Invert Color PDF
 | 
				
			||||||
@ -1275,3 +1279,39 @@ releases.title=Release Notes
 | 
				
			|||||||
releases.header=Release Notes
 | 
					releases.header=Release Notes
 | 
				
			||||||
releases.current.version=Current Release
 | 
					releases.current.version=Current Release
 | 
				
			||||||
releases.note=Release notes are only available in English
 | 
					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
 | 
				
			||||||
 | 
				
			|||||||
@ -154,6 +154,9 @@
 | 
				
			|||||||
                        <div
 | 
					                        <div
 | 
				
			||||||
                          th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
 | 
					                          th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div 
 | 
				
			||||||
 | 
								              th:replace="~{fragments/navbarEntry :: navbarEntry('validate-signature','verified','home.validateSignature.title','home.validateSignature.desc','validateSignature.tags','security')}">
 | 
				
			||||||
 | 
								            </div>
 | 
				
			||||||
                        <div
 | 
					                        <div
 | 
				
			||||||
                          th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
 | 
					                          th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -215,6 +215,9 @@
 | 
				
			|||||||
              <div
 | 
					              <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')}">
 | 
					                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>
 | 
				
			||||||
 | 
								  <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
 | 
					              <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')}">
 | 
					                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>
 | 
					              </div>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										265
									
								
								src/main/resources/templates/security/validate-signature.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								src/main/resources/templates/security/validate-signature.html
									
									
									
									
									
										Normal 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, "&")
 | 
				
			||||||
 | 
					        .replace(/</g, "<")
 | 
				
			||||||
 | 
					        .replace(/>/g, ">")
 | 
				
			||||||
 | 
					        .replace(/"/g, """)
 | 
				
			||||||
 | 
					        .replace(/'/g, "'") || '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>
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user