当我使用我的代码制作 PAdES LT 签名时,Adobe 显示 PKCS7 解析错误

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

我正在开发制作 PAdES 签名的网络应用程序。我已成功实施 PAdES Baseline-B。但是,当我尝试通过添加 here 中描述的所有必要信息(包括 OCSP 响应、CRL 和证书)来创建 PAdES Baseline-LT 时,文件似乎已损坏并且 Adob​​e 显示以下错误:签名验证时出错:PKCS7解析错误

如果你想看一下,这里是签名的PDF:https://easyupload.io/fxkzvs

我在签署 PDF 后追加 DSS,所以我追加以获得 LT 子类型的那些额外对象不会影响签名本身,所以我不确定为什么我会收到 PKCS7 错误,如果相同的签名我使(创建基线-B 时)看起来不错。

这是创建和插入这些附加数据的代码部分:

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;
}

编辑: 我按照@mkl 的建议修复了所有问题。 Adobe 不再抛出此错误,但我的签名仍被视为 Baseline-T 而不是 Baseline-LTA,可在此处查看:https://ec.europa.eu/cefdigital/DSS/webapp-demo/validation

这是 singed pdf 的新版本:https://easyupload.io/i5bs9k

原始签名的 PDF 和您添加的 PDF 中存在多个问题。

原始签名 PDF

您暗示在使用 appendVri 验证扩展您的签名 PDF 之前就可以了。我无法复制这个。

我通过截断为 67127 字节从您共享的 PDF 中提取了最初签名的 PDF,并且对于该文件,我在签名验证期间遇到了 错误。 PKCS7 解析错误:版本不正确。因此,这个问题在扩展之前已经存在于您的 PDF 中。

实际问题在错误消息中也变得清晰:版本不正确。让我们看一下嵌入式 CMS 容器的 ASN.1 转储的开始:

 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
...

这里的INTEGER 5就是CMSVersion,就是问题所在

您创建了一个 SubFilter 值为 ETSI.CAdES.detached 的签名,即 (non-legacy) PAdES签名。

根据当前的 PAdES 标准 (ETSI EN 319 142-1 v1.1.1),这意味着嵌入式签名容器是一个 DER-encoded SignedData 对象,如 CAdES(ETSI EN 319 142-1 第 4.1 节)。 CAdES 反过来要求 CMSVersion 应设置为 1 或 3(ETSI EN 319 122-1 V1.1.1 第 4.4 节)。

因此,您签名中的 INTEGER 5 作为 CMSVersion 是不正确的,该值必须是 13 (取决于签名的其他细节) .

当我修补原始 PDF 中的签名容器以声明 3 版本而不是 5 时,Adobe Acrobat 立即对 PKCS7 解析[=143] 进行了安抚=].

有趣的是,根据普通 CMS 标准 (RFC 5652),值 5 是正确的,因为该签名容器中的 crls 集合有一个other 类型的条目,一个 OCSP 响应。仅仅在 CAdES 的背景下(以及因此在 PAdES 的背景下),该值必须被降低。

在PAdES的上下文中这个其实是可以理解的,这里毕竟没有用到crls集合。另一方面,普通 CAdES 需要在那里添加 OCSP 响应,因此我不确定将版本限制为 13 的基本原理。当 crls 中的 CRL and/or OCSP 响应为 updated/organized/...[=152 时,可能只是不想让该版本来回切换=]

PDF Post-Processed appendVri

appendVri 引入了以下额外错误:

  • 您的交叉引用 table 条目不正确,它们看起来像这样:

    0000067127 00000  n\n
    

    但他们需要像这样:

    0000067127 00000 n \n
    

    即在 00000 代号和 n 文字之间必须恰好有一个 space。由于一个条目必须是 20 个字节长并且您使用 single-byte eol 标记,额外的 space 必须在 之后 n 文字。

  • 在您的预告片中,您只是复制了原始尺寸条目:

    /Size 18
    

    但是您添加了对象编号最多为 28 的对象,因此大小条目必须是

    /Size 29
    
  • 在您的预告片中,您没有 link 之前的原始交叉引用 table。但是对于增量更新,您必须这样做。因此,您需要添加一个

    /Prev 66604
    

    到你的预告片。

有了这些更改,Adobe Reader 就不会再抱怨结构错误了。

杂项

准备 PDF 进行签名时,您似乎在其 目录 中添加了一个 DSS 条目,指向一个尚未定义的对象在准备好的 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>]
>>

然后在您的方法 appendVri 中,您创建了一个对象 19,其中包含 DSS,该引用最初指向任何地方。

虽然可能不是无效的,但这有点值得怀疑。尤其是在 PDF-Insecurity Shadow Attacks 发布之后,将此类悬而未决的引用作为签名准备工作的一部分可能会被认为是可疑的。

此外,如果某些其他 PDF 处理器最终正在处理您签名(但未扩展)的 PDF,它可能会将对象 19 用于其他用途,结果是带有无效数字安全存储的 PDF。

插入的 OCSP 响应

在你说的评论中

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

的确,在前面的部分我只检查了CMS容器和PDF的结构完整性,我没有检查你的精确验证相关信息。

因此,在您更新、更正的 PDF 示例中,我查看了您添加到文档安全存储中的撤销数据,这里确实出现了一个问题:作为 OCSP 响应,您仅嵌入了 BasicOCSPResponse 对象,而不是包装基本 OCSP 响应对象的完整 OCSPResponse 对象。

不过,PAdES 规范要求您嵌入完整的 OCSP 响应对象

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 第 5.4.2.2 节)

因此,请使用完整的 OCSP 响应,而不是仅使用内部的基本 OCSP 响应。如果您无法再访问它们,您可以 re-build 通过根据规范包装它们来从基本响应中提取它们:

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)

简单地假设一个 successful 状态。