如何使用PDFBox对动态创建的PDF文档进行数字签名? [英] How to Digitally Sign a Dynamically Created PDF Document Using PDFBox?

查看:572
本文介绍了如何使用PDFBox对动态创建的PDF文档进行数字签名?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

请原谅我!我在java中很穷。
请在任何我错的地方纠正我并改善我穷人的地方!



我正在尝试使用PDFBox对动态创建的PDF进行数字签名使用以下程序:



程序中的任务:

(i)创建模板PDF

(ii)更新ByteRange,xref,startxref

(iii)为签名创建构建原始文档

(iv)创建独立的包络数字签名

( v)通过连接原始文档部分构建数字签名的PDF文档 - I,独立签名和原始PDF部分 - II



观察:

(i)pdfFileOutputStream.write(documentOutputStream.toByteArray());使用可见签名创建模板PDF文档。



(ii)它创建一些PDF签名文档但有错误(a)无效令牌和(b)几个解析器错误< br>(现在在MKL的有力指导下纠正了!)



请按以下方式向我推荐:



(i)如何在图层2上的可见签名中添加签名文本。



提前致谢!

  package digitalsignature; 

import java.awt.geom.AffineTransform;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.Signature;
import java.util.ArrayList;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.SignerInfoGenerator;
import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import java.util.Map;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDJpeg;
import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectForm;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSSignedGenerator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;


公共类别AffixSignature {
String path =D:\\reports\\;
String onlyFileName =;
String pdfExtension =。pdf;
String pdfFileName =;
String pdfFilePath =;
String signedPdfFileName =;
字符串signedPdfFilePath =;
String ownerPassword =;
String tempSignedPdfFileName =;
String tempSignedPdfFilePath =;
String userPassword =;
String storePath =resources / my.p12;
String entryAlias =signerCert;
String keyStorePassword =password;
ByteArrayOutputStream documentOutputStream = null;
private Certificate [] certChain;
private static BouncyCastleProvider BC = new BouncyCastleProvider();
int offsetContentStart = 0;
int offsetContentEnd = 0;
int secondPartLength = 0;
int offsetStartxrefs = 0;
String contentString =;
OutputStream signedPdfFileOutputStream;
OutputStream pdfFileOutputStream;

public AffixSignature(){
try {
SimpleDateFormat timeFormat = new SimpleDateFormat(hh_mm_ss);

onlyFileName =Report_+ timeFormat.format(new Date());
pdfFileName = onlyFileName +。pdf;
pdfFilePath = path + pdfFileName;
文件pdfFile =新文件(pdfFilePath);
pdfFileOutputStream = new FileOutputStream(pdfFile);

signedPdfFileName =Signed_+ onlyFileName +。pdf;
signedPdfFilePath = path + signedPdfFileName;
文件signedPdfFile =新文件(signedPdfFilePath);
signedPdfFileOutputStream = new FileOutputStream(signedPdfFile);

String tempFileName =Temp_Report_+ timeFormat.format(new Date());
String tempPdfFileName = tempFileName +。pdf;
String tempPdfFilePath = path + tempPdfFileName;
文件tempPdfFile =新文件(tempPdfFilePath);
OutputStream tempSignedPdfFileOutputStream = new FileOutputStream(tempPdfFile);

PDDocument document = new PDDocument();
PDDocumentCatalog catalog = document.getDocumentCatalog();
PDPage page = new PDPage(PDPage.PAGE_SIZE_A4);
PDPageContentStream contentStream = new PDPageContentStream(document,page);


PDFont font = PDType1Font.HELVETICA;
Map< String,PDFont> fonts = new HashMap< String,PDFont>();
fonts = new HashMap< String,PDFont>();
fonts.put(F1,font);

// contentStream.setFont(font,12);
contentStream.setFont(font,12);
contentStream.beginText();
contentStream.moveTextPositionByAmount(100,700);
contentStream.drawString(DIGITAL SIGNATURE TEST);
contentStream.endText();
contentStream.close();
document.addPage(page);

//加贴可见数字签名
PDAcroForm acroForm = new PDAcroForm(document);
catalog.setAcroForm(acroForm);

PDSignatureField sf = new PDSignatureField(acroForm);

PDSignature pdSignature = new PDSignature();
page.getAnnotations()。add(sf.getWidget());
pdSignature.setName(sign);
pdSignature.setByteRange(new int [] {0,0,0,0});
pdSignature.setContents(new byte [4 * 1024]);
pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
pdSignature.setName(NAME);
pdSignature.setLocation(LOCATION);
pdSignature.setReason(SECURITY);
pdSignature.setSignDate(Calendar.getInstance());
列表< PDField> acroFormFields = acroForm.getFields();

sf.setSignature(pdSignature);
sf.getWidget()。setPage(page);

COSDictionary acroFormDict = acroForm.getDictionary();
acroFormDict.setDirect(true);
acroFormDict.setInt(COSName.SIG_FLAGS,3);
acroFormFields.add(sf);

PDRectangle frmRect = new PDRectangle();
// float [] frmRectParams = {lowerLeftX,lowerLeftY,upperRightX,upperRight};
// float [] frmRectLowerLeftUpperRightCoordinates = {5f,page.getMediaBox()。getHeight() - 50f,100f,page.getMediaBox()。getHeight() - 5f};
float [] frmRectLowerLeftUpperRightCoordinates = {5f,5f,205f,55f};
frmRect.setUpperRightX(frmRectLowerLeftUpperRightCoordinates [2]);
frmRect.setUpperRightY(frmRectLowerLeftUpperRightCoordinates [3]);
frmRect.setLowerLeftX(frmRectLowerLeftUpperRightCoordinates [0]);
frmRect.setLowerLeftY(frmRectLowerLeftUpperRightCoordinates [1]);

sf.getWidget()。setRectangle(frmRect);

COSArray procSetArr = new COSArray();
procSetArr.add(COSName.getPDFName(PDF));
procSetArr.add(COSName.getPDFName(Text));
procSetArr.add(COSName.getPDFName(ImageB));
procSetArr.add(COSName.getPDFName(ImageC));
procSetArr.add(COSName.getPDFName(ImageI));

字符串signImageFilePath =resources / sign.JPG;
文件signImageFile = new File(signImageFilePath);
InputStream signImageStream = new FileInputStream(signImageFile);
PDJpeg img = new PDJpeg(document,signImageStream);

PDResources holderFormResources = new PDResources();
PDStream holderFormStream = new PDStream(document);
PDXObjectForm holderForm = new PDXObjectForm(holderFormStream);
holderForm.setResources(holderFormResources);
holderForm.setBBox(frmRect);
holderForm.setFormType(1);

PDAppearanceDictionary外观=新的PDAppearanceDictionary();
appearance.getCOSObject()。setDirect(true);
PDAppearanceStream appearanceStream = new PDAppearanceStream(holderForm.getCOSStream());
appearance.setNormalAppearance(appearanceStream);
sf.getWidget()。setAppearance(appearance);
acroFormDict.setItem(COSName.DR,holderFormResources.getCOSDictionary());

PDResources innerFormResources = new PDResources();
PDStream innerFormStream = new PDStream(document);
PDXObjectForm innerForm = new PDXObjectForm(innerFormStream);
innerForm.setResources(innerFormResources);
innerForm.setBBox(frmRect);
innerForm.setFormType(1);

String innerFormName = holderFormResources.addXObject(innerForm,FRM);

PDResources imageFormResources = new PDResources();
PDStream imageFormStream = new PDStream(document);
PDXObjectForm imageForm = new PDXObjectForm(imageFormStream);
imageForm.setResources(imageFormResources);
byte [] AffineTransformParams = {1,0,0,1,0,0};
AffineTransform affineTransform = new AffineTransform(AffineTransformParams [0],AffineTransformParams [1],AffineTransformParams [2],AffineTransformParams [3],AffineTransformParams [4],AffineTransformParams [5]);
imageForm.setMatrix(affineTransform);
imageForm.setBBox(frmRect);
imageForm.setFormType(1);

String imageFormName = innerFormResources.addXObject(imageForm,n);
String imageName = imageFormResources.addXObject(img,img);

innerForm.getResources()。getCOSDictionary()。setItem(COSName.PROC_SET,procSetArr);
page.getCOSDictionary()。setItem(COSName.PROC_SET,procSetArr);
innerFormResources.getCOSDictionary()。setItem(COSName.PROC_SET,procSetArr);
imageFormResources.getCOSDictionary()。setItem(COSName.PROC_SET,procSetArr);
holderFormResources.getCOSDictionary()。setItem(COSName.PROC_SET,procSetArr);

String holderFormComment =q 1 0 0 1 0 0 cm /+ innerFormName +Do Q \ n;
String innerFormComment =q 1 0 0 1 0 0 cm /+ imageFormName +Do Q\ n;
String imgFormComment =q+ 100 +0 0 50 0 0 cm /+ imageName +Do Q\ n;

appendRawCommands(holderFormStream.createOutputStream(),holderFormComment);
appendRawCommands(innerFormStream.createOutputStream(),innerFormComment);
appendRawCommands(imageFormStream.createOutputStream(),imgFormComment);

documentOutputStream = new ByteArrayOutputStream();
document.save(documentOutputStream);
document.close();
tempSignedPdfFileOutputStream.write(documentOutputStream.toByteArray());
generateSignedPdf();
} catch(例外e){
e.printStackTrace();
}
}

public void appendRawCommands(OutputStream os,String commands)throws IOException {
os.write(commands.getBytes(ISO-8859-1) ));
os.close();
}

public void generateSignedPdf(){
try {
//查找初始字节范围偏移量
字符串docString = new String(documentOutputStream.toByteArray (),ISO-8859-1);
offsetContentStart =(documentOutputStream.toString()。indexOf(Contents<)+ 10 - 1);
offsetContentEnd =(documentOutputStream.toString()。indexOf(000000>)+ 7);
secondPartLength =(documentOutputStream.size() - documentOutputStream.toString()。indexOf(000000>) - 7);
//计算更新的ByteRange
字符串initByteRange =;
if(docString.indexOf(/ ByteRange [0 1000000000 1000000000 1000000000])> 0){
initByteRange =/ ByteRange [0 1000000000 1000000000 1000000000];
} else if(docString.indexOf(/ ByteRange [0 0 0 0])> 0){
initByteRange =/ ByteRange [0 0 0 0];
} else {
System.out.println(No / ByteRange Token is found!);
System.exit(1);
}

字符串interimByteRange =/ ByteRange [0+ offsetContentStart ++ offsetContentEnd ++ secondPartLength +];
int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length();
offsetContentStart = offsetContentStart + byteRangeLengthDifference;
offsetContentEnd = offsetContentEnd + byteRangeLengthDifference;
String finalByteRange =/ ByteRange [0+ offsetContentStart ++ offsetContentEnd ++ secondPartLength +];
byteRangeLengthDifference + = interimByteRange.length() - finalByteRange.length();
//替换ByteRange
docString = docString.replace(initByteRange,finalByteRange);

//更新外部参照表
int xrefOffset = docString.indexOf(xref);
int startObjOffset = docString.indexOf(0000000000 65535 f)+0000000000 65535 f.length()+ 1;
int trailerOffset = docString.indexOf(trailer) - 2;
String initialXrefTable = docString.substring(startObjOffset,trailerOffset);
int signObjectOffset = docString.indexOf(/ Type / Sig) - 3;
String updatedXrefTable =;
while(initialXrefTable.indexOf(n)> 0){
String currObjectRefEntry = initialXrefTable.substring(0,initialXrefTable.indexOf(n)+ 1);
String currObjectRef = currObjectRefEntry.substring(0,currObjectRefEntry.indexOf(00000 n));
int currObjectOffset = Integer.parseInt(currObjectRef.trim()。replaceFirst(^ 0 +(?!$),));
if((currObjectOffset + byteRangeLengthDifference)> signObjectOffset){
currObjectOffset + = byteRangeLengthDifference;
int currObjectOffsetDigitsCount = Integer.toString(currObjectOffset)。length();
currObjectRefEntry = currObjectRefEntry.replace(currObjectRefEntry.substring(currObjectRef.length() - currObjectOffsetDigitsCount,currObjectRef.length()),Integer.toString(currObjectOffset));
updatedXrefTable + = currObjectRefEntry;
} else {
updatedXrefTable + = currObjectRefEntry;
}
initialXrefTable = initialXrefTable.substring(initialXrefTable.indexOf(n)+ 1);
}
//替换为更新的外部参照表
docString = docString.substring(0,startObjOffset).concat(updatedXrefTable).concat(docString.substring(trailerOffset));

//更新startxref
int startxrefOffset = docString.indexOf(startxref);
//替换为更新的startxref
docString = docString.substring(0,startxrefOffset).concat(startxref \\\
.concat(Integer.toString(xrefOffset)))。concat(\ ñ%% EOF\\\
);

//通过删除临时包络的已分离签名内容(000 ... 000)来构造签名的原始文档
contentString = docString.substring(offsetContentStart + 1,offsetContentEnd - 1);
String docFirstPart = docString.substring(0,offsetContentStart);
字符串docSecondPart = docString.substring(offsetContentEnd);
字符串docForSign = docFirstPart.concat(docSecondPart);

//生成签名
pdfFileOutputStream.write(docForSign.getBytes(ISO-8859-1));
文件keyStorefile = new File(storePath);
InputStream keyStoreInputStream = new FileInputStream(keyStorefile);
KeyStore keyStore = KeyStore.getInstance(PKCS12);
keyStore.load(keyStoreInputStream,keyStorePassword.toCharArray());
certChain = keyStore.getCertificateChain(entryAlias);
PrivateKey privateKey =(PrivateKey)keyStore.getKey(entryAlias,keyStorePassword.toCharArray());
列表<证书> certList = new ArrayList< Certificate>();
certList = Arrays.asList(certChain);
Store store = new JcaCertStore(certList);
// String algorithm =SHA1WithRSA;
// String algorithm =SHA2WithRSA;
String algorithm =MD5WithRSA;
// String algorithm =DSA;

//更新的符号方法
CMSTypedData msg = new CMSProcessableByteArray(docForSign.getBytes(ISO-8859-1));
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
/ *构建SignerInfo生成器构建器,它将构建生成器...将生成SignerInformation ... * /
SignerInfoGeneratorBuilder signerInfoBuilder = new SignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder()。setProvider(BC)。建立());
// JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(SHA2withRSA);
JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(algorithm);
contentSigner.setProvider(BC);
SignerInfoGenerator signerInfoGenerator = signerInfoBuilder.build(contentSigner.build(privateKey),new X509CertificateHolder(certList.get(0).getEncoded()));
generator.addSignerInfoGenerator(signerInfoGenerator);
generator.addCertificates(store);
CMSSignedData signedData = generator.generate(msg,false);
String apHexEnvelopedData = org.apache.commons.codec.binary.Hex.encodeHexString(signedData.getEncoded())。toUpperCase();
//构造内容标记数据
contentString = apHexEnvelopedData.concat(contentString).substring(0,contentString.length());
contentString =<。concat(contentString).concat(>);
//构造签名文档
字符串signedDoc = docFirstPart.concat(contentString).concat(docSecondPart);
//将签名文档写入文件
signedPdfFileOutputStream.write(signedDoc.getBytes(ISO-8859-1));
signedPdfFileOutputStream.close();
signedDoc = null;
} catch(异常e){
抛出新的RuntimeException(生成签名数据时出错,e);
}
}

public static void main(String [] args){
AffixSignature affixSignature = new AffixSignature();
}
}

在MKL的指导下,现在更新的代码签署新创建的文档。感谢MKL!

解决方案

虽然最初这些提示是作为对原始问题的评论而提出的,但它们现在值得制定作为答案:



代码问题



虽然有太多代码需要审核和修复没有花费相当多的时间,虽然最初没有样本PDF是一个障碍,快速扫描代码揭示了一些问题:




  • appendRawCommands(XXXFormStream.createOutputStream(),YYY)调用很可能导致PDFBox出现问题:多次为同一表单创建输出流可能是一个问题,并且还在表单之间来回切换。


  • 此外,在写入同一个流的多个字符串之间似乎没有空格崛起为未知的 Qq 运营商。此外, appendRawCommands 方法使用UTF-8,它对PDF来说是陌生的。


  • generateSignedDocument 很可能造成很大的破坏,因为它假设它可以用PDF作为如果他们是文本文件。一般情况并非如此。




结果PDF问题



OP最终提供的样本结果PDF可以查明一些实际发生的问题:




  • 比较两者的字节数文档(Report_08_05_23.pdf和Signed_Report_08_05_23.pdf)发现有许多不需要的更改,乍一看特别是用问号替换某些字节。这是因为使用 ByteArrayOutputStream.toString()来轻松操作文档并最终将其更改回 byte []



    例如比照的JavaDocs ByteArrayOutputStream.toString()

      *< p>此方法始终使用平台的
    *默认字符集的默认替换字符串替换格式错误的输入和不可映射字符
    *序列。当对解码过程的更多控制需要
    *时,应使用{@linkplain java.nio.charset.CharsetDecoder}
    *类。

    某些字节值不代表平台默认字符集中的字符和因此转换为 Unicode 替换字符 并在最终转换为 byte [] 变为0x3f(问号的ASCII码)。此更改会终止内容流和图像流的压缩流内容。



    要解决此问题,必须使用 byte byte [] 操作而不是 String 这里的操作。


  • 流8 0在其XObject资源中引用自身,这可能会使任何pdf查看器抛出。请不要这样的循环。




签名容器问题



签名无法验证。因此,它也会被审查。




  • 检查签名容器可以看出它是错误的:尽管签名是 adbe.pkcs7.detached ,签名容器会嵌入数据。查看代码的原因很明显:

      CMSSignedData sigData = generator.generate(msg,true); 

    true 参数要求BC嵌入 msg 数据。


  • 开始查看签名代码后,另一个问题变得明显: msg 以上数据不仅仅是摘要,它们已经是签名:

     签名签名= Signature.getInstance(算法,BC); 
    signature.initSign(privateKey);
    signature.update(docForSign.getBytes());
    CMSTypedData msg = new CMSProcessableByteArray(signature.sign());




这是后来创建的 SignerInfoGenerator 用于创建实际签名。



编辑:之后提到的问题有已经修复或至少解决了问题,Adobe Reader仍然不接受签名。因此,再看看代码和:



哈希值计算问题



OP构造此 ByteRange

  String finalByteRange =/ ByteRange [0+ offsetContentStart ++ offsetContentEnd ++ secondPartLength +]; 

及以后的套装

  String docFirstPart = docString.substring(0,offsetContentStart + 1); 
String docSecondPart = docString.substring(offsetContentEnd - 1);

+ 1 - 1 旨在使这些文件部分还包括< > 包含签名字节。但OP也使用这些字符串来构造签名数据:

  String docForSign = docFirstPart.concat(docSecondPart); 

这是错误的,签名字节不包含< > 。因此,稍后计算的哈希值也是错误的,并且Adobe Reader有充分的理由假设文档已被操作。



话虽如此,还有其他问题受到限制每隔一段时间出现一次:



偏移和长度更新问题



OP插入字节范围是这样的:

 字符串interimByteRange =/ ByteRange [0+ offsetContentStart ++ offsetContentEnd ++ secondPartLength +]; 
int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length();
offsetContentStart = offsetContentStart + byteRangeLengthDifference;
offsetContentEnd = offsetContentEnd + byteRangeLengthDifference;
String finalByteRange =/ ByteRange [0+ offsetContentStart ++ offsetContentEnd ++ secondPartLength +];
byteRangeLengthDifference + = interimByteRange.length() - finalByteRange.length();
//替换ByteRange
docString = docString.replace(initByteRange,finalByteRange);

每隔一段时间 offsetContentStart offsetContentEnd 将稍微低于10 ^ n并略高于之后。该行

  byteRangeLengthDifference + = interimByteRange.length() -  finalByteRange.length(); 

试图弥补这一点,但 finalByteRange (最终插入到文档中)仍然包含未经修正的值。



以类似的方式,外部参照的表示开始像这样插入

  docString = docString.substring(0,startxrefOffset).concat(startxref \ n.concat(Integer.toString(xrefOffset)))。concat( \\\
%% EOF\\\
);

也可能比以前更长,这使得字节范围(事先计算)不会覆盖整个文档。



此外,使用整个文档的文本搜索查找相关PDF对象的偏移量

  offsetContentStart =(documentOutputStream.toString()。indexOf(Contents<)+ 10  -  1); 
offsetContentEnd =(documentOutputStream.toString()。indexOf(000000>)+ 7);
...
int xrefOffset = docString.indexOf(xref);
...
int startxrefOffset = docString.indexOf(startxref);

对于通用文档将失败。例如。如果文档中已有先前的签名,很可能会像这样识别错误的索引。


Pardon Me! I am poor in java.
Please Correct me wherever I am wrong and improve wherever I am poor!

I am trying to digitally sign a dynamically created pdf using PDFBox with the following program:

Tasks in the Program:
(i) Creating Template PDF
(ii) Updating ByteRange, xref, startxref
(iii) Constructing Original Document for Signature Creation
(iv) Creating Detached Enveloped Digital Signature
(v) Constructing Digitally Signed PDF Document by concatenating Original Doc Part - I, Detached Signature and Original PDF Part - II

Observations:
(i) pdfFileOutputStream.write(documentOutputStream.toByteArray()); createsTemplate PDF Document with Visible Signature.

(ii) It Creates Some PDF Signed Document but has errors (a) invalid tokens and (b) several parser errors
(now corrected under the abled guidance of MKL!)

Please suggest me on the following:

(i) How to add Signature Text in the Visible Signature on the layer2.

Thanks in Advance!

    package digitalsignature;

    import java.awt.geom.AffineTransform;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.security.Signature;
    import java.util.ArrayList;
    import org.bouncycastle.cert.X509CertificateHolder;
    import org.bouncycastle.cert.jcajce.JcaCertStore;
    import org.bouncycastle.cms.CMSProcessableByteArray;
    import org.bouncycastle.cms.CMSTypedData;
    import org.bouncycastle.cms.SignerInfoGenerator;
    import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
    import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
    import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
    import org.bouncycastle.util.Store;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.security.KeyStore;
    import java.security.PrivateKey;
    import java.security.cert.CertStore;
    import java.security.cert.Certificate;
    import java.security.cert.CollectionCertStoreParameters;
    import java.security.cert.X509Certificate;
    import java.text.DecimalFormat;
    import java.text.SimpleDateFormat;
    import java.util.Arrays;
    import java.util.Calendar;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;

    import java.util.Map;
    import org.apache.pdfbox.cos.COSArray;
    import org.apache.pdfbox.cos.COSDictionary;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.pdmodel.PDPage;
    import org.apache.pdfbox.pdmodel.PDResources;
    import org.apache.pdfbox.pdmodel.common.PDRectangle;
    import org.apache.pdfbox.pdmodel.common.PDStream;
    import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
    import org.apache.pdfbox.pdmodel.font.PDFont;
    import org.apache.pdfbox.pdmodel.font.PDType1Font;
    import org.apache.pdfbox.pdmodel.graphics.xobject.PDJpeg;
    import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectForm;
    import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
    import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
    import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
    import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
    import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
    import org.apache.pdfbox.pdmodel.interactive.form.PDField;
    import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
    import org.bouncycastle.cms.CMSSignedData;
    import org.bouncycastle.cms.CMSSignedDataGenerator;
    import org.bouncycastle.cms.CMSSignedGenerator;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;


    public class AffixSignature {
        String path = "D:\\reports\\";
        String onlyFileName = "";
        String pdfExtension = ".pdf";
        String pdfFileName = "";
        String pdfFilePath = "";
        String signedPdfFileName = "";
        String signedPdfFilePath = "";
        String ownerPassword = "";
        String tempSignedPdfFileName = "";
        String tempSignedPdfFilePath = "";
        String userPassword = "";
        String storePath = "resources/my.p12";
        String entryAlias = "signerCert";
        String keyStorePassword = "password";
        ByteArrayOutputStream documentOutputStream = null;
        private Certificate[] certChain;
        private static BouncyCastleProvider BC = new BouncyCastleProvider();
        int offsetContentStart = 0;
        int offsetContentEnd = 0;
        int secondPartLength = 0;
        int offsetStartxrefs = 0;
        String contentString = "";
        OutputStream signedPdfFileOutputStream;
        OutputStream pdfFileOutputStream;

        public AffixSignature() {
        try {
            SimpleDateFormat timeFormat = new SimpleDateFormat("hh_mm_ss");

            onlyFileName = "Report_" + timeFormat.format(new Date());
            pdfFileName = onlyFileName + ".pdf";
            pdfFilePath = path + pdfFileName;
            File pdfFile = new File(pdfFilePath);
            pdfFileOutputStream = new FileOutputStream(pdfFile);

            signedPdfFileName = "Signed_" + onlyFileName + ".pdf";
            signedPdfFilePath = path + signedPdfFileName;
            File signedPdfFile = new File(signedPdfFilePath);
            signedPdfFileOutputStream = new FileOutputStream(signedPdfFile);

            String tempFileName = "Temp_Report_" + timeFormat.format(new Date());
            String tempPdfFileName = tempFileName + ".pdf";
            String tempPdfFilePath = path + tempPdfFileName;
            File tempPdfFile = new File(tempPdfFilePath);
            OutputStream tempSignedPdfFileOutputStream = new FileOutputStream(tempPdfFile);

            PDDocument document = new PDDocument();
            PDDocumentCatalog catalog = document.getDocumentCatalog();
            PDPage page = new PDPage(PDPage.PAGE_SIZE_A4);
            PDPageContentStream contentStream = new PDPageContentStream(document, page);


            PDFont font = PDType1Font.HELVETICA;
            Map<String, PDFont> fonts = new HashMap<String, PDFont>();
            fonts = new HashMap<String, PDFont>();
            fonts.put("F1", font);

//            contentStream.setFont(font, 12);
            contentStream.setFont(font, 12);
            contentStream.beginText();
            contentStream.moveTextPositionByAmount(100, 700);
            contentStream.drawString("DIGITAL SIGNATURE TEST");
            contentStream.endText();
            contentStream.close();
            document.addPage(page);

//To Affix Visible Digital Signature
            PDAcroForm acroForm = new PDAcroForm(document);
            catalog.setAcroForm(acroForm);

            PDSignatureField sf = new PDSignatureField(acroForm);

            PDSignature pdSignature = new PDSignature();
            page.getAnnotations().add(sf.getWidget());
            pdSignature.setName("sign");
            pdSignature.setByteRange(new int[]{0, 0, 0, 0});
            pdSignature.setContents(new byte[4 * 1024]);
            pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
            pdSignature.setName("NAME");
            pdSignature.setLocation("LOCATION");
            pdSignature.setReason("SECURITY");
            pdSignature.setSignDate(Calendar.getInstance());
            List<PDField> acroFormFields = acroForm.getFields();

            sf.setSignature(pdSignature);
            sf.getWidget().setPage(page);

            COSDictionary acroFormDict = acroForm.getDictionary();
            acroFormDict.setDirect(true);
            acroFormDict.setInt(COSName.SIG_FLAGS, 3);
            acroFormFields.add(sf);

            PDRectangle frmRect = new PDRectangle();
//            float[] frmRectParams = {lowerLeftX,lowerLeftY,upperRightX,upperRight};
//            float[] frmRectLowerLeftUpperRightCoordinates = {5f, page.getMediaBox().getHeight() - 50f, 100f, page.getMediaBox().getHeight() - 5f};
            float[] frmRectLowerLeftUpperRightCoordinates = {5f, 5f, 205f, 55f};
            frmRect.setUpperRightX(frmRectLowerLeftUpperRightCoordinates[2]);
            frmRect.setUpperRightY(frmRectLowerLeftUpperRightCoordinates[3]);
            frmRect.setLowerLeftX(frmRectLowerLeftUpperRightCoordinates[0]);
            frmRect.setLowerLeftY(frmRectLowerLeftUpperRightCoordinates[1]);

            sf.getWidget().setRectangle(frmRect);

            COSArray procSetArr = new COSArray();
            procSetArr.add(COSName.getPDFName("PDF"));
            procSetArr.add(COSName.getPDFName("Text"));
            procSetArr.add(COSName.getPDFName("ImageB"));
            procSetArr.add(COSName.getPDFName("ImageC"));
            procSetArr.add(COSName.getPDFName("ImageI"));

            String signImageFilePath = "resources/sign.JPG";
            File signImageFile = new File(signImageFilePath);
            InputStream signImageStream = new FileInputStream(signImageFile);
            PDJpeg img = new PDJpeg(document, signImageStream);

            PDResources holderFormResources = new PDResources();
            PDStream holderFormStream = new PDStream(document);
            PDXObjectForm holderForm = new PDXObjectForm(holderFormStream);
            holderForm.setResources(holderFormResources);
            holderForm.setBBox(frmRect);
            holderForm.setFormType(1);

            PDAppearanceDictionary appearance = new PDAppearanceDictionary();
            appearance.getCOSObject().setDirect(true);
            PDAppearanceStream appearanceStream = new PDAppearanceStream(holderForm.getCOSStream());
            appearance.setNormalAppearance(appearanceStream);
            sf.getWidget().setAppearance(appearance);
            acroFormDict.setItem(COSName.DR, holderFormResources.getCOSDictionary());

            PDResources innerFormResources = new PDResources();
            PDStream innerFormStream = new PDStream(document);
            PDXObjectForm innerForm = new PDXObjectForm(innerFormStream);
            innerForm.setResources(innerFormResources);
            innerForm.setBBox(frmRect);
            innerForm.setFormType(1);

            String innerFormName = holderFormResources.addXObject(innerForm, "FRM");

            PDResources imageFormResources = new PDResources();
            PDStream imageFormStream = new PDStream(document);
            PDXObjectForm imageForm = new PDXObjectForm(imageFormStream);
            imageForm.setResources(imageFormResources);
            byte[] AffineTransformParams = {1, 0, 0, 1, 0, 0};
            AffineTransform affineTransform = new AffineTransform(AffineTransformParams[0], AffineTransformParams[1], AffineTransformParams[2], AffineTransformParams[3], AffineTransformParams[4], AffineTransformParams[5]);
            imageForm.setMatrix(affineTransform);
            imageForm.setBBox(frmRect);
            imageForm.setFormType(1);

            String imageFormName = innerFormResources.addXObject(imageForm, "n");
            String imageName = imageFormResources.addXObject(img, "img");

            innerForm.getResources().getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            page.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            innerFormResources.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            imageFormResources.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);
            holderFormResources.getCOSDictionary().setItem(COSName.PROC_SET, procSetArr);

            String holderFormComment = "q 1 0 0 1 0 0 cm /" + innerFormName + " Do Q \n";
            String innerFormComment = "q 1 0 0 1 0 0 cm /" + imageFormName + " Do Q\n";
            String imgFormComment = "q " + 100 + " 0 0 50 0 0 cm /" + imageName + " Do Q\n";

            appendRawCommands(holderFormStream.createOutputStream(), holderFormComment);
            appendRawCommands(innerFormStream.createOutputStream(), innerFormComment);
            appendRawCommands(imageFormStream.createOutputStream(), imgFormComment);

            documentOutputStream = new ByteArrayOutputStream();
            document.save(documentOutputStream);
            document.close();
            tempSignedPdfFileOutputStream.write(documentOutputStream.toByteArray());
            generateSignedPdf();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void appendRawCommands(OutputStream os, String commands) throws IOException {
        os.write(commands.getBytes("ISO-8859-1"));
        os.close();
    }

    public void generateSignedPdf() {
        try {
            //Find the Initial Byte Range Offsets
            String docString = new String(documentOutputStream.toByteArray(), "ISO-8859-1");
            offsetContentStart = (documentOutputStream.toString().indexOf("Contents <") + 10 - 1);
            offsetContentEnd = (documentOutputStream.toString().indexOf("000000>") + 7);
            secondPartLength = (documentOutputStream.size() - documentOutputStream.toString().indexOf("000000>") - 7);
            //Calculate the Updated ByteRange
            String initByteRange = "";
            if (docString.indexOf("/ByteRange [0 1000000000 1000000000 1000000000]") > 0) {
                initByteRange = "/ByteRange [0 1000000000 1000000000 1000000000]";
            } else if (docString.indexOf("/ByteRange [0 0 0 0]") > 0) {
                initByteRange = "/ByteRange [0 0 0 0]";
            } else {
                System.out.println("No /ByteRange Token is Found!");
                System.exit(1);
            }

            String interimByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
            int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length();
            offsetContentStart = offsetContentStart + byteRangeLengthDifference;
            offsetContentEnd = offsetContentEnd + byteRangeLengthDifference;
            String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
            byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();
            //Replace the ByteRange
            docString = docString.replace(initByteRange, finalByteRange);

            //Update xref Table
            int xrefOffset = docString.indexOf("xref");
            int startObjOffset = docString.indexOf("0000000000 65535 f") + "0000000000 65535 f".length() + 1;
            int trailerOffset = docString.indexOf("trailer") - 2;
            String initialXrefTable = docString.substring(startObjOffset, trailerOffset);
            int signObjectOffset = docString.indexOf("/Type /Sig") - 3;
            String updatedXrefTable = "";
            while (initialXrefTable.indexOf("n") > 0) {
                String currObjectRefEntry = initialXrefTable.substring(0, initialXrefTable.indexOf("n") + 1);
                String currObjectRef = currObjectRefEntry.substring(0, currObjectRefEntry.indexOf(" 00000 n"));
                int currObjectOffset = Integer.parseInt(currObjectRef.trim().replaceFirst("^0+(?!$)", ""));
                if ((currObjectOffset + byteRangeLengthDifference) > signObjectOffset) {
                    currObjectOffset += byteRangeLengthDifference;
                    int currObjectOffsetDigitsCount = Integer.toString(currObjectOffset).length();
                    currObjectRefEntry = currObjectRefEntry.replace(currObjectRefEntry.substring(currObjectRef.length() - currObjectOffsetDigitsCount, currObjectRef.length()), Integer.toString(currObjectOffset));
                    updatedXrefTable += currObjectRefEntry;
                } else {
                    updatedXrefTable += currObjectRefEntry;
                }
                initialXrefTable = initialXrefTable.substring(initialXrefTable.indexOf("n") + 1);
            }
            //Replace with Updated xref Table
            docString = docString.substring(0, startObjOffset).concat(updatedXrefTable).concat(docString.substring(trailerOffset));

            //Update startxref
            int startxrefOffset = docString.indexOf("startxref");
            //Replace with Updated startxref
            docString = docString.substring(0, startxrefOffset).concat("startxref\n".concat(Integer.toString(xrefOffset))).concat("\n%%EOF\n");

            //Construct Original Document For Signature by Removing Temporary Enveloped Detached Signed Content(000...000)
            contentString = docString.substring(offsetContentStart + 1, offsetContentEnd - 1);
            String docFirstPart = docString.substring(0, offsetContentStart);
            String docSecondPart = docString.substring(offsetContentEnd);
            String docForSign = docFirstPart.concat(docSecondPart);

            //Generate Signature
            pdfFileOutputStream.write(docForSign.getBytes("ISO-8859-1"));
            File keyStorefile = new File(storePath);
            InputStream keyStoreInputStream = new FileInputStream(keyStorefile);
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
            certChain = keyStore.getCertificateChain(entryAlias);
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(entryAlias, keyStorePassword.toCharArray());
            List<Certificate> certList = new ArrayList<Certificate>();
            certList = Arrays.asList(certChain);
            Store store = new JcaCertStore(certList);
//            String algorithm="SHA1WithRSA";
//            String algorithm="SHA2WithRSA";
            String algorithm = "MD5WithRSA";
            //String algorithm = "DSA";

            //Updated Sign Method
            CMSTypedData msg = new CMSProcessableByteArray(docForSign.getBytes("ISO-8859-1"));
            CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
            /* Build the SignerInfo generator builder, that will build the generator... that will generate the SignerInformation... */
            SignerInfoGeneratorBuilder signerInfoBuilder = new SignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider(BC).build());
            //JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder("SHA2withRSA");
            JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(algorithm);
            contentSigner.setProvider(BC);
            SignerInfoGenerator signerInfoGenerator = signerInfoBuilder.build(contentSigner.build(privateKey), new X509CertificateHolder(certList.get(0).getEncoded()));
            generator.addSignerInfoGenerator(signerInfoGenerator);
            generator.addCertificates(store);
            CMSSignedData signedData = generator.generate(msg, false);
            String apHexEnvelopedData = org.apache.commons.codec.binary.Hex.encodeHexString(signedData.getEncoded()).toUpperCase();
            //Construct Content Tag Data
            contentString = apHexEnvelopedData.concat(contentString).substring(0, contentString.length());
            contentString = "<".concat(contentString).concat(">");
            //Construct Signed Document
            String signedDoc = docFirstPart.concat(contentString).concat(docSecondPart);
            //Write Signed Document to File
            signedPdfFileOutputStream.write(signedDoc.getBytes("ISO-8859-1"));
            signedPdfFileOutputStream.close();
            signedDoc = null;
        } catch (Exception e) {
            throw new RuntimeException("Error While Generating Signed Data", e);
        }
    }

    public static void main(String[] args) {
        AffixSignature affixSignature = new AffixSignature();
    }
}

Under the abled guidance of MKL, now the updated code signs the newly created document. Thanks to MKL!

解决方案

While initially these hints were presented as comments to the original question, they now merit to be formulated as an answer:

Code issues

While there is too much code to review and fix without spending a considerable amount of time, and while the original absence of a sample PDF was a hindrance, a quick scan of the code revealed some issues:

  • The appendRawCommands(XXXFormStream.createOutputStream(), YYY) calls quite likely cause problems with PDFBox: creating output streams for the same form more than once may be an issue, and also switching back and forth between the forms.

  • Furthermore there does not seem to be a whitespace between the multiple strings written to the same stream giving rise to unknown Qq operators. Furthermore the appendRawCommands method uses UTF-8 which is foreign to PDF.

  • The generateSignedDocument most likely does quite a lot of damage as it assumes it can work with PDFs as if they were text files. That in general is not the case.

Result PDF issues

The sample result PDF eventually provided by the OP allows to pinpoint some actually realized issues:

  • Comparing the bytes of both documents (Report_08_05_23.pdf and Signed_Report_08_05_23.pdf) one finds that there are many unwanted changes, at first glance especially the replacement of certain bytes by question marks. This is due to using ByteArrayOutputStream.toString() to easily operate on the document and eventually changing it back into a byte[].

    E.g. cf. the JavaDocs of ByteArrayOutputStream.toString()

    * <p> This method always replaces malformed-input and unmappable-character
    * sequences with the default replacement string for the platform's
    * default character set. The {@linkplain java.nio.charset.CharsetDecoder}
    * class should be used when more control over the decoding process is
    * required.
    

    Certain byte values do not represent characters in the platform's default character set and therefore are transformed to the Unicode Replacement Character and in the final transformation into a byte[] become 0x3f (ASCII code for the question mark). This change kills compressed stream contents, both of content streams and image streams.

    To fix this, one has to work with byte and byte[] operations instead of String operations here.

  • The stream 8 0 references itself in its XObject resources which might make any pdf viewer throw up. Please refrain from such circularity.

Signature Container issues

The signature does not verify. Thus, it also is reviewed.

  • Inspecting the signature container one can see that it is wrong: In spite of the signature being adbe.pkcs7.detached, the signature container embeds data. Looking at the code the reason becomes clear:

    CMSSignedData sigData = generator.generate(msg, true);
    

    The true parameter asks BC to embed the msg data.

  • Having started to look at the signing code, another issue becomes visible: The msg data above are not merely a digest, they already are a signature:

    Signature signature = Signature.getInstance(algorithm, BC);
    signature.initSign(privateKey);
    signature.update(docForSign.getBytes());
    CMSTypedData msg = new CMSProcessableByteArray(signature.sign());
    

which is wrong as the later created SignerInfoGenerator is used to create the actual signature.

Edit: After the issues mentioned before have been fixed or at least worked-around, the signature is still not accepted by the Adobe Reader. Thus, another look at the code and:

Hash value calculation issue

The OP constructs this ByteRange value

String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";

and later sets

String docFirstPart = docString.substring(0, offsetContentStart + 1);
String docSecondPart = docString.substring(offsetContentEnd - 1);

The + 1 and - 1 are intended to make these document parts also include the < and > enveloping the signature bytes. But the OP also uses these strings to construct the signed data:

String docForSign = docFirstPart.concat(docSecondPart);

This is wrong, the signed bytes do not contain the < and >. Thus, the hash value later on calculated also is wrong and Adobe Reader has good reasons to assume the document has been manipulated.

That been said, there also are other issues bound to come up every once in a while:

Offset and length updating issues

The OP inserts the byte range to be like this:

String interimByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
int byteRangeLengthDifference = interimByteRange.length() - initByteRange.length();
offsetContentStart = offsetContentStart + byteRangeLengthDifference;
offsetContentEnd = offsetContentEnd + byteRangeLengthDifference;
String finalByteRange = "/ByteRange [0 " + offsetContentStart + " " + offsetContentEnd + " " + secondPartLength + "]";
byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();
//Replace the ByteRange
docString = docString.replace(initByteRange, finalByteRange);

Every one in a while offsetContentStart or offsetContentEnd will be slightly below some 10^n and slightly above afterwards. The line

byteRangeLengthDifference += interimByteRange.length() - finalByteRange.length();

tries to make up for this, but finalByteRange (which eventually is inserted into the document) still contains uncorrected values.

In a similar fashion the representation of the xref start inserted like this

docString = docString.substring(0, startxrefOffset).concat("startxref\n".concat(Integer.toString(xrefOffset))).concat("\n%%EOF\n");

may also be longer than before which makes the byte range (calculated beforehand) not cover the whole document.

Furthermore finding offsets of the relevant PDF objects using text searches of the whole document

offsetContentStart = (documentOutputStream.toString().indexOf("Contents <") + 10 - 1);
offsetContentEnd = (documentOutputStream.toString().indexOf("000000>") + 7);
...
int xrefOffset = docString.indexOf("xref");
...
int startxrefOffset = docString.indexOf("startxref");

will fail for generic documents. E.g. if there already are previous signatures in the document, quite likely the wrong indices will be identified like this.

这篇关于如何使用PDFBox对动态创建的PDF文档进行数字签名?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆