parent
b72d38473a
commit
f932a2bb2a
44
README.md
44
README.md
|
@ -5,6 +5,15 @@ And it also can be used in Google Apps Script.
|
|||
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.
|
||||
|
||||
## About signing with TSA
|
||||
|
||||
This tool supports signing with a timestamp from TSA(Time Stamp Authority),
|
||||
but because of the CORS security restrictions in web browser,
|
||||
this function can only be used in Google Apps Script.
|
||||
|
||||
And because node-forge hasn't supported unauthenticated attributes in pkcs7 yet,
|
||||
so when use this function, [the edited version](https://github.com/zboris12/zgapdfsigner/releases/download/1.2.0/forge.min.edited.js) needs to be imported.
|
||||
|
||||
## The Dependencies
|
||||
|
||||
* [pdf-lib](https://pdf-lib.js.org/)
|
||||
|
@ -16,7 +25,7 @@ Just import the dependencies and this tool.
|
|||
```html
|
||||
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js" type="text/javascript"></script>
|
||||
<script src="https://unpkg.com/node-forge/dist/forge.min.js" type="text/javascript"></script>
|
||||
<script src="https://github.com/zboris12/zgapdfsigner/releases/download/1.1.0/zgapdfsigner.js" type="text/javascript"></script>
|
||||
<script src="https://github.com/zboris12/zgapdfsigner/releases/download/1.2.0/zgapdfsigner.js" type="text/javascript"></script>
|
||||
```
|
||||
|
||||
## Let's sign
|
||||
|
@ -94,20 +103,21 @@ var window = globalThis;
|
|||
// Load pdf-lib
|
||||
eval(UrlFetchApp.fetch("https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js").getContentText());
|
||||
// Load node-forge
|
||||
eval(UrlFetchApp.fetch("https://unpkg.com/node-forge@1.3.1/dist/forge.min.js").getContentText());
|
||||
eval(UrlFetchApp.fetch("https://github.com/zboris12/zgapdfsigner/releases/download/1.2.0/forge.min.edited.js").getContentText());
|
||||
// Load ZgaPdfSigner
|
||||
eval(UrlFetchApp.fetch("https://github.com/zboris12/zgapdfsigner/releases/download/1.1.0/zgapdfsigner.js").getContentText());
|
||||
eval(UrlFetchApp.fetch("https://github.com/zboris12/zgapdfsigner/releases/download/1.2.0/zgapdfsigner.js").getContentText());
|
||||
|
||||
// Load pdf, certificate
|
||||
var pdfBlob = DriveApp.getFilesByName("_test.pdf").next().getBlob();
|
||||
var certBlob = DriveApp.getFilesByName("_test.pfx").next().getBlob();
|
||||
// Sign the pdf
|
||||
var sopt = {
|
||||
p12cert: new Uint8Array(certBlob.getBytes()),
|
||||
p12cert: certBlob.getBytes(),
|
||||
pwd: "some passphrase",
|
||||
signdate: "1",
|
||||
};
|
||||
var signer = new Zga.PdfSigner(sopt);
|
||||
var u8arr = await signer.sign(new Uint8Array(pdfBlob.getBytes()));
|
||||
var u8arr = await signer.sign(pdfBlob.getBytes());
|
||||
// Save the result pdf to some folder
|
||||
var fld = DriveApp.getFolderById("a folder's id");
|
||||
fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test.pdf"));
|
||||
|
@ -115,24 +125,36 @@ fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test.
|
|||
|
||||
## Detail of SignOption
|
||||
|
||||
* __p12cert__: (Uint8Array|ArrayBuffer|string) :point_right: Certificate's data
|
||||
* __p12cert__: Array<number>|Uint8Array|ArrayBuffer|string :point_right: Certificate's data
|
||||
* __pwd__: string :point_right: The passphrase of the certificate
|
||||
* __reason__: string :point_right: (Optional) The reason for signing
|
||||
* __location__: string :point_right: (Optional) Your location
|
||||
* __contact__: string :point_right: (Optional) Your contact information
|
||||
* __signdate__: Date :point_right: (Optional) The date and time of signing
|
||||
* __signdate__: Date|string|_TsaServiceInfo_ :point_right: (Optional)
|
||||
* When it is a Date, it means the date and time for signing.
|
||||
* When it is a string, it can be an url of TSA or an index of the preset TSA as below:
|
||||
* "1": http://ts.ssl.com
|
||||
* "2": http://timestamp.digicert.com
|
||||
* "3": http://timestamp.sectigo.com
|
||||
* "4": http://timestamp.entrust.net/TSS/RFC3161sha2TS
|
||||
* "5": http://timestamp.apple.com/ts01
|
||||
* "6": http://www.langedge.jp/tsa
|
||||
* "7": https://freetsa.org/tsr
|
||||
* When it is a _TsaServiceInfo_, it means a full customized information of TSA.
|
||||
* __url__: string :point_right: The url of TSA
|
||||
* __len__: number :point_right: (Optional) The length of signature's placeholder
|
||||
* __signame__: string :point_right: (Optional) The name of the signature
|
||||
* __drawinf__: SignDrawInfo :point_right: (Optional) Visible signature's information
|
||||
* __area__: SignAreaInfo :point_right: The signature's drawing area
|
||||
* __drawinf__: _SignDrawInfo_ :point_right: (Optional) Visible signature's information
|
||||
* __area__: _SignAreaInfo_ :point_right: The signature's drawing area
|
||||
* __x__: number :point_right: Distance from left
|
||||
* __y__: number :point_right: Distance from top
|
||||
* __w__: number :point_right: Width
|
||||
* __h__: number :point_right: Height
|
||||
* __pageidx__: number :point_right: (Optional) The page index for drawing the signature
|
||||
* __imgData__: Uint8Array|ArrayBuffer|string :point_right: (Optional) The image's data
|
||||
* __imgData__: Array<number>|Uint8Array|ArrayBuffer|string :point_right: (Optional) The image's data
|
||||
* __imgType__: string :point_right: (Optional) The image's type, __only support jpg and png__
|
||||
* __text__: string :point_right: (Optional) A text drawing on signature, __not implemented yet__
|
||||
* __fontData__: PDFLib.StandardFonts|Uint8Array|ArrayBuffer|string :point_right: (Optional) The font's data for drawing text, __not implemented yet__
|
||||
* __fontData__: PDFLib.StandardFonts|Array<number>|Uint8Array|ArrayBuffer|string :point_right: (Optional) The font's data for drawing text, __not implemented yet__
|
||||
|
||||
|
||||
## License
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @typedef
|
||||
* {{
|
||||
* url: string,
|
||||
* len: (number|undefined),
|
||||
* }}
|
||||
*/
|
||||
var TsaServiceInfo;
|
||||
/**
|
||||
* the base point of x, y is top left corner.
|
||||
* @typedef
|
||||
|
@ -14,10 +22,10 @@ var SignAreaInfo;
|
|||
* {{
|
||||
* area: SignAreaInfo,
|
||||
* pageidx: (number|undefined),
|
||||
* imgData: (Uint8Array|ArrayBuffer|string|undefined),
|
||||
* imgData: (Array<number>|Uint8Array|ArrayBuffer|string|undefined),
|
||||
* imgType: (string|undefined),
|
||||
* text: (string|undefined),
|
||||
* fontData: (PDFLib.StandardFonts|Uint8Array|ArrayBuffer|string|undefined),
|
||||
* fontData: (PDFLib.StandardFonts|Array<number>|Uint8Array|ArrayBuffer|string|undefined),
|
||||
* img: (PDFLib.PDFImage|undefined),
|
||||
* font: (PDFLib.PDFFont|undefined),
|
||||
* }}
|
||||
|
@ -26,14 +34,15 @@ var SignDrawInfo;
|
|||
/**
|
||||
* @typedef
|
||||
* {{
|
||||
* p12cert: (Uint8Array|ArrayBuffer|string),
|
||||
* p12cert: (Array<number>|Uint8Array|ArrayBuffer|string),
|
||||
* pwd: string,
|
||||
* reason: (string|undefined),
|
||||
* location: (string|undefined),
|
||||
* contact: (string|undefined),
|
||||
* signdate: (Date|undefined),
|
||||
* signdate: (Date|TsaServiceInfo|string|undefined),
|
||||
* signame: (string|undefined),
|
||||
* drawinf: (SignDrawInfo|undefined),
|
||||
* debug: (boolean|undefined),
|
||||
* }}
|
||||
*/
|
||||
var SignOption;
|
||||
|
|
File diff suppressed because one or more lines are too long
208
zgapdfsigner.js
208
zgapdfsigner.js
|
@ -2,6 +2,17 @@
|
|||
|
||||
globalThis.Zga = {
|
||||
|
||||
/** @type {Object<string, TsaServiceInfo>} */
|
||||
TSAURLS: {
|
||||
"1": {url: "http://ts.ssl.com", len: 15100},
|
||||
"2": {url: "http://timestamp.digicert.com", len: 14900},
|
||||
"3": {url: "http://timestamp.sectigo.com", len: 12900},
|
||||
"4": {url: "http://timestamp.entrust.net/TSS/RFC3161sha2TS", len: 13900},
|
||||
"5": {url: "http://timestamp.apple.com/ts01", len: 11600},
|
||||
"6": {url: "http://www.langedge.jp/tsa", len: 8700},
|
||||
"7": {url: "https://freetsa.org/tsr", len: 14000},
|
||||
},
|
||||
|
||||
PdfSigner: class {
|
||||
/**
|
||||
* @constructor
|
||||
|
@ -10,11 +21,49 @@ PdfSigner: class {
|
|||
constructor(signopt){
|
||||
/** @private @type {SignOption} */
|
||||
this.opt = signopt;
|
||||
/** @private @type {TsaServiceInfo} */
|
||||
this.tsainf = null;
|
||||
/** @private @type {boolean} */
|
||||
this.debug = false;
|
||||
|
||||
if(!globalThis.PDFLib){
|
||||
throw new Error("pdf-lib is not imported.");
|
||||
}
|
||||
if(!globalThis.forge){
|
||||
throw new Error("node-forge is not imported.");
|
||||
}
|
||||
if(signopt.signdate){
|
||||
if(typeof signopt.signdate == "string"){
|
||||
this.tsainf = {
|
||||
url: signopt.signdate,
|
||||
};
|
||||
}else if(signopt.signdate.url){
|
||||
this.tsainf = Object.assign({}, signopt.signdate);
|
||||
}
|
||||
}
|
||||
if(this.tsainf){
|
||||
if(!globalThis.UrlFetchApp){
|
||||
throw new Error("Because of the CORS security restrictions, signing with TSA is not supported in web browser.");
|
||||
}
|
||||
if(Zga.TSAURLS[this.tsainf.url]){
|
||||
Object.assign(this.tsainf, Zga.TSAURLS[this.tsainf.url]);
|
||||
}else if(!(new RegExp("^https?://")).test(this.tsainf.url)){
|
||||
throw new Error("Unknown tsa data. " + JSON.stringify(this.tsainf));
|
||||
}
|
||||
if(!this.tsainf.len){
|
||||
this.tsainf.len = 16000;
|
||||
}
|
||||
}
|
||||
if(typeof this.opt.debug == "boolean"){
|
||||
this.debug = this.opt.debug;
|
||||
}else if(globalThis.debug){
|
||||
this.debug = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {PDFLib.PDFDocument|Uint8Array|ArrayBuffer|string} pdf
|
||||
* @param {PDFLib.PDFDocument|Array<number>|Uint8Array|ArrayBuffer|string} pdf
|
||||
* @return {Promise<Uint8Array>}
|
||||
*/
|
||||
async sign(pdf){
|
||||
|
@ -22,15 +71,24 @@ PdfSigner: class {
|
|||
var pdfdoc = null;
|
||||
if(pdf.addPage){
|
||||
pdfdoc = pdf;
|
||||
}else if(Array.isArray(pdf)){
|
||||
pdfdoc = await PDFLib.PDFDocument.load(new Uint8Array(pdf));
|
||||
}else{
|
||||
pdfdoc = await PDFLib.PDFDocument.load(pdf);
|
||||
}
|
||||
|
||||
if(this.opt.drawinf && this.opt.drawinf.imgData && !this.opt.drawinf.img){
|
||||
/** @type {Uint8Array|ArrayBuffer|string} */
|
||||
var imgData2 = null;
|
||||
if(Array.isArray(this.opt.drawinf.imgData)){
|
||||
imgData2 = new Uint8Array(this.opt.drawinf.imgData);
|
||||
}else{
|
||||
imgData2 = this.opt.drawinf.imgData;
|
||||
}
|
||||
if(this.opt.drawinf.imgType == "png"){
|
||||
this.opt.drawinf.img = await pdfdoc.embedPng(this.opt.drawinf.imgData);
|
||||
this.opt.drawinf.img = await pdfdoc.embedPng(imgData2);
|
||||
}else if(this.opt.drawinf.imgType == "jpg"){
|
||||
this.opt.drawinf.img = await pdfdoc.embedJpg(this.opt.drawinf.imgData);
|
||||
this.opt.drawinf.img = await pdfdoc.embedJpg(imgData2);
|
||||
}else{
|
||||
throw new Error("Unkown image type. " + this.opt.drawinf.imgType);
|
||||
}
|
||||
|
@ -40,7 +98,12 @@ PdfSigner: class {
|
|||
var uarr = await pdfdoc.save({"useObjectStreams": false});
|
||||
var pdfstr = Zga.u8arrToRaw(uarr);
|
||||
|
||||
return this.signPdf(pdfstr);
|
||||
this.log("A signature holder has been added to the pdf.");
|
||||
|
||||
/** @type {Uint8Array} */
|
||||
var ret = this.signPdf(pdfstr);
|
||||
this.log("Signing pdf accomplished.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,7 +114,7 @@ PdfSigner: class {
|
|||
/** @const {string} */
|
||||
const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********";
|
||||
/** @const {number} */
|
||||
const SIGNATURE_LENGTH = 3322;
|
||||
const SIGNATURE_LENGTH = this.tsainf ? this.tsainf.len : 3322;
|
||||
|
||||
/** @const {VisualSignature} */
|
||||
const visign = new Zga.VisualSignature(this.opt.drawinf);
|
||||
|
@ -60,8 +123,10 @@ PdfSigner: class {
|
|||
/** @const {PDFLib.PDFPage} */
|
||||
const page = pdfdoc.getPages()[visign.getPageIndex()];
|
||||
|
||||
if(!this.opt.signdate){
|
||||
this.opt.signdate = new Date();
|
||||
/** @type {Date} */
|
||||
var signdate = new Date();
|
||||
if(this.opt.signdate instanceof Date && !this.tsainf){
|
||||
signdate = this.opt.signdate;
|
||||
}
|
||||
|
||||
/** @type {PDFLib.PDFArray} */
|
||||
|
@ -78,7 +143,7 @@ PdfSigner: class {
|
|||
"SubFilter": "adbe.pkcs7.detached",
|
||||
"ByteRange": bytrng,
|
||||
"Contents": PDFLib.PDFHexString.of("0".repeat(SIGNATURE_LENGTH)),
|
||||
"M": PDFLib.PDFString.fromDate(this.opt.signdate),
|
||||
"M": PDFLib.PDFString.fromDate(signdate),
|
||||
"Prop_Build": pdfdoc.context.obj({
|
||||
"App": pdfdoc.context.obj({
|
||||
"Name": "ZgaPdfSinger",
|
||||
|
@ -133,8 +198,10 @@ PdfSigner: class {
|
|||
* @return {Uint8Array}
|
||||
*/
|
||||
signPdf(pdfstr){
|
||||
if(!this.opt.signdate){
|
||||
this.opt.signdate = new Date();
|
||||
/** @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
|
||||
|
@ -217,19 +284,37 @@ PdfSigner: class {
|
|||
"type": forge.pki.oids.messageDigest,
|
||||
}, {
|
||||
"type": forge.pki.oids.signingTime,
|
||||
"value": this.opt.signdate,
|
||||
"value": signdate,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if(this.tsainf){
|
||||
//p7.signers[0].unauthenticatedAttributes.push({type: forge.pki.oids.timeStampToken, value: ""})
|
||||
p7.signers[0].unauthenticatedAttributes.push({type: "1.2.840.113549.1.9.16.2.14", value: ""});
|
||||
}
|
||||
|
||||
// Sign in detached mode.
|
||||
p7.sign({"detached": true});
|
||||
|
||||
if(this.tsainf){
|
||||
var tsatoken = this.queryTsa(p7.signers[0].signature);
|
||||
p7.signerInfos[0].value[6].value[0].value[1] = forge.asn1.create(
|
||||
forge.asn1.Class.UNIVERSAL,
|
||||
forge.asn1.Type.SET,
|
||||
true,
|
||||
[tsatoken]
|
||||
);
|
||||
this.log("Timestamp from " + this.tsainf.url + " has been added to the signature.");
|
||||
}
|
||||
|
||||
// Check if the PDF has a good enough placeholder to fit the signature.
|
||||
var sighex = forge.asn1.toDer(p7.toAsn1()).toHex();
|
||||
// placeholderLength represents the length of the HEXified symbols but we're
|
||||
// checking the actual lengths.
|
||||
this.log("Size of signature is " + sighex.length + "/" + placeholderLength);
|
||||
if(sighex.length > placeholderLength){
|
||||
throw new Error("Signature is too big.");
|
||||
throw new Error("Signature is too big. Needs: " + sighex.length);
|
||||
}else{
|
||||
// Pad the signature with zeroes so the it is the same length as the placeholder
|
||||
sighex += "0".repeat(placeholderLength - sighex.length);
|
||||
|
@ -260,6 +345,105 @@ PdfSigner: class {
|
|||
return PDFLib.PDFString.of(str);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} signature
|
||||
* @return {string}
|
||||
*/
|
||||
genTsrData(signature){
|
||||
// Generate SHA256 hash from signature content for TSA
|
||||
var md = forge.md.sha256.create();
|
||||
md.update(signature);
|
||||
// Generate TSA request
|
||||
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 {Object}
|
||||
*/
|
||||
queryTsa(signature){
|
||||
var tsr = this.genTsrData(signature);
|
||||
var tu8s = Zga.rawToU8arr(tsr);
|
||||
var options = {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/timestamp-query"},
|
||||
"payload": tu8s,
|
||||
};
|
||||
var tblob = UrlFetchApp.fetch(this.tsainf.url, options).getBlob();
|
||||
var tstr = Zga.u8arrToRaw(new Uint8Array(tblob.getBytes()));
|
||||
var token = forge.asn1.fromDer(tstr).value[1];
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} msg
|
||||
*/
|
||||
log(msg){
|
||||
if(this.debug){
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
VisualSignature: class {
|
||||
|
|
Loading…
Reference in New Issue