Skip to content

Commit

Permalink
Merge 7e7a48d into e802f3a
Browse files Browse the repository at this point in the history
  • Loading branch information
vbuch committed Sep 29, 2018
2 parents e802f3a + 7e7a48d commit 18229de
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 16 deletions.
Binary file added bundle.p12
Binary file not shown.
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -51,6 +51,10 @@
{
"name": "alekssakovsky",
"url": "https://github.com/alekssakovsky"
},
{
"name": "samaleksov",
"url": "https://github.com/samaleksov"
}
],
"dependencies": {
Expand Down
67 changes: 51 additions & 16 deletions src/signpdf.js
Expand Up @@ -3,8 +3,6 @@ import SignPdfError from './SignPdfError';

export {default as SignPdfError} from './SignPdfError';

const PKCS12_CERT_BAG = '1.2.840.113549.1.12.10.1.3';
const PKCS12_KEY_BAG = '1.2.840.113549.1.12.10.1.2';
export const DEFAULT_BYTE_RANGE_PLACEHOLDER = '**********';

function pad2(num) {
Expand Down Expand Up @@ -57,6 +55,7 @@ export class SignPdf {
pdf = pdf.slice(0, pdf.length - 1);
}

// Find the ByteRange placeholder.
const byteRangePlaceholder = [
0,
`/${this.byteRangePlaceholder}`,
Expand All @@ -71,6 +70,8 @@ export class SignPdf {
SignPdfError.TYPE_PARSE,
);
}

// Calculate the actual ByteRange that needs to replace the placeholder.
const byteRangeEnd = byteRangePos + byteRangeString.length;
const contentsTagPos = pdf.indexOf('/Contents ', byteRangeEnd);
const placeholderPos = pdf.indexOf('<', contentsTagPos);
Expand All @@ -97,28 +98,55 @@ export class SignPdf {
pdf.slice(byteRange[2], byteRange[2] + byteRange[3]),
]);

// Convert Buffer P12 to a forge implementation.
const forgeCert = forge.util.createBuffer(p12Buffer.toString('binary'));
const p12Asn1 = forge.asn1.fromDer(forgeCert);
const p12 = forge.pkcs12.pkcs12FromAsn1(
p12Asn1,
options.asn1StrictParsing,
options.passphrase,
);
// get bags by type
const certBags = p12.getBags({bagType: PKCS12_CERT_BAG})[PKCS12_CERT_BAG];
const keyBags = p12.getBags({bagType: PKCS12_KEY_BAG})[PKCS12_KEY_BAG];

// Extract safe bags by type.
// We will need all the certificates and the private key.
const certBags = p12.getBags({
bagType: forge.pki.oids.certBag,
})[forge.pki.oids.certBag];
const keyBags = p12.getBags({
bagType: forge.pki.oids.pkcs8ShroudedKeyBag,
})[forge.pki.oids.pkcs8ShroudedKeyBag];

const privateKey = keyBags[0].key;
// Here comes the actual PKCS#7 signing.
const p7 = forge.pkcs7.createSignedData();
// Start off by setting the content.
p7.content = forge.util.createBuffer(pdf.toString('binary'));
let last = certBags[0];

// Then add all the certificates (-cacerts & -clcerts)
// Keep track of the last found client certificate.
// This will be the public key that will be bundled in the signature.
// Note: This first line may still result in setting a CA cert in
// the lastClientCertificate. Keeping it this way for backwards comp.
// Will get rid of it once this lib gets to version 0.3.
let certificate = certBags[0];

Object.keys(certBags).forEach((i) => {
const {publicKey} = certBags[i].cert;

p7.addCertificate(certBags[i].cert);
last = certBags[i];

// Try to find the certificate that matches the private key.
if (privateKey.n.compareTo(publicKey.n) === 0 &&
privateKey.e.compareTo(publicKey.e) === 0
) {
certificate = certBags[i].cert;
}
});

// Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects.
p7.addSigner({
key: keyBags[0].key,
certificate: last.cert,
key: privateKey,
certificate,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{
Expand All @@ -130,37 +158,44 @@ export class SignPdf {
}, {
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});

// Check if the PDF has a good enough placeholder to fit the signature.
const raw = forge.asn1.toDer(p7.toAsn1()).getBytes();
if (raw.length > placeholderLength) {
// placeholderLength represents the length of the HEXified symbols but we're
// checking the actual lengths.
if ((raw.length * 2) > placeholderLength) {
throw new SignPdfError(
`Signature exceeds placeholder length: ${raw.length} > ${placeholderLength}`,
`Signature exceeds placeholder length: ${raw.length * 2} > ${placeholderLength}`,
SignPdfError.TYPE_INPUT,
);
}

let signature = stringToHex(raw);
// Store the HEXified signature. At least useful in tests.
this.lastSignature = signature;

// placeholderLength is for the HEX symbols and we need the raw char length
const placeholderCharCount = placeholderLength / 2;

// Pad with zeroes so the output signature is the same length as the placeholder
// Pad the signature with zeroes so the it is the same length as the placeholder
signature += Buffer
.from(String.fromCharCode(0).repeat(placeholderCharCount - raw.length))
.from(String.fromCharCode(0).repeat((placeholderLength / 2) - raw.length))
.toString('hex');

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

// Magic. Done.
return pdf;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/signpdf.test.js
Expand Up @@ -225,6 +225,17 @@ describe('Test signpdf', () => {
expect(signature1).not.toBe(signature2);
expect(signature1).toHaveLength(signature2.length);
});
it('signs with ca, intermediate and multiple certificates bundle', async () => {
let pdfBuffer = await createPdf();
const p12Buffer = fs.readFileSync(`${__dirname}/../bundle.p12`);

pdfBuffer = signer.sign(pdfBuffer, p12Buffer);
expect(pdfBuffer instanceof Buffer).toBe(true);

const {signature, signedData} = extractSignature(pdfBuffer);
expect(typeof signature === 'string').toBe(true);
expect(signedData instanceof Buffer).toBe(true);
});
it('signs with passphrased certificate', async () => {
let pdfBuffer = await createPdf();
const p12Buffer = fs.readFileSync(`${__dirname}/../withpass.p12`);
Expand Down

0 comments on commit 18229de

Please sign in to comment.