From 3f0dd0af9feef73e1bff8af9d4c46e6b83349349 Mon Sep 17 00:00:00 2001 From: zboris12 Date: Sun, 6 Nov 2022 18:03:48 +0900 Subject: [PATCH] Changed folder structure and working on LTV. --- closure/build.bat | 6 +- closure/forge-ext.js | 87 +- closure/pdflib-ext.js | 17 +- closure/zb-externs.js | 73 +- lib/zgacertsutil.js | 1017 ++++++++++++++++++++++ lib/zgaindex.js | 69 ++ lib/zganode.js | 75 ++ zgapdfcryptor.js => lib/zgapdfcryptor.js | 61 +- zgapdfsigner.js => lib/zgapdfsigner.js | 526 ++++++----- package.json | 26 +- test4node.js | 109 +-- 11 files changed, 1717 insertions(+), 349 deletions(-) create mode 100644 lib/zgacertsutil.js create mode 100644 lib/zgaindex.js create mode 100644 lib/zganode.js rename zgapdfcryptor.js => lib/zgapdfcryptor.js (95%) rename zgapdfsigner.js => lib/zgapdfsigner.js (74%) diff --git a/closure/build.bat b/closure/build.bat index 5503f2b..0579560 100644 --- a/closure/build.bat +++ b/closure/build.bat @@ -5,12 +5,12 @@ doskey csr=%csr% $* set externs=--externs closure\google-ext.js --externs closure\forge-ext.js --externs closure\pdflib-ext.js --externs closure\zb-externs.js rem main -set src=. -set jss=--js %src%\zgapdfcryptor.js --js %src%\zgapdfsigner.js +set src=lib +set jss=--js %src%\zgacertsutil.js --js %src%\zgapdfcryptor.js --js %src%\zgapdfsigner.js --js %src%\zgaindex.js echo $ set chkj=%%externs%% --checks_only %%jss%% echo chkj=csr %chkj% doskey chkj=%csr% %chkj% -set csrj=%%externs%% %%jss%% --js_output_file %src%\dist\zgapdfsigner.min.js +set csrj=%%externs%% %%jss%% --js_output_file dist\zgapdfsigner.min.js echo csrj=csr %csrj% doskey csrj=%csr% %csrj% diff --git a/closure/forge-ext.js b/closure/forge-ext.js index 9979d0e..cb16390 100644 --- a/closure/forge-ext.js +++ b/closure/forge-ext.js @@ -67,6 +67,13 @@ forge.util.hexToBytes = function(hex){}; * @return {string} */ forge.util.decodeUtf8 = function(value){}; +forge.util.binary = {}; +forge.util.binary.hex = {}; +/** + * @param {string} str + * @return {Uint8Array} + */ +forge.util.binary.hex.decode = function(str){}; /** @constructor */ forge.asn1 = function(){}; @@ -119,6 +126,12 @@ forge.asn1.Type.OID; forge.asn1.Type.NULL; /** @type {number} */ forge.asn1.Type.OCTETSTRING; +/** @type {number} */ +forge.asn1.Type.PRINTABLESTRING; +/** @type {number} */ +forge.asn1.Type.ENUMERATED; +/** @type {number} */ +forge.asn1.Type.BITSTRING; forge.asn1.Class = {}; /** @type {number} */ forge.asn1.Class.UNIVERSAL; @@ -133,8 +146,21 @@ forge.asn1.Class.CONTEXT_SPECIFIC; * @return {forge.asn1} */ forge.asn1.create = function(tagClass, type, constructed, value, options){}; -/** @type {Array} */ +/** + * @param {forge.asn1} obj + * @param {Object} v + * @param {Object} capture + * @param {Array} errors + */ +forge.asn1.validate = function(obj, v, capture, errors){}; +/** @type {Array|string} */ forge.asn1.prototype.value; +/** @type {number} */ +forge.asn1.prototype.tagClass; +/** @type {number} */ +forge.asn1.prototype.type; +/** @type {boolean} */ +forge.asn1.prototype.constructed; /** @constructor */ const forge_BigInteger = function(){}; @@ -149,6 +175,20 @@ const forge_cert = function(){}; forge_cert.prototype.publicKey; /** @type {forge_cert_issuer} */ forge_cert.prototype.issuer; +/** @type {string} */ +forge_cert.prototype.serialNumber; +/** @type {forge_cert_issuer} */ +forge_cert.prototype.subject; +/** + * @param {forge_cert} parent + * @return {boolean} + */ +forge_cert.prototype.isIssuer = function(parent){}; +/** + * @param {string|forge_cert_extension} nm + * @return {forge_cert_extension} + */ +forge_cert.prototype.getExtension = function(nm){}; /** @constructor */ const forge_key = function(){}; /** @type {forge_BigInteger} */ @@ -159,6 +199,23 @@ forge_key.prototype.e; const forge_cert_issuer = function(){}; /** @type {Array} */ forge_cert_issuer.prototype.attributes; +/** + * @param {string} sn + * @return {forge.asn1} + */ +forge_cert_issuer.prototype.getField = function(sn){}; +/** + * @typedef + * {{ + * id: (string|undefined), + * name: (string|undefined), + * critical: (boolean|undefined), + * value: (string|undefined), + * cA: (boolean|undefined), + * }} + */ +var forge_cert_extension; + /** * @typedef * {{ @@ -263,9 +320,6 @@ var P12Bag; */ forge.pkcs12.prototype.getBags = function(filter){}; -forge.oids = {}; -/** @type {string} */ -forge.oids.sha256; forge.pki = {}; forge.pki.oids = {}; /** @type {string} */ @@ -273,6 +327,8 @@ forge.pki.oids.certBag; /** @type {string} */ forge.pki.oids.pkcs8ShroudedKeyBag; /** @type {string} */ +forge.pki.oids.sha1; +/** @type {string} */ forge.pki.oids.sha256; /** @type {string} */ forge.pki.oids.contentType; @@ -282,12 +338,35 @@ forge.pki.oids.data; forge.pki.oids.messageDigest; /** @type {string} */ forge.pki.oids.signingTime; +/** @type {string} */ +forge.pki.oids.rsaEncryption; +/** @type {string} */ +forge.pki.oids.sha256WithRSAEncryption; +/** @type {string} */ +forge.pki.oids.commonName; +/** @lends {forge.pki.oids} */ +forge.oids = forge.pki.oids; /** * @param {forge.asn1} obj * @param {boolean=} computeHash * @return {forge_cert} */ forge.pki.certificateFromAsn1 = function(obj, computeHash){}; +/** + * @param {forge_cert} cert + * @return {forge.asn1} + */ +forge.pki.certificateToAsn1 = function(cert){}; +/** + * @param {forge_cert_issuer} obj + * @return {forge.asn1} + */ +forge.pki.distinguishedNameToAsn1 = function(obj){}; +/** + * @param {forge_key} key + * @return {forge.asn1} + */ +forge.pki.publicKeyToRSAPublicKey = function(key){}; forge.md = {}; /** @constructor */ diff --git a/closure/pdflib-ext.js b/closure/pdflib-ext.js index a861c11..98fba94 100644 --- a/closure/pdflib-ext.js +++ b/closure/pdflib-ext.js @@ -108,7 +108,10 @@ PDFLib.PDFAcroForm.prototype.addField = function(field){}; /** @type {PDFLib.PDFDict} */ PDFLib.PDFAcroForm.prototype.dict; -/** @constructor */ +/** + * @constructor + * @extends {PDFLib.PDFDict} + */ PDFLib.PDFCatalog = function(){}; /** * @param {PDFLib.PDFName} name @@ -196,6 +199,7 @@ PDFLib.PDFContext.prototype.enumerateIndirectObjects = function(){}; * @typedef * {{ * Root: PDFLib.PDFRef, + * Info: PDFLib.PDFRef, * ID: (PDFLib.PDFArray|undefined), * }} */ @@ -221,6 +225,12 @@ PDFLib.PDFContext.prototype.nextRef = function(){}; * @return {PDFLib.PDFObject} */ PDFLib.PDFContext.prototype.obj = function(literal){}; +/** + * @param {string|Uint8Array} contents + * @param {PDFLib.PDFDict=} dict + * @return {PDFLib.PDFRawStream} + */ +PDFLib.PDFContext.prototype.flateStream = function(contents, dict){}; /** * @param {PDFLib.PDFRef} ref * @return {PDFLib.PDFObject} @@ -425,6 +435,11 @@ PDFLib.PDFContentStream = function(){}; * @return {PDFLib.PDFContentStream} */ PDFLib.PDFContentStream.of = function(dict, operators, encode){}; +/** + * @constructor + * @extends {PDFLib.PDFStream} + */ +PDFLib.PDFRawStream = function(){}; /** * @constructor diff --git a/closure/zb-externs.js b/closure/zb-externs.js index aab5191..7bd435c 100644 --- a/closure/zb-externs.js +++ b/closure/zb-externs.js @@ -34,12 +34,16 @@ var SignAreaInfo; var SignDrawInfo; /** * In the case of adding a document timestamp, the p12cert and pwd must be omitted. But meanwhile the tsa must be provided. - * + * * permission: (DocMDP) The modification permissions granted for this document. Valid values are: * 1 : No changes to the document are permitted; any change to the document invalidates the signature. * 2 : Permitted changes are filling in forms, instantiating page templates, and signing; other changes invalidate the signature. * 3 : Permitted changes are the same as for 2, as well as annotation creation, deletion, and modification; other changes invalidate the signature. * + * ltv: Type of Long-Term Validation. Valid values are: + * 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. + * * @typedef * {{ * p12cert: (Array|Uint8Array|ArrayBuffer|string|undefined), @@ -51,6 +55,7 @@ var SignDrawInfo; * signdate: (Date|TsaServiceInfo|string|undefined), * signame: (string|undefined), * drawinf: (SignDrawInfo|undefined), + * ltv: (number|undefined), * debug: (boolean|undefined), * }} */ @@ -110,6 +115,23 @@ var CFType; * }} */ var RC4LastInfo; +/** + * @typedef + * {{ + * certs: (Array|undefined), + * ocsps: (Array|undefined), + * crls: (Array|undefined), + * }} + */ +var DSSInfo; +/** + * @typedef + * {{ + * resp: (Uint8Array|undefined), + * cchainIdx: (number|undefined), + * }} + */ +var OcspData; var Zga = {}; /** @@ -142,6 +164,55 @@ Zga.PdfCryptor = function(encopt){}; * @return {Promise} */ Zga.PdfCryptor.prototype.encryptPdf = function(pdf, ref){}; +/** + * @constructor + * @param {Array=} certs + */ +Zga.CertsChain = function(certs){}; +/** + * @return {forge_cert} + */ +Zga.CertsChain.prototype.getSignCert = function(){}; +/** + * @public + * @return {boolean} + */ +Zga.CertsChain.prototype.isSelfSignedCert = function(){}; +/** + * @return {Array} + */ +Zga.CertsChain.prototype.getAllCerts = function(){}; +/** + * @public + * @param {forge_cert} cert + * @return {Promise} + */ +Zga.CertsChain.prototype.buildChain = function(cert){}; +/** + * @param {boolean=} crlOnly + * @return {Promise} + */ +Zga.CertsChain.prototype.prepareDSSInf = function(crlOnly){}; +/** + * @constructor + * @param {TsaServiceInfo} inf + */ +Zga.TsaFetcher = function(inf){}; +/** + * @param {string=} data + * @return {Promise} + */ +Zga.TsaFetcher.prototype.queryTsa = function(data){}; +/** + * @param {boolean=} forP7 + * @return {forge.asn1} + */ +Zga.TsaFetcher.prototype.getToken = function(forP7){}; +/** + * @return {Zga.CertsChain} + */ +Zga.TsaFetcher.prototype.getCertsChain = function(){}; + /** * @constructor * @param {SignOption} signopt diff --git a/lib/zgacertsutil.js b/lib/zgacertsutil.js new file mode 100644 index 0000000..4ad3be1 --- /dev/null +++ b/lib/zgacertsutil.js @@ -0,0 +1,1017 @@ +/** + * @param {Object} z + */ +function supplyZgaCertsChain(z){ + +//Only for nodejs Start// +if(z.forge){ + var forge = z.forge; +} +//Only for nodejs End// + +/** + * When converting the issuer of signature to asn1, forge will encode the + * value of issuer to utf8 if the valueTagClass is UTF8. + * But the value load from a file which is DER format, is already utf8 encoded, + * so the encoding action will break the final data. + * To avoid the broken data issue, we decode the value before the later actions. + * @param {forge_cert} cert + */ +z.fixCertAttributes = function(cert){ + cert.issuer.attributes.forEach(function(a_ele){ + if(a_ele.valueTagClass === forge.asn1.Type.UTF8){ + a_ele.value = forge.util.decodeUtf8(a_ele.value); + } + }); +}; + +/** + * @param {forge.asn1|string} asnder + * @return {forge_cert} + */ +z.loadCert = function(asnder){ + /** @type {forge.asn1} */ + var asn1 = null; + if(typeof asnder === "string"){ + asn1 = forge.asn1.fromDer(asnder); + }else{ + asn1 = asnder; + } + /** @type {forge_cert} */ + var cert = forge.pki.certificateFromAsn1(asn1); + return cert; +}; + +/** + * @param {string} oid + * @return {string} + */ +z.oidToDstr = function(oid){ + return forge.asn1.oidToDer(oid).getBytes(); +}; + +/** + * @param {forge.asn1} asn1 + * @param {Object} vadt + * @param {string|Array=} oid + * @return {Object} + */ +z.parseAsn1 = function(asn1, vadt, oid){ + /** @type {Object} */ + var capt = {}; + /** @type {Array} */ + var errs = []; + forge.asn1.validate(asn1, vadt, capt, errs); + if(errs.length > 0){ +// console.log(errs); + return null; + }else if(oid){ + if(capt.oid){ + if(Array.isArray(oid)){ + if(oid.indexOf(capt.oid) >= 0){ + return capt; + }else{ + return null; + } + }else if(oid == capt.oid){ + return capt; + }else{ + return null; + } + }else{ + return null; + } + }else{ + return capt; + } +}; + +/** + * @param {string|number|boolean|forge.asn1|Array} aval + * @param {number=} atyp + * @param {number=} atag + * @return {forge.asn1} + */ +z.createAsn1 = function(aval, atyp, atag){ + /** @lends {forge.asn1} */ + const asn1 = forge.asn1; + /** @lends {forge.asn1.Class} */ + const asnc = asn1.Class; + /** @lends {forge.asn1.Type} */ + const asnt = asn1.Type; + /** @type {string|number|forge.asn1|Array} */ + var a_val = null; + /** @type {number} */ + var a_typ = (atyp || atyp === asnt.NONE) ? atyp : -1; + /** @type {boolean} */ + var a_con = false; + if(Array.isArray(aval)){ + a_con = true; + }else{ + switch(typeof aval){ + case "string": + if(a_typ == asnt.OID){ + a_val = asn1.oidToDer(aval).getBytes(); + }else if(a_typ < 0){ + a_typ = asnt.OCTETSTRING; + } + break; + case "number": + a_val = asn1.integerToDer(aval).getBytes(); + if(a_typ < 0){ + a_typ = asnt.INTEGER; + } + break; + case "boolean": + if(aval){ + a_val = 1; + }else{ + a_val = 0; + } + if(a_typ < 0){ + a_typ = asnt.BOOLEAN; + } + } + } + if(a_typ < 0){ + a_typ = asnt.SEQUENCE; + } + if(!a_val && a_val !== 0){ + a_val = /** @type {Array|forge.asn1|number|string} */(aval); + } + return asn1.create(atag ? atag : asnc.UNIVERSAL, a_typ, a_con, a_val); +}; + +/** + * @param {string} url + * @param {forge.asn1} asn1Req + * @param {Object} headers + * @return {Promise} + */ +z.queryAsn1 = async function(url, asn1Req, headers){ + /** @type {string} */ + var tsr = forge.asn1.toDer(asn1Req).getBytes(); + /** @type {Uint8Array} */ + var tu8s = z.rawToU8arr(tsr); + /** @type {UrlFetchParams} */ + var options = { + "method": "POST", + "headers": headers, + "payload": tu8s, + }; + /** @type {Uint8Array} */ + var tesp = await z.urlFetch(url, options); + // /** @type {string} */ + // var tstr = z.u8arrToRaw(tesp); + return tesp; +}; + +/** + * @param {forge_cert} cert1 + * @param {forge_cert} cert2 + * @return {boolean} + */ +z.sameCert = function(cert1, cert2){ + /** @type {forge_cert_issuer} */ + var c1 = cert1.subject; + /** @type {forge_cert_issuer} */ + var c2 = cert2.subject; + if(c1.attributes.length === c2.attributes.length) { + // all attributes are the same so issuer matches subject + /** @type {?forge_cert_attr} */ + var attr1 = null; + /** @type {?forge_cert_attr} */ + var attr2 = null; + for(var n = 0; n < c1.attributes.length; n++) { + attr1 = c1.attributes[n]; + attr2 = c2.attributes[n]; + if(attr1.type !== attr2.type || attr1.value !== attr2.value){ + // attribute mismatch + return false; + } + } + return true; + }else{ + return false; + } +}; + +z.CertsChain = class{ + /** + * @param {Array=} certs + */ + constructor(certs){ + /** @private @lends {forge.asn1} */ + this.asn1 = forge.asn1; + /** @private @lends {forge.asn1.Class} */ + this.asnc = forge.asn1.Class; + /** @private @lends {forge.asn1.Type} */ + this.asnt = forge.asn1.Type; + /** @private @const {string} */ + this.ocsp_oid = z.oidToDstr("1.3.6.1.5.5.7.48.1"); + /** @private @const {string} */ + this.issuer_oid = z.oidToDstr("1.3.6.1.5.5.7.48.2"); + /** @private @const {string} */ + this.ocspBasic_oid = z.oidToDstr("1.3.6.1.5.5.7.48.1.1"); + /** @private @const {Object} */ + this.oidval_validator = { + name: "Seq", + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + value: [{ + name: "Oid", + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.OID, + constructed: false, + capture: "oid", + }, { + name: "Ovalue", + tagClass: this.asnc.CONTEXT_SPECIFIC, + type: 6, + constructed: false, + capture: "oval", + }], + }; + /** @private @const {Object} */ + this.oresp_validator = { + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + value: [{ + name: "Status", + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.ENUMERATED, + constructed: false, + capture: "status", + }, { + tagClass: this.asnc.CONTEXT_SPECIFIC, + type: 0, + constructed: true, + value: [{ + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + value: [{ + name: "Oid", + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.OID, + constructed: false, + capture: "oid", + }, { + name: "OcspBasic", + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.OCTETSTRING, + constructed: false, + capture: "ob", + }], + }], + }], + }; + /** @private @const {Object} */ + this.ob_validator = { + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + value: [{ + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + }, { + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + }, { + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.BITSTRING, + constructed: false, + }, { + tagClass: this.asnc.CONTEXT_SPECIFIC, + type: 0, + constructed: true, + optional: true, + value: [{ + name: "Certs", + tagClass: this.asnc.UNIVERSAL, + type: this.asnt.SEQUENCE, + constructed: true, + capture: "certs", + }], + }], + }; + + /** @private @type {Array} */ + this.certs = null; + /** @private @type {forge_cert} */ + this.rootCert = null; + /** @private @type {Array} */ + this.crls = null; + /** @private @type {Array} */ + this.ocspDatas = null; + /** @private @type {Array} */ + this.extraChains = null; + /** @private @type {boolean} */ + this.ocspOk = false; + + if(certs){ + this.setCerts(certs); + } + } + + /** + * @public + * @param {Array} certs + */ + setCerts(certs){ + /** @type {z.CertsChain} */ + var _this = this; + /** @type {Array} */ + var fcertarr = []; + _this.certs = []; + /** @type {forge_cert} */ + var wkcert = null; + certs.forEach(function(/** @type {forge_cert|forge.asn1|string} */a_data){ + if(a_data.issuer){ + wkcert = /** @type {forge_cert} */(a_data); + }else{ + wkcert = z.loadCert(/** @type {forge.asn1|string} */(a_data)); + } + if(wkcert.isIssuer(wkcert)){ + // A selfSigned cert is a root cert. + _this.rootCert = wkcert; + }else{ + /** @type {forge_cert_extension} */ + var bext = wkcert.getExtension("basicConstraints"); + if(bext && bext.cA){ + fcertarr.push(wkcert); + }else{ + _this.certs.push(wkcert); + } + } + }); + /** @type {number} */ + var cnt = fcertarr.length; + if(_this.certs.length == 1){ + wkcert = _this.certs[0]; + while(cnt > 0){ + for(var j=0; j 0){ + _this.certs.push(fcertarr.pop()); + cnt--; + while(cnt > 0){ + for(var j=0; j 0){ + return this.certs[0]; + }else{ + return this.rootCert; + } + } + + /** + * @public + * @return {boolean} + */ + isSelfSignedCert(){ + return (this.certs.length == 0); + } + + /** + * @public + * @return {Array} + */ + getAllCerts(){ + /** @type {Array} */ + var ret = [].concat(this.certs); + if(this.rootCert){ + ret.push(this.rootCert); + } + return ret; + } + + /** + * @public + * @param {forge_cert} cert + * @return {boolean} + */ + isCertInChain(cert){ + return this.isCertInArray(this.getAllCerts(), cert); + } + + /** + * @public + * @param {forge_cert} cert + * @return {Promise} + */ + async buildChain(cert){ + /** @type {z.CertsChain} */ + var _this = this; + if(cert.isIssuer(cert)){ + _this.certs = []; + _this.rootCert = cert; + }else{ + _this.certs = [cert]; + _this.rootCert = null; + } + return /** @type {boolean} */(await _this.amendRootCert()); + } + + /** + * @public + * @param {boolean=} crlOnly + * @return {Promise} + */ + async prepareDSSInf(crlOnly){ + /** @type {z.CertsChain} */ + var _this = this; + var flg = /** @type {boolean} */(await _this.amendRootCert()); + if(!flg){ + throw new Error("Can't prepare DSS infomation because of unable to find the root certificate."); + } + + if(crlOnly){ + flg = false; + }else{ + _this.ocspDatas = []; + _this.extraChains = []; + flg = await _this.checkAllOcsps(_this.extraChains); + } + /** @type {Array} */ + var ocsps = []; + /** @type {Array} */ + var certs = []; + if(flg){ + _this.ocspDatas.forEach(function(/** @type {OcspData} */a_dat){ + ocsps.push(a_dat.resp); + }); + _this.extraChains.forEach(function(/** @type {z.CertsChain} */a_chain){ + a_chain.ocspDatas.forEach(function(/** @type {OcspData} */b_dat){ + ocsps.push(b_dat.resp); + }); + a_chain.certs.forEach(function(/** @type {forge_cert} */b_cert){ + if(!(_this.isCertInArray(_this.certs, b_cert) || _this.isCertInArray(certs, b_cert))){ + certs.push(b_cert); + } + }); + }); + } + + /** @type {Array} */ + var crls = []; + if(!flg){ + _this.crls = []; + for(var i=0; i<_this.certs.length; i++){ + /** @type {forge.asn1} */ + var cdpts = _this.getExtAsn1(_this.certs[i], "cRLDistributionPoints"); //"2.5.29.31" + if(cdpts){ + _this.findCrls(cdpts); + } + } + + for(var i=0; i<_this.crls.length; i++){ + z.log("Query crl for [" + _this.crls[i] + "]"); + /** @type {Uint8Array} */ + var crl = await z.urlFetch(_this.crls[i]); + if(crl){ + crls.push(crl); + } + } + } + + /** @type {DSSInfo} */ + var ret = {}; + if(certs.length > 0){ + ret.certs = certs; + } + if(ocsps.length > 0){ + ret.ocsps = ocsps; + } + if(crls.length > 0){ + ret.crls = crls; + } + return ret; + } + + /** + * @private + * @return {Promise} + */ + async amendRootCert(){ + while(!this.rootCert){ + /** @type {forge_cert} */ + var cert = await this.queryIssuerCert(-1); + if(!cert){ + return false; + } + } + return true; + } + + /** + * @private + * @param {forge_cert|number} certIdx + * @return {Promise} + */ + async queryIssuerCert(certIdx){ + /** @type {z.CertsChain} */ + var _this = this; + /** @type {forge_cert} */ + var cert = null; + if(typeof certIdx === "number"){ + if(certIdx < 0){ + cert = _this.certs[_this.certs.length - 1]; + }else{ + cert = _this.certs[certIdx]; + } + }else{ + cert = /** @type {forge_cert} */(certIdx); + } + /** @type {forge.asn1} */ + var aiass = _this.getExtAsn1(cert, "authorityInfoAccess"); + if(!aiass){ + return null; + } + /** @type {string} */ + var issuerUrl = _this.findOidValue(aiass, _this.issuer_oid); + if(!issuerUrl){ + return null; + } + z.log("Query certificate of [" + issuerUrl + "]"); + /** @type {Uint8Array} */ + var u8cert = await z.urlFetch(issuerUrl); + if(!u8cert){ + return null; + } + cert = z.loadCert(z.u8arrToRaw(u8cert)); + if(typeof certIdx === "number"){ + if(cert.isIssuer(cert)){ + _this.rootCert = cert; + }else if(certIdx < 0){ + _this.certs.push(cert); + }else{ + _this.certs[certIdx + 1] = cert; + } + } + return cert; + } + + /** + * @private + * @param {Array} xchains + * @return {Promise} + */ + async checkAllOcsps(xchains){ + /** @type {z.CertsChain} */ + var _this = this; + /** @type {number} */ + var i = 0; + /** @type {boolean} */ + var ret = (_this.certs.length > 0); + for(i=0; i<_this.certs.length; i++){ + if(!(await _this.queryOcsp(i, xchains))){ + ret = false; + break; + } + } + _this.ocspOk = ret; + return ret; + } + + /** + * @private + * @param {number} idx + * @param {Array} xchains + * @return {Promise} + */ + async queryOcsp(idx, xchains){ + /** @type {z.CertsChain} */ + var _this = this; + + /** @type {forge_cert} */ + var cert = _this.certs[idx]; + z.log("Query ocsp for [" + cert.subject.getField("CN").value + "]"); + /** @type {string} */ + var ocspUrl = ""; + /** @type {forge.asn1} */ + var aiass = _this.getExtAsn1(cert, "authorityInfoAccess"); //"1.3.6.1.5.5.7.1.1" + if(aiass){ + ocspUrl = _this.findOidValue(aiass, _this.ocsp_oid); + } + if(!ocspUrl){ + z.log("Can't find the url of ocsp."); + return false; + } + /** @type {string} */ + var serialNum = z.u8arrToRaw(forge.util.binary.hex.decode(cert.serialNumber)); + + /** @type {number} */ + var pidx = idx + 1; + /** @type {forge_cert} *///parent cert(issuer cert) + var pcert = pidx < _this.certs.length ? _this.certs[pidx] : _this.rootCert; + /** @type {forge.asn1} */ + var subjectAsn1 = forge.pki.distinguishedNameToAsn1(pcert.subject); + /** @type {forge.asn1} */ + var publicKeyAsn1 = forge.pki.publicKeyToRSAPublicKey(pcert.publicKey); + /** @type {string} */ + var dnsha1 = _this.sha1(subjectAsn1); + /** @type {string} */ + var keysha1 = _this.sha1(publicKeyAsn1); + /** @type {string} */ + var nonce = forge.random.getBytesSync(16); + nonce = _this.asn1.toDer(z.createAsn1(nonce)).getBytes(); + /** @type {forge.asn1} */ + var asn1Req = z.createAsn1([ + z.createAsn1([ + z.createAsn1([ + z.createAsn1([ + z.createAsn1([ + z.createAsn1([ + z.createAsn1(forge.oids.sha1, _this.asnt.OID), + z.createAsn1("", _this.asnt.NULL), + ]), + z.createAsn1(dnsha1), + z.createAsn1(keysha1), + z.createAsn1(serialNum, _this.asnt.INTEGER), + ]) + ]) + ]), + z.createAsn1([ + z.createAsn1([ + z.createAsn1([ + z.createAsn1("1.3.6.1.5.5.7.48.1.2", _this.asnt.OID), //OCSP Nonce + z.createAsn1(nonce), + ]) + ]) + ], 2, _this.asnc.CONTEXT_SPECIFIC) + ]) + ]); + + /** @type {Object} */ + var hds = { + "Content-Type": "application/ocsp-request", + }; + /** @type {Uint8Array} */ + var ou8arr = await z.queryAsn1(ocspUrl, asn1Req, hds); +// console.log(forge.util.createBuffer(ou8arr).toHex()); + /** @type {forge.asn1} */ + var asn1Oresp = _this.asn1.fromDer(z.u8arrToRaw(ou8arr)); + /** @type {Object} */ + var ocapt = z.parseAsn1(asn1Oresp, _this.oresp_validator, _this.ocspBasic_oid); + if(ocapt){ + /** @type {number} */ + var respsts = ocapt.status.charCodeAt(0); + if(respsts != 0){ + /** @type {string} */ + var msg = "ocsp response is not successful. "; + switch(respsts){ + case 1: + msg += "malformedRequest"; + break; + case 2: + msg += "internalError"; + break; + case 3: + msg += "tryLater"; + break; + case 5: + msg += "sigRequired"; + break; + case 6: + msg += "unauthorized"; + break; + default: + msg += "unknown error"; + } + msg += "(" + respsts + ")"; + z.log(msg); + return false; + } + }else{ + z.log("ocsp response is an unknown format."); + return false; + } + if(ocapt.ob){ + /** @type {forge.asn1} */ + var asn1Obasic = _this.asn1.fromDer(ocapt.ob); + /** @type {Object} */ + var obcapt = z.parseAsn1(asn1Obasic, _this.ob_validator); + if(obcapt){ + _this.ocspDatas[idx] = { + resp: ou8arr, + }; + if(obcapt.certs){ + /** @type {z.CertsChain} */ + var xchain = new z.CertsChain(obcapt.certs); + /** @type {number} */ + var i = 0; + for(i=0; i} */ + var capt = z.parseAsn1(asn1, _this.oidval_validator, oid); + if(capt && capt.oval){ + return capt.oval; + } + var oval = ""; + if(Array.isArray(asn1.value)){ + for(var i=0; i} certs + * @param {forge_cert} cert + * @return {boolean} + */ + isCertInArray(certs, cert){ + /** @type {number} */ + var i = 0; + for(i=0; i|undefined} */ + this.headers = inf.headers; + + /** @private @type {forge.asn1} */ + this.respAsn1 = null; + } + + /** + * @public + * @param {string=} data + * @return {Promise} Error message + */ + async queryTsa(data){ + /** @type {z.TsaFetcher} */ + var _this = this; + // Generate SHA256 hash from data for TSA + /** @type {forge.md.digest} */ + var md = forge.md.sha256.create(); + md.update(data); + + // Generate TSA request + /** @type {forge.asn1} */ + var asn1Req = z.createAsn1([ + // Version + z.createAsn1(1), + z.createAsn1([ + z.createAsn1([ + z.createAsn1(forge.oids.sha256, _this.asnt.OID), + z.createAsn1("", _this.asnt.NULL), + ]), + // Message imprint + z.createAsn1(md.digest().getBytes()), + ]), + // Get REQ certificates + z.createAsn1(true), + ]); + + /** @type {Object} */ + var hds = _this.headers ? _this.headers : {}; + if(!hds["Content-Type"]){ + hds["Content-Type"] = "application/timestamp-query"; + } + /** @type {Uint8Array} */ + var tu8arr = await z.queryAsn1(_this.url, asn1Req, hds); + /** @type {string} */ + var tstr = z.u8arrToRaw(tu8arr); +// console.log(forge.util.createBuffer(tstr).toHex()); + _this.respAsn1 = _this.asn1.fromDer(tstr); + /** @type {string} */ + var respsts = _this.respAsn1.value[0].value[0].value; + if(respsts == "\x00"){ + return ""; + }else{ + /** @type {forge.asn1} */ + var msgAsn1 = _this.respAsn1.value[0].value[1].value[0]; + var msg = /** @type {string} */(msgAsn1.value); + if(msgAsn1.type == _this.asnt.UTF8){ + msg = forge.util.decodeUtf8(msg); + } + return msg + "(" + respsts.charCodeAt(0) + ")"; + } + } + + /** + * @public + * @param {boolean=} forP7 + * @return {forge.asn1} + */ + getToken(forP7){ + /** @type {z.TsaFetcher} */ + var _this = this; + /** @type {forge.asn1} */ + var token = _this.respAsn1.value[1]; + if(forP7){ + // create the asn1 to append to the p7 signature + return z.createAsn1([ + z.createAsn1([ + // Attribute Type (forge.pki.oids.timeStampToken) + z.createAsn1("1.2.840.113549.1.9.16.2.14", _this.asnt.OID), + // Attribute Value + z.createAsn1([token], _this.asnt.SET), + ]), + ], 1, _this.asnc.CONTEXT_SPECIFIC); + }else{ + return token; + } + } + + /** + * @public + * @return {z.CertsChain} + */ + getCertsChain(){ + /** @type {z.TsaFetcher} */ + var _this = this; + if(!_this.respAsn1){ + throw new Error("You must query tsa first."); + } + var tsdats = /** @type {Array} */(_this.respAsn1.value[1].value[1].value[0].value); + // Get all certs in tsa token. + /** @type {Array} */ + var certAsn1s = null; + for(var i=3; i} */ + var capt = z.parseAsn1(tsdat, { + name: "Certs", + tagClass: _this.asnc.CONTEXT_SPECIFIC, + type: _this.asnt.NONE, + constructed: true, + capture: "certs", + }); + if(capt){ + certAsn1s = capt.certs; + break; + } + } + if(certAsn1s){ + return new z.CertsChain(certAsn1s); + }else{ + return null; + } + } + +}; + +} + +//Only for nodejs Start// +if(typeof exports === "object" && typeof module !== "undefined"){ + module.exports = supplyZgaCertsChain; +} +//Only for nodejs End// diff --git a/lib/zgaindex.js b/lib/zgaindex.js new file mode 100644 index 0000000..7934726 --- /dev/null +++ b/lib/zgaindex.js @@ -0,0 +1,69 @@ +/** + * @return {Object} + */ +function genZga(){ + /** @const {Object} */ + const z = {}; + + /** + * @param {string} msg + */ + z.log = function(msg){ + if(z.debug){ + console.log(msg); + } + }; + + /** + * @param {Uint8Array} uarr + * @return {string} + */ + z.u8arrToRaw = function(uarr){ + /** @type {Array} */ + var arr = []; + for(var i=0; i} + */ + z.urlFetch = function(url, params){ + return new Promise(function(resolve){ + /** @type {GBlob} */ + var tblob = UrlFetchApp.fetch(url, params).getBlob(); + resolve(new Uint8Array(tblob.getBytes())); + }); + }; + } + + return z; +} + +if(typeof exports === "object" && typeof module !== "undefined"){ + module.exports = genZga(); +}else if(!globalThis.Zga){ + globalThis.Zga = genZga(); + supplyZgaCertsChain(globalThis.Zga); + supplyZgaCryptor(globalThis.Zga); + supplyZgaSigner(globalThis.Zga); +} diff --git a/lib/zganode.js b/lib/zganode.js new file mode 100644 index 0000000..e92c230 --- /dev/null +++ b/lib/zganode.js @@ -0,0 +1,75 @@ +const m_urlparser = require("url"); +const m_h = { + "http:": require("http"), + "https:": require("https"), +}; +const z = require("./zgaindex.js"); +z.forge = require("node-forge"); +z.PDFLib = require("pdf-lib"); +/** + * @param {string} url + * @param {UrlFetchParams} params + * @return {Promise} + */ +z.urlFetch = function(url, params){ + return new Promise(function(resolve, reject){ + /** @type {URL} */ + var opts = m_urlparser.parse(url); + var http = m_h[opts.protocol]; + /** @type {string|Buffer} */ + var dat = null; + var encoding = undefined; + opts.method = "GET"; + if(params){ + if(params.payload instanceof Buffer){ + dat = params.payload; + }else if(params.payload instanceof Uint8Array){ + dat = Buffer.from(params.payload.buffer); + }else if(params.payload instanceof ArrayBuffer){ + dat = Buffer.from(params.payload); + }else{ + dat = params.payload; + encoding = "binary"; + } + if(params.headers){ + opts.headers = params.headers; + } + if(params.method){ + opts.method = params.method; + } + if(params.validateHttpsCertificates === false){ + opts.rejectUnauthorized = false; + } + } + + /** @type {http.ClientRequest} */ + var hreq = http.request(opts, function(/** @type {http.IncomingMessage} */a_res){ + if(a_res.statusCode !== 200){ + var a_err = new Error("Failed to request url. " + url + "\n Status Code: " + a_res.statusCode); + a_res.resume(); + throw a_err; + } + /** @type {Array} */ + var a_bufs = []; + var a_bufs_len = 0; + a_res.on("data", function(/** @type {Buffer} */b_chunk){ + a_bufs.push(b_chunk); + a_bufs_len += b_chunk.length; + }); + a_res.on("end", function(){ + /** @type {Buffer} */ + var b_bdat = Buffer.concat(a_bufs, a_bufs_len); + resolve(b_bdat); + }); + }); + hreq.on("error", function(a_err){ + throw a_err; + }); + hreq.end(dat, encoding); + }); +}; + +require("./zgacertsutil.js")(z); +require("./zgapdfcryptor.js")(z); +require("./zgapdfsigner.js")(z); +module.exports = z; diff --git a/zgapdfcryptor.js b/lib/zgapdfcryptor.js similarity index 95% rename from zgapdfcryptor.js rename to lib/zgapdfcryptor.js index d1c4c32..cb66f32 100644 --- a/zgapdfcryptor.js +++ b/lib/zgapdfcryptor.js @@ -1,11 +1,18 @@ -'use strict'; - // This module was migrated from [TCPDF](http://www.tcpdf.org) /** * @param {Object} z */ function supplyZgaCryptor(z){ +//Only for nodejs Start// +if(z.forge){ + var forge = z.forge; +} +if(z.PDFLib){ + var PDFLib = z.PDFLib; +} +//Only for nodejs End// + /** * @param {PDFLib.PDFDocument|Array|Uint8Array|ArrayBuffer|string} pdf * @return {Promise} @@ -23,47 +30,6 @@ z.loadPdf = async function(pdf){ return pdfdoc; }; -/** - * @param {Uint8Array} uarr - * @return {string} - */ -z.u8arrToRaw = function(uarr){ - /** @type {Array} */ - var arr = []; - for(var i=0; i|ArrayBuffer|Uint8Array} */(a_pubkey.c))); } - /** @type {forge.asn1} */ - var a_asn1 = forge.asn1.fromDer(a_cerstr); - a_cert = forge.pki.certificateFromAsn1(a_asn1); + a_cert = z.loadCert(a_cerstr); z.fixCertAttributes(a_cert); } }else{ @@ -991,7 +955,6 @@ z.PdfCryptor = class{ } -if(!globalThis.Zga){ - globalThis.Zga = {}; +if(typeof exports === "object" && typeof module !== "undefined"){ + module.exports = supplyZgaCryptor; } -supplyZgaCryptor(globalThis.Zga); diff --git a/zgapdfsigner.js b/lib/zgapdfsigner.js similarity index 74% rename from zgapdfsigner.js rename to lib/zgapdfsigner.js index 930d9a5..bee9cdb 100644 --- a/zgapdfsigner.js +++ b/lib/zgapdfsigner.js @@ -1,37 +1,28 @@ -'use strict'; - /** * @param {Object} z */ function supplyZgaSigner(z){ +//Only for nodejs Start// +if(z.forge){ + var forge = z.forge; +} +if(z.PDFLib){ + var PDFLib = z.PDFLib; +} +//Only for nodejs End// + /** @type {Object} */ z.TSAURLS = { - "1": {url: "http://ts.ssl.com", len: 15600}, - "2": {url: "http://timestamp.digicert.com", len: 15400}, - "3": {url: "http://timestamp.sectigo.com", len: 13400}, - "4": {url: "http://timestamp.entrust.net/TSS/RFC3161sha2TS", len: 14400}, - "5": {url: "http://timestamp.apple.com/ts01", len: 12100}, - "6": {url: "http://www.langedge.jp/tsa", len: 9200}, - "7": {url: "https://freetsa.org/tsr", len: 14500}, + "1": {url: "http://ts.ssl.com", len: 12100}, + "2": {url: "http://timestamp.digicert.com", len: 11900}, + "3": {url: "http://timestamp.sectigo.com", len: 9900}, + "4": {url: "http://timestamp.entrust.net/TSS/RFC3161sha2TS", len: 10850}, + "5": {url: "http://timestamp.apple.com/ts01", len: 8600}, + "6": {url: "http://www.langedge.jp/tsa", len: 5700}, + "7": {url: "https://freetsa.org/tsr", len: 11000}, }; -// Google Apps Script -if(globalThis.UrlFetchApp){ - /** - * @param {string} url - * @param {UrlFetchParams} params - * @return {Promise} - */ - z.urlFetch = function(url, params){ - return new Promise(function(resolve){ - /** @type {GBlob} */ - var tblob = UrlFetchApp.fetch(url, params).getBlob(); - resolve(new Uint8Array(tblob.getBytes())); - }); - }; -} - z.NewRef = class{ /** * @param {PDFLib.PDFRef} ref @@ -222,6 +213,9 @@ z.PdfSigner = class{ * @param {SignOption} signopt */ constructor(signopt){ + /** @public @type {Zga.TsaFetcher} */ + this.tsaFetcher = null; + /** @private @const {string} */ this.DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********"; /** @private @const {number} */ @@ -230,27 +224,30 @@ z.PdfSigner = class{ this.opt = signopt; /** @type {forge_key} */ this.privateKey = null; - /** @type {Array} */ - this.certs = []; - /** @type {number} */ - this.certIdx = 0; + /** @type {Zga.CertsChain} */ + this.cchain = null; /** @private @type {?TsaServiceInfo} */ this.tsainf = null; + /** @private @type {string} */ + this.signature = ""; /** @private @type {number} */ this.siglen = 0; /** @private @type {PDFLib.PDFHexString} */ this.sigContents = null; /** @private @type {Uint8Array} */ this.oriU8pdf = null; - /** @type {Array} */ - this.apobjs = []; - /** @private @type {boolean} */ - this.debug = false; + /** @private @type {Array} */ + this.apobjs = null; - if(!globalThis.PDFLib){ + if(typeof this.opt.debug == "boolean"){ + z.debug = this.opt.debug; + }else if(globalThis.debug){ + z.debug = true; + } + if(!(globalThis.PDFLib || PDFLib)){ throw new Error("pdf-lib is not imported."); } - if(!globalThis.forge){ + if(!(globalThis.forge || forge)){ throw new Error("node-forge is not imported."); } if(signopt.signdate){ @@ -275,10 +272,12 @@ z.PdfSigner = class{ this.tsainf.len = 16000; } } - if(typeof this.opt.debug == "boolean"){ - this.debug = this.opt.debug; - }else if(globalThis.debug){ - this.debug = true; + if(signopt.ltv && !z.urlFetch){ + throw new Error("Because of the CORS security restrictions, signing with LTV is not supported in web browser."); + } + if(signopt.permission == 1 && signopt.ltv){ + z.log("To enable LTV we need to append informations after signing, this will destroy the signature if full DocMDP protection is set. (Sign with permission = 1)"); + throw new Error("When set full DocMDP protection, LTV can't be enabled."); } } @@ -327,10 +326,20 @@ z.PdfSigner = class{ /** @type {forge_cert} */ var cert = _this.loadP12cert(_this.opt.p12cert, _this.opt.pwd); + /** @type {Zga.CertsChain} */ + var cchain = null; if(cert){ + if(z.urlFetch){ + cchain = new z.CertsChain(); + /** @type {?boolean} */ + var rootok = await cchain.buildChain(cert); + if(rootok){ + _this.cchain = cchain; + } + } z.fixCertAttributes(cert); }else if(_this.tsainf){ - _this.log("No certificate is specified, so only add a document timestamp.") + z.log("No certificate is specified, so only add a document timestamp.") }else{ throw new Error("Nothing to do because no certificate nor tsa is specified."); } @@ -338,25 +347,17 @@ z.PdfSigner = class{ /** @type {boolean} *///append mode or not var apmode = _this.addSignHolder(pdfdoc); await pdfdoc.flush(); - _this.log("A signature holder has been added to the pdf."); + z.log("A signature holder has been added to the pdf."); if(apmode){ if(_this.oriU8pdf){ - _this.log("The pdf has been signed already, so we add a new signature to it."); + z.log("The pdf has been signed already, so we add a new signature to it."); }else{ throw new Error("When adding a new signature to a signed pdf, the original literal datas are necessary."); } // Find the changed objects - /** @type {PDFLib.PDFDocument} */ - var oriPdfdoc = await PDFLib.PDFDocument.load(_this.oriU8pdf); - pdfdoc.context.enumerateIndirectObjects().forEach(function(/** @type {PdfObjEntry} */a_ele){ - /** @type {PDFLib.PDFObject} */ - var a_obj = oriPdfdoc.context.lookup(a_ele[0]); - if(!(a_obj && _this.isamePdfObject(a_ele[1], a_obj))){ - _this.apobjs.push(a_ele); - } - }); + await _this.findChangedObjects(pdfdoc); }else{ // If the definitions of references are too chaotic, a signature contains DocMDP or after adding a new signature, @@ -382,22 +383,17 @@ z.PdfSigner = class{ /** @type {Zga.PdfCryptor} */ var cypt = new z.PdfCryptor(cypopt); await cypt.encryptPdf(pdfdoc, encref); - _this.log("Pdf data has been encrypted."); + z.log("Pdf data has been encrypted."); } } /** @type {Uint8Array} */ var ret = await _this.saveAndSign(pdfdoc); if(!ret){ - _this.log("Change size of signature's placeholder and retry."); - _this.sigContents.value = "0".repeat(_this.siglen); + z.log("Change size of signature's placeholder and retry."); + _this.sigContents.value = "0".repeat(_this.siglen + 10); ret = await _this.saveAndSign(pdfdoc); } - if(ret){ - _this.log("Signing pdf accomplished."); - }else{ - throw new Error("Failed to sign the pdf."); - } // Because PDFRefs in PDFLib are stored staticly, // we need to restore all changed PDFRefs @@ -406,6 +402,30 @@ z.PdfSigner = class{ z.newRefs.restoreAll(); } + if(ret){ + z.log("Pdf has been signed."); + }else{ + throw new Error("Failed to sign the pdf."); + } + + if(_this.opt.ltv == 1 || _this.opt.ltv == 2){ + cchain = _this.cchain ? _this.cchain : _this.tsaFetcher.getCertsChain(); + if(cchain.isSelfSignedCert()){ + z.log("No need to enable LTV because the certificate is a self signed one."); + }else{ + /** @type {boolean} */ + var crlOnly = (_this.opt.ltv == 2); + _this.oriU8pdf = ret; + pdfdoc = await PDFLib.PDFDocument.load(_this.oriU8pdf, {ignoreEncryption: true}); + ret = await _this.applyLTV(pdfdoc, cchain, crlOnly); + if(ret){ + z.log("LTV has been enabled."); + }else{ + throw new Error("Failed to enable the LTV."); + } + } + } + return ret; } @@ -417,8 +437,8 @@ z.PdfSigner = class{ async saveAndSign(pdfdoc){ /** @type {Uint8Array} */ var uarr = null; - if(this.apobjs.length > 0){ - uarr = this.appendSignature(pdfdoc); + if(this.apobjs && this.apobjs.length > 0){ + uarr = this.appendIncrement(pdfdoc); }else{ uarr = await pdfdoc.save({"useObjectStreams": false}); } @@ -427,12 +447,36 @@ z.PdfSigner = class{ return await this.signPdf(pdfstr); } + /** + * @private + * @param {PDFLib.PDFDocument} pdfdoc + * @param {boolean=} ignoreInfo + * @return {Promise} + */ + async findChangedObjects(pdfdoc, ignoreInfo){ + /** @const {z.PdfSigner} */ + const _this = this; + // Find the changed objects + /** @type {PDFLib.PDFDocument} */ + var oriPdfdoc = await PDFLib.PDFDocument.load(_this.oriU8pdf, {ignoreEncryption: true}); + _this.apobjs = []; + pdfdoc.context.enumerateIndirectObjects().forEach(function(/** @type {PdfObjEntry} */a_ele){ + if(!(ignoreInfo && a_ele[0] == pdfdoc.context.trailerInfo.Info)){ + /** @type {PDFLib.PDFObject} */ + var a_obj = oriPdfdoc.context.lookup(a_ele[0]); + if(!(a_obj && _this.isamePdfObject(a_ele[1], a_obj))){ + _this.apobjs.push(a_ele); + } + } + }); + } + /** * @private * @param {PDFLib.PDFDocument} pdfdoc * @return {Uint8Array} */ - appendSignature(pdfdoc){ + appendIncrement(pdfdoc){ /** @const {z.PdfSigner} */ const _this = this; /** @type {PDFLib.PDFCrossRefSection} */ @@ -476,7 +520,7 @@ z.PdfSigner = class{ */ findPrev(u8pdf){ /** @const {Uint8Array} */ - const eof = Zga.rawToU8arr("%%EOF"); + const eof = z.rawToU8arr("%%EOF"); /** @const {number} */ const c0 = "0".charCodeAt(0); /** @const {number} */ @@ -571,6 +615,8 @@ z.PdfSigner = class{ * @return {forge_cert} */ loadP12cert(p12cert, pwd){ + /** @const {z.PdfSigner} */ + const _this = this; // load P12 certificate if(!p12cert){ return null; @@ -592,7 +638,12 @@ z.PdfSigner = class{ var keyBags = p12.getBags({ "bagType": forge.pki.oids.pkcs8ShroudedKeyBag, })[forge.pki.oids.pkcs8ShroudedKeyBag]; - this.privateKey = keyBags[0].key; + _this.privateKey = keyBags[0].key; + + /** @type {Array} */ + var certs = []; + /** @type {number} */ + var certIdx = -1; if(certBags){ // Get all the certificates (-cacerts & -clcerts) // Keep track of the last found client certificate. @@ -601,18 +652,22 @@ z.PdfSigner = class{ /** @type {forge_cert} */ var a_cert = certBags[a_ele].cert; - this.certs.push(a_cert); + certs.push(a_cert); // Try to find the certificate that matches the private key. - if(this.privateKey.n.compareTo(a_cert.publicKey.n) === 0 - && this.privateKey.e.compareTo(a_cert.publicKey.e) === 0){ - this.certIdx = this.certs.length; + if(_this.privateKey.n.compareTo(a_cert.publicKey.n) === 0 + && _this.privateKey.e.compareTo(a_cert.publicKey.e) === 0){ + certIdx = certs.length; } - }.bind(this)); + }); } - if(this.certIdx > 0){ - return this.certs[--this.certIdx]; - // z.fixCertAttributes(this.certs[this.certIdx]); + if(certIdx > 0){ + certIdx--; + _this.cchain = new z.CertsChain(certs); + if(_this.cchain.getSignCert() != certs[certIdx]){ + throw new Error("Chain of certificates is invalid."); + } + return certs[certIdx]; }else{ throw new Error("Failed to find a certificate."); } @@ -627,7 +682,7 @@ z.PdfSigner = class{ /** @const {z.PdfSigner} */ const _this = this; /** @const {number} */ - const docMdp = (_this.certs.length > 0 && _this.opt.permission >= 1 && _this.opt.permission <= 3) ? _this.opt.permission : 0; + const docMdp = (_this.cchain && _this.opt.permission >= 1 && _this.opt.permission <= 3) ? _this.opt.permission : 0; /** @const {PDFLib.PDFContext} */ const pdfcont = pdfdoc.context; /** @const {z.SignatureCreator} */ @@ -692,7 +747,7 @@ z.PdfSigner = class{ }), }), }; - if(_this.certs.length > 0){ + if(_this.cchain){ signObj.M = PDFLib.PDFString.fromDate(signdate); }else{ signObj.Type = "DocTimeStamp"; @@ -748,7 +803,7 @@ z.PdfSigner = class{ var ans = page.node.Annots(); if(!ans){ ans = new PDFLib.PDFArray(pdfcont); - page.node.set(PDFLib.PDFName.Annots, ans); + page.node.set(PDFLib.PDFName.Annots, pdfcont.register(ans)); } ans.push(widgetDictRef); @@ -823,13 +878,15 @@ z.PdfSigner = class{ * @return {Promise} */ async signPdf(pdfstr){ + /** @const {z.PdfSigner} */ + const _this = this; // Finds ByteRange information within a given PDF Buffer if one exists /** @type {Array} */ var byteRangeStrings = pdfstr.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g); /** @type {string|undefined} */ - var byteRangePlaceholder = byteRangeStrings.find(function(a_str){ - return a_str.includes("/"+this.DEFAULT_BYTE_RANGE_PLACEHOLDER); - }.bind(this)); + var byteRangePlaceholder = byteRangeStrings.find(function(/** @type {string} */a_str){ + return a_str.includes("/"+_this.DEFAULT_BYTE_RANGE_PLACEHOLDER); + }); if(!byteRangePlaceholder){ throw new Error("no signature placeholder"); } @@ -862,11 +919,11 @@ z.PdfSigner = class{ /** @type {forge.asn1} */ var asn1sig = null; - if(this.certs.length > 0){ + if(_this.cchain){ /** @type {Date} */ var signdate = new Date(); - if(this.opt.signdate instanceof Date && !this.tsainf){ - signdate = this.opt.signdate; + if(_this.opt.signdate instanceof Date && !_this.tsainf){ + signdate = _this.opt.signdate; } // Here comes the actual PKCS#7 signing. @@ -877,14 +934,14 @@ z.PdfSigner = class{ p7.content = forge.util.createBuffer(pdfstr); // Add all the certificates (-cacerts & -clcerts) to p7 - this.certs.forEach(function(a_cert){ + _this.cchain.getAllCerts().forEach(function(/** @type {forge_cert} */a_cert){ p7.addCertificate(a_cert); }); // Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects. p7.addSigner({ - key: this.privateKey, - certificate: this.certs[this.certIdx], + key: _this.privateKey, + certificate: _this.cchain.getSignCert(), digestAlgorithm: forge.pki.oids.sha256, authenticatedAttributes: [ { @@ -902,25 +959,28 @@ z.PdfSigner = class{ // Sign in detached mode. p7.sign({"detached": true}); - if(this.tsainf){ + if(_this.tsainf){ /** @type {forge.asn1} */ - var tsatoken = await this.queryTsa(p7.signers[0].signature); + var tsatoken = await _this.queryTsa(p7.signers[0].signature, true); p7.signerInfos[0].value.push(tsatoken); } asn1sig = p7.toAsn1(); }else{ - asn1sig = await this.queryTsa(pdfstr, true); + asn1sig = await _this.queryTsa(pdfstr); } // Check if the PDF has a good enough placeholder to fit the signature. + /** @type {forge.util.ByteStringBuffer} */ + var sigbuf = forge.asn1.toDer(asn1sig); /** @type {string} */ - var sighex = forge.asn1.toDer(asn1sig).toHex(); + var sighex = sigbuf.toHex(); + _this.signature = sigbuf.getBytes(); // placeholderLength represents the length of the HEXified symbols but we're // checking the actual lengths. - this.log("Size of signature is " + sighex.length + "/" + placeholderLength); + z.log("Size of signature is " + sighex.length + "/" + placeholderLength); if(sighex.length > placeholderLength){ // throw new Error("Signature is too big. Needs: " + sighex.length); - this.siglen = sighex.length; + _this.siglen = sighex.length; return null; }else{ // Pad the signature with zeroes so the it is the same length as the placeholder @@ -935,137 +995,174 @@ z.PdfSigner = class{ /** * @private * @param {string=} data - * @param {boolean=} nocert + * @param {boolean=} forP7 * @return {Promise} */ - async queryTsa(data, nocert){ - /** @lends {forge.asn1} */ - const asn1 = forge.asn1; - /** @lends {forge.asn1.Class} */ - const asnc = asn1.Class; - /** @lends {forge.asn1.Type} */ - const asnt = asn1.Type; - - /** - * @param {string|number|boolean|forge.asn1|Array} aval - * @param {number=} atyp - * @param {number=} atag - * @return {forge.asn1} - */ - var asncreate = function(aval, atyp, atag){ - /** @type {string|number|forge.asn1|Array} */ - var a_val = null; - /** @type {number} */ - var a_typ = (atyp || atyp === asnt.NONE) ? atyp : -1; - /** @type {boolean} */ - var a_con = false; - if(Array.isArray(aval)){ - a_con = true; - }else{ - switch(typeof aval){ - case "string": - if(a_typ == asnt.OID){ - a_val = asn1.oidToDer(aval).getBytes(); - }else if(a_typ < 0){ - a_typ = asnt.OCTETSTRING; - } - break; - case "number": - a_val = asn1.integerToDer(aval).getBytes(); - if(a_typ < 0){ - a_typ = asnt.INTEGER; - } - break; - case "boolean": - if(aval){ - a_val = 1; - }else{ - a_val = 0; - } - if(a_typ < 0){ - a_typ = asnt.BOOLEAN; - } - } - } - if(a_typ < 0){ - a_typ = asnt.SEQUENCE; - } - if(!a_val && a_val !== 0){ - a_val = /** @type {Array|forge.asn1|number|string} */(aval); - } - return asn1.create(atag ? atag : asnc.UNIVERSAL, a_typ, a_con, a_val); - }; - - // Generate SHA256 hash from data for TSA - /** @type {forge.md.digest} */ - var md = forge.md.sha256.create(); - md.update(data); - - // Generate TSA request - /** @type {forge.asn1} */ - var asn1Req = asncreate([ - // Version - asncreate(1), - asncreate([ - asncreate([ - asncreate(forge.oids.sha256, asnt.OID), - asncreate("", asnt.NULL), - ]), - // Message imprint - asncreate(md.digest().getBytes()), - ]), - // Get REQ certificates - asncreate(true), - ]); - /** @type {string} */ - var tsr = asn1.toDer(asn1Req).getBytes(); - /** @type {Uint8Array} */ - var tu8s = z.rawToU8arr(tsr); - /** @type {Object} */ - var hds = this.tsainf.headers ? this.tsainf.headers : {}; - if(!hds["Content-Type"]){ - hds["Content-Type"] = "application/timestamp-query"; - } - /** @type {UrlFetchParams} */ - var options = { - "method": "POST", - "headers": hds, - "payload": tu8s, - }; - /** @type {Uint8Array} */ - var tesp = await z.urlFetch(this.tsainf.url, options); - /** @type {string} */ - var tstr = z.u8arrToRaw(tesp); - /** @type {forge.asn1} */ - var token = asn1.fromDer(tstr).value[1]; - - /** @type {forge.asn1} */ - var ret = null; - if(nocert){ - ret = token; + async queryTsa(data, forP7){ + /** @const {z.PdfSigner} */ + const _this = this; + _this.tsaFetcher = new z.TsaFetcher(/** @type {TsaServiceInfo} */(_this.tsainf)); + /** @type {?string} */ + var err = await _this.tsaFetcher.queryTsa(data); + if(err){ + throw new Error(err); }else{ - // create the asn1 to append to the signature - ret = asncreate([ - asncreate([ - // Attribute Type (forge.pki.oids.timeStampToken) - asncreate("1.2.840.113549.1.9.16.2.14", asnt.OID), - // Attribute Value - asncreate([token], asnt.SET), - ]), - ], 1, asnc.CONTEXT_SPECIFIC); + /** @type {forge.asn1} */ + var asn1 = _this.tsaFetcher.getToken(forP7); + z.log("Timestamp from " + _this.tsainf.url + " has been obtained."); + return asn1; } - this.log("Timestamp from " + this.tsainf.url + " has been obtained."); - return ret; + } + + /** + * @public + * @param {PDFLib.PDFDocument} pdfdoc + * @param {Zga.CertsChain} chain + * @param {boolean=} crlOnly + * @return {Promise} + */ + async applyLTV(pdfdoc, chain, crlOnly){ + /** @const {z.PdfSigner} */ + const _this = this; + /** @type {?DSSInfo} */ + var dssinf = await chain.prepareDSSInf(crlOnly); + if(!dssinf){ + return null; + } + if(!_this.addDss(pdfdoc, _this.signature, dssinf)){ + return null; + } + await pdfdoc.flush(); + await _this.findChangedObjects(pdfdoc, true); + return _this.appendIncrement(pdfdoc); } /** * @private - * @param {string} msg + * @param {PDFLib.PDFDocument} pdfdoc + * @param {string} sig signature + * @param {DSSInfo=} dssinf + * @return {boolean} */ - log(msg){ - if(this.debug){ - console.log(msg); + addDss(pdfdoc, sig, dssinf){ + /** @type {PDFLib.PDFContext} */ + var pdfcont = pdfdoc.context; + /** @type {Array} */ + var certRefs = null; + /** @type {Array} */ + var ocspRefs = null; + /** @type {Array} */ + var crlRefs = null; + + if(dssinf && dssinf.ocsps && dssinf.ocsps.length > 0){ + ocspRefs = []; + dssinf.ocsps.forEach(function(/** @type {string|Uint8Array} */a_ocsp){ + /** @type {PDFLib.PDFRawStream} */ + var a_strmOcsp = pdfcont.flateStream(a_ocsp); + ocspRefs.push(pdfcont.register(a_strmOcsp)); + }); } + if(dssinf && dssinf.crls && dssinf.crls.length > 0){ + crlRefs = []; + dssinf.crls.forEach(function(/** @type {string|Uint8Array} */a_crl){ + /** @type {PDFLib.PDFRawStream} */ + var a_strmCrl = pdfcont.flateStream(a_crl); + crlRefs.push(pdfcont.register(a_strmCrl)); + }); + } + + if(!(ocspRefs || crlRefs)){ + // Nothing to do. + return false; + } + + if(dssinf && dssinf.certs && dssinf.certs.length > 0){ + certRefs = []; + dssinf.certs.forEach(function(/** @type {forge_cert} */a_cert){ + /** @type {forge.asn1} */ + var a_asn1Cert = forge.pki.certificateToAsn1(a_cert); + /** @type {PDFLib.PDFRawStream} */ + var a_strmCert = pdfcont.flateStream(forge.asn1.toDer(a_asn1Cert).getBytes()); + certRefs.push(pdfcont.register(a_strmCert)); + }); + } + + /** @type {forge.md.digest} */ + var md = forge.md.sha1.create(); + md.update(sig); + /** @type {string} */ + var sighex = md.digest().toHex().toUpperCase(); + + var dss = /** @type {PDFLib.PDFDict} */(pdfdoc.catalog.lookupMaybe(PDFLib.PDFName.of("DSS"), PDFLib.PDFDict)); + /** @type {PDFLib.PDFArray} */ + var certsarr = null; + /** @type {PDFLib.PDFArray} */ + var ocpsarr = null; + /** @type {PDFLib.PDFArray} */ + var crlsarr = null; + /** @type {PDFLib.PDFDict} */ + var vri = null; + /** @type {Object} */ + var vriObj = { + TU: PDFLib.PDFString.fromDate(new Date()), + }; + /** @type {PDFLib.PDFArray} */ + var sigcertsarr = null; + /** @type {PDFLib.PDFArray} */ + var sigocpsarr = null; + /** @type {PDFLib.PDFArray} */ + var sigcrlsarr = null; + if(dss){ + certsarr = /** @type {PDFLib.PDFArray} */(dss.lookupMaybe(PDFLib.PDFName.of("Certs"), PDFLib.PDFArray)); + crlsarr = /** @type {PDFLib.PDFArray} */(dss.lookupMaybe(PDFLib.PDFName.of("CRLs"), PDFLib.PDFArray)); + ocpsarr = /** @type {PDFLib.PDFArray} */(dss.lookupMaybe(PDFLib.PDFName.of("OCSPs"), PDFLib.PDFArray)); + vri = /** @type {PDFLib.PDFDict} */(dss.lookupMaybe(PDFLib.PDFName.of("VRI"), PDFLib.PDFDict)); + }else{ + dss = /** @type {PDFLib.PDFDict} */(pdfcont.obj({})); + pdfdoc.catalog.set(PDFLib.PDFName.of("DSS"), pdfcont.register(dss)); + } + if(certRefs){ + sigcertsarr = new PDFLib.PDFArray(pdfcont); + vriObj["Cert"] = sigcertsarr; + if(!certsarr){ + certsarr = new PDFLib.PDFArray(pdfcont); + dss.set(PDFLib.PDFName.of("Certs"), pdfcont.register(certsarr)); + } + certRefs.forEach(function(/** @type {PDFLib.PDFRef} */a_ref){ + sigcertsarr.push(a_ref); + certsarr.push(a_ref); + }); + } + if(ocspRefs){ + sigocpsarr = new PDFLib.PDFArray(pdfcont); + vriObj["OCSP"] = sigocpsarr; + if(!ocpsarr){ + ocpsarr = new PDFLib.PDFArray(pdfcont); + dss.set(PDFLib.PDFName.of("OCSPs"), pdfcont.register(ocpsarr)); + } + ocspRefs.forEach(function(/** @type {PDFLib.PDFRef} */a_ref){ + sigocpsarr.push(a_ref); + ocpsarr.push(a_ref); + }); + } + if(crlRefs){ + sigcrlsarr = new PDFLib.PDFArray(pdfcont); + vriObj["CRL"] = sigcrlsarr; + if(!crlsarr){ + crlsarr = new PDFLib.PDFArray(pdfcont); + dss.set(PDFLib.PDFName.of("CRLs"), pdfcont.register(crlsarr)); + } + crlRefs.forEach(function(/** @type {PDFLib.PDFRef} */a_ref){ + sigcrlsarr.push(a_ref); + crlsarr.push(a_ref); + }); + } + if(!vri){ + vri = /** @type {PDFLib.PDFDict} */(pdfcont.obj({})); + dss.set(PDFLib.PDFName.of("VRI"), pdfcont.register(vri)); + } + vri.set(PDFLib.PDFName.of(sighex), pdfcont.register(pdfcont.obj(vriObj))); + return true; } }; @@ -1309,7 +1406,8 @@ z.SignatureCreator = class{ } -if(!globalThis.Zga){ - globalThis.Zga = {}; +//Only for nodejs Start// +if(typeof exports === "object" && typeof module !== "undefined"){ + module.exports = supplyZgaSigner; } -supplyZgaSigner(globalThis.Zga); +//Only for nodejs End// diff --git a/package.json b/package.json index 9c47df6..116f098 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,32 @@ { "name": "zgapdfsigner", "version": "2.3.0", + "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", "private": false, + "repository": { + "type": "git", + "url": "https://github.com/zboris12/zgapdfsigner" + }, + "bugs": { + "url": "https://github.com/zboris12/zgapdfsigner/issues" + }, + "license": "MIT", + "main": "lib/zganode.js", + "files": [ + "lib/*.js" + ], + "keywords": [ + "pdf-sign", + "pdf-signature", + "pdf-protection", + "pdf-encryption", + "google-apps-script", + "LTV", + "TSA" + ], "scripts": { - "dev": "", - "build": "" + "testsign": "node test4node.js" }, "dependencies": { "pdf-lib": "1.17.1", diff --git a/test4node.js b/test4node.js index 0cb757e..3e7b5d3 100644 --- a/test4node.js +++ b/test4node.js @@ -1,86 +1,18 @@ const m_fs = require("fs"); const m_path = require("path"); -const m_urlparser = require("url"); -const m_h = { - "http:": require("http"), - "https:": require("http"), -}; +const Zga = require("./lib/zganode.js"); -globalThis.PDFLib = require("pdf-lib"); -globalThis.forge = require("node-forge"); -require("./zgapdfcryptor.js"); -require("./zgapdfsigner.js"); - -/** - * @param {string} url - * @param {UrlFetchParams} params - * @return {Promise} - */ -Zga.urlFetch = function(url, params){ - return new Promise(function(resolve, reject){ - /** @type {URL} */ - var opts = m_urlparser.parse(url); - var http = m_h[opts.protocol]; - /** @type {string|Buffer} */ - var dat = null; - var encoding = undefined; - if(params.payload instanceof Buffer){ - dat = params.payload; - }else if(params.payload instanceof Uint8Array){ - dat = Buffer.from(params.payload.buffer); - }else if(params.payload instanceof ArrayBuffer){ - dat = Buffer.from(params.payload); - }else{ - dat = params.payload; - encoding = "binary"; - } - - if(params.headers){ - opts.headers = params.headers; - } - if(params.method){ - opts.method = params.method; - } - if(params.validateHttpsCertificates === false){ - opts.rejectUnauthorized = false; - } - - /** @type {http.ClientRequest} */ - var hreq = http.request(opts, function(/** @type {http.IncomingMessage} */a_res){ - if(a_res.statusCode !== 200){ - var a_err = new Error("Failed to request url. " + url + "\n Status Code: " + a_res.statusCode); - a_res.resume(); - throw a_err; - } - /** @type {Array} */ - var a_bufs = []; - var a_bufs_len = 0; - a_res.on("data", function(/** @type {Buffer} */b_chunk){ - a_bufs.push(b_chunk); - a_bufs_len += b_chunk.length; - }); - a_res.on("end", function(){ - /** @type {Buffer} */ - var b_bdat = Buffer.concat(a_bufs, a_bufs_len); - resolve(b_bdat); - }); - }); - hreq.on("error", function(a_err){ - throw a_err; - }); - hreq.end(dat, encoding); - }); -}; +const workpath = "test/"; async function main(){ /** @type {string} */ - var pdfPath = m_path.join(__dirname, "test/_test.pdf"); + var pdfPath = m_path.join(__dirname, workpath+"_test.pdf"); /** @type {string} */ - var pfxPath = m_path.join(__dirname, "test/_test.pfx"); + var pfxPath = m_path.join(__dirname, workpath+"_test.pfx"); /** @type {string} */ var ps = ""; /** @type {string} */ - var imgPath = m_path.join(__dirname, "test/_test.png"); + var imgPath = m_path.join(__dirname, workpath+"_test.png"); if(process.argv.length > 3){ pfxPath = process.argv[2]; @@ -116,11 +48,12 @@ async function main(){ sopt = { p12cert: pfx, pwd: ps, - permission: pfx ? 1 : 0, + // permission: pfx ? 2 : 0, signdate: "1", reason: "I have a test reason.", location: "I am on the earth.", contact: "zga@zga.com", + ltv: 1, debug: true, }; if(img){ @@ -157,11 +90,37 @@ async function main(){ if(u8dat){ /** @type {string} */ - var outPath = m_path.join(__dirname, "test/test_test2.pdf"); + var outPath = m_path.join(__dirname, workpath+"test_signed.pdf"); m_fs.writeFileSync(outPath, u8dat); console.log("Output file: " + outPath); } console.log("Done"); } +async function main2(){ + /** @type {string} */ + var pdfPath = m_path.join(__dirname, workpath+"test_signed.pdf"); + /** @type {Buffer} */ + var pdf = m_fs.readFileSync(pdfPath); + /** @type {SignOption} */ + var sopt = { + signdate: "2", + reason: "I have a test reason.", + location: "I am on the earth.", + contact: "zga@zga.com", + ltv: 1, + debug: true, + }; + /** @type {Zga.PdfSigner} */ + var ser = new Zga.PdfSigner(sopt); + /** @type {Uint8Array} */ + var u8dat = await ser.sign(pdf); + /** @type {string} */ + var outPath = m_path.join(__dirname, workpath+"test_signed_tsa.pdf"); + m_fs.writeFileSync(outPath, u8dat); + console.log("Output file: " + outPath); + return; +} + main(); +// main2();