【问题标题】:Adding a new page to PDF and create signature with iText 7向 PDF 添加新页面并使用 iText 7 创建签名
【发布时间】:2016-09-02 15:42:24
【问题描述】:

对于一个项目,我必须在工作流程中由多人在另外创建的页面上对 PDF 进行数字签名。为了实现这一点,我们使用 iText 7 库和以下代码,基于 Bruno Lowagie 的示例:

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 的新签名版本中的签名无效,因为 Adob​​e Acrobat Reader 表示签名后已对其进行了编辑。令人惊讶的是,当我在 Foxit Reader 中打开文件时,它说它没有被修改并且是有效的。

另外我尝试的是省略添加新页面的第一步,只在原始文档的最后一页签名,然后签名在 Adob​​e Reader 中有效,但我的情况没有解决方案,作为额外的页面是必须的。 我尝试的另一件事是没有将certificateLevel设置为CERTIFIED_FORM_FILLING_AND_ANNOTATIONS,而是将其保留为默认NOT_CERTIFIED,这样我在新页面上也有一个有效的签名,但这也不是一个解决方案,因为它赢了以后不要让我添加任何其他签名。

有人知道 Adob​​e Reader 将签名评为无效的原因和/或有解决此问题的方法吗?

提前致谢

大卫

【问题讨论】:

  • 您能出示您的原始 pdf 文件吗?你的描述听起来好像已经签名了。
  • 我尝试了不同的 pdf,都没有签名,所以这不应该是问题
  • 我只是试图重现您的问题,但结果是 java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE。确实,您对提供程序 SunJSSE 的使用几乎没有意义。
  • 我将您的代码更改为使用“BC”而不是“SunJSSE”,并预先注册了 BouncyCastle 提供程序。现在运行它会产生一个正确的签名,“文档自认证以来没有被修改过”。
  • 首先感谢您的帮助!不幸的是,将提供程序更改为 BouncyCastle 对我来说会导致同样的问题,你能让我看看你的代码吗?你也能解释一下为什么使用SunJSSE 没有意义吗?

标签: java pdf adobe signature itext7


【解决方案1】:

总之

我无法重现 OP 的问题。运行他的代码(稍微适应当地情况)导致java.security.NoSuchAlgorithmException: no such algorithm: SHA1 for provider SunJSSE。另一方面,将 sign 调用中的提供程序参数“SunJSSE”替换为“BC”后,代码会创建一个经过适当认证的 PDF。

改编代码

我通常以 JUnit 测试的形式检查来自 stackoverflow 的代码;这意味着一些变化。此外,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);
}

(AddPageAndSign.java)

运行代码

我使用最近的 Oracle Java 8 和 Unlimited Strength JavaTM Cryptography Extension Policy Files、BouncyCastle 1.49 和 iText 7.0.0 或 7.0.1-SNAPSHOT(当前开发分支)。

(绝对使用从其网站下载的 Oracle Java,Oracle JDK 的某些变体(由某些 Linux 发行版提供)包含可能破坏您的代码的安全提供程序的更改。)

使用 sign 调用的提供程序参数“SunJSSE”运行代码会导致

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)

使用 sign 调用的提供者参数“BC”运行代码会生成经过适当认证的 PDF,并在额外页面上显示签名可视化:

为什么使用 SunJSSE 没有意义

“SunJSSE”提供程序的异常实际上并不令人惊讶,因为该提供程序不提供 SHA1 算法。

根据its documentation by Oracle,它根本没有提供MessageDigest算法本身,只是作为签名算法(SHA1withRSA)的组合。

因此,IExternalSignaturesign 中定义为

IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);

会起作用,因为这里将使用 SHA1withRSA,但 ProviderDigest 在那里定义为

ProviderDigest digest = new ProviderDigest(provider);

将失败,因为它尝试使用消息摘要算法 SHA1。

顺便说一句

您使用 SHA1。由于此消息摘要算法在签名创建的上下文中越来越不可信,这不是一个好主意。我建议切换到 SHA2 家族的算法。

【讨论】:

  • 非常感谢您对我的问题的详细解答以及关于 SunJSSE 的解释。我现在正在使用 BouncyCastle,但仅此一项并不能解决问题。我还需要稍微调整的是 PdfDocument 的创建,现在使用设置为 appendMode 的 StampingProperties:StampingProperties properties = new StampingProperties(); properties.useAppendMode(); PdfDocument document = new PdfDocument(new PdfReader(SRC), new PdfWriter(DEST+"_temp"), properties);
  • 但我真的不明白为什么需要这样做,因为 PdfDocument 仅用于添加页面,然后写入文件,然后我们只签署生成的文件,所以 PdfSigner 对象不知道原始文件。
  • 它可能包含使用权限签名吗?除非您的原始文档已经签名,否则所描述的行为没有意义。并且使用真正未签名的源 PDF 我无法重现您的问题。
  • 不,我尝试了不同的 PDF 文件,结果都是一样的。我知道这种行为没有意义,但这让它对我有用。
猜你喜欢
  • 2017-04-13
  • 2022-10-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-03
  • 2020-03-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多