在现代浏览器中本地签署 PDF?

Sign PDF in a modern browser natively?

我想要实现的目标

到目前为止我做了什么

  1. 为了访问本地证书存储,我使用 FortifyApp
  2. Pdf 使用 iText(Sharp) 在服务器上预先签名,然后通过 Ajax 发送到客户端。 相关代码:
using (var fileStream = new MemoryStream())
                    {
                        using (var stamper = PdfStamper.CreateSignature(reader, fileStream, '0', null, true))
                        {
                            var signatureAppearance = stamper.SignatureAppearance;
                            signatureAppearance.SetVisibleSignature(new iTextSharp.text.Rectangle(15,15,15,15), 1, "A");
                            IExternalSignatureContainer external =
                                new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                            signatureAppearance.Reason = "AsdAsd";
                            signatureAppearance.Layer2Text = "Asd";

                            signatureAppearance.SignatureRenderingMode =
                                iTextSharp.text.pdf.PdfSignatureAppearance.RenderingMode.DESCRIPTION;

                            MakeSignature.SignExternalContainer(signatureAppearance, external, 512);

                            return fileStream.ToArray();
                        }
                    }
  1. 跟随this,成功操作pdf,提取byteRange,插入签名等相关代码:
let pdfBuffer = Buffer.from(new Uint8Array(pdf));

            const byteRangeString = `/ByteRange `;
            const byteRangePos = pdfBuffer.indexOf(byteRangeString);

            if (byteRangePos === -1)
                throw new Error('asd');

            let len = pdfBuffer.slice(byteRangePos).indexOf(`]`) + 1;

            // Calculate the actual ByteRange that needs to replace the placeholder.
            const byteRangeEnd = byteRangePos + len;
            const contentsTagPos = pdfBuffer.indexOf('/Contents ', byteRangeEnd);
            const placeholderPos = pdfBuffer.indexOf('<', contentsTagPos);
            const placeholderEnd = pdfBuffer.indexOf('>', placeholderPos);
            const placeholderLengthWithBrackets = placeholderEnd + 1 - placeholderPos;
            const placeholderLength = placeholderLengthWithBrackets - 2;
            const byteRange = [0, 0, 0, 0];
            byteRange[1] = placeholderPos;
            byteRange[2] = byteRange[1] + placeholderLengthWithBrackets;
            byteRange[3] = pdfBuffer.length - byteRange[2];
            let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`;
            actualByteRange += ' '.repeat(len - actualByteRange.length);

            // Replace the /ByteRange placeholder with the actual ByteRange
            pdfBuffer = Buffer.concat([pdfBuffer.slice(0, byteRangePos) as any, Buffer.from(actualByteRange), pdfBuffer.slice(byteRangeEnd)]);

            // Remove the placeholder signature
            pdfBuffer = Buffer.concat([pdfBuffer.slice(0, byteRange[1]) as any, pdfBuffer.slice(byteRange[2], byteRange[2] + byteRange[3])]);

//stringSignature comes from the signature creations below, and is 'hex' encoded
// Pad the signature with zeroes so the it is the same length as the placeholder
            stringSignature += Buffer
                .from(String.fromCharCode(0).repeat((placeholderLength / 2) - len))
                .toString('hex');

            // Place it in the document.
            pdfBuffer = Buffer.concat([
                pdfBuffer.slice(0, byteRange[1]) as any,
                Buffer.from(`<${stringSignature}>`),
                pdfBuffer.slice(byteRange[1])
            ]);

问题

p7.addCertificate(certificate); //certificate is the Certificate from Fortify CertificateStore.getItem(certId)
p7.addSigner({
                key: privateKey, //this is the CryptoKey from Fortify
                certificate: null/*certificate*/, //also tried certificate from Fortify 
                digestAlgorithm: forge.pki.oids.sha256,
                authenticatedAttributes: [
                    {
                        type: forge.pki.oids.contentType,
                        value: forge.pki.oids.data,
                    }, {
                        type: forge.pki.oids.messageDigest,
                        // value will be auto-populated at signing time
                    }, {
                        type: forge.pki.oids.signingTime,
                        // value can also be auto-populated at signing time
                        // We may also support passing this as an option to sign().
                        // Would be useful to match the creation time of the document for example.
                        value: new Date(),
                    },
                ],
            });

            // Sign in detached mode.
            p7.sign({detached: true});
let cmsSigned = new pki.SignedData({
                encapContentInfo: new pki.EncapsulatedContentInfo({
                    eContentType: "1.2.840.113549.1.7.1", // "data" content type
                    eContent: new asn.OctetString({ valueHex: pdfBuffer })
                }),
                signerInfos: [
                    new pki.SignerInfo({
                        sid: new pki.IssuerAndSerialNumber({
                            issuer: certificate.issuer,
                            serialNumber: certificate.serialNumber
                        })
                    })
                ],
                certificates: [certificate]
            });

            let signature = await cmsSigned.sign(privateKey, 0, 'SHA-256');
let signature = await provider.subtle.sign(alg, privateKey, new Uint8Array(pdfBuffer).buffer);

"works",因为它创建了一个无效的签名:

Error during signature verification.
ASN.1 parsing error:
Error encountered while BER decoding:

我尝试了多个证书,没有成功。

问题

  1. 我可以在不手动上传 p12/pfx 文件的情况下实现我的目标吗?
  2. 延迟签名的服务器端实现是否正确,我还需要其他东西吗?
  3. javascript 中的 pdf 操作是否正确?
  4. 我可以将原生 CrytpoKey 转换为 forge 或 pkijs 吗?
  5. 最后一个签名有什么问题?乍一看似乎是对的(至少是格式):

<>>>/ContactInfo()/M(D:20200619143454+02'00')/Filter/Adobe.PPKLite/SubFilter/adbe.pkcs7.detached/ByteRange [0 180165 181191 1492] /Contents <72eb2731c9de4a5ccc94f1e1f2d9b07be0c6eed8144cb73f3dfe2764595dcc8f58b8a55f5026618fd9c79146ea93afdafc00b617c6e70de553600e4520f290bef70c499ea91862bb3acc651b6a7b162c984987f05ec59db5b032af0127a1224cad82e3be38ae74dd110ef5f870f0a0a92a8fba295009f267508c372db680b3d89d3157d3b218f33e7bf30c500d599b977c956e6a6e4b02a0bbd4a86737378b421ae2af0a4a3c03584eaf076c1cdb56d372617da06729ef364605ecd98b6b32d3bb792b4541887b59b686b41db3fc32eb4c651060bb02e2babeb30e6545834b2935993f6ee9edcc8f99fee8ad6edd2958c780177df6071fdc75208f76bbbcc21a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000>>>

感谢: F

原回答:

所以我想通了。

Can I achieve my goal without having to manually upload a p12/pfx file, is it even possible?

是的,是的。 (请参阅下文了解需要更改的内容。)

Is the server-side implementation of the deferred signature correct, do I need something else?

是的,上面的代码没问题。

Is the pdf manipulation in javascript correct?

还可以。

Can I transform the native CrytpoKey to forge or pkijs?

是的,见下文。

What is wrong with the last signature?

@mkl 在评论中回答了,谢谢。

FortifyApp 现在有一个 CMS demo。虽然它不适用于我使用的版本,但它适用于 1.3.4 版本。

所以我选择了 pki.js 实现。签名成功所需的代码更改如下:

  1. 导出证书:
const cryptoCert = await provider.certStorage.getItem(selectedCertificateId);
const certRawData = await provider.certStorage.exportCert('raw', cryptoCert);

const pkiCert = new pki.Certificate({
    schema: asn.fromBER(certRawData).result,
});

return pkiCert;
  1. 以分离模式登录
let cmsSigned = new pki.SignedData({
    version: 1,
    encapContentInfo: new pki.EncapsulatedContentInfo({
        eContentType: '1.2.840.113549.1.7.1',
    }),
    signerInfos: [
        new pki.SignerInfo({
            version: 1,
            sid: new pki.IssuerAndSerialNumber({
                issuer: certificate.issuer,
                serialNumber: certificate.serialNumber
            })
        })
    ],
    certificates: [certificate]
});

let signature = await cmsSigned.sign(privateKey, 0, 'SHA-256', pdfBuffer);

const cms = new pki.ContentInfo({
    contentType: '1.2.840.113549.1.7.2',
    content: cmsSigned.toSchema(true),
});
const result = cms.toSchema().toBER(false);

return result;
  1. 将签名转换为 'HEX' 字符串
let stringSignature = Array.prototype.map.call(new Uint8Array(signature), x => (`00${x.toString(16)}`).slice(-2)).join('');
let len = signature.byteLength;

更新(js方面的总结):

  1. 下载预签名的 pdf(+ byteRange - 这可以用 iText 提取,因此您可以应用多个签名)

  2. 准备签名(见问题第3点第一部分)

  3. 获取私钥:

        const provider = await this.ws.getCrypto(selectedProviderId); // this.ws is a WebcryptoSocket

        provider.sign = provider.subtle.sign.bind(provider.subtle);

        setEngine(
            'newEngine',
            provider,
            new CryptoEngine({
                name: '',
                crypto: provider,
                subtle: provider.subtle,
            })
        );
        const key = await this.getCertificateKey('private', provider, selectedCertificateId); //can be null
        
  1. 见原答题1.和2.这两者之间我还有一个hack:
        let logout = await provider.logout();
        let loggedIn = await provider.isLoggedIn();
        if (!loggedIn) {
            let login = await provider.login();
        }
  1. 在 pdf 上添加签名。使用原始答案第3点,然后问题中第3点的第二部分。