Merge a54166c901 into bbd49bdac0
commit
5fcf0f98cd
|
|
@ -52,6 +52,11 @@ typings/
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
.npm
|
.npm
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
.pnpm-store/
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
# Optional eslint cache
|
# Optional eslint cache
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
|
|
|
||||||
52
README.md
52
README.md
|
|
@ -28,6 +28,58 @@ And I use this name to hope the merits from this application will be dedicated t
|
||||||
* 256bit AES Encryption
|
* 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/).
|
||||||
|
|
||||||
|
## Enforcing CCN-STIC-221 (optional)
|
||||||
|
|
||||||
|
[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.__
|
||||||
|
|
||||||
|
When it is on:
|
||||||
|
|
||||||
|
* __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,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## About signing with [TSA](https://github.com/zboris12/zgapdfsigner/wiki/API#note) and [LTV](https://github.com/zboris12/zgapdfsigner/wiki/API#note)
|
## About signing with [TSA](https://github.com/zboris12/zgapdfsigner/wiki/API#note) and [LTV](https://github.com/zboris12/zgapdfsigner/wiki/API#note)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,15 @@ 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.
|
||||||
*
|
*
|
||||||
|
* 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
|
* @typedef
|
||||||
* {{
|
* {{
|
||||||
* p12cert: (Array<number>|Uint8Array|ArrayBuffer|string|undefined),
|
* p12cert: (Array<number>|Uint8Array|ArrayBuffer|string|undefined),
|
||||||
|
|
@ -107,6 +116,8 @@ 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),
|
||||||
* debug: (boolean|undefined),
|
* debug: (boolean|undefined),
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
|
|
@ -136,6 +147,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.
|
* 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.
|
||||||
*
|
*
|
||||||
|
* 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
|
||||||
* {{
|
* {{
|
||||||
* mode: Zga.Crypto.Mode,
|
* mode: Zga.Crypto.Mode,
|
||||||
|
|
@ -143,6 +156,7 @@ var PubKeyInfo;
|
||||||
* userpwd: (string|undefined),
|
* userpwd: (string|undefined),
|
||||||
* ownerpwd: (string|undefined),
|
* ownerpwd: (string|undefined),
|
||||||
* pubkeys: (Array<PubKeyInfo>|undefined),
|
* pubkeys: (Array<PubKeyInfo>|undefined),
|
||||||
|
* strictCrypto: (boolean|undefined),
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
var EncryptOption;
|
var EncryptOption;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export type EncryptOption = {
|
||||||
userpwd?: string;
|
userpwd?: string;
|
||||||
ownerpwd?: string;
|
ownerpwd?: string;
|
||||||
pubkeys?: Array<PubKeyInfo>;
|
pubkeys?: Array<PubKeyInfo>;
|
||||||
|
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;
|
||||||
|
|
@ -77,6 +78,8 @@ export type SignOption = {
|
||||||
signame?: string;
|
signame?: string;
|
||||||
drawinf?: SignDrawInfo;
|
drawinf?: SignDrawInfo;
|
||||||
ltv?: number;
|
ltv?: number;
|
||||||
|
strictCrypto?: boolean;
|
||||||
|
minRsaKeyBits?: number;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
};
|
};
|
||||||
export type TsaServiceInfo = {
|
export type TsaServiceInfo = {
|
||||||
|
|
@ -98,6 +101,15 @@ export declare class PdfCryptor {
|
||||||
encryptPdf(pdf: PDFLib.PDFDocument | Array<number> | Uint8Array | ArrayBuffer | string, ref?: PDFLib.PDFRef): Promise<PDFLib.PDFDocument>;
|
encryptPdf(pdf: PDFLib.PDFDocument | Array<number> | Uint8Array | ArrayBuffer | string, ref?: PDFLib.PDFRef): Promise<PDFLib.PDFDocument>;
|
||||||
encryptObject(num: number, val: PDFLib.PDFObject): void;
|
encryptObject(num: number, val: PDFLib.PDFObject): void;
|
||||||
}
|
}
|
||||||
|
export declare class RsaSigner {
|
||||||
|
constructor(privateKey: forge.pki.rsa.PrivateKey, certificate: forge.pki.Certificate);
|
||||||
|
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 {
|
export declare class PdfSigner {
|
||||||
constructor(signopt: SignOption);
|
constructor(signopt: SignOption);
|
||||||
sign(pdf: PDFLib.PDFDocument | Array<number> | Uint8Array | ArrayBuffer | string, cypopt?: EncryptOption): Promise<Uint8Array>;
|
sign(pdf: PDFLib.PDFDocument | Array<number> | Uint8Array | ArrayBuffer | string, cypopt?: EncryptOption): Promise<Uint8Array>;
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,17 @@ z.PdfCryptor = class{
|
||||||
this.pubkeys = encopt.pubkeys;
|
this.pubkeys = encopt.pubkeys;
|
||||||
/** @private @type {z.Crypto.Mode} */
|
/** @private @type {z.Crypto.Mode} */
|
||||||
this.mode = /** @type {z.Crypto.Mode} */(encopt.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.
|
||||||
|
// 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 drop strictCrypto to allow legacy encryption.");
|
||||||
|
}
|
||||||
|
|
||||||
/** @private @type {Array<string>|undefined} */
|
/** @private @type {Array<string>|undefined} */
|
||||||
this.permissions = encopt.permissions;
|
this.permissions = encopt.permissions;
|
||||||
/** @private @type {string} */
|
/** @private @type {string} */
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,65 @@ z.NewRefMap = class extends Map{
|
||||||
/** @type {z.NewRefMap<string, z.NewRef>} */
|
/** @type {z.NewRefMap<string, z.NewRef>} */
|
||||||
z.newRefs = new z.NewRefMap();
|
z.newRefs = new z.NewRefMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crypto-agility seam: a Signer produces the raw signature bytes for the
|
||||||
|
* PKCS#7 SignerInfo, keeping the signature algorithm out of the PDF assembly.
|
||||||
|
* RsaSigner captures the current RSA + SHA-256 (PKCS#1 v1.5) behavior.
|
||||||
|
*/
|
||||||
|
z.RsaSigner = class{
|
||||||
|
/**
|
||||||
|
* @param {forge.pki.rsa.PrivateKey} privateKey
|
||||||
|
* @param {forge_cert} certificate
|
||||||
|
*/
|
||||||
|
constructor(privateKey, certificate){
|
||||||
|
/** @type {forge.pki.rsa.PrivateKey} */
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
/** @type {forge_cert} */
|
||||||
|
this.certificate = certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string} the digest algorithm OID used for the PKCS#7 SignerInfo
|
||||||
|
*/
|
||||||
|
getDigestAlgorithmOid(){
|
||||||
|
return forge.pki.oids.sha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string} the signature algorithm OID for the PKCS#7 SignerInfo
|
||||||
|
*/
|
||||||
|
getSignatureAlgorithmOid(){
|
||||||
|
return forge.pki.oids.sha256WithRSAEncryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} data
|
||||||
|
* @return {string} raw signature bytes
|
||||||
|
*/
|
||||||
|
sign(data){
|
||||||
|
/** @type {forge.md.digest} */
|
||||||
|
var md = forge.md.sha256.create();
|
||||||
|
md.update(data);
|
||||||
|
return this.privateKey.sign(md);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a Signer implementation for the given key. Today only RSA keys are
|
||||||
|
* supported; this factory is the single place to extend the seam with ECDSA
|
||||||
|
* or post-quantum signers without touching the PDF assembly.
|
||||||
|
*
|
||||||
|
* @param {forge.pki.rsa.PrivateKey} privateKey
|
||||||
|
* @param {forge_cert} certificate
|
||||||
|
* @return {z.RsaSigner}
|
||||||
|
*/
|
||||||
|
z.createSigner = function(privateKey, certificate){
|
||||||
|
if(privateKey && privateKey.n && privateKey.e){
|
||||||
|
return new z.RsaSigner(privateKey, certificate);
|
||||||
|
}
|
||||||
|
throw new Error("Unsupported signing key type; only RSA keys are supported.");
|
||||||
|
};
|
||||||
|
|
||||||
z.PdfSigner = class{
|
z.PdfSigner = class{
|
||||||
/**
|
/**
|
||||||
* @param {SignOption} signopt
|
* @param {SignOption} signopt
|
||||||
|
|
@ -428,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);
|
||||||
|
|
@ -680,6 +744,33 @@ 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 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(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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {Array<forge_cert>} */
|
/** @type {Array<forge_cert>} */
|
||||||
var certs = [];
|
var certs = [];
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
|
|
@ -993,11 +1084,18 @@ z.PdfSigner = class{
|
||||||
p7.addCertificate(a_cert);
|
p7.addCertificate(a_cert);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects.
|
// Build the signer for this operation. The Signer owns the signature
|
||||||
|
// algorithm choice (digest + key), keeping it out of the PKCS#7 assembly
|
||||||
|
// so it can later be swapped without touching this code. node-forge still
|
||||||
|
// performs the RSA signing via the supplied key (shallow seam).
|
||||||
|
/** @type {z.RsaSigner} */
|
||||||
|
var signer = z.createSigner(_this.privateKey, _this.cchain.getSignCert());
|
||||||
|
|
||||||
|
// Add the signer. sha256 is what Adobe.PPKLite adbe.pkcs7.detached expects.
|
||||||
p7.addSigner({
|
p7.addSigner({
|
||||||
key: _this.privateKey,
|
key: _this.privateKey,
|
||||||
certificate: _this.cchain.getSignCert(),
|
certificate: signer.certificate,
|
||||||
digestAlgorithm: forge.pki.oids.sha256,
|
digestAlgorithm: signer.getDigestAlgorithmOid(),
|
||||||
authenticatedAttributes: [
|
authenticatedAttributes: [
|
||||||
{
|
{
|
||||||
"type": forge.pki.oids.contentType,
|
"type": forge.pki.oids.contentType,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "zgapdfsigner",
|
"name": "zgapdfsigner",
|
||||||
"version": "2.7.6",
|
"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",
|
||||||
|
|
@ -33,8 +33,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
"server": "node test4node.js webserver",
|
"server": "node test4node.js webserver",
|
||||||
"test": "node test4node.js ${pfxpwd}",
|
"test": "node --test",
|
||||||
"test2": "node test4node.js fetch"
|
"test:fixtures": "node testutil/make-demo.js",
|
||||||
|
"test:manual": "node test4node.js",
|
||||||
|
"test:fetch": "node test4node.js fetch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.16.0",
|
"follow-redirects": "^1.16.0",
|
||||||
|
|
|
||||||
63
test4node.js
63
test4node.js
|
|
@ -156,7 +156,10 @@ async function main1(angle){
|
||||||
var imgPath = m_path.join(__dirname, workpath+"_test.png");
|
var imgPath = m_path.join(__dirname, workpath+"_test.png");
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
var fontPath = m_path.join(__dirname, workpath+"_test.ttf");
|
var fontPath = m_path.join(__dirname, workpath+"_test.ttf");
|
||||||
// var fontPath = Zga.PDFLib.StandardFonts.CourierBold;
|
if(!m_fs.existsSync(fontPath)){
|
||||||
|
// Fall back to a built-in font when no custom TTF has been supplied.
|
||||||
|
fontPath = Zga.PDFLib.StandardFonts.CourierBold;
|
||||||
|
}
|
||||||
|
|
||||||
if(process.argv.length > 3){
|
if(process.argv.length > 3){
|
||||||
pfxPath = process.argv[2];
|
pfxPath = process.argv[2];
|
||||||
|
|
@ -171,7 +174,12 @@ async function main1(angle){
|
||||||
}
|
}
|
||||||
|
|
||||||
if(pfxPath){
|
if(pfxPath){
|
||||||
|
// Standard fonts are WinAnsi-encoded and cannot render Japanese text.
|
||||||
|
if(Zga.PDFLib.isStandardFont(fontPath)){
|
||||||
|
await sign_protect(pdfPath, pfxPath, ps, 1, imgPath, "This is a test of text!\n", fontPath);
|
||||||
|
}else{
|
||||||
await sign_protect(pdfPath, pfxPath, ps, 1, imgPath, "あいうえおあいうえおか\r\n\nThis is a test of text!\n", fontPath);
|
await sign_protect(pdfPath, pfxPath, ps, 1, imgPath, "あいうえおあいうえおか\r\n\nThis is a test of text!\n", fontPath);
|
||||||
|
}
|
||||||
if(Zga.PDFLib.isStandardFont(fontPath)){
|
if(Zga.PDFLib.isStandardFont(fontPath)){
|
||||||
pdfPath = await sign_protect(pdfPath, pfxPath, ps, 2, imgPath, "This is an another test of text!\n", fontPath);
|
pdfPath = await sign_protect(pdfPath, pfxPath, ps, 2, imgPath, "This is an another test of text!\n", fontPath);
|
||||||
pdfPath = await sign_protect(pdfPath, pfxPath, ps, 0, undefined, "This is a test for same font!\n", fontPath);
|
pdfPath = await sign_protect(pdfPath, pfxPath, ps, 0, undefined, "This is a test for same font!\n", fontPath);
|
||||||
|
|
@ -189,8 +197,15 @@ async function main1(angle){
|
||||||
|
|
||||||
// test urlFetch
|
// test urlFetch
|
||||||
async function main2(){
|
async function main2(){
|
||||||
|
// Boot a throwaway server on an ephemeral port so this test is self
|
||||||
|
// contained and cannot collide with an already running `npm run server`.
|
||||||
|
/** @type {http.Server} */
|
||||||
|
var srv = await startWebserver(0);
|
||||||
|
/** @type {number} */
|
||||||
|
var port = srv.address().port;
|
||||||
|
try{
|
||||||
/** @type {Uint8Array} */
|
/** @type {Uint8Array} */
|
||||||
var u8arr = await Zga.urlFetch("http://localhost:8080", {
|
var u8arr = await Zga.urlFetch("http://localhost:"+port, {
|
||||||
"headers": {
|
"headers": {
|
||||||
"testzb": "pineapple"
|
"testzb": "pineapple"
|
||||||
}
|
}
|
||||||
|
|
@ -202,10 +217,18 @@ async function main2(){
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
var str = txtdec.decode(u8arr);
|
var str = txtdec.decode(u8arr);
|
||||||
console.log(str);
|
console.log(str);
|
||||||
|
}finally{
|
||||||
|
srv.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function webserver(){
|
/**
|
||||||
require("http").createServer(function(req, res){
|
* @param {number} port Port to listen on. 0 picks a free ephemeral port.
|
||||||
|
* @return {Promise<http.Server>} The listening server.
|
||||||
|
*/
|
||||||
|
function startWebserver(port){
|
||||||
|
/** @type {http.Server} */
|
||||||
|
var srv = require("http").createServer(function(req, res){
|
||||||
if(req.method == "GET"){
|
if(req.method == "GET"){
|
||||||
if(req.headers["testzb"]){
|
if(req.headers["testzb"]){
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
@ -233,7 +256,14 @@ function webserver(){
|
||||||
res.statusMessage = "CORS OK";
|
res.statusMessage = "CORS OK";
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
}).listen(8080, function(){console.log("Server http://localhost:8080")});
|
});
|
||||||
|
return new Promise(function(resolve, reject){
|
||||||
|
srv.once("error", reject);
|
||||||
|
srv.listen(port, function(){
|
||||||
|
console.log("Server http://localhost:"+srv.address().port);
|
||||||
|
resolve(srv);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(){
|
async function main(){
|
||||||
|
|
@ -246,10 +276,21 @@ async function main(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(process.argv[2] == "webserver"){
|
/**
|
||||||
webserver();
|
* @param {Promise<*>} p
|
||||||
}else if(process.argv[2] == "fetch"){
|
*/
|
||||||
main2();
|
function run(p){
|
||||||
}else{
|
p.catch(function(err){
|
||||||
main();
|
console.error(err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(process.argv[2] == "webserver"){
|
||||||
|
// Fixed port, because test.html expects the server on 8080.
|
||||||
|
run(startWebserver(8080));
|
||||||
|
}else if(process.argv[2] == "fetch"){
|
||||||
|
run(main2());
|
||||||
|
}else{
|
||||||
|
run(main());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"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. 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("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", strictCrypto: true}),
|
||||||
|
/not authorized by CCN-STIC-221/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects RC4-128 encryption under strictCrypto", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => new Zga.PdfCryptor({mode: Mode.RC4_128, userpwd: "x", strictCrypto: true}),
|
||||||
|
/not authorized by CCN-STIC-221/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects AES-128 encryption under strictCrypto", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => new Zga.PdfCryptor({mode: Mode.AES_128, userpwd: "x", strictCrypto: true}),
|
||||||
|
/not authorized by CCN-STIC-221/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts AES-256 encryption under strictCrypto", () => {
|
||||||
|
assert.doesNotThrow(
|
||||||
|
() => new Zga.PdfCryptor({mode: Mode.AES_256, userpwd: "x", strictCrypto: true}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
"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 and the public exponent must
|
||||||
|
// 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("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 under strictCrypto", () => {
|
||||||
|
const p12 = makeP12(3072, PWD);
|
||||||
|
const signer = new Zga.PdfSigner({strictCrypto: true});
|
||||||
|
assert.doesNotThrow(() => signer.loadP12cert(p12, 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 under strictCrypto", () => {
|
||||||
|
const p12 = makeP12(1024, PWD, 3);
|
||||||
|
const signer = new Zga.PdfSigner({strictCrypto: true, minRsaKeyBits: 1024});
|
||||||
|
assert.throws(
|
||||||
|
() => signer.loadP12cert(p12, PWD),
|
||||||
|
/public exponent is too small/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
|
@ -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,85 @@
|
||||||
|
"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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a minimal one-page PDF with pdf-lib.
|
||||||
|
* @return {Promise<Uint8Array>}
|
||||||
|
*/
|
||||||
|
async function minimalPdf(){
|
||||||
|
const doc = await Zga.PDFLib.PDFDocument.create();
|
||||||
|
doc.addPage([300, 300]);
|
||||||
|
return doc.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// End-to-end safety net for the crypto-agility refactor: signing a real PDF
|
||||||
|
// must keep producing a valid detached PKCS#7 signature dictionary.
|
||||||
|
test("PdfSigner.sign produces a detached PKCS#7 signature over a real PDF", async () => {
|
||||||
|
const pdfBytes = await minimalPdf();
|
||||||
|
const signer = new Zga.PdfSigner({p12cert: makeP12(3072, PWD), pwd: PWD});
|
||||||
|
|
||||||
|
const signed = await signer.sign(pdfBytes);
|
||||||
|
const dump = Buffer.from(signed).toString("latin1");
|
||||||
|
|
||||||
|
assert.ok(signed instanceof Uint8Array, "returns a Uint8Array");
|
||||||
|
assert.ok(signed.length > pdfBytes.length, "signed output is larger than the input");
|
||||||
|
assert.match(dump, /adbe\.pkcs7\.detached/, "uses the detached PKCS#7 SubFilter");
|
||||||
|
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 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/);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {test} = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const Zga = require("../lib/zganode.js");
|
||||||
|
const {makeKeyCert} = require("../testutil/fixtures.js");
|
||||||
|
const forge = Zga.forge;
|
||||||
|
|
||||||
|
// Crypto-agility seam (Update 4): a Signer abstraction decouples the signature
|
||||||
|
// algorithm from the PDF/PKCS#7 assembly. RsaSigner captures the current
|
||||||
|
// RSA + SHA-256 behavior behind that interface.
|
||||||
|
|
||||||
|
test("RsaSigner.sign produces a SHA-256 RSA signature that verifies against the certificate public key", () => {
|
||||||
|
const {privateKey, certificate} = makeKeyCert(3072);
|
||||||
|
const signer = new Zga.RsaSigner(privateKey, certificate);
|
||||||
|
const data = "hello zgapdfsigner";
|
||||||
|
|
||||||
|
const signature = signer.sign(data);
|
||||||
|
|
||||||
|
const md = forge.md.sha256.create();
|
||||||
|
md.update(data);
|
||||||
|
assert.strictEqual(certificate.publicKey.verify(md.digest().bytes(), signature), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("RsaSigner reports SHA-256 as its digest algorithm OID", () => {
|
||||||
|
const {privateKey, certificate} = makeKeyCert(3072);
|
||||||
|
const signer = new Zga.RsaSigner(privateKey, certificate);
|
||||||
|
|
||||||
|
assert.strictEqual(signer.getDigestAlgorithmOid(), forge.pki.oids.sha256);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("RsaSigner reports sha256WithRSAEncryption as its signature algorithm OID", () => {
|
||||||
|
const {privateKey, certificate} = makeKeyCert(3072);
|
||||||
|
const signer = new Zga.RsaSigner(privateKey, certificate);
|
||||||
|
|
||||||
|
assert.strictEqual(signer.getSignatureAlgorithmOid(), forge.pki.oids.sha256WithRSAEncryption);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createSigner returns an RsaSigner for an RSA key", () => {
|
||||||
|
const {privateKey, certificate} = makeKeyCert(3072);
|
||||||
|
|
||||||
|
const signer = Zga.createSigner(privateKey, certificate);
|
||||||
|
|
||||||
|
assert.ok(signer instanceof Zga.RsaSigner);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createSigner rejects an unsupported (non-RSA) key type", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => Zga.createSigner({}, null),
|
||||||
|
/only RSA keys are supported/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Zga = require("../lib/zganode.js");
|
||||||
|
const forge = Zga.forge;
|
||||||
|
|
||||||
|
/** @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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate (and cache) a self-signed RSA key pair + certificate of the given
|
||||||
|
* 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, 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: exponent});
|
||||||
|
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 entry = {privateKey: keys.privateKey, publicKey: keys.publicKey, certificate: cert};
|
||||||
|
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 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, e){
|
||||||
|
const kc = makeKeyCert(bits, e);
|
||||||
|
const asn1 = forge.pkcs12.toPkcs12Asn1(kc.privateKey, [kc.certificate], pwd, {algorithm: "3des"});
|
||||||
|
return forge.asn1.toDer(asn1).getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {makeKeyCert: makeKeyCert, makeP12: makeP12};
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the demo fixtures that test4node.js reads from the (gitignored)
|
||||||
|
* `test/` directory: a source PDF for every rotation the manual demo walks
|
||||||
|
* through, a stamp image, and a self-signed PKCS#12 certificate.
|
||||||
|
*
|
||||||
|
* Usage: node testutil/make-demo.js [pfxPassword]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const m_fs = require("fs");
|
||||||
|
const m_path = require("path");
|
||||||
|
const m_zlib = require("zlib");
|
||||||
|
const Zga = require("../lib/zganode.js");
|
||||||
|
const {makeP12} = require("./fixtures.js");
|
||||||
|
|
||||||
|
/** @type {string} Output directory, matching the `workpath` of test4node.js. */
|
||||||
|
const outDir = m_path.join(__dirname, "..", "test");
|
||||||
|
/** @type {Array<number>} Page rotations the manual demo iterates over. */
|
||||||
|
const ROTATIONS = [0, 90, 180, 270];
|
||||||
|
/** @type {string} */
|
||||||
|
const DEFAULT_PWD = "zgatest";
|
||||||
|
/** @type {number} CCN-STIC-221 requires at least 3000 bits. */
|
||||||
|
const RSA_BITS = 3072;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a one-page demo PDF with the given page rotation.
|
||||||
|
*
|
||||||
|
* @param {number} angle Page rotation in degrees.
|
||||||
|
* @return {Promise<Uint8Array>}
|
||||||
|
*/
|
||||||
|
async function makePdf(angle){
|
||||||
|
const doc = await Zga.PDFLib.PDFDocument.create();
|
||||||
|
const font = await doc.embedFont(Zga.PDFLib.StandardFonts.Helvetica);
|
||||||
|
const page = doc.addPage([595, 842]);
|
||||||
|
page.drawText("zgapdfsigner demo document", {x: 60, y: 760, size: 18, font: font});
|
||||||
|
page.drawText("Page rotation: " + angle + " degrees", {x: 60, y: 730, size: 12, font: font});
|
||||||
|
page.drawText("Generated by testutil/make-demo.js", {x: 60, y: 710, size: 10, font: font});
|
||||||
|
if(angle){
|
||||||
|
page.setRotation(Zga.PDFLib.degrees(angle));
|
||||||
|
}
|
||||||
|
return doc.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRC-32 as specified by the PNG format.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buf
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
function crc32(buf){
|
||||||
|
let crc = 0xffffffff;
|
||||||
|
for(let i = 0; i < buf.length; i++){
|
||||||
|
crc ^= buf[i];
|
||||||
|
for(let bit = 0; bit < 8; bit++){
|
||||||
|
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (crc ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a payload into a length-prefixed, CRC-suffixed PNG chunk.
|
||||||
|
*
|
||||||
|
* @param {string} type Four-character chunk type.
|
||||||
|
* @param {Buffer} data
|
||||||
|
* @return {Buffer}
|
||||||
|
*/
|
||||||
|
function pngChunk(type, data){
|
||||||
|
const head = Buffer.alloc(4);
|
||||||
|
head.writeUInt32BE(data.length, 0);
|
||||||
|
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
||||||
|
const tail = Buffer.alloc(4);
|
||||||
|
tail.writeUInt32BE(crc32(body), 0);
|
||||||
|
return Buffer.concat([head, body, tail]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a small solid-color PNG, used as the signature stamp image.
|
||||||
|
*
|
||||||
|
* @return {Buffer}
|
||||||
|
*/
|
||||||
|
function makePng(){
|
||||||
|
const size = 64;
|
||||||
|
const stride = 1 + size * 3;
|
||||||
|
const raw = Buffer.alloc(size * stride);
|
||||||
|
for(let y = 0; y < size; y++){
|
||||||
|
const row = y * stride;
|
||||||
|
raw[row] = 0; // filter type: none
|
||||||
|
for(let x = 0; x < size; x++){
|
||||||
|
const px = row + 1 + x * 3;
|
||||||
|
const edge = x < 4 || y < 4 || x >= size - 4 || y >= size - 4;
|
||||||
|
raw[px] = edge ? 0x1f : 0x8a;
|
||||||
|
raw[px + 1] = edge ? 0x6f : 0xc8;
|
||||||
|
raw[px + 2] = edge ? 0xd5 : 0xf0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ihdr = Buffer.alloc(13);
|
||||||
|
ihdr.writeUInt32BE(size, 0);
|
||||||
|
ihdr.writeUInt32BE(size, 4);
|
||||||
|
ihdr[8] = 8; // bit depth
|
||||||
|
ihdr[9] = 2; // color type: truecolor
|
||||||
|
return Buffer.concat([
|
||||||
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||||
|
pngChunk("IHDR", ihdr),
|
||||||
|
pngChunk("IDAT", m_zlib.deflateSync(raw)),
|
||||||
|
pngChunk("IEND", Buffer.alloc(0)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(){
|
||||||
|
const pwd = process.argv[2] || DEFAULT_PWD;
|
||||||
|
m_fs.mkdirSync(outDir, {recursive: true});
|
||||||
|
|
||||||
|
for(const angle of ROTATIONS){
|
||||||
|
const name = "_test" + (angle ? "_" + angle : "") + ".pdf";
|
||||||
|
m_fs.writeFileSync(m_path.join(outDir, name), await makePdf(angle));
|
||||||
|
console.log("Wrote " + m_path.join(outDir, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pngPath = m_path.join(outDir, "_test.png");
|
||||||
|
m_fs.writeFileSync(pngPath, makePng());
|
||||||
|
console.log("Wrote " + pngPath);
|
||||||
|
|
||||||
|
const pfxPath = m_path.join(outDir, "_test.pfx");
|
||||||
|
m_fs.writeFileSync(pfxPath, Buffer.from(makeP12(RSA_BITS, pwd), "latin1"));
|
||||||
|
console.log("Wrote " + pfxPath + " (password: " + pwd + ")");
|
||||||
|
|
||||||
|
console.log("\nRun the manual demo with: npm run test:manual -- " + pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(function(err){
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue