se.swedenconnect.sigval.pdf.verify.impl.SVTenabledPDFDocumentSigVerifier Maven / Gradle / Ivy
package se.swedenconnect.sigval.pdf.verify.impl;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.commons.codec.binary.Base64;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.asn1.cms.SignedData;
import org.bouncycastle.cms.CMSSignedDataParser;
import org.bouncycastle.cms.CMSTypedStream;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.extern.slf4j.Slf4j;
import se.idsec.signservice.security.certificate.CertificateValidationResult;
import se.idsec.signservice.security.certificate.CertificateValidator;
import se.idsec.signservice.security.certificate.impl.DefaultCertificateValidationResult;
import se.idsec.signservice.security.sign.SignatureValidationResult;
import se.swedenconnect.sigval.commons.algorithms.JWSAlgorithmRegistry;
import se.swedenconnect.sigval.commons.data.PubKeyParams;
import se.swedenconnect.sigval.commons.data.SigValIdentifiers;
import se.swedenconnect.sigval.commons.data.SignedDocumentValidationResult;
import se.swedenconnect.sigval.commons.data.TimeValidationResult;
import se.swedenconnect.sigval.commons.utils.GeneralCMSUtils;
import se.swedenconnect.sigval.commons.utils.SVAUtils;
import se.swedenconnect.sigval.pdf.data.ExtendedPdfSigValResult;
import se.swedenconnect.sigval.pdf.pdfstruct.PDFSignatureContext;
import se.swedenconnect.sigval.pdf.pdfstruct.PDFSignatureContextFactory;
import se.swedenconnect.sigval.pdf.svt.PDFSVTValidator;
import se.swedenconnect.sigval.pdf.timestamp.PDFDocTimeStamp;
import se.swedenconnect.sigval.pdf.utils.CMSVerifyUtils;
import se.swedenconnect.sigval.pdf.utils.PDFSVAUtils;
import se.swedenconnect.sigval.pdf.verify.ExtendedPDFSignatureValidator;
import se.swedenconnect.sigval.pdf.verify.PDFSingleSignatureValidator;
import se.swedenconnect.sigval.svt.algorithms.SVTAlgoRegistry;
import se.swedenconnect.sigval.svt.claims.PolicyValidationClaims;
import se.swedenconnect.sigval.svt.claims.SignatureClaims;
import se.swedenconnect.sigval.svt.claims.TimeValidationClaims;
import se.swedenconnect.sigval.svt.claims.ValidationConclusion;
import se.swedenconnect.sigval.svt.validation.SignatureSVTValidationResult;
/**
* This class provides the functionality to validate signatures on a PDF where the signature validation process is enhanced with validation
* based on SVA (Signature Validation Assertions). The latest valid SVA that can be verified given the provided trust validation resources is selected.
* Signatures covered by this SVA is validated based on SVA. Any other signatures are validated through traditional signature validation methods.
*
* @author Martin Lindström ([email protected])
* @author Stefan Santesson ([email protected])
*/
@Slf4j
public class SVTenabledPDFDocumentSigVerifier implements ExtendedPDFSignatureValidator {
public static Logger LOG = Logger.getLogger(SVTenabledPDFDocumentSigVerifier.class.getName());
/** SVT token validator **/
private final PDFSVTValidator pdfsvtValidator;
/** Signature verifier for signatures not supported by SVT. This verifier is also performing validation of signature timestamps **/
private final PDFSingleSignatureValidator pdfSingleSignatureValidator;
private final PDFSignatureContextFactory pdfSignatureContextFactory;
/**
* Constructor if no SVT validation is supported
*
* @param pdfSingleSignatureValidator The verifier used to verify signatures not supported by SVA
* @param pdfSignatureContextFactory factory for creating an instance of signature context for the validated document
*/
public SVTenabledPDFDocumentSigVerifier(PDFSingleSignatureValidator pdfSingleSignatureValidator,
PDFSignatureContextFactory pdfSignatureContextFactory) {
this.pdfSingleSignatureValidator = pdfSingleSignatureValidator;
this.pdfSignatureContextFactory = pdfSignatureContextFactory;
this.pdfsvtValidator = null;
}
/**
* Constructor
*
* @param pdfSingleSignatureValidator The verifier used to verify signatures not supported by SVA
* @param pdfsvtValidator Certificate verifier for the certificate used to sign SVA tokens
* @param pdfSignatureContextFactory factory for creating an instance of signature context for the validated document
*/
public SVTenabledPDFDocumentSigVerifier(PDFSingleSignatureValidator pdfSingleSignatureValidator,
PDFSVTValidator pdfsvtValidator, PDFSignatureContextFactory pdfSignatureContextFactory) {
this.pdfSingleSignatureValidator = pdfSingleSignatureValidator;
this.pdfsvtValidator = pdfsvtValidator;
this.pdfSignatureContextFactory = pdfSignatureContextFactory;
}
/**
* Verifies the signatures of a PDF document. Validation based on SVT is given preference over traditional signature validation.
*
* @param pdfDoc signed PDF document to verify
* @return Validation result from PDF verification
* @throws SignatureException on error
*/
public List validate(File pdfDoc) throws SignatureException {
byte[] docBytes = null;
try {
docBytes = IOUtils.toByteArray(new FileInputStream(pdfDoc));
}
catch (IOException ex) {
throw new SignatureException("Unable to read signed file", ex);
}
return validate(docBytes);
}
/**
* Verifies the signatures of a PDF document. Validation based on SVA is given preference over traditional signature validation.
*
* @param pdfDocBytes signed PDF document to verify
* @return Validation result from PDF verification
* @throws SignatureException on error
*/
@Override public List validate(byte[] pdfDocBytes) throws SignatureException {
try {
PDFSignatureContext signatureContext = pdfSignatureContextFactory.getPdfSignatureContext(pdfDocBytes);
List allSignatureList = signatureContext.getSignatures();
List docTsSigList = new ArrayList<>();
List signatureList = new ArrayList<>();
for (PDSignature signature : allSignatureList) {
String type;
try {
byte[] contents = signature.getContents(pdfDocBytes);
type = PDFSVAUtils.getSignatureType(signature, contents);
}
catch (Exception e) {
type = PDFSVAUtils.ILLEGAL_SIGNATURE_TYPE;
log.debug("Error parsing signature data: {}", e.getMessage());
}
switch (type) {
case PDFSVAUtils.SVT_TYPE:
case PDFSVAUtils.DOC_TIMESTAMP_TYPE:
docTsSigList.add(signature);
break;
case PDFSVAUtils.SIGNATURE_TYPE:
case PDFSVAUtils.ILLEGAL_SIGNATURE_TYPE:
signatureList.add(signature);
}
}
// Create empty result list
List sigVerifyResultList = new ArrayList<>();
// This list starts empty. It is only filled with objects if there is a signature that is validated without SVT.
List docTimeStampList = new ArrayList<>();
boolean docTsVerified = false;
// Obtain any SVT validation results from a present SVT validator
List svtValidationResults =
pdfsvtValidator == null ? null : pdfsvtValidator.validate(pdfDocBytes);
for (PDSignature signature : signatureList) {
SignatureSVTValidationResult svtValResult = null;
try {
svtValResult = getMatchingSvtValidation(PDFSVAUtils.getSignatureValueBytes(signature, pdfDocBytes), svtValidationResults);
} catch (Exception e) {
log.debug("Error looking for signature validation result: {}", e.getMessage());
}
if (svtValResult == null) {
// This signature is not covered by a valid SVT. Perform normal signature verification
try {
//Get verified documentTimestamps if not previously loaded
if (!docTsVerified) {
docTimeStampList = pdfSingleSignatureValidator.verifyDocumentTimestamps(docTsSigList, pdfDocBytes);
docTsVerified = true;
}
SignatureValidationResult directVerifyResult = pdfSingleSignatureValidator.verifySignature(signature,
pdfDocBytes, docTimeStampList,
signatureContext);
sigVerifyResultList.add(directVerifyResult);
}
catch (Exception e) {
LOG.warning("Error parsing the PDF signature: " + e.getMessage());
sigVerifyResultList.add(getErrorResult(signature, e.getMessage()));
}
}
else {
// There is SVT validation results. Use them.
sigVerifyResultList.add(
compliePDFSigValResultsFromSvtValidation(svtValResult, signature, pdfDocBytes, signatureContext));
}
}
return sigVerifyResultList;
}
catch (Exception ex) {
throw new SignatureException("Error validating signatures on PDF document", ex);
}
}
/** {@inheritDoc} */
@Override public boolean isSigned(byte[] document) throws IllegalArgumentException {
PDDocument pdfDocument = null;
try {
pdfDocument = Loader.loadPDF(document);
return !pdfDocument.getSignatureDictionaries().isEmpty();
}
catch (IOException e) {
throw new IllegalArgumentException("Invalid document", e);
}
finally {
try {
if (pdfDocument != null) {
pdfDocument.close();
}
}
catch (IOException e) {
}
}
}
/**
* This implementation allways perform PKIX validation and returns an empty list for this function
*
* @return empty list
*/
@Override public List getRequiredSignerCertificates() {
return new ArrayList<>();
}
/** {@inheritDoc} */
@Override public CertificateValidator getCertificateValidator() {
return pdfSingleSignatureValidator.getCertificateValidator();
}
/**
* Use the results obtained from SVT validation to produce general signature validation result as if the signature was validated using
* complete validation.
*
* @param svtValResult results from SVT validation
* @param signature the signature being validated
* @param pdfDocBytes the bytes of the PDF document
* @param signatureContext the context of the signature in the PDF document
* @return {@link ExtendedPdfSigValResult} signature results
*/
private ExtendedPdfSigValResult compliePDFSigValResultsFromSvtValidation(SignatureSVTValidationResult svtValResult,
PDSignature signature, byte[] pdfDocBytes, PDFSignatureContext signatureContext) {
ExtendedPdfSigValResult cmsSVResult = new ExtendedPdfSigValResult();
cmsSVResult.setPdfSignature(signature);
try {
byte[] sigBytes = signature.getContents(pdfDocBytes);
cmsSVResult.setSignedData(sigBytes);
//Reaching this point means that the signature is valid and verified through the SVA.
SignedData signedData = SVAUtils.getSignedDataFromSignature(sigBytes);
cmsSVResult.setEtsiAdes(signature.getSubFilter().equalsIgnoreCase(PDFSVAUtils.CADES_SIG_SUBFILETER_LC));
cmsSVResult.setInvalidSignCert(false);
cmsSVResult.setClaimedSigningTime(PDFSVAUtils.getClaimedSigningTime(signature.getSignDate(), signedData));
cmsSVResult.setCoversDocument(signatureContext.isCoversWholeDocument(signature));
byte[] signedDocumentBytes = null;
try {
signedDocumentBytes = signatureContext.getSignedDocument(signature);
}
catch (Exception ex) {
log.warn("Error extracting the document version signed by this signature: {}", ex.getMessage());
}
cmsSVResult.setSignedDocument(signedDocumentBytes);
//Get algorithms and public key type. Note that the source of these values is the SVA signature which is regarded as the algorithm
//That is effectively protecting the integrity of the signature, superseding the use of the original algorithms.
SignedJWT signedJWT = svtValResult.getSignedJWT();
JWSAlgorithm svtJwsAlgo = signedJWT.getHeader().getAlgorithm();
String algoUri = JWSAlgorithmRegistry.getUri(svtJwsAlgo);
cmsSVResult.setSignatureAlgorithm(algoUri);
PubKeyParams pkParams = GeneralCMSUtils.getPkParams(getCert(svtValResult.getSignerCertificate()).getPublicKey());
cmsSVResult.setPubKeyParams(pkParams);
//Set signed SVT JWT
cmsSVResult.setSvtJWT(signedJWT);
/**
* Set the signature certs as the result certs and set the validated certs as the validated path in cert validation results
* The reason for this is that the SVT issuer must decide whether to just include a hash of the certs in the signature
* or to include all explicit certs of the validated path. The certificates in the CertificateValidationResult represents the
* validated path. If the validation was done by SVT, then the certificates obtained from SVT validation represents the validated path
*/
// Get the signature certificates
CMSSignedDataParser cmsSignedDataParser = CMSVerifyUtils.getCMSSignedDataParser(signature, pdfDocBytes);
CMSTypedStream signedContent = cmsSignedDataParser.getSignedContent();
signedContent.drain();
GeneralCMSUtils.CMSSigCerts CMSSigCerts = GeneralCMSUtils.extractCertificates(cmsSignedDataParser);
cmsSVResult.setSignerCertificate(CMSSigCerts.getSigCert());
cmsSVResult.setSignatureCertificateChain(CMSSigCerts.getChain());
// Store the svt validated certificates as path of certificate validation results
CertificateValidationResult cvr = new DefaultCertificateValidationResult(
SVAUtils.getOrderedCertList(svtValResult.getSignerCertificate(), svtValResult.getCertificateChain()));
cmsSVResult.setCertificateValidationResult(cvr);
// Finalize
SignatureClaims signatureClaims = svtValResult.getSignatureClaims();
List policyValidationClaims = signatureClaims.getSig_val();
if (svtValResult.isSvtValidationSuccess()) {
cmsSVResult.setStatus(getStatusFromPolicyValidationClaims(policyValidationClaims));
}
else {
cmsSVResult.setStatus(SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
cmsSVResult.setStatusMessage("Unable to verify SVT signature");
}
cmsSVResult.setSignatureClaims(signatureClaims);
cmsSVResult.setValidationPolicyResultList(policyValidationClaims);
// Since we verify with SVA. We ignore any present signature timestamps.
// cmsSVResult.setSignatureTimeStampList(new ArrayList<>());
//Add SVT document timestamp that was used to perform this SVT validation to verified times
//This ensures that this time stamp gets added when SVT issuance is based on a previous SVT.
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
List timeValidationClaimsList = signatureClaims.getTime_val();
timeValidationClaimsList.add(TimeValidationClaims.builder()
.iss(jwtClaimsSet.getIssuer())
.time(jwtClaimsSet.getIssueTime().getTime() / 1000)
.type(SigValIdentifiers.TIME_VERIFICATION_TYPE_SVT)
.id(jwtClaimsSet.getJWTID())
.val(Arrays.asList(PolicyValidationClaims.builder()
// TODO Get policy from certificate validator
.pol(SigValIdentifiers.SIG_VALIDATION_POLICY_PKIX_VALIDATION)
.res(ValidationConclusion.PASSED)
.build()))
.build());
cmsSVResult.setTimeValidationResults(timeValidationClaimsList.stream()
.map(timeValidationClaims -> new TimeValidationResult(
timeValidationClaims, null, null))
.collect(Collectors.toList())
);
}
catch (Exception ex) {
cmsSVResult.setStatus(SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
cmsSVResult.setStatusMessage("Unable to process SVA token or signature data");
return cmsSVResult;
}
return cmsSVResult;
}
private SignatureValidationResult.Status getStatusFromPolicyValidationClaims(List policyValidationClaims) {
if (policyValidationClaims == null) {
return SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE;
}
if (policyValidationClaims.isEmpty()) {
return SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE;
}
if (policyValidationClaims.stream().anyMatch(pvc -> pvc.getRes().equals(ValidationConclusion.PASSED))) {
return SignatureValidationResult.Status.SUCCESS;
}
if (policyValidationClaims.stream().anyMatch(pvc -> pvc.getRes().equals(ValidationConclusion.FAILED))) {
return SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE;
}
if (policyValidationClaims.stream().anyMatch(pvc -> pvc.getRes().equals(ValidationConclusion.INDETERMINATE))) {
return SignatureValidationResult.Status.INTERDETERMINE;
}
return SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE;
}
/**
* Compare if the signature value match any of the listed SVT validation results
*
* @param sigValueBytes signature value bytes
* @param svtValidationResults validation result from SVT validation
* @return The SVT validation results, or null on no match
*/
private SignatureSVTValidationResult getMatchingSvtValidation(byte[] sigValueBytes,
List svtValidationResults) {
if (svtValidationResults == null)
return null;
for (SignatureSVTValidationResult svtValResult : svtValidationResults) {
try {
MessageDigest md = SVTAlgoRegistry.getMessageDigestInstance(
svtValResult.getSignedJWT().getHeader().getAlgorithm());
String sigValueHashStr = Base64.encodeBase64String(md.digest(sigValueBytes));
if (sigValueHashStr.equals(svtValResult.getSignatureClaims().getSig_ref().getSig_hash())
&& svtValResult.isSvtValidationSuccess()) {
return svtValResult;
}
}
catch (NoSuchAlgorithmException e) {
continue;
}
}
return null;
}
private ExtendedPdfSigValResult getErrorResult(PDSignature signature, String message) {
ExtendedPdfSigValResult sigResult = new ExtendedPdfSigValResult();
sigResult.setPdfSignature(signature);
sigResult.setStatus(SignatureValidationResult.Status.ERROR_INVALID_SIGNATURE);
sigResult.setStatusMessage("Failed to process signature: " + message);
return sigResult;
}
private X509Certificate getCert(byte[] certBytes) throws CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certBytes));
}
@SuppressWarnings("unused")
private List getCertList(List certificateChain) throws CertificateException {
List certList = new ArrayList<>();
for (byte[] certBytes : certificateChain) {
certList.add(getCert(certBytes));
}
return certList;
}
/**
* Compile a complete PDF signature verification result object from the list of individual signature results
*
* @param pdfDocBytes validate the complete PDF document and return concluding validation results for the complete document.
* @return PDF signature validation result objects
*/
@Override
public SignedDocumentValidationResult extendedResultValidation(byte[] pdfDocBytes)
throws SignatureException {
List validationResults = validate(pdfDocBytes);
return getConcludingSigVerifyResult(validationResults);
}
/**
* Compile a complete PDF signature verification result object from the list of individual signature results
*
* @param sigVerifyResultList list of individual signature validation results. Each result must be of type {@link ExtendedPdfSigValResult}
* @return PDF signature validation result objects
*/
public static SignedDocumentValidationResult getConcludingSigVerifyResult(
List sigVerifyResultList) {
SignedDocumentValidationResult sigVerifyResult = new SignedDocumentValidationResult<>();
List extendedPdfSigValResults = new ArrayList<>();
try {
extendedPdfSigValResults = sigVerifyResultList.stream()
.map(signatureValidationResult -> (ExtendedPdfSigValResult) signatureValidationResult)
.collect(Collectors.toList());
sigVerifyResult.setSignatureValidationResults(extendedPdfSigValResults);
}
catch (Exception ex) {
throw new IllegalArgumentException("Provided results are not instances of ExtendedPdfSigValResult");
}
//sigVerifyResult.setDocTimeStampList(docTimeStampList);
// Test if there are no signatures
if (sigVerifyResultList.isEmpty()) {
sigVerifyResult.setSignatureCount(0);
sigVerifyResult.setStatusMessage("No signatures");
sigVerifyResult.setValidSignatureCount(0);
sigVerifyResult.setCompleteSuccess(false);
sigVerifyResult.setSigned(false);
return sigVerifyResult;
}
//Get valid signatures
sigVerifyResult.setSigned(true);
sigVerifyResult.setSignatureCount(sigVerifyResultList.size());
List validSignatureResultList = extendedPdfSigValResults.stream()
.filter(cmsSigVerifyResult -> cmsSigVerifyResult.isSuccess())
.collect(Collectors.toList());
sigVerifyResult.setValidSignatureCount(validSignatureResultList.size());
if (validSignatureResultList.isEmpty()) {
//No valid signatures
sigVerifyResult.setCompleteSuccess(false);
sigVerifyResult.setStatusMessage("No valid signatures");
return sigVerifyResult;
}
//Reaching this point means that there are valid signatures.
if (sigVerifyResult.getSignatureCount() == validSignatureResultList.size()) {
sigVerifyResult.setStatusMessage("OK");
sigVerifyResult.setCompleteSuccess(true);
}
else {
sigVerifyResult.setStatusMessage("Some signatures are valid and some are invalid");
sigVerifyResult.setCompleteSuccess(false);
}
//Check if any valid signature signs the whole document
boolean validSigSignsWholeDoc = validSignatureResultList.stream()
.filter(signatureValidationResult -> signatureValidationResult.isCoversDocument())
.findFirst().isPresent();
sigVerifyResult.setValidSignatureSignsWholeDocument(validSigSignsWholeDoc);
return sigVerifyResult;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy