Added support to sign with TSA.

pull/2/head 1.2.0
zboris12 2022-09-24 13:59:30 +09:00
parent b72d38473a
commit f932a2bb2a
4 changed files with 245 additions and 28 deletions

View File

@ -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
* __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
* __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
* __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

View File

@ -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;

2
forge.min.edited.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -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 {