Validate signatures in a signed PDF document
The Pdftools SDK lets you validate digital signatures that have been added to approve or certify a PDF document. The validation process can reference online and offline sources such as:
- Certificates, OCSP and CRL data embedded in the PDF file
- Certificates, OCSP and CRL data downloaded from online sources
- Certificates stored on the local file system
- Certificates stored in the system's default trust store
Download the full sample now in C# and Java.
Interested in C or other language samples? Let us know on the contact page and we'll add it to our samples backlog.
This example shows how to validate a digital signature using certificate information embedded in the PDF file, and a Custom Trust List built from certificate files stored on the local file system.
Steps to validate signatures in a document:
- Create a validator object.
- Configure the validation parameters.
- (Optional) Add certificates for offline validation.
- Open and validate the document.
- Check and output the results
You need to initialize the library.
Creating a Validator object
The Validator
object validates the signatures in the input PDF document against trusted sources.
It is used to process the signatures as defined in the validation parameters and calculate the overall validation result.
- .NET
- Java
// Create a Validator object
var validator = new Validator();
// Create a Validator object
Validator validator = new Validator();
Configuring the validation parameters
The validator object lets you configure the validation process to handle your business logic.
For example, you can validate all signatures in a document and use several trusted sources for signature validation.
To do this, you can use the Default
profile and the SignatureSelector.ALL
.
- .NET
- Java
// Use the default validation profile as a base for further settings
var profile = new Profiles.Default();
// Validate ALL signatures in the document (not only the latest)
var signatureSelector = SignatureSelector.All;
You can adjust the validation settings to your specific business case using the profile's ValidationOptions
, SigningCertTrustConstraints
, and TimeStampTrustConstraints
.
// Use the default validation profile as a base for further settings
Profile profile = new Default();
// Validate ALL signatures in the document (not only the latest)
SignatureSelector signatureSelector = SignatureSelector.ALL;
You can adjust the validation settings to your specific business case using the profile's getValidationOptions
, getSigningCertTrustConstraints
, and getTimeStampTrustConstraints
.
Adding certificates for offline validation
Certificate files (e.g. PFX or CER) from the local file system can be added as trusted sources using a CustomTrustList
.
- .NET
- Java
// create a CustomTrustList to hold the certificates
var ctl = new CustomTrustList();
// Iterate through files in the certificate directory and add certificates
// to the custom trust list
if (Directory.Exists(certDir))
{
var directoryListing = Directory.EnumerateFiles(certDir);
foreach (string fileName in directoryListing)
{
try
{
using var certStr = File.OpenRead(fileName);
if (fileName.EndsWith(".cer") || fileName.EndsWith(".pem"))
{
ctl.AddCertificates(certStr);
}
else if (fileName.EndsWith(".p12") || fileName.EndsWith(".pfx"))
{
// If a password is required, use addArchive(certStr, password).
ctl.AddArchive(certStr);
}
}
catch (Exception e)
{
Console.WriteLine("Could not add certificate '" + fileName + "' to custom trust list: " + e.Message);
}
}
}
else
{
// Handle the case where dir is not a directory
Console.WriteLine("Directory " + certDir + " is missing. No certificates were added to the custom trust list.");
}
Console.WriteLine();
// Assign the custom trust list to the validation profile
profile.CustomTrustList = ctl;
// Allow validation from embedded file sources and the custom trust list
var vo = profile.ValidationOptions;
vo.TimeSource = TimeSource.ProofOfExistence | TimeSource.ExpiredTimeStamp | TimeSource.SignatureTime;
vo.CertificateSources = DataSource.EmbedInSignature | DataSource.EmbedInDocument | DataSource.CustomTrustList;
// Disable revocation checks.
profile.SigningCertTrustConstraints.RevocationCheckPolicy = RevocationCheckPolicy.NoCheck;
profile.TimeStampTrustConstraints.RevocationCheckPolicy = RevocationCheckPolicy.NoCheck;
// create a CustomTrustList to hold the certificates
CustomTrustList ctl = new CustomTrustList();
// Iterate through files in the certificate directory and add certificates
// to the custom trust list
File dir = new File(certDir);
File[] directoryListing = dir.listFiles();
if (directoryListing != null)
{
for (File child : directoryListing)
{
String fileName = child.getName();
try (
FileStream certStr = new FileStream(child.getPath(), FileStream.Mode.READ_ONLY))
{
if (fileName.endsWith(".cer") || fileName.endsWith(".pem"))
{
ctl.addCertificates(certStr);
}
else if (fileName.endsWith(".p12") || fileName.endsWith(".pfx"))
{
// If a password is required, use addArchive(certStr, password).
ctl.addArchive(certStr);
}
}
catch (Exception e)
{
System.out.println("Could not add certificate '" + child.getName() + "' to custom trust list: " + e.getMessage());
}
}
}
else
{
// Handle the case where dir is not a directory
System.out.println("Directory " + certDir + " is missing. No certificates were added to the custom trust list.");
}
System.out.println();
// Assign the custom trust list to the validation profile
profile.setCustomTrustList(ctl);
// Allow validation from embedded file sources and the custom trust list
ValidationOptions vo = profile.getValidationOptions();
vo.setTimeSource(EnumSet.of(TimeSource.PROOF_OF_EXISTENCE, TimeSource.EXPIRED_TIME_STAMP, TimeSource.SIGNATURE_TIME));
vo.setCertificateSources(EnumSet.of(DataSource.EMBED_IN_SIGNATURE, DataSource.EMBED_IN_DOCUMENT, DataSource.CUSTOM_TRUST_LIST));
// Disable revocation checks.
profile.getSigningCertTrustConstraints().setRevocationCheckPolicy(RevocationCheckPolicy.NO_CHECK);
profile.getTimeStampTrustConstraints().setRevocationCheckPolicy(RevocationCheckPolicy.NO_CHECK);
If you are using a Java KeyStore
that has already been loaded, e.g. ks
, you can use a MemoryStream
to add the certificate to the CustomTrustList
.
// Create an OutputStream to write the KeyStore to (into memory)
ByteArrayOutputStream os = new ByteArrayOutputStream();
// Write the KeyStore to the OutputStream
ks.store(os, password.toCharArray());
// Put the OutputStream bytes into a MemoryStream
Stream certStr = new MemoryStream(os.toByteArray());
// Create a CustomTrustList to hold the certificates
CustomTrustList ctl = new CustomTrustList();
// Add the MemoryStream to the CustomTrustList
ctl.addArchive(certStr, password);
The KeyStore
must be of type PKCS12
.
For more information on the KeyStore
please refer to the Java API Reference.
Opening and validating the document
After instantiating the validator object and configuring the validation profile, you are ready to validate the digital signatures in a document.
The input document is created as a stream (in this example, as file streams). The validator object is used to validate the digital signatures.
- .NET
- Java
using var inStr = File.OpenRead(inputFile);
// Open input document
// If a password is required, use Open(inStr, password)
using var document = Document.Open(inStr);
// Run the validate method passing the document, profile, and selector
var results = validator.Validate(document, profile, signatureSelector);
FileStream inStr = new FileStream(inputFile, FileStream.Mode.READ_ONLY);
// Open input document
// If a password is required, use open(inStr, password)
Document document = Document.open(inStr);
// Run the validate method passing the document, profile, and selector
ValidationResults results = validator.validate(document, profile, signatureSelector);
Checking and outputting the results
Checking validation results
When validation is completed, the validator object returns a ValidationResults
object that contains all the information obtained during the validation process.
This list contains a ValidationResult
object for each signature with all the required information needed to build custom business logic.
In this example, all available information is printed to the console.
- .NET
- Java
// Print results
foreach (var result in results)
{
var field = result.SignatureField;
Console.WriteLine(field.FieldName + " of " + field.Name);
try
{
Console.WriteLine(" - Revision : " + (field.Revision.IsLatest ? "latest" : "intermediate"));
}
catch (Exception ex)
{
Console.WriteLine("Unable to validate document Revision: " + ex.Message);
}
PrintContent(result.SignatureContent);
Console.WriteLine();
}
To implement PrintContent
, see print and to string functions.
// Print results
results.forEach(result -> {
SignedSignatureField field = result.getSignatureField();
System.out.println(field.getFieldName() + " of " + field.getName());
try
{
System.out.println(" - Revision : " + (field.getRevision().getIsLatest() ? "latest" : "intermediate"));
}
catch (Exception ex)
{
System.out.println("Unable to validate document Revision: " + ex.getMessage());
}
printContent(result.getSignatureContent());
System.out.println();
});
To implement printContent
, see print and to string functions.
Checking constraint events
In addition to the validation results, the validator object raises constraint events for each signature Constraint
checked.
A handler function can listen to these events to track the progress of the validation process. This way, you can link any errors to the signature causing the issue.
In this example, a simple console printer is used to output the signature information.
- .NET
- Java
validator.Constraint += (s, e) =>
{
Console.WriteLine(" - " + e.Signature.Name + (e.DataPart.Length > 0 ? (": " + e.DataPart) : "") + ": " +
ConstraintToString(e.Indication, e.SubIndication, e.Message));
};
To implement ConstraintToString
, see print and to string functions.
validator.addConstraintListener(e -> {
System.out.println(" - " + e.getSignature().getName() + (e.getDataPart().length() > 0 ? (": " + e.getDataPart()) : "") + ": " +
constraintToString(e.getIndication(), e.getSubIndication(), e.getMessage()));
});
To implement constraintToString
, see print and to string functions.
Full example
- .NET
- Java
// Use the default validation profile as a base for further settings
var profile = new Default();
// For offline operation, build a custom trust list from the file system
// and disable external revocation checks
if (certDir != null && certDir.Length != 0)
{
Console.WriteLine("Using 'offline' validation mode with custom trust list.");
Console.WriteLine();
// create a CustomTrustList to hold the certificates
var ctl = new CustomTrustList();
// Iterate through files in the certificate directory and add certificates
// to the custom trust list
if (Directory.Exists(certDir))
{
var directoryListing = Directory.EnumerateFiles(certDir);
foreach (string fileName in directoryListing)
{
try
{
using var certStr = File.OpenRead(fileName);
if (fileName.EndsWith(".cer") || fileName.EndsWith(".pem"))
{
ctl.AddCertificates(certStr);
}
else if (fileName.EndsWith(".p12") || fileName.EndsWith(".pfx"))
{
// If a password is required, use addArchive(certStr, password).
ctl.AddArchive(certStr);
}
}
catch (Exception e)
{
Console.WriteLine("Could not add certificate '" + fileName + "' to custom trust list: " + e.Message);
}
}
}
else
{
// Handle the case where dir is not a directory
Console.WriteLine("Directory " + certDir + " is missing. No certificates were added to the custom trust list.");
}
Console.WriteLine();
// Assign the custom trust list to the validation profile
profile.CustomTrustList = ctl;
// Allow validation from embedded file sources and the custom trust list
var vo = profile.ValidationOptions;
vo.TimeSource = TimeSource.ProofOfExistence | TimeSource.ExpiredTimeStamp | TimeSource.SignatureTime;
vo.CertificateSources = DataSource.EmbedInSignature | DataSource.EmbedInDocument | DataSource.CustomTrustList;
// Disable revocation checks.
profile.SigningCertTrustConstraints.RevocationCheckPolicy = RevocationCheckPolicy.NoCheck;
profile.TimeStampTrustConstraints.RevocationCheckPolicy = RevocationCheckPolicy.NoCheck;
}
// Validate ALL signatures in the document (not only the latest)
var signatureSelector = SignatureSelector.All;
// Create the validator object and event listeners
var validator = new Validator();
validator.Constraint += (s, e) =>
{
Console.WriteLine(" - " + e.Signature.Name + (e.DataPart.Length > 0 ? (": " + e.DataPart) : "") + ": " +
ConstraintToString(e.Indication, e.SubIndication, e.Message));
};
try
{
using var inStr = File.OpenRead(inputFile);
// Open input document
// If a password is required, use Open(inStr, password)
using var document = Document.Open(inStr);
// Run the validate method passing the document, profile and selector
Console.WriteLine("Validation Constraints");
var results = validator.Validate(document, profile, signatureSelector);
Console.WriteLine();
Console.WriteLine("Signatures validated: " + results.Count);
Console.WriteLine();
// Print results
foreach (var result in results)
{
var field = result.SignatureField;
Console.WriteLine(field.FieldName + " of " + field.Name);
try
{
Console.WriteLine(" - Revision : " + (field.Revision.IsLatest ? "latest" : "intermediate"));
}
catch (Exception ex)
{
Console.WriteLine("Unable to validate document Revision: " + ex.Message);
}
PrintContent(result.SignatureContent);
Console.WriteLine();
}
return 0;
}
catch (Exception ex)
{
Console.WriteLine("Unable to validate file: " + ex.Message);
return 5;
}
// Use the default validation profile as a base for further settings
Profile profile = new Default();
// For offline operation, build a custom trust list from the file system
// and disable external revocation checks
if (certDir != null && !certDir.isEmpty())
{
System.out.println("Using 'offline' validation mode with custom trust list.");
System.out.println();
// create a CustomTrustList to hold the certificates
CustomTrustList ctl = new CustomTrustList();
// Iterate through files in the certificate directory and add certificates
// to the custom trust list
File dir = new File(certDir);
File[] directoryListing = dir.listFiles();
if (directoryListing != null)
{
for (File child : directoryListing)
{
String fileName = child.getName();
try (
FileStream certStr = new FileStream(child.getPath(), FileStream.Mode.READ_ONLY))
{
if (fileName.endsWith(".cer") || fileName.endsWith(".pem"))
{
ctl.addCertificates(certStr);
}
else if (fileName.endsWith(".p12") || fileName.endsWith(".pfx"))
{
// If a password is required, use addArchive(certStr, password).
ctl.addArchive(certStr);
}
}
catch (Exception e)
{
System.out.println("Could not add certificate '" + child.getName() + "' to custom trust list: " + e.getMessage());
}
}
}
else
{
// Handle the case where dir is not a directory
System.out.println("Directory " + certDir + " is missing. No certificates were added to the custom trust list.");
}
System.out.println();
// Assign the custom trust list to the validation profile
profile.setCustomTrustList(ctl);
// Allow validation from embedded file sources and the custom trust list
ValidationOptions vo = profile.getValidationOptions();
vo.setTimeSource(EnumSet.of(TimeSource.PROOF_OF_EXISTENCE, TimeSource.EXPIRED_TIME_STAMP, TimeSource.SIGNATURE_TIME));
vo.setCertificateSources(EnumSet.of(DataSource.EMBED_IN_SIGNATURE, DataSource.EMBED_IN_DOCUMENT, DataSource.CUSTOM_TRUST_LIST));
// Disable revocation checks.
profile.getSigningCertTrustConstraints().setRevocationCheckPolicy(RevocationCheckPolicy.NO_CHECK);
profile.getTimeStampTrustConstraints().setRevocationCheckPolicy(RevocationCheckPolicy.NO_CHECK);
}
// Validate ALL signatures in the document (not only the latest)
SignatureSelector signatureSelector = SignatureSelector.ALL;
// Create the validator object and event listeners
Validator validator = new Validator();
validator.addConstraintListener(e -> {
System.out.println(" - " + e.getSignature().getName() + (e.getDataPart().length() > 0 ? (": " + e.getDataPart()) : "") + ": " +
constraintToString(e.getIndication(), e.getSubIndication(), e.getMessage()));
});
try (
FileStream inStr = new FileStream(inputFile, FileStream.Mode.READ_ONLY);
// Open input document
// If a password is required, use open(inStr, password)
Document document = Document.open(inStr);
)
{
// Run the validate method passing the document, profile and selector
System.out.println("Validation Constraints");
ValidationResults results = validator.validate(document, profile, signatureSelector);
System.out.println();
System.out.println("Signatures validated: " + results.size());
System.out.println();
// Print results
results.forEach(result -> {
SignedSignatureField field = result.getSignatureField();
System.out.println(field.getFieldName() + " of " + field.getName());
try
{
System.out.println(" - Revision : " + (field.getRevision().getIsLatest() ? "latest" : "intermediate"));
}
catch (Exception ex)
{
System.out.println("Unable to validate document Revision: " + ex.getMessage());
}
printContent(result.getSignatureContent());
System.out.println();
});
return 0;
}
catch (Exception ex)
{
System.out.println("Unable to validate file: " + ex.getMessage());
return 5;
}
Print and to string functions
The following print and to string functions let you list all the properties of the signatures and the signature results.
- .NET
- Java
For example, you can use PrintContent
to print certification details such as subject, issuer, and validity.
Using ConstraintsToString
, you can convert a constraint to a string representation.
// Helper functions to print signature validation details
private static void PrintContent(SignatureContent content)
{
if(content != null)
{
Console.WriteLine(" - Validity : " + ConstraintToString(content.Validity));
switch (content)
{
case UnsupportedSignatureContent:
break;
case CmsSignatureContent signature:
{
Console.WriteLine(" - Validation: " + signature.ValidationTime + " from " + signature.ValidationTimeSource);
Console.WriteLine(" - Hash : " + signature.HashAlgorithm);
Console.WriteLine(" - Signing Cert");
PrintContent(signature.SigningCertificate);
Console.WriteLine(" - Chain");
foreach (var cert in signature.CertificateChain)
{
Console.WriteLine(" - Issuer Cert " + (signature.CertificateChain.IndexOf(cert) + 1));
PrintContent(cert);
}
Console.WriteLine(" - Chain : " + (signature.CertificateChain.IsComplete ? "complete" : "incomplete") + " chain");
Console.WriteLine(" Time-Stamp");
PrintContent(signature.TimeStamp);
break;
}
case TimeStampContent timeStamp:
{
Console.WriteLine(" - Validation: " + timeStamp.ValidationTime + " from " + timeStamp.ValidationTimeSource);
Console.WriteLine(" - Hash : " + timeStamp.HashAlgorithm);
Console.WriteLine(" - Time : " + timeStamp.Date);
Console.WriteLine(" - Signing Cert");
PrintContent(timeStamp.SigningCertificate);
Console.WriteLine(" - Chain");
foreach (var cert in timeStamp.CertificateChain)
{
Console.WriteLine(" - Issuer Cert " + (timeStamp.CertificateChain.IndexOf(cert) + 1));
PrintContent(cert);
}
Console.WriteLine(" - Chain : " + (timeStamp.CertificateChain.IsComplete ? "complete" : "incomplete") + " chain");
break;
}
default:
Console.WriteLine("Unsupported signature content type " + content.GetType().Name);
break;
}
}
else
{
Console.WriteLine(" - null");
}
}
private static void PrintContent(Certificate cert)
{
if(cert != null)
{
Console.WriteLine(" - Subject : " + cert.SubjectName);
Console.WriteLine(" - Issuer : " + cert.IssuerName);
Console.WriteLine(" - Validity : " + cert.NotBefore + " - " + cert.NotAfter);
try
{
Console.WriteLine(" - Fingerprint: " + FormatSha1Digest(new BigInteger(SHA1.Create().ComputeHash(cert.RawData)).ToByteArray(), "-"));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine(" - Source : " + cert.Source);
Console.WriteLine(" - Validity : " + ConstraintToString(cert.Validity));
}
else
{
Console.WriteLine(" - null");
}
}
private static String ConstraintToString(ConstraintResult constraint)
{
return ConstraintToString(constraint.Indication, constraint.SubIndication, constraint.Message);
}
private static String ConstraintToString(Indication indication, SubIndication subIndication, String message)
{
return (indication == Indication.Valid ? "" : (indication == Indication.Indeterminate ? "?" : "!")) + "" +
subIndication + " " +
message;
}
// Helper function to generate a delimited SHA-1 digest string
private static String FormatSha1Digest(byte[] bytes, String delimiter)
{
var result = new StringBuilder();
foreach (byte aByte in bytes)
{
int number = (int)aByte & 0xff;
String hex = number.ToString("X2");
result.Append(hex.ToUpper() + delimiter);
}
return result.ToString().Substring(0, result.Length - delimiter.Length);
}
For example, you can use printContent
to print certification details such as subject, issuer, and validity.
Using constraintsToString
, you can convert a constraint to a string representation.
// Helper functions to print signature validation details
private static void printContent(SignatureContent content)
{
if(content != null)
{
System.out.println(" - Validity : " + constraintToString(content.getValidity()));
switch (content.getClass().getSimpleName())
{
case "UnsupportedSignatureContent":
break;
case "CmsSignatureContent":
{
CmsSignatureContent signature = (CmsSignatureContent)content;
System.out.println(" - Validation: " + signature.getValidationTime() + " from " + signature.getValidationTimeSource());
System.out.println(" - Hash : " + signature.getHashAlgorithm());
System.out.println(" - Signing Cert");
printContent(signature.getSigningCertificate());
System.out.println(" - Chain");
signature.getCertificateChain().forEach(cert -> {
System.out.println(" - Issuer Cert " + (signature.getCertificateChain().indexOf(cert) + 1));
printContent(cert);
});
System.out.println(" - Chain : " + (signature.getCertificateChain().getIsComplete() ? "complete" : "incomplete") + " chain");
System.out.println(" Time-Stamp");
printContent(signature.getTimeStamp());
break;
}
case "TimeStampContent":
{
TimeStampContent timeStamp = (TimeStampContent)content;
System.out.println(" - Validation: " + timeStamp.getValidationTime() + " from " + timeStamp.getValidationTimeSource());
System.out.println(" - Hash : " + timeStamp.getHashAlgorithm());
System.out.println(" - Time : " + timeStamp.getDate());
System.out.println(" - Signing Cert");
printContent(timeStamp.getSigningCertificate());
System.out.println(" - Chain");
timeStamp.getCertificateChain().forEach(cert -> {
System.out.println(" - Issuer Cert " + (timeStamp.getCertificateChain().indexOf(cert) + 1));
printContent(cert);
});
System.out.println(" - Chain : " + (timeStamp.getCertificateChain().getIsComplete() ? "complete" : "incomplete") + " chain");
break;
}
default:
System.out.println("Unsupported signature content type " + content.getClass().getName());
break;
}
}
else
{
System.out.println(" - null");
}
}
private static void printContent(Certificate cert)
{
if(cert != null)
{
System.out.println(" - Subject : " + cert.getSubjectName());
System.out.println(" - Issuer : " + cert.getIssuerName());
System.out.println(" - Validity : " + cert.getNotBefore() + " - " + cert.getNotAfter());
try {
System.out.println(" - Fingerprint: " + formatSha1Digest(new java.math.BigInteger(1, (MessageDigest.getInstance("SHA-1").digest(cert.getRawData()))).toByteArray(), "-"));
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
System.out.println(" - Source : " + cert.getSource());
System.out.println(" - Validity : " + constraintToString(cert.getValidity()));
}
else
{
System.out.println(" - null");
}
}
private static String constraintToString(ConstraintResult constraint)
{
return constraintToString(constraint.getIndication(), constraint.getSubIndication(), constraint.getMessage());
}
private static String constraintToString(Indication indication, SubIndication subIndication, String message)
{
return (indication == Indication.VALID ? "" : (indication == Indication.INDETERMINATE ? "?" : "!")) + "" +
subIndication + " " +
message;
}
// Helper function to generate a delimited SHA-1 digest string
private static String formatSha1Digest(byte[] bytes, String delimiter) {
StringBuilder result = new StringBuilder();
for (byte aByte : bytes) {
int decimal = (int) aByte & 0xff;
String hex = Integer.toHexString(decimal);
if (hex.length() % 2 == 1)
hex = "0" + hex;
result.append(hex.toUpperCase() + delimiter);
}
return result.substring(0, result.length() - delimiter.length());
}