refactor: make CCN-STIC-221 enforcement opt-in via strictCrypto
Replace the always-on `allowLegacyEncryption` / default-on RSA checks with a single `strictCrypto` flag on both `SignOption` and `EncryptOption`. All modes and key lengths work by default; set `strictCrypto: true` to enforce the approved-algorithms rules. Update tests, externs, type declarations, and docs accordingly.pull/14/head
parent
162110c1fb
commit
a54166c901
75
README.md
75
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:)
|
* 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:)
|
* 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:
|
* Set password protection to a pdf. Supported algorithms:
|
||||||
* 256bit AES Encryption (default; the only algorithm allowed out of the box)
|
* 40bit RC4 Encryption
|
||||||
* 128bit AES Encryption ( :warning: legacy, opt-in)
|
* 128bit RC4 Encryption
|
||||||
* 128bit RC4 Encryption ( :warning: legacy, opt-in)
|
* 128bit AES Encryption
|
||||||
* 40bit RC4 Encryption ( :warning: legacy, opt-in)
|
* 256bit AES Encryption
|
||||||
* Set public-key certificate protection to a pdf.
|
* Set public-key certificate protection to a pdf.
|
||||||
Supported algorithms are as same as the password protection.
|
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/) is the approved-algorithms guide of
|
||||||
[CCN-STIC-221](https://www.ccn-cert.cni.es/) by default. Two rules apply:
|
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
|
When it is on:
|
||||||
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
|
* __Only AES-256 encryption is accepted.__ RC4 is a stream cipher and is not
|
||||||
var eopt = {
|
authorized, and the RC4 / AES-128 PDF security handlers derive their key with
|
||||||
mode: Zga.Crypto.Mode.RC4_128,
|
MD5. Any other mode throws.
|
||||||
allowLegacyEncryption: true, // required, otherwise this throws
|
* __Signing keys must be RSA of at least 3000 bits, with log2(e) > 16.__ A
|
||||||
userpwd: upwd,
|
certificate carrying a shorter key is rejected when it is loaded. The standard
|
||||||
};
|
public exponent 65537 already satisfies the exponent rule.
|
||||||
```
|
|
||||||
|
|
||||||
__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
|
```js
|
||||||
var sopt = {
|
var sopt = {
|
||||||
p12cert: cert,
|
p12cert: cert,
|
||||||
pwd: pwd,
|
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){
|
async function protect1(pdf, upwd, opwd){
|
||||||
/** @type {EncryptOption} */
|
/** @type {EncryptOption} */
|
||||||
var eopt = {
|
var eopt = {
|
||||||
mode: Zga.Crypto.Mode.AES_256,
|
mode: Zga.Crypto.Mode.RC4_40,
|
||||||
permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
|
permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
|
||||||
userpwd: upwd,
|
userpwd: upwd,
|
||||||
ownerpwd: opwd,
|
ownerpwd: opwd,
|
||||||
|
|
@ -427,7 +448,7 @@ Set public-key certificate protection to the pdf.
|
||||||
async function protect2(pdf, cert){
|
async function protect2(pdf, cert){
|
||||||
/** @type {EncryptOption} */
|
/** @type {EncryptOption} */
|
||||||
var eopt = {
|
var eopt = {
|
||||||
mode: Zga.Crypto.Mode.AES_256,
|
mode: Zga.Crypto.Mode.AES_128,
|
||||||
pubkeys: [{
|
pubkeys: [{
|
||||||
c: cert,
|
c: cert,
|
||||||
p: ["copy", "modify", "copy-extract", "annot-forms", "fill-forms", "extract", "assemble"],
|
p: ["copy", "modify", "copy-extract", "annot-forms", "fill-forms", "extract", "assemble"],
|
||||||
|
|
@ -458,7 +479,7 @@ async function signAndProtect1(pdf, cert, pwd, opwd){
|
||||||
};
|
};
|
||||||
/** @type {EncryptOption} */
|
/** @type {EncryptOption} */
|
||||||
var eopt = {
|
var eopt = {
|
||||||
mode: Zga.Crypto.Mode.AES_256,
|
mode: Zga.Crypto.Mode.RC4_128,
|
||||||
permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
|
permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
|
||||||
ownerpwd: opwd,
|
ownerpwd: opwd,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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.
|
* 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.
|
* 2 : crl only; Only try using crl to enable the LTV.
|
||||||
*
|
*
|
||||||
* minRsaKeyBits: Minimum accepted RSA modulus length in bits. Defaults to 3000
|
* strictCrypto: Enforce the algorithms approved by CCN-STIC-221. Defaults to
|
||||||
* per CCN-STIC-221; signing keys shorter than this are rejected.
|
* 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
|
* @typedef
|
||||||
* {{
|
* {{
|
||||||
|
|
@ -110,6 +116,7 @@ var SignDrawInfo;
|
||||||
* signame: (string|undefined),
|
* signame: (string|undefined),
|
||||||
* drawinf: (SignDrawInfo|undefined),
|
* drawinf: (SignDrawInfo|undefined),
|
||||||
* ltv: (number|undefined),
|
* ltv: (number|undefined),
|
||||||
|
* strictCrypto: (boolean|undefined),
|
||||||
* minRsaKeyBits: (number|undefined),
|
* minRsaKeyBits: (number|undefined),
|
||||||
* debug: (boolean|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.
|
* 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
|
* @typedef
|
||||||
* {{
|
* {{
|
||||||
|
|
@ -149,7 +156,7 @@ var PubKeyInfo;
|
||||||
* userpwd: (string|undefined),
|
* userpwd: (string|undefined),
|
||||||
* ownerpwd: (string|undefined),
|
* ownerpwd: (string|undefined),
|
||||||
* pubkeys: (Array<PubKeyInfo>|undefined),
|
* pubkeys: (Array<PubKeyInfo>|undefined),
|
||||||
* allowLegacyEncryption: (boolean|undefined),
|
* strictCrypto: (boolean|undefined),
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
var EncryptOption;
|
var EncryptOption;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export type EncryptOption = {
|
||||||
userpwd?: string;
|
userpwd?: string;
|
||||||
ownerpwd?: string;
|
ownerpwd?: string;
|
||||||
pubkeys?: Array<PubKeyInfo>;
|
pubkeys?: Array<PubKeyInfo>;
|
||||||
allowLegacyEncryption?: boolean;
|
strictCrypto?: boolean;
|
||||||
};
|
};
|
||||||
export type PubKeyInfo = {
|
export type PubKeyInfo = {
|
||||||
c?: Array<number> | Uint8Array | ArrayBuffer | string | forge.pki.Certificate;
|
c?: Array<number> | Uint8Array | ArrayBuffer | string | forge.pki.Certificate;
|
||||||
|
|
@ -78,6 +78,7 @@ export type SignOption = {
|
||||||
signame?: string;
|
signame?: string;
|
||||||
drawinf?: SignDrawInfo;
|
drawinf?: SignDrawInfo;
|
||||||
ltv?: number;
|
ltv?: number;
|
||||||
|
strictCrypto?: boolean;
|
||||||
minRsaKeyBits?: number;
|
minRsaKeyBits?: number;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -311,13 +311,12 @@ z.PdfCryptor = class{
|
||||||
|
|
||||||
// CCN-STIC-221: stream ciphers such as RC4 are not authorized,
|
// 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
|
// and the MD5-based key derivation used by the RC4 / AES-128 PDF security
|
||||||
// handlers is not authorized. Only the AES-256 handler avoids
|
// handlers is not authorized. Only the AES-256 handler avoids both.
|
||||||
// both. Reject weaker modes unless the caller explicitly opts into legacy
|
// Enforcement is opt-in, so every existing mode keeps working by default.
|
||||||
// encryption for backward compatibility.
|
if(encopt.strictCrypto && this.mode !== z.Crypto.Mode.AES_256){
|
||||||
if(!encopt.allowLegacyEncryption && this.mode !== z.Crypto.Mode.AES_256){
|
|
||||||
throw new Error("Encryption mode " + this.mode + " is not authorized by "
|
throw new Error("Encryption mode " + this.mode + " is not authorized by "
|
||||||
+ "CCN-STIC-221 (RC4 and MD5-based key derivation are prohibited). "
|
+ "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<string>|undefined} */
|
/** @private @type {Array<string>|undefined} */
|
||||||
|
|
|
||||||
|
|
@ -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} */
|
/** @type {Zga.PdfCryptor} */
|
||||||
_this.cyptr = new z.PdfCryptor(cypopt);
|
_this.cyptr = new z.PdfCryptor(cypopt);
|
||||||
await _this.cyptr.encryptPdf(pdfdoc, encref);
|
await _this.cyptr.encryptPdf(pdfdoc, encref);
|
||||||
|
|
@ -739,21 +744,29 @@ z.PdfSigner = class{
|
||||||
})[forge.pki.oids.pkcs8ShroudedKeyBag];
|
})[forge.pki.oids.pkcs8ShroudedKeyBag];
|
||||||
_this.privateKey = keyBags[0].key;
|
_this.privateKey = keyBags[0].key;
|
||||||
|
|
||||||
// CCN-STIC-221: Reject shorter keys so a non-compliant certificate
|
// CCN-STIC-221: Reject certificates whose RSA parameters are not approved,
|
||||||
// cannot be used to sign. Override the minimum via opt.minRsaKeyBits.
|
// so a non-compliant certificate cannot be used to sign. Enforcement is
|
||||||
if(_this.privateKey && _this.privateKey.n){
|
// 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} */
|
/** @type {number} */
|
||||||
var minRsaKeyBits = (_this.opt && typeof _this.opt.minRsaKeyBits === "number")
|
var minRsaKeyBits = hasMinRsaKeyBits ? _this.opt.minRsaKeyBits : 3000;
|
||||||
? _this.opt.minRsaKeyBits : 3000;
|
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
var rsaKeyBits = _this.privateKey.n.bitLength();
|
var rsaKeyBits = _this.privateKey.n.bitLength();
|
||||||
if(rsaKeyBits < minRsaKeyBits){
|
if(rsaKeyBits < minRsaKeyBits){
|
||||||
throw new Error("RSA key length " + rsaKeyBits + " bits is below the "
|
throw new Error("RSA key length " + rsaKeyBits + " bits is below the "
|
||||||
+ minRsaKeyBits + "-bit minimum required by CCN-STIC-221.");
|
+ minRsaKeyBits + "-bit minimum required by CCN-STIC-221.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// CCN-STIC-221 requires the public exponent to satisfy log2(e) > 16.
|
// CCN-STIC-221 requires the public exponent to satisfy log2(e) > 16.
|
||||||
// The de-facto standard value 65537 already meets this.
|
// 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.");
|
throw new Error("RSA public exponent is too small; CCN-STIC-221 requires log2(e) > 16.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "zgapdfsigner",
|
"name": "zgapdfsigner",
|
||||||
"version": "3.0.0",
|
"version": "2.8.0",
|
||||||
"author": "zboris12",
|
"author": "zboris12",
|
||||||
"description": "A javascript tool to sign a pdf or set protection to a pdf in web browser, Google Apps Script and nodejs.",
|
"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",
|
"homepage": "https://github.com/zboris12/zgapdfsigner",
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,39 @@ const Zga = require("../lib/zganode.js");
|
||||||
const Mode = Zga.Crypto.Mode;
|
const Mode = Zga.Crypto.Mode;
|
||||||
|
|
||||||
// CCN-STIC-221: RC4 (any stream cipher) and MD5-based key derivation are not
|
// 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
|
// authorized. Only the AES-256 handler avoids both. Enforcement is opt-in via
|
||||||
// makes these fail is removing the mode guard added in the PdfCryptor constructor.
|
// 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(
|
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/,
|
/not authorized by CCN-STIC-221/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects RC4-128 encryption by default", () => {
|
test("rejects RC4-128 encryption under strictCrypto", () => {
|
||||||
assert.throws(
|
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/,
|
/not authorized by CCN-STIC-221/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects AES-128 encryption by default", () => {
|
test("rejects AES-128 encryption under strictCrypto", () => {
|
||||||
assert.throws(
|
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/,
|
/not authorized by CCN-STIC-221/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("accepts AES-256 encryption", () => {
|
test("accepts AES-256 encryption under strictCrypto", () => {
|
||||||
assert.doesNotThrow(() => new Zga.PdfCryptor({mode: Mode.AES_256, userpwd: "x"}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("allows legacy modes only when allowLegacyEncryption is set", () => {
|
|
||||||
assert.doesNotThrow(
|
assert.doesNotThrow(
|
||||||
() => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x", allowLegacyEncryption: true}),
|
() => new Zga.PdfCryptor({mode: Mode.AES_256, userpwd: "x", strictCrypto: true}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,43 +8,66 @@ const {makeP12} = require("../testutil/fixtures.js");
|
||||||
const PWD = "test-pw";
|
const PWD = "test-pw";
|
||||||
|
|
||||||
// CCN-STIC-221: RSA modulus must be >= 3000 bits and the public exponent must
|
// 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
|
// satisfy log2(e) > 16. Enforcement is opt-in via strictCrypto, except that
|
||||||
// the corresponding guard added in PdfSigner.loadP12cert.
|
// 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 p12 = makeP12(2048, PWD);
|
||||||
const signer = new Zga.PdfSigner({});
|
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(
|
assert.throws(
|
||||||
() => signer.loadP12cert(p12, PWD),
|
() => signer.loadP12cert(p12, PWD),
|
||||||
/below the 3000-bit minimum/,
|
/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 p12 = makeP12(3072, PWD);
|
||||||
const signer = new Zga.PdfSigner({});
|
const signer = new Zga.PdfSigner({strictCrypto: true});
|
||||||
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("honors a custom minRsaKeyBits threshold below the default", () => {
|
test("minRsaKeyBits enforces the key length without strictCrypto", () => {
|
||||||
const p12 = makeP12(2048, PWD);
|
const p12 = makeP12(1024, PWD);
|
||||||
const signer = new Zga.PdfSigner({minRsaKeyBits: 2048});
|
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));
|
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
||||||
});
|
});
|
||||||
|
|
||||||
// The modulus check runs first, so minRsaKeyBits is lowered here purely to let
|
// The modulus check runs first, so minRsaKeyBits is lowered here purely to let
|
||||||
// a cheap-to-generate key reach the exponent check.
|
// 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 p12 = makeP12(1024, PWD, 3);
|
||||||
const signer = new Zga.PdfSigner({minRsaKeyBits: 1024});
|
const signer = new Zga.PdfSigner({strictCrypto: true, minRsaKeyBits: 1024});
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => signer.loadP12cert(p12, PWD),
|
() => signer.loadP12cert(p12, PWD),
|
||||||
/public exponent is too small/,
|
/public exponent is too small/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("accepts the standard 65537 public exponent", () => {
|
test("accepts a small public exponent when strictCrypto is off", () => {
|
||||||
const p12 = makeP12(1024, PWD, 65537);
|
const p12 = makeP12(1024, PWD, 3);
|
||||||
const signer = new Zga.PdfSigner({minRsaKeyBits: 1024});
|
const signer = new Zga.PdfSigner({minRsaKeyBits: 1024});
|
||||||
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
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));
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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");
|
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 pdfBytes = await minimalPdf();
|
||||||
const signer = new Zga.PdfSigner({p12cert: makeP12(3072, PWD), pwd: PWD});
|
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(
|
await assert.rejects(
|
||||||
() => signer.sign(pdfBytes, {mode: Zga.Crypto.Mode.RC4_128, userpwd: "user-pw"}),
|
() => signer.sign(pdfBytes, {mode: Zga.Crypto.Mode.RC4_128, userpwd: "user-pw"}),
|
||||||
/not authorized by CCN-STIC-221/,
|
/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/);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue