Adding LTV in signature makes pdf invalid using C#

2020-04-30 05:22发布

问题:

After Adding LTV to digital signature it shows document has changed.

After taking ref from this que : After LTV Certification Signature, PDF shows "Document has been Changed"

I made changes in my code, It works fine with all document but for this document : https://www.sendspace.com/file/3ulwn7 - It shows Invalid signature.

we are also using document signing service from global sign for same.

Below code for adding LTV :

public void AddLtv(string src, string dest, IOcspClient ocsp, ICrlClient crl, ITSAClient tsa)
{
    using (PdfReader r = new PdfReader(src))
    {
        using (FileStream fos =new FileStream(dest,FileMode.CreateNew))
        {
            PdfStamper stp = new PdfStamper(r, fos, '\0', true);
            LtvVerification v = stp.LtvVerification;
            AcroFields fields = stp.AcroFields;
            List<String> names = fields.GetSignatureNames();
            String sigName = names[names.Count - 1];
            PdfPKCS7 pkcs7 = fields.VerifySignature(sigName);
            if (pkcs7.IsTsp)
            {
                v.AddVerification(sigName, ocsp, crl,
                        LtvVerification.CertificateOption.SIGNING_CERTIFICATE,
                        LtvVerification.Level.OCSP_CRL,
                        LtvVerification.CertificateInclusion.NO);
            }
            else
            {
                foreach (var name in names)
                {
                    v.AddVerification(name, ocsp, crl,
                            LtvVerification.CertificateOption.WHOLE_CHAIN,
                            LtvVerification.Level.OCSP_CRL,
                            LtvVerification.CertificateInclusion.NO);
                }
            }
            stp.Close();
        }
    }
}

Edit: I think the way i operate pdf in code is causing issue in reading/writing pdf . Somehow i could not get any validator for pdf which can identify issue which @mkl told about cross pdf ref. Yet i am sharing my code as below if there is any problem in how i operate pdf. Help would be appreciated.

This pdf's old signature becomes invalid when I add new one. and if i add LTV then it is invalid for even single signature.

unsigned pdf URL: https://www.sendspace.com/file/n0ckem

Signed pdf without LTV URL : https://www.sendspace.com/file/t1gwp9

Signed pdf with LTV single sign: https://www.sendspace.com/file/ba8leq

Signed pdf with LTV two sign: https://www.sendspace.com/file/6b53z1

Below code for creating empty container and adding signature :

private async Task<string> SignPdf(string ocspResponse, string cert, string unsignedPdf, DocumentShapeModel annotations, string caCertraw, int pageHeight, UserProfileModel user)
{
    var trustedSignedpdf = Path.Combine(_env.WebRootPath, "TempPath", annotations.userId, annotations.Id.ToString(), "trustedSignedpdf.pdf");
    if (!Directory.Exists(Path.GetDirectoryName(trustedSignedpdf)))
    {
        Directory.CreateDirectory(trustedSignedpdf);
    }

    var tempPdf = Path.Combine(_env.WebRootPath, "TempPath", annotations.userId, annotations.Id.ToString());
    if (!Directory.Exists(tempPdf))
    {
        Directory.CreateDirectory(tempPdf);
    }
    tempPdf = Path.Combine(tempPdf, "tempSignedpdfglobal.pdf");
    string finalsignedPdf = trustedSignedpdf;
    var ocsp = new OcspClientBouncyCastle();
    byte[] oc2 = Convert.FromBase64String(oc1);
    OcspResp ocspResp = new OcspResp(oc2);
    BasicOcspResp basicResp = (BasicOcspResp)ocspResp.GetResponseObject();
    byte[] oc = basicResp.GetEncoded();

    bool check = false;
    string hexencodedDigest = null;
    PdfPKCS7 sgn = null;
    byte[] hash = null;
    Org.BouncyCastle.X509.X509Certificate[] chain = new Org.BouncyCastle.X509.X509Certificate[2];
    var cer = new Org.BouncyCastle.X509.X509CertificateParser()
        .ReadCertificate((new X509Certificate2(cert)).GetRawCertData());
    chain[0] = cer;
    var caCert = new Org.BouncyCastle.X509.X509CertificateParser()
        .ReadCertificate((new X509Certificate2(caCertraw)).GetRawCertData());
    chain[1] = caCert;
    while (!check)
    {
        PdfReader.unethicalreading = true;
        //create empty signature
        using (PdfReader reader = new PdfReader(unsignedPdf))
        {
            using (FileStream os = File.OpenWrite(tempPdf))
            {
                PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, os, '\0', null, true);
                PdfSignatureAppearance signatureAppearance = pdfStamper.SignatureAppearance;

                // Sets Signature Appearance

                signatureAppearance.Certificate = chain[0];
                signatureAppearance.CertificationLevel = PdfSignatureAppearance.NOT_CERTIFIED;
                signatureAppearance.Reason = "E Signed by " + user.FirstName + " " + user.LastName + " (" + user.Email + ").";

                signatureAppearance.Acro6Layers = false;
                signatureAppearance.Layer4Text = PdfSignatureAppearance.questionMark;

                float shapeH = annotations.IsResponsive == true ? annotations.h : ((annotations.h * 72 / 150) / (float)Convert.ToDouble(annotations.ratio)); 
                float shapeX = annotations.IsResponsive == true ? annotations.x : ((annotations.x * 72 / 150) / (float)Convert.ToDouble(annotations.ratio)); 
                float shapeY = annotations.IsResponsive == true ? annotations.y : ((annotations.y * 72 / 150) / (float)Convert.ToDouble(annotations.ratio)); 
                float shapeW = annotations.IsResponsive == true ? annotations.w : ((annotations.w * 72 / 150) / (float)Convert.ToDouble(annotations.ratio)); 

                double yaxis = (float)Convert.ToDouble(pageHeight) - (shapeH + shapeY);

                // Sets Layer2 text and acro6layers

                signatureAppearance.Layer2Text = " "; //Left blank so that it do not overwrite Esignature.
                signatureAppearance.SetVisibleSignature(new iTextSharp.text.Rectangle((int)(shapeX), (int)yaxis, (int)(shapeX) + (int)shapeW, (int)yaxis + (int)shapeH), annotations.p, annotations.Id.ToString());

                IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);

                MakeSignature.SignExternalContainer(signatureAppearance, external, 8192);

                Stream data = signatureAppearance.GetRangeStream();
                string hashAlgorithm = "SHA256";

                sgn = new PdfPKCS7(null, chain, hashAlgorithm, false);
                hash = DigestAlgorithms.Digest(data, hashAlgorithm);
                byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, oc, null, CryptoStandard.CADES);

                //create sha256 message digest
                using (SHA256.Create())
                {
                    sh = SHA256.Create().ComputeHash(sh);
                }

                //create hex encoded sha256 message digest
                hexencodedDigest = new BigInteger(1, sh).ToString(16);
                hexencodedDigest = hexencodedDigest.ToUpper();
                if (hexencodedDigest.Length == 64)
                {
                    check = true;
                }
            }
        }
    }

    var identityGetResult = await IdentityGet(_appConfiguration.TrustedSignSettings.Url + "/identity", AccessToken, IdentityJson, hexencodedDigest);

    //decode hex
    byte[] dsg = FromHex(identityGetResult);

    //include signature on PDF
    sgn.SetExternalDigest(dsg, null, "RSA");

    //create TimeStamp Client
    ITSAClient tsc = new DssClient(AccessToken, _env, _appConfiguration.TrustedSignSettings.Url);

    //byte[] ocspResponse = ocsp.GetEncoded(chain[0],chain[chain.Length -1], CertificateUtil.GetCRLURL(chain[0]));
    //Collection<byte[]> crlBytes = CertificateUtil.fetchCrlBytes(x509certificate, chain);

    byte[] encodedpkcs7 = sgn.GetEncodedPKCS7(hash, tsc, oc, null, CryptoStandard.CADES);
    //adds PKCS7 format Signature on empty signature container
    CreateSignature(tempPdf, finalsignedPdf, annotations.Id.ToString(), encodedpkcs7);

    var finaltrustedSignedpdf = Path.Combine(_env.WebRootPath, "TempPath", annotations.userId, annotations.Id.ToString());
    if (!Directory.Exists(finaltrustedSignedpdf))
    {
        Directory.CreateDirectory(finaltrustedSignedpdf);
    }
    finaltrustedSignedpdf = Path.Combine(finaltrustedSignedpdf, "FinaltrustedSignedpdf.pdf");

    //adds LTV to signed document
    AddLtv(finalsignedPdf, finaltrustedSignedpdf, ocsp, new CrlClientOnline(), tsc);
    return finaltrustedSignedpdf;
}

For creating signature

public void CreateSignature(string src, string dest, string fieldname, byte[] sig)
{
    using (PdfReader reader = new PdfReader(src))
    {
        using (FileStream os = File.OpenWrite(dest))
        {
            IExternalSignatureContainer external = new MyExternalSignatureContainer(sig);
            MakeSignature.SignDeferred(reader, fieldname, os, external);
        }
    }
}

回答1:

There is an error in the cross reference table of the original PDF. Adobe signature validation is known to be sensitive to such errors (see this answer and this answer), under certain circumstances it shows signatures of such files as invalid.

You should ask the source of that document to provide a version without that error

The details

The cross reference table of the first, unsigned revision of the document looks like this:

xref
0 55
0000000000 65535 f
0000000018 00000 n
0000000164 00000 n
0000000216 00000 n
0000000554 00000 n
0000003363 00000 n
0000003529 00000 n
0000003764 00000 n
0000003815 00000 n
0000003866 00000 n
0000004038 00000 n
0000004279 00000 n
0000004439 00000 n
0000004662 00000 n
0000004792 00000 n
0000004818 00000 n
0000004991 00000 n
0000005061 00000 n
0000005297 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000005466 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000006188 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000006236 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
0000000000 00000 n
54 18
0000006284 00000 n
0000006350 00000 n
0000006648 00000 n
0000171077 00000 n
0000171435 00000 n
0000171726 00000 n
0000171973 00000 n
0000323100 00000 n
0000323123 00000 n
0000324290 00000 n
0000324333 00000 n
0000324715 00000 n
0000326153 00000 n
0000328056 00000 n
0000328093 00000 n
0000328132 00000 n
0000328214 00000 n
0000328377 00000 n

As you see it consists of two subsections, the first one for 55 objects starting at 0, the second one of 18 objects starting at 54.

This is invalid for two reasons:

  • First of all (as already has been explained in the two answers referenced above) the cross reference table of the initial PDF revision must consist of a single section only!

    For a file that has never been incrementally updated, the cross-reference section shall contain only one subsection, whose object numbering begins at 0.

    (ISO 32000-1 and ISO 32000-2, in both cases section 7.5.4 "Cross-Reference Table")

  • Furthermore, that cross reference table has two entries for the same object, both the last entry of the first subsection and the first entry of the second subsection relate to object 54. This also is forbidden:

    A given object number shall not have an entry in more than one subsection within a single section.

    (ibidem)

Depending on details of the respective code this may or may not result in arbitrary problems when processing the PDF with some PDF processor, e.g. Adobe Acrobat Reader.

Your edit

In your edit you shared a number of files. In particular you shared

  • VeriFinger_SDK_Brochure_2017-12-27.pdf

    unsigned pdf URL: https://www.sendspace.com/file/n0ckem

  • VeriFinger_SDK_Brochure_signed_witoutltv.Pdf

    Signed pdf without LTV URL : https://www.sendspace.com/file/t1gwp9

  • FinaltrustedSignedpdf.pdf

    Signed pdf with LTV single sign: https://www.sendspace.com/file/ba8leq

  • FinaltrustedSignedpdf.pdf (same name but differing from the previous)

    Signed pdf with LTV two sign: https://www.sendspace.com/file/6b53z1

According to your code you apply all changes in append mode. Thus, the three latter files each have to consist of the first file VeriFinger_SDK_Brochure_2017-12-27.pdf plus some appended data. But that is not the case, the three latter files actually all are shorter than the first one. Thus, I have to assume that that first file is first processed somehow and then signed.

Now looking at the cross reference table of the "original file" VeriFinger_SDK_Brochure_2017-12-27.pdf (simply open it in a text viewer and scroll to its end) we see it is in one piece, just a single subsection. It contains a number of entries marked as free but no gaps.

Looking at the cross reference tables of the first revisions of the latter three files, though, we see that each of them is split into multiple subsections. Apparently runs of entries marked free have been cut out of the table resulting in a table with many subsections. Probably this has been designed as an optimization attempt but the result is a damaged PDF.

Thus, whatever PDF processor it is which you apply to your files before signing, exactly that processor damages the PDF.

The PDF processor that damages the PDF

Comparing the document information of your original file and the initial revisions in the other three files, a PDF processor handling the file before it is signed appears to be Aspose.PDF for .NET 19.1 because the Producer value is changed to that.

And indeed, this appears to be a known Aspose issue, see for example the PDF/A-1 conversion creates invalid XRef table thread on the Aspose free support forum, started in August 2016.

It has been filed as PDFNET-41272 and marked as fixed in Aspose.Pdf for .NET 17.2.0 in February 2017, but as reported on that very forum thread in the same month, it was not really fixed at all.

Apparently Aspose has not yet fixed this bug and still is working on it.