com.itextpdf.signatures.SignatureUtil Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2024 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package com.itextpdf.signatures;
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.PdfFormCreator;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.source.IRandomAccessSource;
import com.itextpdf.io.source.PdfTokenizer;
import com.itextpdf.io.source.RASInputStream;
import com.itextpdf.io.source.RandomAccessFileOrArray;
import com.itextpdf.io.source.RandomAccessSourceFactory;
import com.itextpdf.io.source.WindowRandomAccessSource;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDate;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfIndirectReference;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.signatures.exceptions.SignExceptionMessageConstant;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class that provides several convenience methods concerning digital signatures.
*/
public class SignatureUtil {
private final PdfDocument document;
private final PdfAcroForm acroForm;
private Map sigNames;
private List orderedSignatureNames;
private int totalRevisions;
/**
* Creates a SignatureUtil instance. Sets the acroForm field to the acroForm in the PdfDocument.
* iText will create a new AcroForm if the PdfDocument doesn't contain one.
*
* @param document PdfDocument to be inspected
*/
public SignatureUtil(PdfDocument document) {
this.document = document;
// Only create new AcroForm if there is a writer
this.acroForm = PdfFormCreator.getAcroForm(document, document.getWriter() != null);
}
/**
* Prepares an {@link PdfPKCS7} instance for the given signature.
* This method handles signature parsing and might throw an exception if
* signature is malformed.
*
* The returned {@link PdfPKCS7} can be used to fetch additional info about the signature
* and also to perform integrity check of data signed by the given signature field.
*
* In order to validate the signature it is required to check if it covers the entire file,
* otherwise one cannot be sure that signature in question indeed signs the data
* that constitutes current {@link PdfDocument} with all its contents.
* In order to check that given signature covers the current {@link PdfDocument} please
* use {@link #signatureCoversWholeDocument(String)} method.
*
* @param signatureFieldName the signature field name
* @return a {@link PdfPKCS7} instance which can be used to fetch additional info about the signature
* and also to perform integrity check of data signed by the given signature field.
*/
public PdfPKCS7 readSignatureData(String signatureFieldName) {
return readSignatureData(signatureFieldName, null);
}
/**
* Prepares an {@link PdfPKCS7} instance for the given signature.
* This method handles signature parsing and might throw an exception if
* signature is malformed.
*
* The returned {@link PdfPKCS7} can be used to fetch additional info about the signature
* and also to perform integrity check of data signed by the given signature field.
*
* Prepared {@link PdfPKCS7} instance calculates digest based on signature's /ByteRange entry.
* In order to check that /ByteRange is properly defined and given signature indeed covers the current PDF document
* revision please use {@link #signatureCoversWholeDocument(String)} method.
*
* @param signatureFieldName the signature field name
* @param securityProvider the security provider or null for the default provider
* @return a {@link PdfPKCS7} instance which can be used to fetch additional info about the signature
* and also to perform integrity check of data signed by the given signature field.
*/
public PdfPKCS7 readSignatureData(String signatureFieldName, String securityProvider) {
PdfSignature signature = getSignature(signatureFieldName);
if (signature == null) {
return null;
}
try {
PdfName sub = signature.getSubFilter();
PdfString contents = signature.getContents();
PdfPKCS7 pk = null;
if (sub.equals(PdfName.Adbe_x509_rsa_sha1)) {
PdfString cert = signature.getPdfObject().getAsString(PdfName.Cert);
if (cert == null) {
cert = signature.getPdfObject().getAsArray(PdfName.Cert).getAsString(0);
}
pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), cert.getValueBytes(),
securityProvider);
} else {
pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), sub, securityProvider);
}
updateByteRange(pk, signature);
PdfString date = signature.getDate();
if (date != null) {
pk.setSignDate(PdfDate.decode(date.toString()));
}
String signName = signature.getName();
pk.setSignName(signName);
String reason = signature.getReason();
if (reason != null) {
pk.setReason(reason);
}
String location = signature.getLocation();
if (location != null) {
pk.setLocation(location);
}
return pk;
} catch (Exception e) {
throw new PdfException(e);
}
}
/**
* Get {@link PdfSignature} dictionary based on the provided name.
*
* @param name signature name
*
* @return {@link PdfSignature} instance corresponding to the provided name. {@code null} otherwise
*/
public PdfSignature getSignature(String name) {
PdfDictionary sigDict = getSignatureDictionary(name);
return sigDict != null
? new PdfSignature(sigDict)
: null;
}
/**
* Gets the signature dictionary, the one keyed by /V.
*
* @param name the field name
* @return the signature dictionary keyed by /V or null
if the field is not
* a signature
*/
public PdfDictionary getSignatureDictionary(String name) {
PdfDictionary merged = getSignatureFormFieldDictionary(name);
return merged == null ? null : merged.getAsDictionary(PdfName.V);
}
public PdfDictionary getSignatureFormFieldDictionary(String name) {
getSignatureNames();
if (acroForm == null || !sigNames.containsKey(name)) {
return null;
}
PdfFormField field = acroForm.getField(name);
return field.getPdfObject();
}
/* Updates the /ByteRange with the provided value */
private void updateByteRange(PdfPKCS7 pkcs7, PdfSignature signature) {
PdfArray b = signature.getByteRange();
RandomAccessFileOrArray rf = document.getReader().getSafeFile();
InputStream rg = null;
try {
rg = new RASInputStream(
new RandomAccessSourceFactory().createRanged(rf.createSourceView(), b.toLongArray()));
byte[] buf = new byte[8192];
int rd;
while ((rd = rg.read(buf, 0, buf.length)) > 0) {
pkcs7.update(buf, 0, rd);
}
} catch (Exception e) {
throw new PdfException(e);
} finally {
try {
if (rg != null) rg.close();
} catch (IOException e) {
// this really shouldn't ever happen - the source view we use is based on a Safe view, which is a no-op anyway
throw new PdfException(e);
}
}
}
/**
* Gets the field names that have signatures and are signed.
*
* @return List containing the field names that have signatures and are signed
*/
public List getSignatureNames() {
if (sigNames != null) {
return new ArrayList<>(orderedSignatureNames);
}
sigNames = new HashMap<>();
orderedSignatureNames = new ArrayList<>();
populateSignatureNames();
return new ArrayList<>(orderedSignatureNames);
}
/**
* Gets the field names that have blank signatures.
*
* @return List containing the field names that have blank signatures
*/
public List getBlankSignatureNames() {
getSignatureNames();
List sigs = new ArrayList<>();
if (acroForm != null) {
for (Map.Entry entry : acroForm.getAllFormFields().entrySet()) {
PdfFormField field = entry.getValue();
PdfDictionary merged = field.getPdfObject();
if (!PdfName.Sig.equals(merged.getAsName(PdfName.FT))) {
continue;
}
if (sigNames.containsKey(entry.getKey())) {
continue;
}
sigs.add(entry.getKey());
}
}
return sigs;
}
/**
* Get the amount of signed document revisions.
*
* @return {@code int} amount of signed document revisions
*/
public int getTotalRevisions() {
getSignatureNames();
return totalRevisions;
}
/**
* Get signed document revision number, which corresponds to the provided signature name.
*
* @param field signature name
*
* @return {@code int} revision number
*/
public int getRevision(String field) {
getSignatureNames();
field = getTranslatedFieldName(field);
if (!sigNames.containsKey(field)) {
return 0;
}
return sigNames.get(field)[1];
}
/**
* Get field name, translated using XFA, if any present in the document.
*
* @param name field name to be translated
*
* @return translated field name if XFA is present, original name otherwise
*/
public String getTranslatedFieldName(String name) {
if (acroForm != null && acroForm.getXfaForm().isXfaPresent()) {
String namex = acroForm.getXfaForm().findFieldName(name);
if (namex != null)
name = namex;
}
return name;
}
/**
* Extracts a revision from the document.
*
* @param field the signature field name
* @return an InputStream covering the revision. Returns null if it's not a signature field
*/
public InputStream extractRevision(String field) {
getSignatureNames();
if (!sigNames.containsKey(field)) {
return null;
}
int length = sigNames.get(field)[0];
RandomAccessFileOrArray raf = document.getReader().getSafeFile();
return new RASInputStream(new WindowRandomAccessSource(raf.createSourceView(), 0, length));
}
/**
* Checks if the signature covers the entire document (except for signature's Contents) or just a part of it.
*
* If this method does not return {@code true} it means that signature in question does not cover the entire
* contents of current {@link PdfDocument}. Such signatures cannot be considered as verifying the PDF document,
* because content that is not covered by signature might have been modified since the signature creation.
*
* @param name the signature field name
* @return true if the signature covers the entire document, false if it doesn't
*/
public boolean signatureCoversWholeDocument(String name) {
getSignatureNames();
if (!sigNames.containsKey(name)) {
return false;
}
try {
ContentsChecker signatureReader = new ContentsChecker(
document.getReader().getSafeFile().createSourceView(), document);
return signatureReader.checkWhetherSignatureCoversWholeDocument(acroForm.getField(name));
} catch (IOException e) {
throw new PdfException(e);
}
}
/**
* Checks whether a name exists as a signature field or not. It checks both signed fields and blank signatures.
*
* @param name name of the field
* @return boolean does the signature field exist
*/
public boolean doesSignatureFieldExist(String name) {
return getBlankSignatureNames().contains(name) || getSignatureNames().contains(name);
}
private void populateSignatureNames() {
if (acroForm == null) {
return;
}
List