Search code examples
javascriptpdfdigital-signaturepades

Adobe displays PKCS7 parsing error when I make a PAdES LT signature using my code


I'm working on the webapp which makes PAdES signatures. I have successfully implemented PAdES Baseline-B. However, when I'm trying to create PAdES Baseline-LT by adding all the necessary information described here, including OCSP responses, CRLs and Certificates, it seems like the file gets corrupted and Adobe displays the following error: Error during signature verification: PKCS7 parsing error

Here is the signed PDF if you want to have a look: https://easyupload.io/fxkzvs

I append DSS after signing the PDF, so those additional objects that I append to get LT subtype doesn't influence the signature itself, so I'm not sure why do I get PKCS7 error, if the same signature that I make (when creating Baseline-B) seems fine.

Here's the part of the code where those additional data are created and inserted:

public appendVri(pdfRaw, pdfToSign, vri: VRI) {
    if (pdfRaw instanceof ArrayBuffer) {
        pdfRaw = new Uint8Array(pdfRaw);
    }

    const pdf = this.loadPdf(pdfRaw);
    const root = this.findRootEntry(pdf.xref);
    const rootSuccessor = this.findSuccessorEntry(pdf.xref.entries, root);

    const certsEntry = [];

    const xObjects = [];
    let offsetDss;
    let offsetVri;
    // let offsetCerts[];
    // let offsetOcsp[];
    // let offsetCrls[];

    const dummy = this.findFreeXrefNr(pdf.xref.entries, xObjects);
    xObjects.push(dummy);
    const dssEntry = this.findFreeXrefNr(pdf.xref.entries, xObjects);
    xObjects.push(dssEntry);
    const vriEntry = this.findFreeXrefNr(pdf.xref.entries, xObjects);
    xObjects.push(vriEntry);

    for (let i = 0; i < vri.certs.length; i++) {
        certsEntry[i] = this.findFreeXrefNr(pdf.xref.entries, xObjects);
        xObjects.push(certsEntry[i]);
    }

    const ocspEntry = [];

    for (let i = 0; i < vri.OCSPs.length; i++) {
        ocspEntry[i] = this.findFreeXrefNr(pdf.xref.entries, xObjects);
        xObjects.push(ocspEntry[i]);
    }

    const crlsEntry = [];

    for (let i = 0; i < vri.CRLs.length; i++) {
        crlsEntry[i] = this.findFreeXrefNr(pdf.xref.entries, xObjects);
        xObjects.push(crlsEntry[i]);
    }

    let certsReference = '/Certs [';
    for (let i = 0; i < vri.certs.length; i++) {
        certsReference = certsReference + certsEntry[i] + ' 0 R';

        if (i === vri.certs.length - 1) {
            certsReference = certsReference + '] \n';
        } else {
            certsReference = certsReference + ' ';
        }
    }

    let ocspReference = '/OCSPs [';
    for (let i = 0; i < vri.OCSPs.length; i++) {
        ocspReference = ocspReference + ocspEntry[i] + ' 0 R';

        if (i === vri.OCSPs.length - 1) {
            ocspReference = ocspReference + '] \n';
        } else {
            ocspReference = ocspReference + ' ';
        }
    }

    let crlsReference = '/CRLs [';
    for (let i = 0; i < vri.CRLs.length; i++) {
        crlsReference = crlsReference + crlsEntry[i] + ' 0 R';

        if (i === vri.CRLs.length - 1) {
            crlsReference = crlsReference + '] \n';
        } else {
            crlsReference = crlsReference + ' ';
        }
    }

    const offsets = [];

    const appendDss = '\n' + pdfToSign.dssEntry + ' 0 obj\n' +
        '<< \n' +
        '/VRI ' + vriEntry + ' 0 R \n' +
        certsReference +
        ocspReference +
        crlsReference +
        '>>';
    offsetDss = (pdf.stream.bytes.length);
    offsets.push(offsetDss);
    let array = this.insertIntoArray(pdf.stream.bytes, offsetDss, appendDss);

    const sigHash = this.strHex(this.digest(forge.util.decode64(pdfToSign.sig), 'SHA1')).toUpperCase();

    const appendVri = '\n' + vriEntry + ' 0 obj\n' +
        '<< \n' +
        '/' + sigHash + ' << \n' +
        certsReference +
        ocspReference +
        crlsReference +
        '>> \n' +
        '>>\n';
    offsetVri = offsetDss + appendDss.length;
    offsets.push(offsetVri);
    array = this.insertIntoArray(array, offsetVri, appendVri);

    let lastOffset = offsetVri + appendVri.length;
    const appendCerts = [];
    const appendOcsp = [];
    const appendCrls = [];

    let offsetDelta = 0;

    appendCerts[-1] = '';
    for (let i = 0; i < vri.certs.length; i++) {
        appendCerts[i] = certsEntry[i] + ' 0 obj\n' +
            '<< \n' +
            '/Length ' + vri.certs[i].length + ' \n' +
            '>>\n' +
            'stream\n' +
            atob(vri.certs[i]) + '\n' +
            'endstream\n' +
            'endobj\n';
        const currentOffset = lastOffset + appendCerts[i - 1].length;
        offsets.push(currentOffset);
        array = this.insertIntoArray(array, currentOffset, appendCerts[i]);
        lastOffset = currentOffset;
        offsetDelta += currentOffset;
    }

    lastOffset = lastOffset + appendCerts[appendCerts.length - 1].length;

    appendOcsp[-1] = '';
    for (let i = 0; i < vri.OCSPs.length; i++) {
        appendOcsp[i] = ocspEntry[i] + ' 0 obj\n' +
            '<< \n' +
            '/Length ' + vri.OCSPs[i].length + ' \n' +
            '>>\n' +
            'stream\n' +
            atob(vri.OCSPs[i]) + '\n' +
            'endstream\n' +
            'endobj\n';
        const currentOffset = lastOffset + appendOcsp[i - 1].length;
        offsets.push(currentOffset);
        array = this.insertIntoArray(array, currentOffset, appendOcsp[i]);
        lastOffset = currentOffset;
        offsetDelta += currentOffset;
    }

    lastOffset = lastOffset + appendOcsp[appendOcsp.length - 1].length;

    appendCrls[-1] = '';
    for (let i = 0; i < vri.CRLs.length; i++) {
        appendCrls[i] = crlsEntry[i] + ' 0 obj\n' +
            '<< \n' +
            '/Length ' + vri.CRLs[i].length + ' \n' +
            '>>\n' +
            'stream\n' +
            atob(vri.CRLs[i]) + '\n' +
            'endstream\n' +
            'endobj\n';
        const currentOffset = lastOffset + appendCrls[i - 1].length;
        offsets.push(currentOffset);
        array = this.insertIntoArray(array, currentOffset, appendCrls[i]);
        lastOffset = currentOffset;
        offsetDelta += currentOffset;
    }

    offsetDelta += appendDss.length + appendVri.length;

    let middle = '';
    offsets.forEach(offset => {
        offset = offset + '';
        middle += offset.padStart(10, '0') + ' ' + '00000' + ' ' + ' n' + '\n';
    });

    let xref = '\nxref\n' +
        pdfToSign.dssEntry + ' ' + (2 + vri.certs.length + vri.CRLs.length + vri.OCSPs.length) + '\n' +
    middle;

    const sha256Hex = sha256(array, false);

    xref += this.createTrailer(pdf.xref.topDict, array.length, sha256Hex, pdf.xref.entries.length);
    array = this.insertIntoArray(array, array.length, xref);

    return array;
}

EDIT: I fixed everything as @mkl has suggested. Adobe isn't throwing this error anymore, however my signature is still seen as Baseline-T instead of Baseline-LTA which can be checked here: https://ec.europa.eu/cefdigital/DSS/webapp-demo/validation

Here's the new version of the singed pdf: https://easyupload.io/i5bs9k


Solution

  • There are multiple issues in the PDF, both in the originally signed PDF and your additions.

    The Originally Signed PDF

    You imply that before extending your signed PDF using appendVri validates just fine. I cannot reproduce this.

    I extracted that originally signed PDF from your shared PDF by truncating to 67127 bytes, and already for that file I get the Error during signature verification. PKCS7 parsing error: Incorrect version. Thus, this issue already is in your PDF before extension.

    The actual problem also becomes clear in the error message: Incorrect version. Let's look at the start of a ASN.1 dump of the embedded CMS container:

     0 15733: SEQUENCE {
     4     9: . OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
            : . . (PKCS #7)
    15 15718: . [0] {
    19 15714: . . SEQUENCE {
    23     1: . . . INTEGER 5
    ...
    

    That INTEGER 5 is the CMSVersion here, and that is the problem.

    You create a signature with a SubFilter value of ETSI.CAdES.detached, i.e. a (non-legacy) PAdES signature.

    According to the current PAdES standard (ETSI EN 319 142-1 v1.1.1) this implies that the embedded signature container is a DER-encoded SignedData object as specified in CAdES (ETSI EN 319 142-1 section 4.1). CAdES in turn requires The CMSVersion shall be set to either 1 or 3 (ETSI EN 319 122-1 V1.1.1 section 4.4).

    Thus, the INTEGER 5 as CMSVersion in your signature is incorrect, the value has to be 1 or 3 (depending on other details of the signature).

    When I patched the signature container in your original PDF to claim a version of 3 instead of 5, Adobe Acrobat immediately was pacified concerning the PKCS7 parsing.

    Interestingly, the value 5 is correct according to the plain CMS standard (RFC 5652) as the crls collection in that signature container has an entry with a type of other, an OCSP response. Merely in the context of CAdES (and, consequentially, of PAdES) that value must be decreased.

    In the context of PAdES this actually is understandable, here that crls collection is not used after all. Plain CAdES on the other hand requires to add OCSP responses there, so I'm not sure about the rationale of limiting versions to 1 and 3. Probably one simply didn't want that version to have to switch back and forth when the CRLs and/or OCSP responses in crls are updated/organized/...

    The PDF Post-Processed by appendVri

    The appendVri introduced the following extra errors:

    • Your cross reference table entries are incorrect, they looked like this:

      0000067127 00000  n\n
      

      But they need to be like this:

      0000067127 00000 n \n
      

      I.e. between the 00000 generation number and the n literal there must be exactly one space. As an entry must be 20 bytes long and you use single-byte eol markers, the extra space must go after the n literal.

    • In your trailer you simply copied the original size entry:

      /Size 18
      

      But you added objects with object numbers up to 28, so the size entry must be

      /Size 29
      
    • In your trailer you don't link to the previous, original cross reference table. But for an incremental update you have to do so. Thus, you need to add a

      /Prev 66604
      

      to your trailer.

    With these changes in place, Adobe Reader does not complain about structural errors anymore.

    Miscellanea

    When preparing the PDF for signing, you appear to have added a DSS entry in its Catalog pointing to an object not yet defined in the prepared PDF:

    1 0 obj
    <</AcroForm<</Fields[11 0 R] /SigFlags 3>>
    /DSS 19 0 R
    /Type /Catalog
    /Outlines 2 0 R
    /Pages 3 0 R
    >>
    ...
    trailer <<
      /Size 18
      /Root 1 0 R
      /Info 10 0 R
      /ID [<1f7703d1f61b41d20c76b866132baa8b><6a44acaeb3052d4c807f6782f2eed88c>]
    >>
    

    Then in your method appendVri you created an object 19 with the DSS in it to be referred to by that reference originally pointing nowhere.

    While probably not invalid, this is a bit questionable. In particular in the aftermath of the publication of the PDF-Insecurity Shadow Attacks, leaving such dangling references as part of the preparations for signing might be considered suspicious.

    Also if some other PDF processor eventually is processing your signed (but not extended) PDF, it may use object 19 for something else, and the result is a PDF with an invalid digital security store.

    The Inserted OCSP Responses

    In a comment you say

    my signature is still validated as Baseline-T and not LT, even though I fixed all the issues you found

    Indeed, in the previous sections I only inspected the structural integrity of the CMS container and the PDF, I did not check your precise validation related information.

    In your updated, corrected PDF example, therefore, I took a look at the revocation data you add to the document security store, and here indeed an issue became visible: As OCSP responses you embedded only the BasicOCSPResponse objects, not the full OCSPResponse objects which wrap basic OCSP response objects.

    The PAdES specification, though, requires you to embed the full OCSP response objects

    OCSPs Array (Optional) An array of indirect references to streams, each containing a DER-encoded Online Certificate Status Protocol (OCSP) response (that shall be as defined in IETF RFC 6960 [5]).

    (ETSI EN 319 142-1 V1.1.1 section 5.4.2.2)

    Thus, please use the full OCSP responses instead of only the inner, basic OCSP responses. If you cannot access them anymore, you can re-build them from the basic responses by wrapping them according to specification:

    OCSPResponse ::= SEQUENCE {
       responseStatus         OCSPResponseStatus,
       responseBytes          [0] EXPLICIT ResponseBytes OPTIONAL }
    
    OCSPResponseStatus ::= ENUMERATED {
       successful            (0),  -- Response has valid confirmations
       malformedRequest      (1),  -- Illegal confirmation request
       internalError         (2),  -- Internal error in issuer
       tryLater              (3),  -- Try again later
                                   -- (4) is not used
       sigRequired           (5),  -- Must sign the request
       unauthorized          (6)   -- Request unauthorized
    }
    
    ResponseBytes ::= SEQUENCE {
       responseType   OBJECT IDENTIFIER,
       response       OCTET STRING }
    

    For a basic OCSP responder, responseType will be id-pkix-ocsp-basic.

    id-pkix-ocsp           OBJECT IDENTIFIER ::= { id-ad-ocsp }
    id-pkix-ocsp-basic     OBJECT IDENTIFIER ::= { id-pkix-ocsp 1 }
    

    (RFC 6960 section 4.2.1)

    Simply assume a successful status.