-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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>
- Loading branch information
1 parent
0e38656
commit cce9f74
Showing
12 changed files
with
26,669 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
...main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
src/main/java/stirling/software/SPDF/model/api/security/SignatureValidationRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
31 changes: 31 additions & 0 deletions
31
src/main/java/stirling/software/SPDF/model/api/security/SignatureValidationResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
} |
Oops, something went wrong.