Removed nodejs crypto inclusion for node-forge

Added loadConfiguration function
Moved completely from Openssl for signature generation and crypto for sha1 generation to node-forge
Moved configuration to another file
Added init function
This commit is contained in:
Alexander Cerutti
2018-06-06 23:34:57 +02:00
parent 74700fedd0
commit 662ae69e60
4 changed files with 228 additions and 82 deletions

View File

@@ -2,12 +2,12 @@
"certificates": {
"dir": "certificates",
"files": {
"wwdr_pem": "WWDR.pem",
"certificate": "passcertificate.pem",
"key": "passkey.pem"
"wwdr": "WWDR.pem",
"signerCert": "passcertificate.pem",
"signerKey": "passkey.pem"
},
"credentials": {
"dev_pem_key": "123456"
"privateKeySecret": "123456"
}
},
"output": {

266
index.js
View File

@@ -1,17 +1,19 @@
const os = require("os");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const forge = require("node-forge");
const { spawn } = require("child_process");
const archiver = require("archiver");
const async = require("async");
const _configuration = Object.freeze(require("./config.json"));
let _configuration = Object.freeze(require("./config.json"));
const supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard)/i;
const passModelsDir = _configuration.models.dir;
const outputDir = _configuration.output.dir;
const Certificates = _configuration.certificates;
const Certificates = {
status: false
};
/**
Apply a filter to arg0 to remove hidden files names (starting with dot)
@@ -28,6 +30,61 @@ function capitalizeFirst(str) {
return str[0].toUpperCase()+str.slice(1);
}
function loadConfiguration(configurationPath) {
let setup = require(path.resolve(__dirname, configurationPath));
let reqFilesKeys = ["wwdr", "signerCert", "signerKey"];
// Node-Forge also accepts .cer certificates
if (!setup.certificates.dir || fs.accessSync(path.resolve(setup.certificates.dir)) !== undefined) {
throw new Error("Unable to load certificates directory. Check its existence or the permissions.");
}
if (!setup.certificates.files) {
throw new Error("Expected key 'files' in configuration file but not found.");
}
if (!setup.certificates.files.wwdr) {
throw new Error("Expected file path or content for key certificates.files.wwdr. Please provide a valid certificate from https://apple.co/2sc2pvv");
}
if (!setup.certificates.files.signerCert) {
throw new Error("Expected file path or content for key certificates.files.signerCert. Please provide a valid signer certificate.")
}
if (!setup.certificates.files.signerKey || !setup.certificates.credentials.privateKeySecret) {
throw new Error("Expected file path or content for key certificates.files.signerKey with an associated password at certificates.credentials.privateKeySecret but not found.")
}
let certPaths = reqFilesKeys.map(e => path.resolve(setup.certificates.dir, setup.certificates.files[e]));
return new Promise(function(success, reject) {
let docStruct = {};
async.concat(certPaths, fs.readFile, function(err, contents) {
// contents is a Buffer array
if (err) {
return reject(err);
}
return success(
contents.map(function(file, index) {
if (file.includes("PRIVATE KEY")) {
return forge.pki.decryptRsaPrivateKey(
file,
setup.certificates.credentials.privateKeySecret
);
} else if (file.includes("CERTIFICATE")) {
return forge.pki.certificateFromPem(file);
} else {
throw new Error("File not allowed in configuration. Only .pems files containing certificates and private keys are allowed");
}
})
)
});
});
}
/**
@function fileStreamToBuffer
@params {String} path - the path of the file to be read
@@ -62,11 +119,11 @@ function fileStreamToBuffer(path, ...callbacks) {
function checkSignatureRequirements() {
let checkCertificate = new Promise(function(available, notAvailable) {
fs.access(`${Certificates.dir}/${Certificates.files.certificate}`, (e) => (!!e ? notAvailable : available)() );
fs.access(path.resolve(Certificates.dir, Certificates.files.signerCert), (e) => (!!e ? notAvailable : available)() );
});
let checkKey = new Promise(function(available, notAvailable) {
fs.access(`${Certificates.dir}/${Certificates.files.key}`, (e) => (!!e ? notAvailable : available)() );
fs.access(path.resolve(Certificates.dir, Certificates.files.signerKey), (e) => (!!e ? notAvailable : available)() );
});
return Promise.all([checkCertificate, checkKey]);
@@ -88,49 +145,107 @@ function UUIDGen(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+
@returns {Object} Promise
*/
function generateManifestSignature(manifestUUID) {
return new Promise(function(done, rejected) {
checkSignatureRequirements()
.then(function() {
let opensslError = false;
let opensslBuffer = [];
let opensslProcess = spawn("openssl", [
"smime",
"-binary",
"-sign",
"-certfile", path.resolve(Certificates.dir, Certificates.files["wwdr_pem"]),
"-signer", path.resolve(Certificates.dir, Certificates.files["certificate"]),
"-inkey", path.resolve(Certificates.dir, Certificates.files["key"]),
"-in", path.resolve(`${os.tmpdir()}/manifest-${manifestUUID}.json`),
// "-out", path.resolve("passCreator", "event.pass", "./signature"),
"-outform", "DER",
"-passin", `pass:${Certificates.credentials["dev_pem_key"]}`
]);
function generateManifestSignature(manifest) {
// return new Promise(function(done, rejected) {
let signature = forge.pkcs7.createSignedData();
opensslProcess.stdout.on("data", function(data) {
opensslBuffer.push(data);
});
opensslProcess.stderr.on("data", function(data) {
opensslBuffer.push(data);
opensslError = true;
});
opensslProcess.stdout.on("end", function() {
if (opensslError) {
return rejected(Buffer.concat(opensslBuffer));
if (typeof manifest === "object") {
signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8")
} else if (typeof manifest === "string") {
signature.content = manifest;
} else {
throw new Error(`Manifest content must be a string or an object. Unable to accept manifest of type ${typeof manifest}`);
}
return done(Buffer.concat(opensslBuffer));
});
})
.catch(function(e) {
return rejected(`Cannot fulfill signature requirements.\n${e}`);
});
signature.addCertificate(Certificates.wwdr);
signature.addCertificate(Certificates.signerCert);
signature.addSigner({
key: Certificates.signerKey,
certificate: Certificates.signerCert,
digestAlgorithm: forge.pki.oids.sha1,
authenticatedAttributes: [{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data
}, {
type: forge.pki.oids.messageDigest,
}, {
// the value is autogenerated
type: forge.pki.oids.signingTime,
}]
});
signature.sign();
/*
* Signing creates in contentInfo a JSON object nested BER/TLV (X.690 standard) structure.
* Each object represents a component of ASN.1 (Abstract Syntax Notation)
* For a more complete reference, refer to: https://en.wikipedia.org/wiki/X.690#BER_encoding
*
* signature.contentInfo.type => SEQUENCE OF (16)
* signature.contentInfo.value[0].type => OBJECT IDENTIFIER (6)
* signature.contantInfo.value[1].type => END OF CONTENT (EOC - 0)
*
* EOC are only present only in constructed indefinite-length methods
* Since `signature.contentInfo.value[1].value` contains an object whose value contains the content we passed,
* we have to pop the whole object away to avoid signature content invalidation.
*
*/
signature.contentInfo.value.pop();
// Converting the JSON Structure into a DER (which is a subset of BER), ASN.1 valid structure
// Returning the buffer of the signature
// return done(Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary'));
return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary');
// });
}
// function generateManifestSignature(manifestUUID) {
// return new Promise(function(done, rejected) {
// checkSignatureRequirements()
// .then(function() {
// let opensslError = false;
// let opensslBuffer = [];
// let opensslProcess = spawn("openssl", [
// "smime",
// "-binary",
// "-sign",
// "-certfile", path.resolve(Certificates.dir, Certificates.files["wwdr_pem"]),
// "-signer", path.resolve(Certificates.dir, Certificates.files["certificate"]),
// "-inkey", path.resolve(Certificates.dir, Certificates.files["key"]),
// "-in", path.resolve(`${os.tmpdir()}/manifest-${manifestUUID}.json`),
// // "-out", path.resolve("passCreator", "event.pass", "./signature"),
// "-outform", "DER",
// "-passin", `pass:${Certificates.credentials["privateKeySecret"]}`
// ]);
// opensslProcess.stdout.on("data", function(data) {
// opensslBuffer.push(data);
// });
// opensslProcess.stderr.on("data", function(data) {
// opensslBuffer.push(data);
// opensslError = true;
// });
// opensslProcess.stdout.on("end", function() {
// if (opensslError) {
// return rejected(Buffer.concat(opensslBuffer));
// }
// return done(Buffer.concat(opensslBuffer));
// });
// })
// .catch(function(e) {
// return rejected(`Cannot fulfill signature requirements.\n${e}`);
// });
// });
// }
/**
Generates a Buffer of JSON file (manifest)
@function generateManifest
@@ -157,7 +272,8 @@ function generateManifest(fromObject, manifestUUID) {
manifestWS.write(source);
manifestWS.end();
return done(Buffer.from(source));
//return done(Buffer.from(source));
return done(Buffer.from(source).toString());
});
}
@@ -245,8 +361,11 @@ function editPassStructure(options, passBuffer) {
});
}
function RequestHandler(request, response) {
if (!Certificates.status) {
throw new Error("passkit requires initialization by calling .init() method.");
}
if (!supportedTypesOfPass.test(request.params.type)) {
// 😊
response.set("Content-Type", "application/json");
@@ -283,11 +402,12 @@ function RequestHandler(request, response) {
fileStreamToBuffer(`${passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) {
editPassStructure(filterPassOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) {
// Manifest dictionary
let manifestRaw = {};
let manifest = {};
let archive = archiver("zip");
archive.append(passFileBuffer, { name: "pass.json" });
manifestRaw["pass.json"] = crypto.createHash("sha1").update(passFileBuffer).digest("hex").trim();
manifest["pass.json"] = forge.md.sha1.create().update(passFileBuffer.toString("binary")).digest().toHex();
async.each(list, function getHashAndArchive(file, callback) {
if (/(manifest|signature|pass)/ig.test(file)) {
@@ -298,17 +418,17 @@ function RequestHandler(request, response) {
// adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS
archive.file(`${passModelsDir}/${request.params.type}.pass/${file}`, { name: file });
let hashFlow = crypto.createHash("sha1");
let hashFlow = forge.md.sha1.create();
fs.createReadStream(`${passModelsDir}/${request.params.type}.pass/${file}`)
.on("data", function(data) {
hashFlow.update(data);
hashFlow.update(data.toString("binary"));
})
.on("error", function(e) {
return callback(e);
})
.on("end", function() {
manifestRaw[file] = hashFlow.digest("hex").trim();
manifest[file] = hashFlow.digest().toHex().trim();
return callback();
});
}, function end(error) {
@@ -316,15 +436,17 @@ function RequestHandler(request, response) {
throw new Error(`Unable to compile manifest. ${error}`);
}
let uuid = UUIDGen();
// let uuid = UUIDGen();
generateManifest(manifestRaw, uuid)
.then(function(manifestBuffer) {
// generateManifest(manifestRaw, uuid)
// .then(function(manifestBuffer) {
archive.append(manifestBuffer, { name: "manifest.json" });
archive.append(Buffer.from(JSON.stringify(manifest), "utf8"), { name: "manifest.json" });
generateManifestSignature(uuid)
.then(function(signatureBuffer) {
let signatureBuffer = generateManifestSignature(manifest);
console.log(signatureBuffer)
if (!fs.existsSync("output")) {
fs.mkdirSync("output");
@@ -345,18 +467,16 @@ function RequestHandler(request, response) {
}
});
});
})
.catch(function(buffer) {
throw buffer.toString();
});
})
.catch(function(error) {
throw error;
});
// })
// .catch(function(error) {
// throw error;
// });
});
})
.catch(function(err) {
throw err;
// 😊
response.set("Content-Type", "application/json");
response.status(418).send({ ecode: 418, status: false, message: `Got error while parsing pass.json file: ${err}` });
@@ -368,4 +488,24 @@ function RequestHandler(request, response) {
});
}
module.exports = { RequestHandler };
function init(configPath) {
if (Certificates.status) {
throw new Error("Initialization must be triggered only once.");
}
if (!configPath || fs.accessSync(path.resolve(__dirname, configPath)) !== undefined) {
throw new Error(`Cannot load configuration from 'path' (${configPath}). File not existing or missing path.`);
}
loadConfiguration(configPath).then(function(config) {
Certificates.wwdr = config[0];
Certificates.signerCert = config[1];
Certificates.signerKey = config[2];
Certificates.status = true;
})
.catch(function(error) {
throw new Error(`Error: ${error}`);
});
}
module.exports = { init, RequestHandler };

5
package-lock.json generated
View File

@@ -417,6 +417,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"node-forge": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
"integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ=="
},
"normalize-path": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": {
"archiver": "^2.1.1",
"async": "^2.6.0",
"express": "^4.16.3"
"express": "^4.16.3",
"node-forge": "^0.7.5"
}
}