Itext 的多重签名 - 现有签名块的问题
Multiple signatures with Itext - issue with existing signature blocks
所以我目前正在进行一个签署 PDF 文档的项目,我 运行 遇到了问题。因此,如果我指定要签名的块,我可以正确地签署文档而不会出现任何问题,即
signatureAppearance.setVisibleSignature(矩形、页面、signingBlockName);
我可以毫无问题地添加多个签名,并且所有签名仍然有效。现在我更改了代码,首先添加空签名块,然后使用我添加的签名块名称对这些块进行签名,即
signatureAppearance.setVisibleSignature(signingBlockName);
原因是我们要生成带有签名字段的 PDF 文档(可以有多个),但这里的问题是,自从我开始使用这种签名块签名方法以来,即使签名是在附加模式下完成的,第二个签名也会使第一个签名无效,并且只有最后一个签名具有 PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED 认证级别。
我是不是遗漏了什么或者旧版本的 IText 中有错误?
我目前使用的是 IText 版本 4.2.0。
提前致谢。
-----代码------------
/**
* The method is used to sign a pdf document using a certificate, image of the signature as well as specified signature blocks
*
* @param document the document to be signed
* @param certificate the certificate to be used to do the signing
* @param signature the image of the signature used to sign the document
* @param signatureBlocks the blocks in the pdf document to sign
* @return the signed document bytes
* @throws Exception
*/
public byte[] signPDFDocument(PDFDocument document, PKSCertificate certificate, SignatureImage signature, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
document.addSignatureBlocks(signatureBlocks);
PDFDocument signedDocument = signPDFDocumentSignatureBlocks(document, certificate, signature, signatureBlocks, certifyDocument);
return signedDocument.getDocumentBytes();
}
/**
* The method is used to sign a pdf document using a certificate, image of the signature as well as specified signature blocks
*
* @param document the document to be signed
* @param certificate the certificate to be used to do the signing
* @param signatureBlocks the blocks in the pdf document to sign
* @return the signed document bytes
* @throws Exception
*/
public byte[] signPDFDocument(PDFDocument document, PKSCertificate certificate, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
document.addSignatureBlocks(signatureBlocks);
PDFDocument signedDocument = signPDFDocumentSignatureBlocks(document, certificate, null, signatureBlocks, certifyDocument);
return signedDocument.getDocumentBytes();
}
/**
* The method is used to get the names of all signature fields
*
* @param document the document to check for the signature fields
* @return the list of signature field names used and unused
* @throws Exception
*/
public List<String> getAvailableSignatureBlocks(PDFDocument document) throws Exception
{
PdfReader reader = new PdfReader(document.getDocumentBytes());
ArrayList arrayList = reader.getAcroFields().getBlankSignatureNames();
return Arrays.asList((String[]) arrayList.toArray(new String[arrayList.size()]));
}
/**
* This method is used to loop over the signature blocks and sign them
*
* @param document the document to be signed
* @param certificate the certificate to apply to the signature
* @param signature the image of the client signature
* @param signatureBlocks the signature blocks to create and sign
* @param certifyDocument flag to indicate if the document should be signed
* @throws Exception
*/
private PDFDocument signPDFDocumentSignatureBlocks(PDFDocument document, PKSCertificate certificate, SignatureImage signature, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
for (int i = 0; i < signatureBlocks.size();i++)
{
PDFDocument signedDocument = new PDFDocument(new ByteArrayOutputStream());
PdfReader reader = new PdfReader(document.getDocumentBytes());
PdfStamper stamper = createPDFStamper(signedDocument, reader);
document = signPDFDocumentSignatureBlock(signedDocument, certificate, signature, signatureBlocks.get(i), stamper, certifyDocument && i == (signatureBlocks.size() - 1));
}
return document;
}
/**
* The method is used to sign the pdf document, it also marks the signing process if it is the final signature process
*
* @param signedDocument the signed document to be generated
* @param certificate the certificate to be used to do the signing
* @param signature the image of the signature used to sign the document
* @param signingBlock the current block to sign in the document
* @param stamper the current document stamper reference
* @param certifyDocument indicate if this signing should certify the document
* @return the signed document object
* @throws Exception
*/
private PDFDocument signPDFDocumentSignatureBlock(PDFDocument signedDocument, PKSCertificate certificate, SignatureImage signature, SigningBlock signingBlock, PdfStamper stamper, boolean certifyDocument) throws Exception
{
PdfSignatureAppearance appearance = signWithSignatureImage(stamper, signature, signingBlock.getName(), certifyDocument);
signWithCertificate(certificate, appearance);
signWithTimeStampingAuthority(appearance, certificate);
signedDocument.updateBytesFromByteStream();
return signedDocument;
}
/**
* The method is used to get the instance of the PDF stamper to stamp the document
*
* @param signedDocument the document that is currently being signed
* @param reader the reader that is reading the document to sign.
* @return the stamper instance
* @throws Exception
*/
private PdfStamper createPDFStamper(PDFDocument signedDocument, PdfReader reader) throws Exception
{
return PdfStamper.createSignature(reader, signedDocument.getByteStream(), '[=10=]', null, true);
}
/**
* The method is used to add the signature image to the signing block
*
* @param stamper the current pdf stamping reference
* @param signature the image to apply to the stamper
* @param signingBlockName the block to sign
* @param certifyDocument indicate if this signing should certify the document
* @throws Exception
*/
private PdfSignatureAppearance signWithSignatureImage(PdfStamper stamper, SignatureImage signature, String signingBlockName, boolean certifyDocument) throws Exception
{
PdfSignatureAppearance signatureAppearance = stamper.getSignatureAppearance();
signatureAppearance.setVisibleSignature(signingBlockName);
setImageForSignature(signatureAppearance, signature);
certifyDocumentSignature(signatureAppearance, certifyDocument);
return signatureAppearance;
}
/**
* The method is used to add an image to the signature block
*
* @param signatureAppearance the reference to the current document appearance
* @param signature the image to apply to the signature
* @throws Exception
*/
private void setImageForSignature(PdfSignatureAppearance signatureAppearance, SignatureImage signature) throws Exception
{
if(signature != null)
{
signatureAppearance.setImage(Image.getInstance(signature.getSignatureImage(), null));
}
}
/**
* The method is used to mark the signature as the certification signature
*
* @param signatureAppearance the reference to the current document appearance
* @param certifyDocument indicates if the document should be certified
*/
private void certifyDocumentSignature(PdfSignatureAppearance signatureAppearance, boolean certifyDocument)
{
if(certifyDocument)
{
signatureAppearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
}
}
/**
* The method is used to add the text containing information about the certificate to the signing appearance
*
* @param certificate the certificate to be used to do the signing
* @param signatureAppearance the appearance of the signature on the document
* @throws Exception
*/
private void signWithCertificate(PKSCertificate certificate, PdfSignatureAppearance signatureAppearance) throws Exception
{
signatureAppearance.setLayer2Text(buildTextSignature(certificate));
signatureAppearance.setLayer2Font(new Font(Font.COURIER, 9));
signatureAppearance.setAcro6Layers(true);
signatureAppearance.setRender(PdfSignatureAppearance.SignatureRenderDescription);
}
/**
* The method is used to encrypt the document using the certificate as well as a timestamping authority
*
* @param appearance the appearance of the signature on the document
* @param certificate the certificate to be used to do the signing
* @throws Exception
*/
private void signWithTimeStampingAuthority(PdfSignatureAppearance appearance, PKSCertificate certificate) throws Exception
{
_timestampingService.signWithTimestampingAuthority(appearance, certificate);
}
/**
* The method builds the text that is used in the text representation of the signature
*
* @param certificate the certificate to be used to do the signing
* @return the text representation of certificate information
* @throws Exception
*/
private String buildTextSignature(PKSCertificate certificate) throws Exception
{
String organization = certificate.getCertificateFieldByName("O");
String commonName = certificate.getCertificateFieldByName("CN");
String signDate = new SimpleDateFormat(_datetimeFormat).format(Calendar.getInstance().getTime());
String expirationDate = new SimpleDateFormat(_datetimeFormat).format(((X509Certificate)certificate.getCertificateChain()[0]).getNotAfter());
return "Digitally signed by " + organization + "\nSignee: " + commonName + "\nSign date: " + signDate + "\n" + "Expiration date: " + expirationDate;
}
the second signature invalidates the first one even though the signing is done in append mode, and only the last signature has the PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED
certification level.
正如在最初的评论中已经推测的那样,这实际上是问题所在:只有文档的第一个签名可能是认证签名。以后的签名可能仅 (PDF 2.0) 使用签名字段锁定条目限制访问权限, cf.规格:
A PDF document may contain [...]
At most one certification signature (PDF 1.5). [...] The signature dictionary shall contain a signature reference dictionary (see Table 253) that has a DocMDP transform method. [...]
A document can contain only one signature field that contains a DocMDP transform method; it shall be the first signed field in the document.
(sections 12.8.1 and 12.8.2.2.1 of ISO 32000-1)
在即将发布的 PDF 标准 (ISO-32000-2) 2.0 版中,可以使用签名字段锁定条目来限制访问权限:
P number (Optional; PDF 2.0) The access permissions granted for this document. Valid values follow:
1 no changes to the document are permitted; any change to the document invalidates the signature.
2 permitted changes are filling in forms, instantiating page templates, and signing; other changes invalidate the signature.
3 permitted changes are the same as for 2, as well as annotation creation, deletion, and modification; other changes invalidate the signature.
There is no default value; absence of this key shall result in no effect on signature validation rules.
If MDP permission is already in effect from an earlier incremental save section or the original part of the document, the number shall specify permissions less than or equal to the permissions already in effect based on signatures earlier in the document. That is, permissions can be denied but not added. If the number specifies greater permissions than an MDP value already in effect, the new number is ignored.
If the document does not have an author signature, the initial permissions in effect are those based on the number 3.
The new permission applies to any incremental changes to the document following the signature of which this key is part.
(Table "Entries in a signature field lock dictionary" of a PDF 2.0 draft)
Adobe Reader/Acrobat 和 iText(自某些 5.3'ish 版本 IIRC 起)已支持此功能,并且似乎允许 OP 之后的效果,即不允许在最终签名字段后进行任何更改。
所以我目前正在进行一个签署 PDF 文档的项目,我 运行 遇到了问题。因此,如果我指定要签名的块,我可以正确地签署文档而不会出现任何问题,即
signatureAppearance.setVisibleSignature(矩形、页面、signingBlockName);
我可以毫无问题地添加多个签名,并且所有签名仍然有效。现在我更改了代码,首先添加空签名块,然后使用我添加的签名块名称对这些块进行签名,即
signatureAppearance.setVisibleSignature(signingBlockName);
原因是我们要生成带有签名字段的 PDF 文档(可以有多个),但这里的问题是,自从我开始使用这种签名块签名方法以来,即使签名是在附加模式下完成的,第二个签名也会使第一个签名无效,并且只有最后一个签名具有 PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED 认证级别。
我是不是遗漏了什么或者旧版本的 IText 中有错误? 我目前使用的是 IText 版本 4.2.0。
提前致谢。
-----代码------------
/**
* The method is used to sign a pdf document using a certificate, image of the signature as well as specified signature blocks
*
* @param document the document to be signed
* @param certificate the certificate to be used to do the signing
* @param signature the image of the signature used to sign the document
* @param signatureBlocks the blocks in the pdf document to sign
* @return the signed document bytes
* @throws Exception
*/
public byte[] signPDFDocument(PDFDocument document, PKSCertificate certificate, SignatureImage signature, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
document.addSignatureBlocks(signatureBlocks);
PDFDocument signedDocument = signPDFDocumentSignatureBlocks(document, certificate, signature, signatureBlocks, certifyDocument);
return signedDocument.getDocumentBytes();
}
/**
* The method is used to sign a pdf document using a certificate, image of the signature as well as specified signature blocks
*
* @param document the document to be signed
* @param certificate the certificate to be used to do the signing
* @param signatureBlocks the blocks in the pdf document to sign
* @return the signed document bytes
* @throws Exception
*/
public byte[] signPDFDocument(PDFDocument document, PKSCertificate certificate, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
document.addSignatureBlocks(signatureBlocks);
PDFDocument signedDocument = signPDFDocumentSignatureBlocks(document, certificate, null, signatureBlocks, certifyDocument);
return signedDocument.getDocumentBytes();
}
/**
* The method is used to get the names of all signature fields
*
* @param document the document to check for the signature fields
* @return the list of signature field names used and unused
* @throws Exception
*/
public List<String> getAvailableSignatureBlocks(PDFDocument document) throws Exception
{
PdfReader reader = new PdfReader(document.getDocumentBytes());
ArrayList arrayList = reader.getAcroFields().getBlankSignatureNames();
return Arrays.asList((String[]) arrayList.toArray(new String[arrayList.size()]));
}
/**
* This method is used to loop over the signature blocks and sign them
*
* @param document the document to be signed
* @param certificate the certificate to apply to the signature
* @param signature the image of the client signature
* @param signatureBlocks the signature blocks to create and sign
* @param certifyDocument flag to indicate if the document should be signed
* @throws Exception
*/
private PDFDocument signPDFDocumentSignatureBlocks(PDFDocument document, PKSCertificate certificate, SignatureImage signature, List<SigningBlock> signatureBlocks, boolean certifyDocument) throws Exception
{
for (int i = 0; i < signatureBlocks.size();i++)
{
PDFDocument signedDocument = new PDFDocument(new ByteArrayOutputStream());
PdfReader reader = new PdfReader(document.getDocumentBytes());
PdfStamper stamper = createPDFStamper(signedDocument, reader);
document = signPDFDocumentSignatureBlock(signedDocument, certificate, signature, signatureBlocks.get(i), stamper, certifyDocument && i == (signatureBlocks.size() - 1));
}
return document;
}
/**
* The method is used to sign the pdf document, it also marks the signing process if it is the final signature process
*
* @param signedDocument the signed document to be generated
* @param certificate the certificate to be used to do the signing
* @param signature the image of the signature used to sign the document
* @param signingBlock the current block to sign in the document
* @param stamper the current document stamper reference
* @param certifyDocument indicate if this signing should certify the document
* @return the signed document object
* @throws Exception
*/
private PDFDocument signPDFDocumentSignatureBlock(PDFDocument signedDocument, PKSCertificate certificate, SignatureImage signature, SigningBlock signingBlock, PdfStamper stamper, boolean certifyDocument) throws Exception
{
PdfSignatureAppearance appearance = signWithSignatureImage(stamper, signature, signingBlock.getName(), certifyDocument);
signWithCertificate(certificate, appearance);
signWithTimeStampingAuthority(appearance, certificate);
signedDocument.updateBytesFromByteStream();
return signedDocument;
}
/**
* The method is used to get the instance of the PDF stamper to stamp the document
*
* @param signedDocument the document that is currently being signed
* @param reader the reader that is reading the document to sign.
* @return the stamper instance
* @throws Exception
*/
private PdfStamper createPDFStamper(PDFDocument signedDocument, PdfReader reader) throws Exception
{
return PdfStamper.createSignature(reader, signedDocument.getByteStream(), '[=10=]', null, true);
}
/**
* The method is used to add the signature image to the signing block
*
* @param stamper the current pdf stamping reference
* @param signature the image to apply to the stamper
* @param signingBlockName the block to sign
* @param certifyDocument indicate if this signing should certify the document
* @throws Exception
*/
private PdfSignatureAppearance signWithSignatureImage(PdfStamper stamper, SignatureImage signature, String signingBlockName, boolean certifyDocument) throws Exception
{
PdfSignatureAppearance signatureAppearance = stamper.getSignatureAppearance();
signatureAppearance.setVisibleSignature(signingBlockName);
setImageForSignature(signatureAppearance, signature);
certifyDocumentSignature(signatureAppearance, certifyDocument);
return signatureAppearance;
}
/**
* The method is used to add an image to the signature block
*
* @param signatureAppearance the reference to the current document appearance
* @param signature the image to apply to the signature
* @throws Exception
*/
private void setImageForSignature(PdfSignatureAppearance signatureAppearance, SignatureImage signature) throws Exception
{
if(signature != null)
{
signatureAppearance.setImage(Image.getInstance(signature.getSignatureImage(), null));
}
}
/**
* The method is used to mark the signature as the certification signature
*
* @param signatureAppearance the reference to the current document appearance
* @param certifyDocument indicates if the document should be certified
*/
private void certifyDocumentSignature(PdfSignatureAppearance signatureAppearance, boolean certifyDocument)
{
if(certifyDocument)
{
signatureAppearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
}
}
/**
* The method is used to add the text containing information about the certificate to the signing appearance
*
* @param certificate the certificate to be used to do the signing
* @param signatureAppearance the appearance of the signature on the document
* @throws Exception
*/
private void signWithCertificate(PKSCertificate certificate, PdfSignatureAppearance signatureAppearance) throws Exception
{
signatureAppearance.setLayer2Text(buildTextSignature(certificate));
signatureAppearance.setLayer2Font(new Font(Font.COURIER, 9));
signatureAppearance.setAcro6Layers(true);
signatureAppearance.setRender(PdfSignatureAppearance.SignatureRenderDescription);
}
/**
* The method is used to encrypt the document using the certificate as well as a timestamping authority
*
* @param appearance the appearance of the signature on the document
* @param certificate the certificate to be used to do the signing
* @throws Exception
*/
private void signWithTimeStampingAuthority(PdfSignatureAppearance appearance, PKSCertificate certificate) throws Exception
{
_timestampingService.signWithTimestampingAuthority(appearance, certificate);
}
/**
* The method builds the text that is used in the text representation of the signature
*
* @param certificate the certificate to be used to do the signing
* @return the text representation of certificate information
* @throws Exception
*/
private String buildTextSignature(PKSCertificate certificate) throws Exception
{
String organization = certificate.getCertificateFieldByName("O");
String commonName = certificate.getCertificateFieldByName("CN");
String signDate = new SimpleDateFormat(_datetimeFormat).format(Calendar.getInstance().getTime());
String expirationDate = new SimpleDateFormat(_datetimeFormat).format(((X509Certificate)certificate.getCertificateChain()[0]).getNotAfter());
return "Digitally signed by " + organization + "\nSignee: " + commonName + "\nSign date: " + signDate + "\n" + "Expiration date: " + expirationDate;
}
the second signature invalidates the first one even though the signing is done in append mode, and only the last signature has the
PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED
certification level.
正如在最初的评论中已经推测的那样,这实际上是问题所在:只有文档的第一个签名可能是认证签名。以后的签名可能仅 (PDF 2.0) 使用签名字段锁定条目限制访问权限, cf.规格:
A PDF document may contain [...]
At most one certification signature (PDF 1.5). [...] The signature dictionary shall contain a signature reference dictionary (see Table 253) that has a DocMDP transform method. [...]
A document can contain only one signature field that contains a DocMDP transform method; it shall be the first signed field in the document.
(sections 12.8.1 and 12.8.2.2.1 of ISO 32000-1)
在即将发布的 PDF 标准 (ISO-32000-2) 2.0 版中,可以使用签名字段锁定条目来限制访问权限:
P number (Optional; PDF 2.0) The access permissions granted for this document. Valid values follow:
1 no changes to the document are permitted; any change to the document invalidates the signature.
2 permitted changes are filling in forms, instantiating page templates, and signing; other changes invalidate the signature.
3 permitted changes are the same as for 2, as well as annotation creation, deletion, and modification; other changes invalidate the signature.
There is no default value; absence of this key shall result in no effect on signature validation rules.
If MDP permission is already in effect from an earlier incremental save section or the original part of the document, the number shall specify permissions less than or equal to the permissions already in effect based on signatures earlier in the document. That is, permissions can be denied but not added. If the number specifies greater permissions than an MDP value already in effect, the new number is ignored.
If the document does not have an author signature, the initial permissions in effect are those based on the number 3.
The new permission applies to any incremental changes to the document following the signature of which this key is part.
(Table "Entries in a signature field lock dictionary" of a PDF 2.0 draft)
Adobe Reader/Acrobat 和 iText(自某些 5.3'ish 版本 IIRC 起)已支持此功能,并且似乎允许 OP 之后的效果,即不允许在最终签名字段后进行任何更改。