feat: enforce CCN-STIC-221 crypto constraints on sign and encrypt
parent
bbd49bdac0
commit
185f760790
|
|
@ -52,6 +52,11 @@ typings/
|
|||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
pnpm-lock.yaml
|
||||
pnpm-debug.log*
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ 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.
|
||||
*
|
||||
* @typedef
|
||||
* {{
|
||||
* p12cert: (Array<number>|Uint8Array|ArrayBuffer|string|undefined),
|
||||
|
|
@ -107,6 +110,7 @@ var SignDrawInfo;
|
|||
* signame: (string|undefined),
|
||||
* drawinf: (SignDrawInfo|undefined),
|
||||
* ltv: (number|undefined),
|
||||
* minRsaKeyBits: (number|undefined),
|
||||
* debug: (boolean|undefined),
|
||||
* }}
|
||||
*/
|
||||
|
|
@ -136,6 +140,8 @@ 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.
|
||||
*
|
||||
* @typedef
|
||||
* {{
|
||||
* mode: Zga.Crypto.Mode,
|
||||
|
|
@ -143,6 +149,7 @@ var PubKeyInfo;
|
|||
* userpwd: (string|undefined),
|
||||
* ownerpwd: (string|undefined),
|
||||
* pubkeys: (Array<PubKeyInfo>|undefined),
|
||||
* allowLegacyEncryption: (boolean|undefined),
|
||||
* }}
|
||||
*/
|
||||
var EncryptOption;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export type EncryptOption = {
|
|||
userpwd?: string;
|
||||
ownerpwd?: string;
|
||||
pubkeys?: Array<PubKeyInfo>;
|
||||
allowLegacyEncryption?: boolean;
|
||||
};
|
||||
export type PubKeyInfo = {
|
||||
c?: Array<number> | Uint8Array | ArrayBuffer | string | forge.pki.Certificate;
|
||||
|
|
@ -77,6 +78,7 @@ export type SignOption = {
|
|||
signame?: string;
|
||||
drawinf?: SignDrawInfo;
|
||||
ltv?: number;
|
||||
minRsaKeyBits?: number;
|
||||
debug?: boolean;
|
||||
};
|
||||
export type TsaServiceInfo = {
|
||||
|
|
|
|||
|
|
@ -308,6 +308,18 @@ z.PdfCryptor = class{
|
|||
this.pubkeys = encopt.pubkeys;
|
||||
/** @private @type {z.Crypto.Mode} */
|
||||
this.mode = /** @type {z.Crypto.Mode} */(encopt.mode);
|
||||
|
||||
// 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){
|
||||
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.");
|
||||
}
|
||||
|
||||
/** @private @type {Array<string>|undefined} */
|
||||
this.permissions = encopt.permissions;
|
||||
/** @private @type {string} */
|
||||
|
|
|
|||
|
|
@ -680,6 +680,25 @@ 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 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){
|
||||
throw new Error("RSA public exponent is too small; CCN-STIC-221 requires log2(e) > 16.");
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Array<forge_cert>} */
|
||||
var certs = [];
|
||||
/** @type {number} */
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"build": "./build.sh",
|
||||
"server": "node test4node.js webserver",
|
||||
"test": "node test4node.js ${pfxpwd}",
|
||||
"test1": "node --test",
|
||||
"test2": "node test4node.js fetch"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
"use strict";
|
||||
|
||||
const {test} = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
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.
|
||||
|
||||
test("rejects RC4-40 encryption by default", () => {
|
||||
assert.throws(
|
||||
() => new Zga.PdfCryptor({mode: Mode.RC4_40, userpwd: "x"}),
|
||||
/not authorized by CCN-STIC-221/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects RC4-128 encryption by default", () => {
|
||||
assert.throws(
|
||||
() => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x"}),
|
||||
/not authorized by CCN-STIC-221/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects AES-128 encryption by default", () => {
|
||||
assert.throws(
|
||||
() => new Zga.PdfCryptor({mode: Mode.AES_128, userpwd: "x"}),
|
||||
/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", () => {
|
||||
assert.doesNotThrow(
|
||||
() => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x", allowLegacyEncryption: true}),
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
"use strict";
|
||||
|
||||
const {test} = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const Zga = require("../lib/zganode.js");
|
||||
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.
|
||||
|
||||
test("rejects an RSA key shorter than 3000 bits", () => {
|
||||
const p12 = makeP12(2048, PWD);
|
||||
const signer = new Zga.PdfSigner({});
|
||||
assert.throws(
|
||||
() => signer.loadP12cert(p12, PWD),
|
||||
/below the 3000-bit minimum/,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts an RSA key of 3072 bits", () => {
|
||||
const p12 = makeP12(3072, PWD);
|
||||
const signer = new Zga.PdfSigner({});
|
||||
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
||||
});
|
||||
|
||||
test("honors a custom minRsaKeyBits threshold below the default", () => {
|
||||
const p12 = makeP12(2048, PWD);
|
||||
const signer = new Zga.PdfSigner({minRsaKeyBits: 2048});
|
||||
assert.doesNotThrow(() => signer.loadP12cert(p12, PWD));
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"use strict";
|
||||
|
||||
const {test} = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const Zga = require("../lib/zganode.js");
|
||||
|
||||
// Characterization tests: lock in existing behavior so future refactors
|
||||
// (e.g. the crypto-agility work) cannot silently change it.
|
||||
|
||||
test("rawToU8arr and u8arrToRaw round-trip binary data", () => {
|
||||
const raw = "\x00\x01\x41\xff\x7e";
|
||||
const u8 = Zga.rawToU8arr(raw);
|
||||
assert.ok(u8 instanceof Uint8Array);
|
||||
assert.strictEqual(u8.length, raw.length);
|
||||
assert.strictEqual(Zga.u8arrToRaw(u8), raw);
|
||||
});
|
||||
|
||||
test("Crypto.Mode enumerates the four PDF encryption handlers", () => {
|
||||
assert.deepStrictEqual(Zga.Crypto.Mode, {RC4_40: 0, RC4_128: 1, AES_128: 2, AES_256: 3});
|
||||
});
|
||||
|
||||
test("getUserPermissionCode clears exactly the print bit when print is blocked", () => {
|
||||
const base = Zga.Crypto.getUserPermissionCode([], Zga.Crypto.Mode.AES_256);
|
||||
const noPrint = Zga.Crypto.getUserPermissionCode(["print"], Zga.Crypto.Mode.AES_256);
|
||||
assert.strictEqual(base - noPrint, Zga.Crypto.Permission["print"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
const Zga = require("../lib/zganode.js");
|
||||
const forge = Zga.forge;
|
||||
|
||||
/** @type {Map<string, string>} in-process cache to avoid regenerating keys */
|
||||
const cache = new Map();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {number} bits RSA modulus length in bits.
|
||||
* @param {string} pwd PKCS#12 password.
|
||||
* @return {string} DER-encoded PKCS#12 as a binary string.
|
||||
*/
|
||||
function makeP12(bits, pwd){
|
||||
const cacheKey = bits + ":" + pwd;
|
||||
if(cache.has(cacheKey)){
|
||||
return cache.get(cacheKey);
|
||||
}
|
||||
const keys = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = "01";
|
||||
cert.validity.notBefore = new Date(2020, 0, 1);
|
||||
cert.validity.notAfter = new Date(2030, 0, 1);
|
||||
const attrs = [{name: "commonName", value: "zgapdfsigner-test"}];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
const asn1 = forge.pkcs12.toPkcs12Asn1(keys.privateKey, [cert], pwd, {algorithm: "3des"});
|
||||
const der = forge.asn1.toDer(asn1).getBytes();
|
||||
cache.set(cacheKey, der);
|
||||
return der;
|
||||
}
|
||||
|
||||
module.exports = {makeP12: makeP12};
|
||||
Loading…
Reference in New Issue