diff --git a/README.md b/README.md index 5ed8eff..e9af894 100644 --- a/README.md +++ b/README.md @@ -22,41 +22,62 @@ 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: - * 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) + * 40bit RC4 Encryption + * 128bit RC4 Encryption + * 128bit AES Encryption + * 256bit AES Encryption * Set public-key certificate protection to a pdf. Supported algorithms are as same as the password protection. +* Optionally enforce the algorithms approved by [CCN-STIC-221](https://www.ccn-cert.cni.es/). -## Cryptographic constraints (CCN-STIC-221) +## Enforcing CCN-STIC-221 (optional) -This tool enforces the cryptographic requirements of -[CCN-STIC-221](https://www.ccn-cert.cni.es/) by default. Two rules apply: +[CCN-STIC-221](https://www.ccn-cert.cni.es/) is the approved-algorithms guide of +the Spanish national cryptology centre. Set `strictCrypto: true` to make this +tool refuse anything the guide does not authorize. __It is off by default, so +nothing changes unless you ask for it.__ -__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: +When it is on: -```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: +* __Only AES-256 encryption is accepted.__ RC4 is a stream cipher and is not + authorized, and the RC4 / AES-128 PDF security handlers derive their key with + MD5. Any other mode throws. +* __Signing keys must be RSA of at least 3000 bits, with log2(e) > 16.__ A + certificate carrying a shorter key is rejected when it is loaded. The standard + public exponent 65537 already satisfies the exponent rule. ```js var sopt = { p12cert: cert, pwd: pwd, - minRsaKeyBits: 2048, // accepts a 2048bit key; not CCN-STIC-221 compliant + strictCrypto: true, +}; +var eopt = { + mode: Zga.Crypto.Mode.AES_256, // anything else throws under strictCrypto + userpwd: upwd, +}; +// strictCrypto on the SignOption also applies to the encryption step. +var u8arr = await new Zga.PdfSigner(sopt).sign(pdf, eopt); +``` + +`PdfCryptor` accepts the same flag when used on its own: + +```js +var cyptor = new Zga.PdfCryptor({ + mode: Zga.Crypto.Mode.AES_256, + userpwd: upwd, + strictCrypto: true, +}); +``` + +`minRsaKeyBits` sets the minimum RSA modulus length. Setting it turns on the key +length check on its own, and it overrides the 3000-bit default of `strictCrypto`: + +```js +var sopt = { + p12cert: cert, + pwd: pwd, + minRsaKeyBits: 2048, // rejects keys under 2048bit; not CCN-STIC-221 compliant }; ``` @@ -404,7 +425,7 @@ Set password protection to the pdf. async function protect1(pdf, upwd, opwd){ /** @type {EncryptOption} */ var eopt = { - mode: Zga.Crypto.Mode.AES_256, + mode: Zga.Crypto.Mode.RC4_40, permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"], userpwd: upwd, ownerpwd: opwd, @@ -427,7 +448,7 @@ Set public-key certificate protection to the pdf. async function protect2(pdf, cert){ /** @type {EncryptOption} */ var eopt = { - mode: Zga.Crypto.Mode.AES_256, + mode: Zga.Crypto.Mode.AES_128, pubkeys: [{ c: cert, p: ["copy", "modify", "copy-extract", "annot-forms", "fill-forms", "extract", "assemble"], @@ -458,7 +479,7 @@ async function signAndProtect1(pdf, cert, pwd, opwd){ }; /** @type {EncryptOption} */ var eopt = { - mode: Zga.Crypto.Mode.AES_256, + mode: Zga.Crypto.Mode.RC4_128, permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"], ownerpwd: opwd, }; diff --git a/closure/zb-externs.js b/closure/zb-externs.js index ad41531..8cfc8dd 100644 --- a/closure/zb-externs.js +++ b/closure/zb-externs.js @@ -95,8 +95,14 @@ var SignDrawInfo; * 1 : auto; Try using ocsp only to enable the LTV first; If can't, try using crl to enable the LTV. * 2 : crl only; Only try using crl to enable the LTV. * - * minRsaKeyBits: Minimum accepted RSA modulus length in bits. Defaults to 3000 - * per CCN-STIC-221; signing keys shorter than this are rejected. + * strictCrypto: Enforce the algorithms approved by CCN-STIC-221. Defaults to + * false, which keeps the current behaviour. When true, signing keys must be + * RSA of at least 3000 bits with a public exponent satisfying log2(e) > 16. + * It also turns on strict encryption when an EncryptOption is passed to sign(). + * + * minRsaKeyBits: Minimum accepted RSA modulus length in bits. Setting it enables + * the key length check on its own and overrides the 3000-bit default of + * strictCrypto. * * @typedef * {{ @@ -110,6 +116,7 @@ var SignDrawInfo; * signame: (string|undefined), * drawinf: (SignDrawInfo|undefined), * ltv: (number|undefined), + * strictCrypto: (boolean|undefined), * minRsaKeyBits: (number|undefined), * debug: (boolean|undefined), * }} @@ -140,7 +147,7 @@ var PubKeyInfo; * * pubkeys: Array of recipients containing public-key certificates ('c') and permissions ('p'). If want to encrypt the pdf by the certificate of signing, just apply a PubKeyInfo without c. * - * allowLegacyEncryption: Allow non-authorized encryption modes (RC4-40, RC4-128, AES-128). Defaults to false; only AES-256 is accepted per CCN-STIC-221 unless this is set to true. + * strictCrypto: Enforce the algorithms approved by CCN-STIC-221. Defaults to false, which keeps every encryption mode available. When true, only AES-256 is accepted. * * @typedef * {{ @@ -149,7 +156,7 @@ var PubKeyInfo; * userpwd: (string|undefined), * ownerpwd: (string|undefined), * pubkeys: (Array|undefined), - * allowLegacyEncryption: (boolean|undefined), + * strictCrypto: (boolean|undefined), * }} */ var EncryptOption; diff --git a/lib/zganode.d.ts b/lib/zganode.d.ts index 9fa5fff..347ac37 100644 --- a/lib/zganode.d.ts +++ b/lib/zganode.d.ts @@ -24,7 +24,7 @@ export type EncryptOption = { userpwd?: string; ownerpwd?: string; pubkeys?: Array; - allowLegacyEncryption?: boolean; + strictCrypto?: boolean; }; export type PubKeyInfo = { c?: Array | Uint8Array | ArrayBuffer | string | forge.pki.Certificate; @@ -78,6 +78,7 @@ export type SignOption = { signame?: string; drawinf?: SignDrawInfo; ltv?: number; + strictCrypto?: boolean; minRsaKeyBits?: number; debug?: boolean; }; diff --git a/lib/zgapdfcryptor.js b/lib/zgapdfcryptor.js index 88cb20f..fc80903 100644 --- a/lib/zgapdfcryptor.js +++ b/lib/zgapdfcryptor.js @@ -311,13 +311,12 @@ z.PdfCryptor = class{ // CCN-STIC-221: stream ciphers such as RC4 are not authorized, // and the MD5-based key derivation used by the RC4 / AES-128 PDF security - // handlers is not authorized. Only the AES-256 handler avoids - // both. Reject weaker modes unless the caller explicitly opts into legacy - // encryption for backward compatibility. - if(!encopt.allowLegacyEncryption && this.mode !== z.Crypto.Mode.AES_256){ + // handlers is not authorized. Only the AES-256 handler avoids both. + // Enforcement is opt-in, so every existing mode keeps working by default. + if(encopt.strictCrypto && this.mode !== z.Crypto.Mode.AES_256){ throw new Error("Encryption mode " + this.mode + " is not authorized by " + "CCN-STIC-221 (RC4 and MD5-based key derivation are prohibited). " - + "Use AES-256, or set allowLegacyEncryption:true to override."); + + "Use AES-256, or drop strictCrypto to allow legacy encryption."); } /** @private @type {Array|undefined} */ diff --git a/lib/zgapdfsigner.js b/lib/zgapdfsigner.js index e8480ed..f79e61b 100644 --- a/lib/zgapdfsigner.js +++ b/lib/zgapdfsigner.js @@ -487,6 +487,11 @@ z.PdfSigner = class{ }); } } + // A signer asking for CCN-STIC-221 enforcement expects it to cover + // the encryption too, unless the EncryptOption says otherwise. + if(_this.opt.strictCrypto && cypopt.strictCrypto === undefined){ + cypopt.strictCrypto = true; + } /** @type {Zga.PdfCryptor} */ _this.cyptr = new z.PdfCryptor(cypopt); await _this.cyptr.encryptPdf(pdfdoc, encref); @@ -739,21 +744,29 @@ z.PdfSigner = class{ })[forge.pki.oids.pkcs8ShroudedKeyBag]; _this.privateKey = keyBags[0].key; - // CCN-STIC-221: Reject shorter keys so a non-compliant certificate - // cannot be used to sign. Override the minimum via opt.minRsaKeyBits. - if(_this.privateKey && _this.privateKey.n){ - /** @type {number} */ - var minRsaKeyBits = (_this.opt && typeof _this.opt.minRsaKeyBits === "number") - ? _this.opt.minRsaKeyBits : 3000; - /** @type {number} */ - var rsaKeyBits = _this.privateKey.n.bitLength(); - if(rsaKeyBits < minRsaKeyBits){ - throw new Error("RSA key length " + rsaKeyBits + " bits is below the " - + minRsaKeyBits + "-bit minimum required by CCN-STIC-221."); + // CCN-STIC-221: Reject certificates whose RSA parameters are not approved, + // so a non-compliant certificate cannot be used to sign. Enforcement is + // opt-in via opt.strictCrypto, so existing callers are unaffected. + // Setting opt.minRsaKeyBits enables the modulus check on its own and + // overrides the 3000-bit default. + if(_this.privateKey && _this.privateKey.n && _this.opt){ + /** @type {boolean} */ + var strictCrypto = !!_this.opt.strictCrypto; + /** @type {boolean} */ + var hasMinRsaKeyBits = typeof _this.opt.minRsaKeyBits === "number"; + if(strictCrypto || hasMinRsaKeyBits){ + /** @type {number} */ + var minRsaKeyBits = hasMinRsaKeyBits ? _this.opt.minRsaKeyBits : 3000; + /** @type {number} */ + var rsaKeyBits = _this.privateKey.n.bitLength(); + if(rsaKeyBits < minRsaKeyBits){ + throw new Error("RSA key length " + rsaKeyBits + " bits is below the " + + minRsaKeyBits + "-bit minimum required by CCN-STIC-221."); + } } // CCN-STIC-221 requires the public exponent to satisfy log2(e) > 16. // The de-facto standard value 65537 already meets this. - if(_this.privateKey.e && _this.privateKey.e.bitLength() <= 16){ + if(strictCrypto && _this.privateKey.e && _this.privateKey.e.bitLength() <= 16){ throw new Error("RSA public exponent is too small; CCN-STIC-221 requires log2(e) > 16."); } } diff --git a/package.json b/package.json index add2650..8a7d644 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zgapdfsigner", - "version": "3.0.0", + "version": "2.8.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", diff --git a/tests/compliance-encryption.test.js b/tests/compliance-encryption.test.js index 5e34e63..42761d3 100644 --- a/tests/compliance-encryption.test.js +++ b/tests/compliance-encryption.test.js @@ -7,36 +7,39 @@ const Zga = require("../lib/zganode.js"); const Mode = Zga.Crypto.Mode; // CCN-STIC-221: RC4 (any stream cipher) and MD5-based key derivation are not -// authorized. Only the AES-256 handler avoids both. The production change that -// makes these fail is removing the mode guard added in the PdfCryptor constructor. +// authorized. Only the AES-256 handler avoids both. Enforcement is opt-in via +// strictCrypto, so the default stays backward compatible. The production change +// that makes these fail is removing the mode guard in the PdfCryptor constructor. -test("rejects RC4-40 encryption by default", () => { +test("allows every legacy mode by default", () => { + assert.doesNotThrow(() => new Zga.PdfCryptor({mode: Mode.RC4_40, userpwd: "x"})); + assert.doesNotThrow(() => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x"})); + assert.doesNotThrow(() => new Zga.PdfCryptor({mode: Mode.AES_128, userpwd: "x"})); +}); + +test("rejects RC4-40 encryption under strictCrypto", () => { assert.throws( - () => new Zga.PdfCryptor({mode: Mode.RC4_40, userpwd: "x"}), + () => new Zga.PdfCryptor({mode: Mode.RC4_40, userpwd: "x", strictCrypto: true}), /not authorized by CCN-STIC-221/, ); }); -test("rejects RC4-128 encryption by default", () => { +test("rejects RC4-128 encryption under strictCrypto", () => { assert.throws( - () => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x"}), + () => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x", strictCrypto: true}), /not authorized by CCN-STIC-221/, ); }); -test("rejects AES-128 encryption by default", () => { +test("rejects AES-128 encryption under strictCrypto", () => { assert.throws( - () => new Zga.PdfCryptor({mode: Mode.AES_128, userpwd: "x"}), + () => new Zga.PdfCryptor({mode: Mode.AES_128, userpwd: "x", strictCrypto: true}), /not authorized by CCN-STIC-221/, ); }); -test("accepts AES-256 encryption", () => { - assert.doesNotThrow(() => new Zga.PdfCryptor({mode: Mode.AES_256, userpwd: "x"})); -}); - -test("allows legacy modes only when allowLegacyEncryption is set", () => { +test("accepts AES-256 encryption under strictCrypto", () => { assert.doesNotThrow( - () => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x", allowLegacyEncryption: true}), + () => new Zga.PdfCryptor({mode: Mode.AES_256, userpwd: "x", strictCrypto: true}), ); }); diff --git a/tests/compliance-rsa.test.js b/tests/compliance-rsa.test.js index 3aa00bb..77991ad 100644 --- a/tests/compliance-rsa.test.js +++ b/tests/compliance-rsa.test.js @@ -8,43 +8,66 @@ const {makeP12} = require("../testutil/fixtures.js"); const PWD = "test-pw"; // 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. +// satisfy log2(e) > 16. Enforcement is opt-in via strictCrypto, except that +// setting minRsaKeyBits enables the modulus check on its own. The production +// change that makes these fail is removing the corresponding guard in +// PdfSigner.loadP12cert. -test("rejects an RSA key shorter than 3000 bits", () => { +test("accepts a short RSA key by default", () => { const p12 = makeP12(2048, PWD); const signer = new Zga.PdfSigner({}); + assert.doesNotThrow(() => signer.loadP12cert(p12, PWD)); +}); + +test("rejects an RSA key shorter than 3000 bits under strictCrypto", () => { + const p12 = makeP12(2048, PWD); + const signer = new Zga.PdfSigner({strictCrypto: true}); assert.throws( () => signer.loadP12cert(p12, PWD), /below the 3000-bit minimum/, ); }); -test("accepts an RSA key of 3072 bits", () => { +test("accepts an RSA key of 3072 bits under strictCrypto", () => { const p12 = makeP12(3072, PWD); - const signer = new Zga.PdfSigner({}); + const signer = new Zga.PdfSigner({strictCrypto: true}); assert.doesNotThrow(() => signer.loadP12cert(p12, PWD)); }); -test("honors a custom minRsaKeyBits threshold below the default", () => { - const p12 = makeP12(2048, PWD); +test("minRsaKeyBits enforces the key length without strictCrypto", () => { + const p12 = makeP12(1024, PWD); const signer = new Zga.PdfSigner({minRsaKeyBits: 2048}); + assert.throws( + () => signer.loadP12cert(p12, PWD), + /below the 2048-bit minimum/, + ); +}); + +test("minRsaKeyBits overrides the strictCrypto default", () => { + const p12 = makeP12(2048, PWD); + const signer = new Zga.PdfSigner({strictCrypto: true, 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", () => { +test("rejects an RSA public exponent with log2(e) <= 16 under strictCrypto", () => { const p12 = makeP12(1024, PWD, 3); - const signer = new Zga.PdfSigner({minRsaKeyBits: 1024}); + const signer = new Zga.PdfSigner({strictCrypto: true, 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); +test("accepts a small public exponent when strictCrypto is off", () => { + const p12 = makeP12(1024, PWD, 3); const signer = new Zga.PdfSigner({minRsaKeyBits: 1024}); assert.doesNotThrow(() => signer.loadP12cert(p12, PWD)); }); + +test("accepts the standard 65537 public exponent under strictCrypto", () => { + const p12 = makeP12(1024, PWD, 65537); + const signer = new Zga.PdfSigner({strictCrypto: true, minRsaKeyBits: 1024}); + assert.doesNotThrow(() => signer.loadP12cert(p12, PWD)); +}); diff --git a/tests/sign-integration.test.js b/tests/sign-integration.test.js index da0da83..0f5c0fe 100644 --- a/tests/sign-integration.test.js +++ b/tests/sign-integration.test.js @@ -50,12 +50,36 @@ test("PdfSigner.sign encrypts the output when an AES-256 EncryptOption is given" assert.match(dump, /adbe\.pkcs7\.detached/, "still carries the detached signature"); }); -test("PdfSigner.sign refuses a legacy encryption mode", async () => { +test("PdfSigner.sign accepts a legacy encryption mode by default", 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.RC4_128, userpwd: "user-pw"}); + + assert.match(Buffer.from(signed).toString("latin1"), /\/Encrypt/); +}); + +// strictCrypto on the SignOption has to reach the cryptor, otherwise a caller +// asking for compliance would silently get a non-compliant encryption handler. +test("PdfSigner.sign propagates strictCrypto to the encryption step", async () => { + const pdfBytes = await minimalPdf(); + const signer = new Zga.PdfSigner({p12cert: makeP12(3072, PWD), pwd: PWD, strictCrypto: true}); + await assert.rejects( () => signer.sign(pdfBytes, {mode: Zga.Crypto.Mode.RC4_128, userpwd: "user-pw"}), /not authorized by CCN-STIC-221/, ); }); + +test("an explicit strictCrypto on the EncryptOption wins over the SignOption", async () => { + const pdfBytes = await minimalPdf(); + const signer = new Zga.PdfSigner({p12cert: makeP12(3072, PWD), pwd: PWD, strictCrypto: true}); + + const signed = await signer.sign(pdfBytes, { + mode: Zga.Crypto.Mode.RC4_128, + userpwd: "user-pw", + strictCrypto: false, + }); + + assert.match(Buffer.from(signed).toString("latin1"), /\/Encrypt/); +});