ferikeem
ferikeem

Reputation: 537

Sign PDF in a modern browser natively?

What I'm trying to achieve

What I did so far

  1. For accessing the local cert store I use FortifyApp.
  2. Pdf is pre-signed on the server using iText(Sharp), then sent to the client via Ajax. Relevant code:
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. Following this, I managed to manipulate the pdf, extract byteRange, insert signature, etc. Relevant code:
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])]);

and

//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])
            ]);

The problem

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", because it creates an invalid signature:

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

I tried multiple certificates, no luck.

Questions

  1. Can I achieve my goal without having to manually upload a p12/pfx file, is it even possible?
  2. Is the server-side implementation of the deferred signature correct, do I need something else?
  3. Is the pdf manipulation in javascript correct?
  4. Can I transform the native CrytpoKey to forge or pkijs?
  5. What is wrong with the last signature? At first glance it seems right (at least the format):

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

Thanks: F

Upvotes: 2

Views: 3063

Answers (1)

ferikeem
ferikeem

Reputation: 537

Original answer:

So I figured it out.

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

Yes, it is. (See below on what needs to be changed.)

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

Yes, the code above is fine.

Is the pdf manipulation in javascript correct?

Also fine.

Can I transform the native CrytpoKey to forge or pkijs?

Yes, see below.

What is wrong with the last signature?

@mkl answered it in a comment, thank you.

FortifyApp has a CMS demo now. Although it didn't work with the version I was using, it works with version 1.3.4.

So I went with the pki.js implementation. The code changes need for the signing to be successful are the following:

  1. Export the certificate:
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. Sign in detached mode
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. Convert signature to 'HEX' string
let stringSignature = Array.prototype.map.call(new Uint8Array(signature), x => (`00${x.toString(16)}`).slice(-2)).join('');
let len = signature.byteLength;

Update (summary on the js side of things):

  1. Download the pre-signed pdf (+ byteRange - this can be extracted with iText, so you can apply multiple signatures)

  2. Prepare the signature (see first part of point 3. in the question)

  3. Get private key:

        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. See Original answer points 1. and 2. Between theese I also have a hack:
        let logout = await provider.logout();
        let loggedIn = await provider.isLoggedIn();
        if (!loggedIn) {
            let login = await provider.login();
        }
  1. Add the signature on the pdf. Use original answer point 3., then the second part of point 3 in the question.

Upvotes: 2

Related Questions