将新页面添加到 PDF 并使用 iText 7 创建签名
Adding a new page to PDF and create signature with iText 7
对于一个项目,我必须在工作流程中由多人在额外创建的页面上对 PDF 进行数字签名。为实现这一点,我们根据 Bruno Lowagie 的示例使用带有以下代码的 iText 7 库:
public static void main(String[] args) throws IOException, GeneralSecurityException, XMPException {
String path = "F:/Java/keystores/testPdfSign";
char[] pass = "test".toCharArray();
KeyStore ks = KeyStore.getInstance("pkcs12", "SunJSSE");
ks.load(new FileInputStream(path), pass);
String alias = "";
Enumeration<String> aliases = ks.aliases();
while (alias.equals("tester")==false && aliases.hasMoreElements())
{
alias = aliases.nextElement();
}
PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
Certificate[] chain = ks.getCertificateChain(alias);
PDFSign app = new PDFSign();
app.sign(SRC, DEST, chain, pk, DigestAlgorithms.SHA1, "SunJSSE", PdfSigner.CryptoStandard.CMS, "Test", "Test", null, null, null, 0);
}
public void sign(String src, String dest,
Certificate[] chain, PrivateKey pk,
String digestAlgorithm, String provider, PdfSigner.CryptoStandard subfilter,
String reason, String location,
Collection<ICrlClient> crlList,
IOcspClient ocspClient,
ITSAClient tsaClient,
int estimatedSize)
throws GeneralSecurityException, IOException, XMPException {
// Creating the reader and the signer
PdfDocument document = new PdfDocument(new PdfReader(SRC), new PdfWriter(DEST+"_temp"));
if (initial == true)
{
document.addNewPage();
}
int pageCount = document.getNumberOfPages();
document.close();
PdfSigner signer = new PdfSigner(new PdfReader(DEST+"_temp"), new FileOutputStream(DEST), true);
// Creating the appearance
if (initial == true)
{
signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
}
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
.setReason(reason)
.setLocation(location)
.setReuseAppearance(false);
Rectangle rect = new Rectangle(10, 400, 100, 100);
appearance
.setPageRect(rect)
.setPageNumber(pageCount);
appearance.setRenderingMode(RenderingMode.NAME_AND_DESCRIPTION);
signer.setFieldName(signer.getNewSigFieldName());
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
ProviderDigest digest = new ProviderDigest(provider);
signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}
这会导致 PDF 的新签名版本中的签名无效,因为 Adobe Acrobat Reader 表示它在签名后已被编辑。令人惊讶的是,当我在 Foxit Reader 中打开文件时,它说它没有被修改并且是有效的。
另外我尝试的是省去添加新页面的第一步,只在原始文档的最后一页签名,然后签名在 Adobe 中有效 Reader,但没有解决方案我的情况,作为一个额外的页面是必须的。
我尝试的另一件事是没有将 certificationLevel 设置为 CERTIFIED_FORM_FILLING_AND_ANNOTATIONS
,而是将其保留为默认值 NOT_CERTIFIED
,这样我在新页面上也有一个有效的签名,但这也不是解决方案, 因为它不会让我稍后添加任何额外的签名。
有人知道 Adobe Reader 将签名评为无效的原因是什么 and/or 有解决此问题的方法吗?
提前致谢
大卫
简而言之
我无法重现 OP 的问题。 运行 他的代码(根据当地情况略作调整)产生了 java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE
。另一方面,将 sign
调用的提供者参数“SunJSSE”替换为“BC”后,代码会创建经过适当认证的 PDF。
适配代码
我通常以 JUnit 测试的形式检查来自 Whosebug 的代码;这意味着一些变化。此外,OP 的代码包含许多被引用但未定义的变量;必须给这些定义。最后我加载文件以从资源作为流签名,而不是从文件系统作为文件。
因此:
final static File RESULT_FOLDER = new File("target/test-outputs", "signature");
@BeforeClass
public static void setUpBeforeClass() throws Exception
{
RESULT_FOLDER.mkdirs();
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
}
@Test
public void testSignLikeXinDHA() throws GeneralSecurityException, IOException, XMPException
{
String path = "keystores/demo-rsa2048.p12";
char[] pass = "demo-rsa2048".toCharArray();
KeyStore ks = KeyStore.getInstance("pkcs12", "SunJSSE");
ks.load(new FileInputStream(path), pass);
String alias = "";
Enumeration<String> aliases = ks.aliases();
while (alias.equals("demo") == false && aliases.hasMoreElements())
{
alias = aliases.nextElement();
}
PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
Certificate[] chain = ks.getCertificateChain(alias);
try ( InputStream resource = getClass().getResourceAsStream("/mkl/testarea/itext7/content/test.pdf"))
{
sign(resource, new File(RESULT_FOLDER, "test_XinDHA_signed_initial.pdf").getAbsolutePath(),
chain, pk, DigestAlgorithms.SHA1, /*"SunJSSE"*/"BC", PdfSigner.CryptoStandard.CMS, "Test", "Test",
null, null, null, 0, true);
}
}
public void sign(InputStream src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm,
String provider, PdfSigner.CryptoStandard subfilter, String reason, String location,
Collection<ICrlClient> crlList, IOcspClient ocspClient, ITSAClient tsaClient, int estimatedSize,
boolean initial)
throws GeneralSecurityException, IOException, XMPException
{
// Creating the reader and the signer
PdfDocument document = new PdfDocument(new PdfReader(src), new PdfWriter(dest + "_temp"));
if (initial == true)
{
document.addNewPage();
}
int pageCount = document.getNumberOfPages();
document.close();
PdfSigner signer = new PdfSigner(new PdfReader(dest + "_temp"), new FileOutputStream(dest), true);
// Creating the appearance
if (initial == true)
{
signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
}
PdfSignatureAppearance appearance = signer.getSignatureAppearance().setReason(reason).setLocation(location)
.setReuseAppearance(false);
Rectangle rect = new Rectangle(10, 400, 100, 100);
appearance.setPageRect(rect).setPageNumber(pageCount);
appearance.setRenderingMode(RenderingMode.NAME_AND_DESCRIPTION);
signer.setFieldName(signer.getNewSigFieldName());
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
ProviderDigest digest = new ProviderDigest(provider);
signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}
运行代码
我 运行 代码使用相当新的 Oracle Java 8 具有无限强度 JavaTM 加密扩展策略文件,BouncyCastle 1.49和 iText 版本 7.0.0 或 7.0.1-SNAPSHOT(当前开发 b运行ch)。
(最终使用从其网站下载的 Oracle Java,Oracle JDK 的某些变体(由某些 Linux 发行版提供)包含安全提供程序的更改可以破解你的密码。)
运行 将提供程序参数“SunJSSE”用于 sign
调用的代码导致
java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE
at sun.security.jca.GetInstance.getService(GetInstance.java:87)
at sun.security.jca.GetInstance.getInstance(GetInstance.java:206)
at java.security.Security.getImpl(Security.java:698)
at java.security.MessageDigest.getInstance(MessageDigest.java:227)
at com.itextpdf.signatures.SignUtils.getMessageDigest(SignUtils.java:134)
at com.itextpdf.signatures.DigestAlgorithms.getMessageDigest(DigestAlgorithms.java:182)
at com.itextpdf.signatures.ProviderDigest.getMessageDigest(ProviderDigest.java:69)
at com.itextpdf.signatures.SignUtils.getMessageDigest(SignUtils.java:127)
at com.itextpdf.signatures.PdfSigner.signDetached(PdfSigner.java:528)
at mkl.testarea.itext7.signature.AddPageAndSign.sign(AddPageAndSign.java:125)
at mkl.testarea.itext7.signature.AddPageAndSign.testSignLikeXinDHA(AddPageAndSign.java:81)
运行 使用提供者参数“BC”调用 sign
的代码会生成经过适当认证的 PDF,并在额外的页面上显示签名:
为什么使用 SunJSSE 没有意义
我在“SunJSSE”提供程序中遇到的异常实际上并不奇怪,因为该提供程序不提供 SHA1 算法。
根据its documentation by Oracle,它根本不提供 MessageDigest 算法本身,只是作为签名算法 (SHA1withRSA) 的组合。
因此,在sign
中定义的IExternalSignature
为
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
会起作用,因为这里将使用 SHA1withRSA,但 ProviderDigest
在那里定义为
ProviderDigest digest = new ProviderDigest(provider);
将失败,因为它尝试使用消息摘要算法 SHA1。
顺便说一句
您使用 SHA1。由于此消息摘要算法在签名创建的上下文中越来越不可信,所以这不是一个好主意。我建议切换到 SHA2 算法。
对于一个项目,我必须在工作流程中由多人在额外创建的页面上对 PDF 进行数字签名。为实现这一点,我们根据 Bruno Lowagie 的示例使用带有以下代码的 iText 7 库:
public static void main(String[] args) throws IOException, GeneralSecurityException, XMPException {
String path = "F:/Java/keystores/testPdfSign";
char[] pass = "test".toCharArray();
KeyStore ks = KeyStore.getInstance("pkcs12", "SunJSSE");
ks.load(new FileInputStream(path), pass);
String alias = "";
Enumeration<String> aliases = ks.aliases();
while (alias.equals("tester")==false && aliases.hasMoreElements())
{
alias = aliases.nextElement();
}
PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
Certificate[] chain = ks.getCertificateChain(alias);
PDFSign app = new PDFSign();
app.sign(SRC, DEST, chain, pk, DigestAlgorithms.SHA1, "SunJSSE", PdfSigner.CryptoStandard.CMS, "Test", "Test", null, null, null, 0);
}
public void sign(String src, String dest,
Certificate[] chain, PrivateKey pk,
String digestAlgorithm, String provider, PdfSigner.CryptoStandard subfilter,
String reason, String location,
Collection<ICrlClient> crlList,
IOcspClient ocspClient,
ITSAClient tsaClient,
int estimatedSize)
throws GeneralSecurityException, IOException, XMPException {
// Creating the reader and the signer
PdfDocument document = new PdfDocument(new PdfReader(SRC), new PdfWriter(DEST+"_temp"));
if (initial == true)
{
document.addNewPage();
}
int pageCount = document.getNumberOfPages();
document.close();
PdfSigner signer = new PdfSigner(new PdfReader(DEST+"_temp"), new FileOutputStream(DEST), true);
// Creating the appearance
if (initial == true)
{
signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
}
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
.setReason(reason)
.setLocation(location)
.setReuseAppearance(false);
Rectangle rect = new Rectangle(10, 400, 100, 100);
appearance
.setPageRect(rect)
.setPageNumber(pageCount);
appearance.setRenderingMode(RenderingMode.NAME_AND_DESCRIPTION);
signer.setFieldName(signer.getNewSigFieldName());
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
ProviderDigest digest = new ProviderDigest(provider);
signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}
这会导致 PDF 的新签名版本中的签名无效,因为 Adobe Acrobat Reader 表示它在签名后已被编辑。令人惊讶的是,当我在 Foxit Reader 中打开文件时,它说它没有被修改并且是有效的。
另外我尝试的是省去添加新页面的第一步,只在原始文档的最后一页签名,然后签名在 Adobe 中有效 Reader,但没有解决方案我的情况,作为一个额外的页面是必须的。
我尝试的另一件事是没有将 certificationLevel 设置为 CERTIFIED_FORM_FILLING_AND_ANNOTATIONS
,而是将其保留为默认值 NOT_CERTIFIED
,这样我在新页面上也有一个有效的签名,但这也不是解决方案, 因为它不会让我稍后添加任何额外的签名。
有人知道 Adobe Reader 将签名评为无效的原因是什么 and/or 有解决此问题的方法吗?
提前致谢
大卫
简而言之
我无法重现 OP 的问题。 运行 他的代码(根据当地情况略作调整)产生了 java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE
。另一方面,将 sign
调用的提供者参数“SunJSSE”替换为“BC”后,代码会创建经过适当认证的 PDF。
适配代码
我通常以 JUnit 测试的形式检查来自 Whosebug 的代码;这意味着一些变化。此外,OP 的代码包含许多被引用但未定义的变量;必须给这些定义。最后我加载文件以从资源作为流签名,而不是从文件系统作为文件。
因此:
final static File RESULT_FOLDER = new File("target/test-outputs", "signature");
@BeforeClass
public static void setUpBeforeClass() throws Exception
{
RESULT_FOLDER.mkdirs();
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
}
@Test
public void testSignLikeXinDHA() throws GeneralSecurityException, IOException, XMPException
{
String path = "keystores/demo-rsa2048.p12";
char[] pass = "demo-rsa2048".toCharArray();
KeyStore ks = KeyStore.getInstance("pkcs12", "SunJSSE");
ks.load(new FileInputStream(path), pass);
String alias = "";
Enumeration<String> aliases = ks.aliases();
while (alias.equals("demo") == false && aliases.hasMoreElements())
{
alias = aliases.nextElement();
}
PrivateKey pk = (PrivateKey) ks.getKey(alias, pass);
Certificate[] chain = ks.getCertificateChain(alias);
try ( InputStream resource = getClass().getResourceAsStream("/mkl/testarea/itext7/content/test.pdf"))
{
sign(resource, new File(RESULT_FOLDER, "test_XinDHA_signed_initial.pdf").getAbsolutePath(),
chain, pk, DigestAlgorithms.SHA1, /*"SunJSSE"*/"BC", PdfSigner.CryptoStandard.CMS, "Test", "Test",
null, null, null, 0, true);
}
}
public void sign(InputStream src, String dest, Certificate[] chain, PrivateKey pk, String digestAlgorithm,
String provider, PdfSigner.CryptoStandard subfilter, String reason, String location,
Collection<ICrlClient> crlList, IOcspClient ocspClient, ITSAClient tsaClient, int estimatedSize,
boolean initial)
throws GeneralSecurityException, IOException, XMPException
{
// Creating the reader and the signer
PdfDocument document = new PdfDocument(new PdfReader(src), new PdfWriter(dest + "_temp"));
if (initial == true)
{
document.addNewPage();
}
int pageCount = document.getNumberOfPages();
document.close();
PdfSigner signer = new PdfSigner(new PdfReader(dest + "_temp"), new FileOutputStream(dest), true);
// Creating the appearance
if (initial == true)
{
signer.setCertificationLevel(PdfSigner.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS);
}
PdfSignatureAppearance appearance = signer.getSignatureAppearance().setReason(reason).setLocation(location)
.setReuseAppearance(false);
Rectangle rect = new Rectangle(10, 400, 100, 100);
appearance.setPageRect(rect).setPageNumber(pageCount);
appearance.setRenderingMode(RenderingMode.NAME_AND_DESCRIPTION);
signer.setFieldName(signer.getNewSigFieldName());
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
ProviderDigest digest = new ProviderDigest(provider);
signer.signDetached(digest, pks, chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);
}
运行代码
我 运行 代码使用相当新的 Oracle Java 8 具有无限强度 JavaTM 加密扩展策略文件,BouncyCastle 1.49和 iText 版本 7.0.0 或 7.0.1-SNAPSHOT(当前开发 b运行ch)。
(最终使用从其网站下载的 Oracle Java,Oracle JDK 的某些变体(由某些 Linux 发行版提供)包含安全提供程序的更改可以破解你的密码。)
运行 将提供程序参数“SunJSSE”用于 sign
调用的代码导致
java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE
at sun.security.jca.GetInstance.getService(GetInstance.java:87)
at sun.security.jca.GetInstance.getInstance(GetInstance.java:206)
at java.security.Security.getImpl(Security.java:698)
at java.security.MessageDigest.getInstance(MessageDigest.java:227)
at com.itextpdf.signatures.SignUtils.getMessageDigest(SignUtils.java:134)
at com.itextpdf.signatures.DigestAlgorithms.getMessageDigest(DigestAlgorithms.java:182)
at com.itextpdf.signatures.ProviderDigest.getMessageDigest(ProviderDigest.java:69)
at com.itextpdf.signatures.SignUtils.getMessageDigest(SignUtils.java:127)
at com.itextpdf.signatures.PdfSigner.signDetached(PdfSigner.java:528)
at mkl.testarea.itext7.signature.AddPageAndSign.sign(AddPageAndSign.java:125)
at mkl.testarea.itext7.signature.AddPageAndSign.testSignLikeXinDHA(AddPageAndSign.java:81)
运行 使用提供者参数“BC”调用 sign
的代码会生成经过适当认证的 PDF,并在额外的页面上显示签名:
为什么使用 SunJSSE 没有意义
我在“SunJSSE”提供程序中遇到的异常实际上并不奇怪,因为该提供程序不提供 SHA1 算法。
根据its documentation by Oracle,它根本不提供 MessageDigest 算法本身,只是作为签名算法 (SHA1withRSA) 的组合。
因此,在sign
中定义的IExternalSignature
为
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
会起作用,因为这里将使用 SHA1withRSA,但 ProviderDigest
在那里定义为
ProviderDigest digest = new ProviderDigest(provider);
将失败,因为它尝试使用消息摘要算法 SHA1。
顺便说一句
您使用 SHA1。由于此消息摘要算法在签名创建的上下文中越来越不可信,所以这不是一个好主意。我建议切换到 SHA2 算法。