如何使用PDFBox对动态创建的PDF文档进行数字签名?

对不起! 我在java中很穷。
请在任何我错的地方纠正我,并在我穷人的哪个地方改善!

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

该计划的任务:
(i)创建模板PDF
(ii)更新ByteRange,xref,startxref
(iii)为签名创建构建原始文件
(iv)创建独立的包络数字签名
(v)通过连接原始文档部分构建数字签名PDF文档 – I,独立签名和原始PDF部分 – II

观察:
(i)pdfFileOutputStream.write(documentOutputStream.toByteArray()); 使用Visible Signature创建模板PDF文档。

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

请建议我以下内容:

(i)如何在layer2上的Visible Signature中添加签名文本。

提前致谢!

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 fonts = new HashMap(); fonts = new HashMap(); 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 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 ") + 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 certList = new ArrayList(); 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 = ""); //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(); } } 

在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[]

    例如参见 ByteArrayOutputStream.toString()的JavaDocs

     * 

    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.

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

    要解决这个问题,必须在这里使用bytebyte[]操作而不是String操作。

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

签名容器问题

签名无法validation。 因此,它也被审查。

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

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

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

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

     Signature signature = Signature.getInstance(algorithm, 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插入字节范围如下:

 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); 

在一段时间内, offsetContentStartoffsetContentEnd中的每一个都会稍微低于某个10 ^ n,稍后会略高于此。 这条线

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

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

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

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

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

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

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

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