【问题标题】:multiple signing pdf iText多重签名pdf iText
【发布时间】:2020-06-17 16:58:54
【问题描述】:

我尝试多次使用 itext 5.5.13.1 模拟不同用户签名的文档,PdfStamper 处于 AppendMode。如果文档没有签名,则认证级别为 CERTIFIED_NO_CHANGES_ALLOWED 或 CERTIFIED_FORM_FILLING_AND_ANNOTATIONS,否则我不会为 PdfSignatureAppearence 设置此参数。第二次签名后第一次签名无效,因为文件被修改了。任何想法如何解决这一问题? 这是我的代码:

        public void Sign(string Thumbprint, string document, string logoPath) {

        X509Certificate2 certificate = FindCertificate(Thumbprint);

        PdfReader reader = new PdfReader(document);

        //Append mode
        PdfStamper st = PdfStamper.CreateSignature(reader, new FileStream(SignedDocumentPath(document), FileMode.Create, FileAccess.Write), '\0', null, true);


        int signatureWidth = 250;
        int signatureHeight = 100;       
        int NewXPos = 0;
        int NewYPos = 0;

        SetStampCoordinates(reader, st, ref NewXPos, ref NewYPos, signatureWidth, signatureHeight);            

        PdfSignatureAppearance sap = st.SignatureAppearance;

        if (reader.AcroFields.GetSignatureNames().Count == 0)
        {
            SetSignatureFieldOptions(certificate, sap, reader, "1", 1, NewXPos, NewYPos, signatureWidth, signatureHeight);
        }
        else {
            SetSignatureFieldOptions(certificate, sap, reader, "2", NewXPos, NewYPos, signatureWidth, signatureHeight);
        }

        Image image = Image.GetInstance(logoPath);
        image.ScaleAbsolute(50, 50);

        Font font1 = SetFont("TIMES.TTF", BaseColor.BLUE, 10, 0);
        Font font2 = SetFont("TIMES.TTF", BaseColor.BLUE, 8, 0);

        PdfTemplate layer = sap.GetLayer(2);
        Chunk chunk1 = new Chunk($"\r\nДОКУМЕНТ ПОДПИСАН\r\nЭЛЕКТРОННОЙ ПОДПИСЬЮ\r\n", font1);
        Chunk chunk2 = new Chunk($"Сертификат {certificate.Thumbprint}\r\n" +
                             $"Владелец {certificate.GetNameInfo(X509NameType.SimpleName, false)}\r\n" +
                             $"Действителен с {Convert.ToDateTime(certificate.GetEffectiveDateString()).Date.ToShortDateString()} " +
                             $"по {Convert.ToDateTime(certificate.GetExpirationDateString()).Date.ToShortDateString()}\r\n", font2);

        PdfTemplate layer0 = sap.GetLayer(0);
        image.SetAbsolutePosition(5, 50);
        layer0.AddImage(image);

        Paragraph para1 = SetParagraphOptions(chunk1, 1, 50, 0, 2, 1.1f);
        Paragraph para2 = SetParagraphOptions(chunk2, 0, 5, 15, 0.5f, 1.1f);

        ColumnText ct = new ColumnText(layer);
        ct.SetSimpleColumn(3f, 3f, layer.BoundingBox.Width - 3f, layer.BoundingBox.Height);
        ct.AddElement(para1);
        ct.AddElement(para2);
        ct.Go();

        layer.SetLineWidth(3);
        layer.SetRGBColorStroke(0, 0, 255);
        layer.Rectangle(0, 0, layer.BoundingBox.Right, layer.BoundingBox.Top);
        layer.Stroke();

        EncryptDocument(certificate, sap);
    }

    public X509Certificate2 FindCertificate(string Thumbprint) {
        X509Store store = new X509Store("My", StoreLocation.CurrentUser);
        store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
        X509Certificate2Collection found = store.Certificates.Find(
            X509FindType.FindByThumbprint, Thumbprint, true);

        X509Certificate2 certificate = found[0];

        if (certificate.PrivateKey is Gost3410_2012_256CryptoServiceProvider cert_key)
        {
            var cspParameters = new CspParameters
            {
                KeyContainerName = cert_key.CspKeyContainerInfo.KeyContainerName,
                ProviderType = cert_key.CspKeyContainerInfo.ProviderType,
                ProviderName = cert_key.CspKeyContainerInfo.ProviderName,
                Flags = cert_key.CspKeyContainerInfo.MachineKeyStore
               ? (CspProviderFlags.UseExistingKey | CspProviderFlags.UseMachineKeyStore)
               : (CspProviderFlags.UseExistingKey),
                KeyPassword = new SecureString()
            };

            certificate = new X509Certificate2(certificate.RawData)
            {
                PrivateKey = new Gost3410_2012_256CryptoServiceProvider(cspParameters)
            };
        }

        return certificate;
    }

    public Font SetFont(string TTFFontName, BaseColor color, float size, int style) {
        string ttf = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), TTFFontName);
        BaseFont baseFont = BaseFont.CreateFont(ttf, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

        Font font = new Font(baseFont, size, style);
        font.Color = color;

        return font;
    }

    public Paragraph SetParagraphOptions(Chunk chunk, int ParagraphAligment, float marginLeft, float marginTop, float fixedLeading, float multipledLeading) {
        Paragraph paragraph = new Paragraph();
        paragraph.Alignment = ParagraphAligment;
        paragraph.IndentationLeft = marginLeft;
        paragraph.SpacingBefore = marginTop;
        paragraph.SetLeading(fixedLeading, multipledLeading);
        paragraph.Add(chunk);

        return paragraph;
    }

    public string SignedDocumentPath(string document) {
        string filename = String.Concat(Path.GetFileNameWithoutExtension(document), "_signed.pdf");
        string path = Path.Combine(Path.GetDirectoryName(document), filename);

        return path;
    }

    public void SetSignatureFieldOptions(X509Certificate2 certificate, PdfSignatureAppearance sap, PdfReader reader, string field, int level, int XPos, int YPos, int width, int height)
    {
        X509CertificateParser parser = new X509CertificateParser();

        try
        {
            Rectangle rectangle = new Rectangle(XPos, YPos, XPos + width, YPos + height);
            sap.SetVisibleSignature(rectangle, reader.NumberOfPages, field);
            sap.Certificate = parser.ReadCertificate(certificate.RawData);
            sap.Reason = "I agree";
            sap.Location = "Location";
            sap.Acro6Layers = true;
            sap.SignDate = DateTime.Now;
            sap.CertificationLevel = level;
        }

        catch (Exception ex)
        {
            if (ex.Message == $"The field {certificate.Thumbprint} already exists.")
                throw new Exception("Вы уже подписали данный документ");
        }
    }

    public void SetSignatureFieldOptions(X509Certificate2 certificate, PdfSignatureAppearance sap, PdfReader reader,string field, int XPos, int YPos, int width, int height) {
        X509CertificateParser parser = new X509CertificateParser();

        try
        {
            Rectangle rectangle = new Rectangle(XPos, YPos, XPos + width, YPos + height);
            sap.SetVisibleSignature(rectangle, reader.NumberOfPages, field);
            sap.Certificate = parser.ReadCertificate(certificate.RawData);
            sap.Reason = "I agree";
            sap.Location = "Location";
            sap.Acro6Layers = true;
            sap.SignDate = DateTime.Now;
        }

        catch (Exception ex) {
            if (ex.Message == $"The field {certificate.Thumbprint} already exists.")
                throw new Exception("Вы уже подписали данный документ");
        }
    }

    public void EncryptDocument(X509Certificate2 certificate, PdfSignatureAppearance sap) {

        PdfName filterName;
        if (certificate.PrivateKey is Gost3410CryptoServiceProvider)
            filterName = new PdfName("CryptoPro#20PDF");
        else
            filterName = PdfName.ADOBE_PPKLITE;

        PdfSignature dic = new PdfSignature(filterName, PdfName.ADBE_PKCS7_DETACHED);
        dic.Date = new PdfDate(sap.SignDate);
        dic.Name = certificate.GetNameInfo(X509NameType.SimpleName, false);
        if (sap.Reason != null)
            dic.Reason = sap.Reason;
        if (sap.Location != null)
            dic.Location = sap.Location;
        sap.CryptoDictionary = dic;

        int intCSize = 4000;
        Dictionary<PdfName, int> hashtable = new Dictionary<PdfName, int>();
        hashtable[PdfName.CONTENTS] = intCSize * 2 + 2;
        sap.PreClose(hashtable);
        Stream s = sap.GetRangeStream();
        MemoryStream ss = new MemoryStream();
        int read = 0;
        byte[] buff = new byte[8192];
        while ((read = s.Read(buff, 0, 8192)) > 0)
        {
            ss.Write(buff, 0, read);
        }

        ContentInfo contentInfo = new ContentInfo(ss.ToArray());
        SignedCms signedCms = new SignedCms(contentInfo, true);
        CmsSigner cmsSigner = new CmsSigner(certificate);
        signedCms.ComputeSignature(cmsSigner, false);
        byte[] pk = signedCms.Encode();

        byte[] outc = new byte[intCSize];
        PdfDictionary dic2 = new PdfDictionary();
        Array.Copy(pk, 0, outc, 0, pk.Length);
        dic2.Put(PdfName.CONTENTS, new PdfString(outc).SetHexWriting(true));
        sap.Close(dic2);
    }

【问题讨论】:

  • 首先,如果你设置了“CERTIFIED_NO_CHANGES_ALLOWED”,你不应该对第二个签名会破坏第一个签名感到惊讶:如果不允许更改,那么特别是不允许第二个签名。甚至“CERTIFIED_FORM_FILLING_AND_ANNOTATIONS”也不允许创建新的签名字段,而只能签署现有的空白字段。有关允许和不允许更改已签名 PDF 的详细信息,请阅读 this answer
  • 我检查了您的链接并选择未认证。当我用它签署文件 2 次时,第一个签名仍然被破坏。我应该在签名之前创建 2 个签名字段,还是其他一些选项?。
  • 请分享一份您签署过两次的 pdf 文件来说明问题。通常分析 pdf 会更快地找出问题所在。

标签: itext


【解决方案1】:

变化

截图中最重要的部分

是签名面板上签名之间的文本“1 Page(s) Modified”。这告诉我们除了添加和填充签名字段之外,您还可以进行其他更改。检查文件本身可以快速识别更改:

  • 在原sample.pdf中

    只有一个内容流。

  • 在带有一个签名的 sample_signed.pdf 中

    共有三个内容流,原始内容流被新内容流包围。

  • 在有两个签名的 sample_signed_signed.pdf 中

    有五个内容流,前三个被两个新的包围。

因此,在每个签名过程中,您都会更改页面内容。正如您在this answer 中看到的那样,始终不允许更改已签名文档的页面内容。添加的流的内容是微不足道的,这甚至没有帮助,前面添加的每个流都包含:

q

最后添加的每个流都包含

Q
q
Q

即只发生了一些保存和恢复图形状态。

原因

上述更改是由PdfStamper 方法GetOverContent 完成的典型准备步骤,将原始内容包装在q ... Q(保存和恢复图形状态)信封中,以防止那里的更改影响@987654335 中的添加@ 并开始一个新的块也被这样一个信封所包围。后一个块保持为空,表明OverContent 未被编辑。

我在您发布的代码中没有找到这样的调用,但在您的代码中缺少方法 SetStampCoordinates。您可能会为该方法中的PdfStamper 参数调用GetOverContent 吗?

【讨论】:

  • 先生,您完全正确,非常感谢。在构建代码时犯了这个错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-13
  • 2015-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-06
相关资源
最近更新 更多