Sign multiple location with same response xml signature in PKCS7 (CMS)

后端 未结 1 436
执念已碎
执念已碎 2021-01-14 10:28

PDF document needs to signed with national digital identity.
National digital identity WebService provide facility to sign document, in my project I have integrated same

相关标签:
1条回答
  • 2021-01-14 11:21

    A first quick look through your code revealed two major errors.

    Hashing twice

    You hash the document data twice (using different APIs for that... weird!):

            Stream data = appearance.GetRangeStream();
    
            byte[] hash = DigestAlgorithms.Digest(data, "SHA256");
    
            [...]
    
            _signatureHash = hash;// signatureHash;
        }
    }
    
    [...]
    using (SHA256.Create())
    {
        _signatureHash = SHA256.Create().ComputeHash(_signatureHash);
    }
    

    This is wrong, this makes no sense.

    Injecting the wrong signature container

    You say

    Requesting Esign services give response in PKCS7(CMS) format.

    But instead of using the CMS signature container from the result as such, you try to build an own CMS container, injecting the Esign response CMS container as if it was a mere signed hash:

    XmlNodeList UserX509Certificate = xmlDoc.GetElementsByTagName("UserX509Certificate");
    byte[] rawdat = Convert.FromBase64String(UserX509Certificate[0].InnerText);
    var chain = new List<Org.BouncyCastle.X509.X509Certificate>
    {
        Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(new X509Certificate2(rawdat))
    };
    var signaturee = new PdfPKCS7(null, chain, "SHA256", false);
    _signature = signaturee;
    
    _signature.SetExternalDigest(Convert.FromBase64String(signature), null, "RSA");
    
    byte[] encodedSignature = _signature.GetEncodedPKCS7(_hash, null, null, null, CryptoStandard.CMS);
    

    According to your comments in the XML

        <DocSignature error="" id="1" sigHashAlgorithm="SHA256">--Signature in base 64 in PKCS7(CMS)---</DocSignature>
    

    this DocSignature element contains the CMS signature container.

    Thus, remove the code segment above and instead put the content of the DocSignature element (don't forget to base64 decode) into the byte[] encodedSignature. Now you can inject it into the prepared signature as before:

    IExternalSignatureContainer external = new MyExternalSignatureContainer(encodedSignature);
    
    MakeSignature.SignDeferred(reader, "sign1", os, external);
    

    After you fixed the issues above, two more became apparent:

    Using the wrong file mode

    You open the stream to write to like this:

    using (FileStream os = System.IO.File.OpenWrite(signedPdf))
    

    File.OpenWrite is documented on docs.microsoft.com to be

    equivalent to the FileStream(String, FileMode, FileAccess, FileShare) constructor overload with file mode set to OpenOrCreate, the access set to Write, and the share mode set to None.

    The file mode OpenOrCreate in turn is documented to specify

    that the operating system should open a file if it exists; otherwise, a new file should be created.

    Thus, if there already is a file at the given location, that file remains and you start writing into it.

    If the new file you create is longer than the old one, this is no problem, you eventually overwrite all the old file content and then the file grows to house the additional new content.

    But if the new file you create is shorter than the old one, you have a problem: After the end of the new file there still is data from the old, longer file. Thus, your result is a hodgepodge of two files.

    This happened in case of the example files you shared, your new content of "signedPdf.pdf" is only 175982 bytes long but there appears to have been some older file with that name which was 811986 bytes long. Thus, the "signedPdf.pdf" file you shared is 811986 bytes long, the first 175982 bytes containing the result of your operation, the rest data from some other file.

    If you cut down your shared "signedPdf.pdf" file to its first 175982 bytes, the result looks much better!

    To solve this issue you should use the file mode Create which is documented to be

    equivalent to requesting that if the file does not exist, use CreateNew; otherwise, use Truncate.

    using (FileStream os = new FileStream(signedPdf, FileMode.Create, FileAccess.Write, FileShare.None))
    

    An issue with your signing service - identity not yet valid

    As mentioned above, if you cut down your shared "signedPdf.pdf" file to its first 175982 bytes, the result looks much better! Unfortunately merely better, not yet good:

    The reason for your "identity has expired or is not yet valid" becomes clearer by looking at the details:

    I.e. the signing time claimed by the PDF is 09:47:59 UTC+1.

    But looking at the certificate:

    I.e. your certificate is valid not before 09:48:40 UTC+1.

    Thus, the claimed signing time is more than half a minute before your user certificate became valid! This obviously cannot be accepted by a validator...

    Apparently your signing service creates a short-time certificate for you on demand, valid from just then for half an hour. And the time at which you started creating the PDF signature is not in that interval.

    I doubt they will change the design of the signing service for your requirements. Thus, you'll have to cheat a bit and use a signing time slightly in the future.

    By default the signing time is set to the current by the PdfSignatureAppearance constructor, i.e. when this line executes:

    PdfSignatureAppearance appearance = stamper.SignatureAppearance;
    

    Fortunately you can change this claimed signing time if you immediately use

    appearance.SignDate = [some other date time];
    

    The date time you should use here has to be shortly (I'd propose not more than 5 minutes) after the time you will call your signing service.

    This of course implies that you cannot arbitrarily wait until executing that service call. As soon as you assigned the claimed signing time above, you are committed to have successfully called your signing service shortly before that claimed time!

    Furthermore, if that signing service turns out to react only slowly or only after some retries, your software should definitively check the certificate in the signature container you retrieve from it and compare its validity interval with your claimed signing time. If the claimed signing time is not in that interval, start signing again!


    Now it became apparent that the AllPagesSignatureContainer you used was designed for a very special use case and still had to be adapted to your use case.

    Adapting the AllPagesSignatureContainer for append mode

    The AllPagesSignatureContainer implementation essentially copied from this answer worked fine when not signing in append mode but when signing in append mode it failed.

    This at first was plausible because that class has to predict the object number that will be used for the signature value. This prediction depends on the exact use case, and switching on append mode changes this use case considerably. Thus, my advice in a comment was

    If you need append mode, try to replace the

    PdfLiteral PRefLiteral = ...
    

    line in the AllPagesSignatureContainer by

    PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages) + " 0 R");
    

    In my tests that worked but in your tests it still didn't. An analysis of your signed file turned up the cause: My test file was using cross reference tables while yours was using cross reference streams.

    Adapting the AllPagesSignatureContainer for append mode and object streams

    iText in append mode uses the compression features of the original file, i.e. in case of your file it creates an object stream as soon as storing an indirect object that allows storage in an object stream.

    In case of your file iText reserved an object number for the object stream, and it did so between the time the AllPagesSignatureContainer predicted the signature value object number and the time the signature value actually was generated. Thus, in your file the actual signature value object number was higher than the predicted number by 1.

    To solve this for PDFs with cross reference streams, therefore, one can simply replace the PdfLiteral PRefLiteral = ... line by

    PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages + 1) + " 0 R");
    

    i.e. by adding 1 to the originally predicted value. Unfortunately now the prediction is wrong for PDFs with cross reference tables...

    A better way to fix this is to force iText to reserve an object number for the object stream for cross reference stream PDFs before predicting the signature value object number and then use the original prediction code. One way to do this is by creating and writing an indirect object right before the prediction, e.g. like this:

    stamper.Writer.AddToBody(new PdfNull(), stamper.Writer.PdfIndirectReference, true);
    
    PdfIndirectReference PRef = stamper.Writer.PdfIndirectReference;
    PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages) + " 0 R");
    

    The answer the AllPagesSignatureContainer implementation essentially was copied from has been updated accordingly.

    0 讨论(0)
提交回复
热议问题