feat: enforce CCN-STIC-221 crypto constraints and bump to v3.0.0
parent
179fdf11e4
commit
162110c1fb
45
README.md
45
README.md
|
|
@ -22,13 +22,44 @@ And I use this name to hope the merits from this application will be dedicated t
|
|||
* Sign a pdf with a timestamp from [TSA](https://github.com/zboris12/zgapdfsigner/wiki/API#note). ( :no_entry_sign:__Not__ available in web browser :sunflower:)
|
||||
* Enable signature's [LTV](https://github.com/zboris12/zgapdfsigner/wiki/API#note). ( :no_entry_sign:__Not__ available in web browser :sunflower:)
|
||||
* Set password protection to a pdf. Supported algorithms:
|
||||
* 40bit RC4 Encryption
|
||||
* 128bit RC4 Encryption
|
||||
* 128bit AES Encryption
|
||||
* 256bit AES Encryption
|
||||
* 256bit AES Encryption (default; the only algorithm allowed out of the box)
|
||||
* 128bit AES Encryption ( :warning: legacy, opt-in)
|
||||
* 128bit RC4 Encryption ( :warning: legacy, opt-in)
|
||||
* 40bit RC4 Encryption ( :warning: legacy, opt-in)
|
||||
* Set public-key certificate protection to a pdf.
|
||||
Supported algorithms are as same as the password protection.
|
||||
|
||||
## Cryptographic constraints (CCN-STIC-221)
|
||||
|
||||
This tool enforces the cryptographic requirements of
|
||||
[CCN-STIC-221](https://www.ccn-cert.cni.es/) by default. Two rules apply:
|
||||
|
||||
__1. Only AES-256 encryption is allowed.__ RC4 is a stream cipher and is not
|
||||
authorized, and the RC4 / AES-128 PDF security handlers derive their key with
|
||||
MD5. Passing any other mode throws. Set `allowLegacyEncryption` to opt out when
|
||||
you need backward compatibility with old readers:
|
||||
|
||||
```js
|
||||
var eopt = {
|
||||
mode: Zga.Crypto.Mode.RC4_128,
|
||||
allowLegacyEncryption: true, // required, otherwise this throws
|
||||
userpwd: upwd,
|
||||
};
|
||||
```
|
||||
|
||||
__2. Signing keys must be RSA of at least 3000 bits, with log2(e) > 16.__
|
||||
A certificate carrying a shorter key (2048 bits, for example) is rejected when
|
||||
it is loaded. The standard public exponent 65537 already satisfies the exponent
|
||||
rule. Lower the modulus threshold with `minRsaKeyBits` if you must:
|
||||
|
||||
```js
|
||||
var sopt = {
|
||||
p12cert: cert,
|
||||
pwd: pwd,
|
||||
minRsaKeyBits: 2048, // accepts a 2048bit key; not CCN-STIC-221 compliant
|
||||
};
|
||||
```
|
||||
|
||||
## About signing with [TSA](https://github.com/zboris12/zgapdfsigner/wiki/API#note) and [LTV](https://github.com/zboris12/zgapdfsigner/wiki/API#note)
|
||||
|
||||
Because of the [CORS](https://github.com/zboris12/zgapdfsigner/wiki/API#note) security restrictions in web browser,
|
||||
|
|
@ -373,7 +404,7 @@ Set password protection to the pdf.
|
|||
async function protect1(pdf, upwd, opwd){
|
||||
/** @type {EncryptOption} */
|
||||
var eopt = {
|
||||
mode: Zga.Crypto.Mode.RC4_40,
|
||||
mode: Zga.Crypto.Mode.AES_256,
|
||||
permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
|
||||
userpwd: upwd,
|
||||
ownerpwd: opwd,
|
||||
|
|
@ -396,7 +427,7 @@ Set public-key certificate protection to the pdf.
|
|||
async function protect2(pdf, cert){
|
||||
/** @type {EncryptOption} */
|
||||
var eopt = {
|
||||
mode: Zga.Crypto.Mode.AES_128,
|
||||
mode: Zga.Crypto.Mode.AES_256,
|
||||
pubkeys: [{
|
||||
c: cert,
|
||||
p: ["copy", "modify", "copy-extract", "annot-forms", "fill-forms", "extract", "assemble"],
|
||||
|
|
@ -427,7 +458,7 @@ async function signAndProtect1(pdf, cert, pwd, opwd){
|
|||
};
|
||||
/** @type {EncryptOption} */
|
||||
var eopt = {
|
||||
mode: Zga.Crypto.Mode.RC4_128,
|
||||
mode: Zga.Crypto.Mode.AES_256,
|
||||
permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
|
||||
ownerpwd: opwd,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -105,8 +105,10 @@ export declare class RsaSigner {
|
|||
privateKey: forge.pki.rsa.PrivateKey;
|
||||
certificate: forge.pki.Certificate;
|
||||
getDigestAlgorithmOid(): string;
|
||||
getSignatureAlgorithmOid(): string;
|
||||
sign(data: string): string;
|
||||
}
|
||||
export declare function createSigner(privateKey: forge.pki.rsa.PrivateKey, certificate: forge.pki.Certificate): RsaSigner;
|
||||
export declare class PdfSigner {
|
||||
constructor(signopt: SignOption);
|
||||
sign(pdf: PDFLib.PDFDocument | Array<number> | Uint8Array | ArrayBuffer | string, cypopt?: EncryptOption): Promise<Uint8Array>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zgapdfsigner",
|
||||
"version": "2.7.6",
|
||||
"version": "3.0.0",
|
||||
"author": "zboris12",
|
||||
"description": "A javascript tool to sign a pdf or set protection to a pdf in web browser, Google Apps Script and nodejs.",
|
||||
"homepage": "https://github.com/zboris12/zgapdfsigner",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ const {makeP12} = require("../testutil/fixtures.js");
|
|||
|
||||
const PWD = "test-pw";
|
||||
|
||||
// CCN-STIC-221: RSA modulus must be >= 3000 bits.
|
||||
// The production change that makes these fail is removing the key-length
|
||||
// guard added in PdfSigner.loadP12cert.
|
||||
// CCN-STIC-221: RSA modulus must be >= 3000 bits and the public exponent must
|
||||
// satisfy log2(e) > 16. The production change that makes these fail is removing
|
||||
// the corresponding guard added in PdfSigner.loadP12cert.
|
||||
|
||||
test("rejects an RSA key shorter than 3000 bits", () => {
|
||||
const p12 = makeP12(2048, PWD);
|
||||
|
|
@ -31,3 +31,20 @@ test("honors a custom minRsaKeyBits threshold below the default", () => {
|
|||
const signer = new Zga.PdfSigner({minRsaKeyBits: 2048});
|
||||
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
||||
});
|
||||
|
||||
// The modulus check runs first, so minRsaKeyBits is lowered here purely to let
|
||||
// a cheap-to-generate key reach the exponent check.
|
||||
test("rejects an RSA public exponent with log2(e) <= 16", () => {
|
||||
const p12 = makeP12(1024, PWD, 3);
|
||||
const signer = new Zga.PdfSigner({minRsaKeyBits: 1024});
|
||||
assert.throws(
|
||||
() => signer.loadP12cert(p12, PWD),
|
||||
/public exponent is too small/,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts the standard 65537 public exponent", () => {
|
||||
const p12 = makeP12(1024, PWD, 65537);
|
||||
const signer = new Zga.PdfSigner({minRsaKeyBits: 1024});
|
||||
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,3 +32,30 @@ test("PdfSigner.sign produces a detached PKCS#7 signature over a real PDF", asyn
|
|||
assert.match(dump, /ByteRange/, "embeds a ByteRange");
|
||||
assert.match(dump, /\/Type\s*\/Sig/, "embeds a signature dictionary");
|
||||
});
|
||||
|
||||
// The combined sign + encrypt path: PdfSigner.sign delegates to PdfCryptor, so
|
||||
// the CCN-STIC-221 mode guard must hold here too, not only on direct use.
|
||||
test("PdfSigner.sign encrypts the output when an AES-256 EncryptOption is given", async () => {
|
||||
const pdfBytes = await minimalPdf();
|
||||
const signer = new Zga.PdfSigner({p12cert: makeP12(3072, PWD), pwd: PWD});
|
||||
|
||||
const signed = await signer.sign(pdfBytes, {
|
||||
mode: Zga.Crypto.Mode.AES_256,
|
||||
permissions: ["copy", "print-high"],
|
||||
userpwd: "user-pw",
|
||||
});
|
||||
const dump = Buffer.from(signed).toString("latin1");
|
||||
|
||||
assert.match(dump, /\/Encrypt/, "installs an encryption dictionary");
|
||||
assert.match(dump, /adbe\.pkcs7\.detached/, "still carries the detached signature");
|
||||
});
|
||||
|
||||
test("PdfSigner.sign refuses a legacy encryption mode", async () => {
|
||||
const pdfBytes = await minimalPdf();
|
||||
const signer = new Zga.PdfSigner({p12cert: makeP12(3072, PWD), pwd: PWD});
|
||||
|
||||
await assert.rejects(
|
||||
() => signer.sign(pdfBytes, {mode: Zga.Crypto.Mode.RC4_128, userpwd: "user-pw"}),
|
||||
/not authorized by CCN-STIC-221/,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
const Zga = require("../lib/zganode.js");
|
||||
const forge = Zga.forge;
|
||||
|
||||
/** @type {Map<number, {privateKey: *, publicKey: *, certificate: *}>} key cache */
|
||||
/** @type {number} The de-facto standard public exponent (F4). */
|
||||
const DEFAULT_EXPONENT = 0x10001;
|
||||
|
||||
/** @type {Map<string, {privateKey: *, publicKey: *, certificate: *}>} key cache */
|
||||
const keyCache = new Map();
|
||||
|
||||
/**
|
||||
|
|
@ -11,13 +14,16 @@ const keyCache = new Map();
|
|||
* modulus length, as raw node-forge objects.
|
||||
*
|
||||
* @param {number} bits RSA modulus length in bits.
|
||||
* @param {number=} e RSA public exponent. Defaults to 65537.
|
||||
* @return {{privateKey: *, publicKey: *, certificate: *}}
|
||||
*/
|
||||
function makeKeyCert(bits){
|
||||
if(keyCache.has(bits)){
|
||||
return keyCache.get(bits);
|
||||
function makeKeyCert(bits, e){
|
||||
const exponent = e === undefined ? DEFAULT_EXPONENT : e;
|
||||
const cacheKey = bits + ":" + exponent;
|
||||
if(keyCache.has(cacheKey)){
|
||||
return keyCache.get(cacheKey);
|
||||
}
|
||||
const keys = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
|
||||
const keys = forge.pki.rsa.generateKeyPair({bits: bits, e: exponent});
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = "01";
|
||||
|
|
@ -28,21 +34,23 @@ function makeKeyCert(bits){
|
|||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
const entry = {privateKey: keys.privateKey, publicKey: keys.publicKey, certificate: cert};
|
||||
keyCache.set(bits, entry);
|
||||
keyCache.set(cacheKey, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a self-signed PKCS#12 (returned as a DER binary string) carrying an
|
||||
* RSA key of the given modulus length. Used to exercise the CCN-STIC-221
|
||||
* key-length guard in loadP12cert without shipping binary fixtures.
|
||||
* key-length and public-exponent guards in loadP12cert without shipping
|
||||
* binary fixtures.
|
||||
*
|
||||
* @param {number} bits RSA modulus length in bits.
|
||||
* @param {string} pwd PKCS#12 password.
|
||||
* @param {number=} e RSA public exponent. Defaults to 65537.
|
||||
* @return {string} DER-encoded PKCS#12 as a binary string.
|
||||
*/
|
||||
function makeP12(bits, pwd){
|
||||
const kc = makeKeyCert(bits);
|
||||
function makeP12(bits, pwd, e){
|
||||
const kc = makeKeyCert(bits, e);
|
||||
const asn1 = forge.pkcs12.toPkcs12Asn1(kc.privateKey, [kc.certificate], pwd, {algorithm: "3des"});
|
||||
return forge.asn1.toDer(asn1).getBytes();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue