parent
528eaad5eb
commit
8b008ccc34
12
README.md
12
README.md
|
@ -1,6 +1,6 @@
|
||||||
# ZgaPdfSigner
|
# ZgaPdfSigner
|
||||||
A javascript tool to sign a pdf or set protection of a pdf in web browser.
|
A javascript tool to sign a pdf or set protection of a pdf in web browser.
|
||||||
And it also can be used in Google Apps Script.
|
And it also can be used in Google Apps Script and nodejs.
|
||||||
|
|
||||||
PS: __ZGA__ is the abbreviation of my father's name.
|
PS: __ZGA__ is the abbreviation of my father's name.
|
||||||
And I use this name to hope the merits from this application will be dedicated to my parents.
|
And I use this name to hope the merits from this application will be dedicated to my parents.
|
||||||
|
@ -11,7 +11,8 @@ And I use this name to hope the merits from this application will be dedicated t
|
||||||
* Sign a pdf with a visible pkcs#7 signature by drawing an image.
|
* Sign a pdf with a visible pkcs#7 signature by drawing an image.
|
||||||
* Sign a pdf and set DocMDP(document modification detection and prevention).
|
* Sign a pdf and set DocMDP(document modification detection and prevention).
|
||||||
* Add a new signature to a pdf if it has been signed already. (An incremental update)
|
* Add a new signature to a pdf if it has been signed already. (An incremental update)
|
||||||
* Sign a pdf with a timestamp from TSA(Time Stamp Authority). (Only in Google Apps Script)
|
* Add a document timestamp from TSA(Time Stamp Authority). (Only in Google Apps Script and nodejs)
|
||||||
|
* Sign a pdf with a timestamp from TSA. (Only in Google Apps Script and nodejs)
|
||||||
* Set password protection to a pdf. Supported algorithms:
|
* Set password protection to a pdf. Supported algorithms:
|
||||||
* 40bit RC4 Encryption
|
* 40bit RC4 Encryption
|
||||||
* 128bit RC4 Encryption
|
* 128bit RC4 Encryption
|
||||||
|
@ -138,8 +139,8 @@ fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test.
|
||||||
|
|
||||||
## Detail of SignOption
|
## Detail of SignOption
|
||||||
|
|
||||||
* __p12cert__: Array<number>|Uint8Array|ArrayBuffer|string :point_right: Certificate's data
|
* __p12cert__: Array<number>|Uint8Array|ArrayBuffer|string :point_right: (Optional) Certificate's data. In the case of adding a document timestamp, it must be omitted.
|
||||||
* __pwd__: string :point_right: The passphrase of the certificate
|
* __pwd__: string :point_right: (Optional) The passphrase of the certificate. In the case of adding a document timestamp, it must be omitted.
|
||||||
* __permission__: number :point_right: (Optional) The modification permissions granted for this document.
|
* __permission__: number :point_right: (Optional) The modification permissions granted for this document.
|
||||||
This is a setting of DocMDP(document modification detection and prevention). Valid values are:
|
This is a setting of DocMDP(document modification detection and prevention). Valid values are:
|
||||||
* 1: No changes to the document are permitted; any change to the document invalidates the signature.
|
* 1: No changes to the document are permitted; any change to the document invalidates the signature.
|
||||||
|
@ -148,7 +149,7 @@ fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test.
|
||||||
* __reason__: string :point_right: (Optional) The reason for signing
|
* __reason__: string :point_right: (Optional) The reason for signing
|
||||||
* __location__: string :point_right: (Optional) Your location
|
* __location__: string :point_right: (Optional) Your location
|
||||||
* __contact__: string :point_right: (Optional) Your contact information
|
* __contact__: string :point_right: (Optional) Your contact information
|
||||||
* __signdate__: Date|string|_TsaServiceInfo_ :point_right: (Optional)
|
* __signdate__: Date|string|_TsaServiceInfo_ :point_right: (Optional) In the case of adding a document timestamp, it can't be omitted and can't be a Date.
|
||||||
* When it is a Date, it means the date and time of signing.
|
* When it is a Date, it means the date and time of signing.
|
||||||
* When it is a string, it can be an url of TSA or an index of the preset TSAs as below:
|
* When it is a string, it can be an url of TSA or an index of the preset TSAs as below:
|
||||||
* "1": http://ts.ssl.com
|
* "1": http://ts.ssl.com
|
||||||
|
@ -161,6 +162,7 @@ fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test.
|
||||||
* When it is a _TsaServiceInfo_, it means a full customized information of a TSA.
|
* When it is a _TsaServiceInfo_, it means a full customized information of a TSA.
|
||||||
* __url__: string :point_right: The url of TSA
|
* __url__: string :point_right: The url of TSA
|
||||||
* __len__: number :point_right: (Optional) The length of signature's placeholder
|
* __len__: number :point_right: (Optional) The length of signature's placeholder
|
||||||
|
* __headers__: Object<string, *> :point_right: (Optional) The customized headers for sending to tsa server
|
||||||
* When it is omitted, the system timestamp will be used.
|
* When it is omitted, the system timestamp will be used.
|
||||||
* __signame__: string :point_right: (Optional) The name of the signature
|
* __signame__: string :point_right: (Optional) The name of the signature
|
||||||
* __drawinf__: _SignDrawInfo_ :point_right: (Optional) Visible signature's information
|
* __drawinf__: _SignDrawInfo_ :point_right: (Optional) Visible signature's information
|
||||||
|
|
|
@ -102,6 +102,8 @@ forge.asn1.integerToDer = function(num){};
|
||||||
forge.asn1.oidToDer = function(oid){};
|
forge.asn1.oidToDer = function(oid){};
|
||||||
forge.asn1.Type = {};
|
forge.asn1.Type = {};
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
|
forge.asn1.Type.NONE;
|
||||||
|
/** @type {number} */
|
||||||
forge.asn1.Type.UTF8;
|
forge.asn1.Type.UTF8;
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
forge.asn1.Type.SET;
|
forge.asn1.Type.SET;
|
||||||
|
@ -126,7 +128,7 @@ forge.asn1.Class.CONTEXT_SPECIFIC;
|
||||||
* @param {number} tagClass
|
* @param {number} tagClass
|
||||||
* @param {number} type
|
* @param {number} type
|
||||||
* @param {boolean} constructed
|
* @param {boolean} constructed
|
||||||
* @param {Array<string>|string} value
|
* @param {Array<forge.asn1>|forge.asn1|number|string} value
|
||||||
* @param {Object=} options
|
* @param {Object=} options
|
||||||
* @return {forge.asn1}
|
* @return {forge.asn1}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* {{
|
* {{
|
||||||
* url: string,
|
* url: string,
|
||||||
* len: (number|undefined),
|
* len: (number|undefined),
|
||||||
|
* headers: (Object<string, *>|undefined),
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
var TsaServiceInfo;
|
var TsaServiceInfo;
|
||||||
|
@ -32,6 +33,8 @@ var SignAreaInfo;
|
||||||
*/
|
*/
|
||||||
var SignDrawInfo;
|
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:
|
* 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.
|
* 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.
|
* 2 : Permitted changes are filling in forms, instantiating page templates, and signing; other changes invalidate the signature.
|
||||||
|
@ -39,8 +42,8 @@ var SignDrawInfo;
|
||||||
*
|
*
|
||||||
* @typedef
|
* @typedef
|
||||||
* {{
|
* {{
|
||||||
* p12cert: (Array<number>|Uint8Array|ArrayBuffer|string),
|
* p12cert: (Array<number>|Uint8Array|ArrayBuffer|string|undefined),
|
||||||
* pwd: string,
|
* pwd: (string|undefined),
|
||||||
* permission: (number|undefined),
|
* permission: (number|undefined),
|
||||||
* reason: (string|undefined),
|
* reason: (string|undefined),
|
||||||
* location: (string|undefined),
|
* location: (string|undefined),
|
||||||
|
|
15
test4node.js
15
test4node.js
|
@ -11,13 +11,12 @@ globalThis.forge = require("node-forge");
|
||||||
require("./zgapdfcryptor.js");
|
require("./zgapdfcryptor.js");
|
||||||
require("./zgapdfsigner.js");
|
require("./zgapdfsigner.js");
|
||||||
|
|
||||||
Zga.UrlFetchApp = {};
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {UrlFetchParams} params
|
* @param {UrlFetchParams} params
|
||||||
* @return {Promise<Uint8Array>}
|
* @return {Promise<Uint8Array>}
|
||||||
*/
|
*/
|
||||||
Zga.UrlFetchApp.fetch = function(url, params){
|
Zga.urlFetch = function(url, params){
|
||||||
return new Promise(function(resolve, reject){
|
return new Promise(function(resolve, reject){
|
||||||
/** @type {URL} */
|
/** @type {URL} */
|
||||||
var opts = m_urlparser.parse(url);
|
var opts = m_urlparser.parse(url);
|
||||||
|
@ -91,13 +90,17 @@ async function main(){
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!ps){
|
if(!ps){
|
||||||
throw new Error("The passphrase is not specified.");
|
// throw new Error("The passphrase is not specified.");
|
||||||
|
pfxPath = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Buffer} */
|
/** @type {Buffer} */
|
||||||
var pdf = m_fs.readFileSync(pdfPath);
|
var pdf = m_fs.readFileSync(pdfPath);
|
||||||
/** @type {Buffer} */
|
/** @type {Buffer} */
|
||||||
var pfx = m_fs.readFileSync(pfxPath);
|
var pfx = null;
|
||||||
|
if(pfxPath){
|
||||||
|
pfx = m_fs.readFileSync(pfxPath);
|
||||||
|
}
|
||||||
/** @type {Buffer} */
|
/** @type {Buffer} */
|
||||||
var img = null;
|
var img = null;
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
|
@ -109,11 +112,11 @@ async function main(){
|
||||||
|
|
||||||
/** @type {SignOption} */
|
/** @type {SignOption} */
|
||||||
var sopt = null;
|
var sopt = null;
|
||||||
if(pfx){
|
if(pdf){
|
||||||
sopt = {
|
sopt = {
|
||||||
p12cert: pfx,
|
p12cert: pfx,
|
||||||
pwd: ps,
|
pwd: ps,
|
||||||
permission: 1,
|
permission: pfx ? 1 : 0,
|
||||||
signdate: "1",
|
signdate: "1",
|
||||||
reason: "I have a test reason.",
|
reason: "I have a test reason.",
|
||||||
location: "I am on the earth.",
|
location: "I am on the earth.",
|
||||||
|
|
482
zgapdfsigner.js
482
zgapdfsigner.js
|
@ -18,13 +18,12 @@ z.TSAURLS = {
|
||||||
|
|
||||||
// Google Apps Script
|
// Google Apps Script
|
||||||
if(globalThis.UrlFetchApp){
|
if(globalThis.UrlFetchApp){
|
||||||
z.UrlFetchApp = {};
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @param {UrlFetchParams} params
|
* @param {UrlFetchParams} params
|
||||||
* @return {Promise<Uint8Array>}
|
* @return {Promise<Uint8Array>}
|
||||||
*/
|
*/
|
||||||
z.UrlFetchApp.fetch = function(url, params){
|
z.urlFetch = function(url, params){
|
||||||
return new Promise(function(resolve){
|
return new Promise(function(resolve){
|
||||||
/** @type {GBlob} */
|
/** @type {GBlob} */
|
||||||
var tblob = UrlFetchApp.fetch(url, params).getBlob();
|
var tblob = UrlFetchApp.fetch(url, params).getBlob();
|
||||||
|
@ -264,7 +263,7 @@ z.PdfSigner = class{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(this.tsainf){
|
if(this.tsainf){
|
||||||
if(!z.UrlFetchApp){
|
if(!z.urlFetch){
|
||||||
throw new Error("Because of the CORS security restrictions, signing with TSA is not supported in web browser.");
|
throw new Error("Because of the CORS security restrictions, signing with TSA is not supported in web browser.");
|
||||||
}
|
}
|
||||||
if(z.TSAURLS[this.tsainf.url]){
|
if(z.TSAURLS[this.tsainf.url]){
|
||||||
|
@ -326,18 +325,21 @@ 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.");
|
|
||||||
|
|
||||||
/** @type {forge_cert} */
|
/** @type {forge_cert} */
|
||||||
var cert = _this.loadP12cert(_this.opt.p12cert, _this.opt.pwd);
|
var cert = _this.loadP12cert(_this.opt.p12cert, _this.opt.pwd);
|
||||||
if(cert){
|
if(cert){
|
||||||
z.fixCertAttributes(cert);
|
z.fixCertAttributes(cert);
|
||||||
|
}else if(_this.tsainf){
|
||||||
|
_this.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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @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.");
|
||||||
|
|
||||||
if(apmode){
|
if(apmode){
|
||||||
if(_this.oriU8pdf){
|
if(_this.oriU8pdf){
|
||||||
_this.log("The pdf has been signed already, so we add a new signature to it.");
|
_this.log("The pdf has been signed already, so we add a new signature to it.");
|
||||||
|
@ -562,22 +564,78 @@ z.PdfSigner = class{
|
||||||
return buff.length - before;
|
return buff.length - before;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {Array<number>|Uint8Array|ArrayBuffer|string=} p12cert
|
||||||
|
* @param {string=} pwd
|
||||||
|
* @return {forge_cert}
|
||||||
|
*/
|
||||||
|
loadP12cert(p12cert, pwd){
|
||||||
|
// load P12 certificate
|
||||||
|
if(!p12cert){
|
||||||
|
return null;
|
||||||
|
}else if(typeof p12cert !== "string"){
|
||||||
|
p12cert = z.u8arrToRaw(new Uint8Array(p12cert));
|
||||||
|
}
|
||||||
|
// Convert Buffer P12 to a forge implementation.
|
||||||
|
/** @type {forge.asn1} */
|
||||||
|
var p12Asn1 = forge.asn1.fromDer(p12cert);
|
||||||
|
/** @type {forge.pkcs12} */
|
||||||
|
var p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, true, pwd);
|
||||||
|
// Extract safe bags by type.
|
||||||
|
// We will need all the certificates and the private key.
|
||||||
|
/** @type {Object<string|number, P12Bag>} */
|
||||||
|
var certBags = p12.getBags({
|
||||||
|
"bagType": forge.pki.oids.certBag,
|
||||||
|
})[forge.pki.oids.certBag];
|
||||||
|
/** @type {Object<string|number, P12Bag>} */
|
||||||
|
var keyBags = p12.getBags({
|
||||||
|
"bagType": forge.pki.oids.pkcs8ShroudedKeyBag,
|
||||||
|
})[forge.pki.oids.pkcs8ShroudedKeyBag];
|
||||||
|
this.privateKey = keyBags[0].key;
|
||||||
|
if(certBags){
|
||||||
|
// Get all the certificates (-cacerts & -clcerts)
|
||||||
|
// Keep track of the last found client certificate.
|
||||||
|
// This will be the public key that will be bundled in the signature.
|
||||||
|
Object.keys(certBags).forEach(function(a_ele){
|
||||||
|
/** @type {forge_cert} */
|
||||||
|
var a_cert = certBags[a_ele].cert;
|
||||||
|
|
||||||
|
this.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;
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
if(this.certIdx > 0){
|
||||||
|
return this.certs[--this.certIdx];
|
||||||
|
// z.fixCertAttributes(this.certs[this.certIdx]);
|
||||||
|
}else{
|
||||||
|
throw new Error("Failed to find a certificate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {PDFLib.PDFDocument} pdfdoc
|
* @param {PDFLib.PDFDocument} pdfdoc
|
||||||
* @return {boolean} append mode or not
|
* @return {boolean} append mode or not
|
||||||
*/
|
*/
|
||||||
addSignHolder(pdfdoc){
|
addSignHolder(pdfdoc){
|
||||||
|
/** @const {z.PdfSigner} */
|
||||||
|
const _this = this;
|
||||||
/** @const {number} */
|
/** @const {number} */
|
||||||
const docMdp = (this.opt.permission >= 1 && this.opt.permission <= 3) ? this.opt.permission : 0;
|
const docMdp = (_this.certs.length > 0 && _this.opt.permission >= 1 && _this.opt.permission <= 3) ? _this.opt.permission : 0;
|
||||||
/** @const {PDFLib.PDFContext} */
|
/** @const {PDFLib.PDFContext} */
|
||||||
const pdfcont = pdfdoc.context;
|
const pdfcont = pdfdoc.context;
|
||||||
/** @const {z.SignatureCreator} */
|
/** @const {z.SignatureCreator} */
|
||||||
const signcrt = new z.SignatureCreator(this.opt.drawinf);
|
const signcrt = new z.SignatureCreator(_this.opt.drawinf);
|
||||||
/** @const {PDFLib.PDFPage} */
|
/** @const {PDFLib.PDFPage} */
|
||||||
const page = pdfdoc.getPages()[signcrt.getPageIndex()];
|
const page = pdfdoc.getPages()[signcrt.getPageIndex()];
|
||||||
/** @type {PDFLib.PDFRef} */
|
/** @type {PDFLib.PDFRef} */
|
||||||
var strmRef = signcrt.createStream(pdfdoc, this.opt.signame);
|
var strmRef = signcrt.createStream(pdfdoc, _this.opt.signame);
|
||||||
|
|
||||||
if(docMdp && !strmRef){
|
if(docMdp && !strmRef){
|
||||||
strmRef = signcrt.createEmptyField(pdfcont);
|
strmRef = signcrt.createEmptyField(pdfcont);
|
||||||
|
@ -603,23 +661,23 @@ z.PdfSigner = class{
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
var signm = this.fixSigName(oldSigs, this.opt.signame);
|
var signm = _this.fixSigName(oldSigs, _this.opt.signame);
|
||||||
|
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
var signdate = new Date();
|
var signdate = new Date();
|
||||||
if(this.opt.signdate instanceof Date && !this.tsainf){
|
if(_this.opt.signdate instanceof Date && !_this.tsainf){
|
||||||
signdate = this.opt.signdate;
|
signdate = _this.opt.signdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {PDFLib.PDFArray} */
|
/** @type {PDFLib.PDFArray} */
|
||||||
var bytrng = new PDFLib.PDFArray(pdfcont);
|
var bytrng = new PDFLib.PDFArray(pdfcont);
|
||||||
bytrng.push(PDFLib.PDFNumber.of(0));
|
bytrng.push(PDFLib.PDFNumber.of(0));
|
||||||
bytrng.push(PDFLib.PDFName.of(this.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
bytrng.push(PDFLib.PDFName.of(_this.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
||||||
bytrng.push(PDFLib.PDFName.of(this.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
bytrng.push(PDFLib.PDFName.of(_this.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
||||||
bytrng.push(PDFLib.PDFName.of(this.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
bytrng.push(PDFLib.PDFName.of(_this.DEFAULT_BYTE_RANGE_PLACEHOLDER));
|
||||||
|
|
||||||
this.siglen = /** @type {number} */(this.tsainf ? this.tsainf.len : 3322);
|
_this.siglen = /** @type {number} */(_this.tsainf ? _this.tsainf.len : 3322);
|
||||||
this.sigContents = PDFLib.PDFHexString.of("0".repeat(this.siglen));
|
_this.sigContents = PDFLib.PDFHexString.of("0".repeat(_this.siglen));
|
||||||
|
|
||||||
/** @type {Object<string, *>} */
|
/** @type {Object<string, *>} */
|
||||||
var signObj = {
|
var signObj = {
|
||||||
|
@ -627,14 +685,19 @@ z.PdfSigner = class{
|
||||||
"Filter": "Adobe.PPKLite",
|
"Filter": "Adobe.PPKLite",
|
||||||
"SubFilter": "adbe.pkcs7.detached",
|
"SubFilter": "adbe.pkcs7.detached",
|
||||||
"ByteRange": bytrng,
|
"ByteRange": bytrng,
|
||||||
"Contents": this.sigContents,
|
"Contents": _this.sigContents,
|
||||||
"M": PDFLib.PDFString.fromDate(signdate),
|
|
||||||
"Prop_Build": pdfcont.obj({
|
"Prop_Build": pdfcont.obj({
|
||||||
"App": pdfcont.obj({
|
"App": pdfcont.obj({
|
||||||
"Name": "ZgaPdfSinger",
|
"Name": "ZgaPdfSinger",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
if(_this.certs.length > 0){
|
||||||
|
signObj.M = PDFLib.PDFString.fromDate(signdate);
|
||||||
|
}else{
|
||||||
|
signObj.Type = "DocTimeStamp";
|
||||||
|
signObj.SubFilter = "ETSI.RFC3161";
|
||||||
|
}
|
||||||
if(docMdp){
|
if(docMdp){
|
||||||
/** @type {PDFLib.PDFArray} */
|
/** @type {PDFLib.PDFArray} */
|
||||||
var rfrc = new PDFLib.PDFArray(pdfcont);
|
var rfrc = new PDFLib.PDFArray(pdfcont);
|
||||||
|
@ -649,14 +712,14 @@ z.PdfSigner = class{
|
||||||
}));
|
}));
|
||||||
signObj["Reference"] = rfrc;
|
signObj["Reference"] = rfrc;
|
||||||
}
|
}
|
||||||
if(this.opt.reason){
|
if(_this.opt.reason){
|
||||||
signObj["Reason"] = this.convToPDFString(this.opt.reason);
|
signObj["Reason"] = _this.convToPDFString(_this.opt.reason);
|
||||||
}
|
}
|
||||||
if(this.opt.location){
|
if(_this.opt.location){
|
||||||
signObj["Location"] = this.convToPDFString(this.opt.location);
|
signObj["Location"] = _this.convToPDFString(_this.opt.location);
|
||||||
}
|
}
|
||||||
if(this.opt.contact){
|
if(_this.opt.contact){
|
||||||
signObj["ContactInfo"] = this.convToPDFString(this.opt.contact);
|
signObj["ContactInfo"] = _this.convToPDFString(_this.opt.contact);
|
||||||
}
|
}
|
||||||
/** @type {PDFLib.PDFRef} */
|
/** @type {PDFLib.PDFRef} */
|
||||||
var signatureDictRef = pdfcont.register(pdfcont.obj(signObj));
|
var signatureDictRef = pdfcont.register(pdfcont.obj(signObj));
|
||||||
|
@ -668,7 +731,7 @@ z.PdfSigner = class{
|
||||||
"FT": "Sig",
|
"FT": "Sig",
|
||||||
"Rect": signcrt.getSignRect(),
|
"Rect": signcrt.getSignRect(),
|
||||||
"V": signatureDictRef,
|
"V": signatureDictRef,
|
||||||
"T": this.convToPDFString(signm),
|
"T": _this.convToPDFString(signm),
|
||||||
"F": 132,
|
"F": 132,
|
||||||
"P": page.ref,
|
"P": page.ref,
|
||||||
};
|
};
|
||||||
|
@ -734,53 +797,23 @@ z.PdfSigner = class{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {Array<number>|Uint8Array|ArrayBuffer|string} p12cert
|
* @param {string} str
|
||||||
* @param {string} pwd
|
* @return {PDFLib.PDFString|PDFLib.PDFHexString}
|
||||||
* @return {forge_cert}
|
|
||||||
*/
|
*/
|
||||||
loadP12cert(p12cert, pwd){
|
convToPDFString(str){
|
||||||
// load P12 certificate
|
// Check if there is a multi-bytes char in the string.
|
||||||
if(typeof p12cert !== "string"){
|
/** @type {boolean} */
|
||||||
p12cert = z.u8arrToRaw(new Uint8Array(p12cert));
|
var flg = false;
|
||||||
|
for(var i=0; i<str.length; i++){
|
||||||
|
if(str.charCodeAt(i) > 0xFF){
|
||||||
|
flg = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Convert Buffer P12 to a forge implementation.
|
if(flg){
|
||||||
/** @type {forge.asn1} */
|
return PDFLib.PDFHexString.fromText(str);
|
||||||
var p12Asn1 = forge.asn1.fromDer(p12cert);
|
|
||||||
/** @type {forge.pkcs12} */
|
|
||||||
var p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, true, pwd);
|
|
||||||
// Extract safe bags by type.
|
|
||||||
// We will need all the certificates and the private key.
|
|
||||||
/** @type {Object<string|number, P12Bag>} */
|
|
||||||
var certBags = p12.getBags({
|
|
||||||
"bagType": forge.pki.oids.certBag,
|
|
||||||
})[forge.pki.oids.certBag];
|
|
||||||
/** @type {Object<string|number, P12Bag>} */
|
|
||||||
var keyBags = p12.getBags({
|
|
||||||
"bagType": forge.pki.oids.pkcs8ShroudedKeyBag,
|
|
||||||
})[forge.pki.oids.pkcs8ShroudedKeyBag];
|
|
||||||
this.privateKey = keyBags[0].key;
|
|
||||||
if(certBags){
|
|
||||||
// Get all the certificates (-cacerts & -clcerts)
|
|
||||||
// Keep track of the last found client certificate.
|
|
||||||
// This will be the public key that will be bundled in the signature.
|
|
||||||
Object.keys(certBags).forEach(function(a_ele){
|
|
||||||
/** @type {forge_cert} */
|
|
||||||
var a_cert = certBags[a_ele].cert;
|
|
||||||
|
|
||||||
this.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;
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
if(this.certIdx > 0){
|
|
||||||
return this.certs[--this.certIdx];
|
|
||||||
// z.fixCertAttributes(this.certs[this.certIdx]);
|
|
||||||
}else{
|
}else{
|
||||||
throw new Error("Failed to find a certificate.");
|
return PDFLib.PDFString.of(str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -790,12 +823,6 @@ z.PdfSigner = class{
|
||||||
* @return {Promise<Uint8Array>}
|
* @return {Promise<Uint8Array>}
|
||||||
*/
|
*/
|
||||||
async signPdf(pdfstr){
|
async signPdf(pdfstr){
|
||||||
/** @type {Date} */
|
|
||||||
var signdate = new Date();
|
|
||||||
if(this.opt.signdate instanceof Date && !this.tsainf){
|
|
||||||
signdate = this.opt.signdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds ByteRange information within a given PDF Buffer if one exists
|
// Finds ByteRange information within a given PDF Buffer if one exists
|
||||||
/** @type {Array<string>} */
|
/** @type {Array<string>} */
|
||||||
var byteRangeStrings = pdfstr.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
|
var byteRangeStrings = pdfstr.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
|
||||||
|
@ -833,48 +860,61 @@ z.PdfSigner = class{
|
||||||
// Remove the placeholder signature
|
// Remove the placeholder signature
|
||||||
pdfstr = pdfstr.slice(0, byteRange[1]) + pdfstr.slice(byteRange[2], byteRange[2] + byteRange[3]);
|
pdfstr = pdfstr.slice(0, byteRange[1]) + pdfstr.slice(byteRange[2], byteRange[2] + byteRange[3]);
|
||||||
|
|
||||||
// Here comes the actual PKCS#7 signing.
|
/** @type {forge.asn1} */
|
||||||
/** @type {forge.pkcs7} */
|
var asn1sig = null;
|
||||||
var p7 = forge.pkcs7.createSignedData();
|
if(this.certs.length > 0){
|
||||||
// Start off by setting the content.
|
/** @type {Date} */
|
||||||
p7.content = forge.util.createBuffer(pdfstr);
|
var signdate = new Date();
|
||||||
|
if(this.opt.signdate instanceof Date && !this.tsainf){
|
||||||
|
signdate = this.opt.signdate;
|
||||||
|
}
|
||||||
|
|
||||||
// Add all the certificates (-cacerts & -clcerts) to p7
|
// Here comes the actual PKCS#7 signing.
|
||||||
this.certs.forEach(function(a_cert){
|
/** @type {forge.pkcs7} */
|
||||||
p7.addCertificate(a_cert);
|
var p7 = null;
|
||||||
});
|
p7 = forge.pkcs7.createSignedData();
|
||||||
|
// Start off by setting the content.
|
||||||
|
p7.content = forge.util.createBuffer(pdfstr);
|
||||||
|
|
||||||
// Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects.
|
// Add all the certificates (-cacerts & -clcerts) to p7
|
||||||
p7.addSigner({
|
this.certs.forEach(function(a_cert){
|
||||||
key: this.privateKey,
|
p7.addCertificate(a_cert);
|
||||||
certificate: this.certs[this.certIdx],
|
});
|
||||||
digestAlgorithm: forge.pki.oids.sha256,
|
|
||||||
authenticatedAttributes: [
|
|
||||||
{
|
|
||||||
"type": forge.pki.oids.contentType,
|
|
||||||
"value": forge.pki.oids.data,
|
|
||||||
}, {
|
|
||||||
"type": forge.pki.oids.messageDigest,
|
|
||||||
}, {
|
|
||||||
"type": forge.pki.oids.signingTime,
|
|
||||||
"value": signdate,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sign in detached mode.
|
// Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects.
|
||||||
p7.sign({"detached": true});
|
p7.addSigner({
|
||||||
|
key: this.privateKey,
|
||||||
|
certificate: this.certs[this.certIdx],
|
||||||
|
digestAlgorithm: forge.pki.oids.sha256,
|
||||||
|
authenticatedAttributes: [
|
||||||
|
{
|
||||||
|
"type": forge.pki.oids.contentType,
|
||||||
|
"value": forge.pki.oids.data,
|
||||||
|
}, {
|
||||||
|
"type": forge.pki.oids.messageDigest,
|
||||||
|
}, {
|
||||||
|
"type": forge.pki.oids.signingTime,
|
||||||
|
"value": signdate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
if(this.tsainf){
|
// Sign in detached mode.
|
||||||
/** @type {forge.asn1} */
|
p7.sign({"detached": true});
|
||||||
var tsatoken = await this.queryTsa(p7.signers[0].signature);
|
|
||||||
p7.signerInfos[0].value.push(tsatoken);
|
if(this.tsainf){
|
||||||
this.log("Timestamp from " + this.tsainf.url + " has been added to the signature.");
|
/** @type {forge.asn1} */
|
||||||
|
var tsatoken = await this.queryTsa(p7.signers[0].signature);
|
||||||
|
p7.signerInfos[0].value.push(tsatoken);
|
||||||
|
}
|
||||||
|
asn1sig = p7.toAsn1();
|
||||||
|
}else{
|
||||||
|
asn1sig = await this.queryTsa(pdfstr, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the PDF has a good enough placeholder to fit the signature.
|
// Check if the PDF has a good enough placeholder to fit the signature.
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
var sighex = forge.asn1.toDer(p7.toAsn1()).toHex();
|
var sighex = forge.asn1.toDer(asn1sig).toHex();
|
||||||
// placeholderLength represents the length of the HEXified symbols but we're
|
// placeholderLength represents the length of the HEXified symbols but we're
|
||||||
// checking the actual lengths.
|
// checking the actual lengths.
|
||||||
this.log("Size of signature is " + sighex.length + "/" + placeholderLength);
|
this.log("Size of signature is " + sighex.length + "/" + placeholderLength);
|
||||||
|
@ -894,134 +934,128 @@ z.PdfSigner = class{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {string} str
|
* @param {string=} data
|
||||||
* @return {PDFLib.PDFString|PDFLib.PDFHexString}
|
* @param {boolean=} nocert
|
||||||
*/
|
|
||||||
convToPDFString(str){
|
|
||||||
// Check if there is a multi-bytes char in the string.
|
|
||||||
/** @type {boolean} */
|
|
||||||
var flg = false;
|
|
||||||
for(var i=0; i<str.length; i++){
|
|
||||||
if(str.charCodeAt(i) > 0xFF){
|
|
||||||
flg = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(flg){
|
|
||||||
return PDFLib.PDFHexString.fromText(str);
|
|
||||||
}else{
|
|
||||||
return PDFLib.PDFString.of(str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {string=} signature
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
genTsrData(signature){
|
|
||||||
// Generate SHA256 hash from signature content for TSA
|
|
||||||
/** @type {forge.md.digest} */
|
|
||||||
var md = forge.md.sha256.create();
|
|
||||||
md.update(signature);
|
|
||||||
// Generate TSA request
|
|
||||||
/** @type {forge.asn1} */
|
|
||||||
var asn1Req = forge.asn1.create(
|
|
||||||
forge.asn1.Class.UNIVERSAL,
|
|
||||||
forge.asn1.Type.SEQUENCE,
|
|
||||||
true,
|
|
||||||
[
|
|
||||||
// Version
|
|
||||||
{
|
|
||||||
composed: false,
|
|
||||||
constructed: false,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.INTEGER,
|
|
||||||
value: forge.asn1.integerToDer(1).data,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
composed: true,
|
|
||||||
constructed: true,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.SEQUENCE,
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
composed: true,
|
|
||||||
constructed: true,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.SEQUENCE,
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
composed: false,
|
|
||||||
constructed: false,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.OID,
|
|
||||||
value: forge.asn1.oidToDer(forge.oids.sha256).data,
|
|
||||||
}, {
|
|
||||||
composed: false,
|
|
||||||
constructed: false,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.NULL,
|
|
||||||
value: ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, {// Message imprint
|
|
||||||
composed: false,
|
|
||||||
constructed: false,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.OCTETSTRING,
|
|
||||||
value: md.digest().data,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, {
|
|
||||||
composed: false,
|
|
||||||
constructed: false,
|
|
||||||
tagClass: forge.asn1.Class.UNIVERSAL,
|
|
||||||
type: forge.asn1.Type.BOOLEAN,
|
|
||||||
value: 1, // Get REQ certificates
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return forge.asn1.toDer(asn1Req).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @param {string=} signature
|
|
||||||
* @return {Promise<forge.asn1>}
|
* @return {Promise<forge.asn1>}
|
||||||
*/
|
*/
|
||||||
async queryTsa(signature){
|
async queryTsa(data, nocert){
|
||||||
/** @lends {forge.asn1} */
|
/** @lends {forge.asn1} */
|
||||||
var asn1 = 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<forge.asn1>} aval
|
||||||
|
* @param {number=} atyp
|
||||||
|
* @param {number=} atag
|
||||||
|
* @return {forge.asn1}
|
||||||
|
*/
|
||||||
|
var asncreate = function(aval, atyp, atag){
|
||||||
|
/** @type {string|number|forge.asn1|Array<forge.asn1>} */
|
||||||
|
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} */
|
/** @type {string} */
|
||||||
var tsr = this.genTsrData(signature);
|
var tsr = asn1.toDer(asn1Req).getBytes();
|
||||||
/** @type {Uint8Array} */
|
/** @type {Uint8Array} */
|
||||||
var tu8s = z.rawToU8arr(tsr);
|
var tu8s = z.rawToU8arr(tsr);
|
||||||
|
/** @type {Object<string, *>} */
|
||||||
|
var hds = this.tsainf.headers ? this.tsainf.headers : {};
|
||||||
|
if(!hds["Content-Type"]){
|
||||||
|
hds["Content-Type"] = "application/timestamp-query";
|
||||||
|
}
|
||||||
/** @type {UrlFetchParams} */
|
/** @type {UrlFetchParams} */
|
||||||
var options = {
|
var options = {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"headers": {"Content-Type": "application/timestamp-query"},
|
"headers": hds,
|
||||||
"payload": tu8s,
|
"payload": tu8s,
|
||||||
};
|
};
|
||||||
/** @type {Uint8Array} */
|
/** @type {Uint8Array} */
|
||||||
var tesp = await z.UrlFetchApp.fetch(this.tsainf.url, options);
|
var tesp = await z.urlFetch(this.tsainf.url, options);
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
var tstr = z.u8arrToRaw(tesp);
|
var tstr = z.u8arrToRaw(tesp);
|
||||||
/** @type {forge.asn1} */
|
/** @type {forge.asn1} */
|
||||||
var token = asn1.fromDer(tstr).value[1];
|
var token = asn1.fromDer(tstr).value[1];
|
||||||
|
|
||||||
// create the asn1 to append to the signature
|
/** @type {forge.asn1} */
|
||||||
/** @type {string} *///forge.pki.oids.timeStampToken
|
var ret = null;
|
||||||
var typstr = asn1.oidToDer("1.2.840.113549.1.9.16.2.14").getBytes();
|
if(nocert){
|
||||||
return asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
|
ret = token;
|
||||||
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
|
}else{
|
||||||
// Attribute Type
|
// create the asn1 to append to the signature
|
||||||
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, typstr),
|
ret = asncreate([
|
||||||
// Attribute Value
|
asncreate([
|
||||||
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [token]),
|
// 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);
|
||||||
|
}
|
||||||
|
this.log("Timestamp from " + this.tsainf.url + " has been obtained.");
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue