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": { "certificates": {
"dir": "certificates", "dir": "certificates",
"files": { "files": {
"wwdr_pem": "WWDR.pem", "wwdr": "WWDR.pem",
"certificate": "passcertificate.pem", "signerCert": "passcertificate.pem",
"key": "passkey.pem" "signerKey": "passkey.pem"
}, },
"credentials": { "credentials": {
"dev_pem_key": "123456" "privateKeySecret": "123456"
} }
}, },
"output": { "output": {

294
index.js
View File

@@ -1,17 +1,19 @@
const os = require("os"); const os = require("os");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const crypto = require("crypto"); const forge = require("node-forge");
const { spawn } = require("child_process"); const { spawn } = require("child_process");
const archiver = require("archiver"); const archiver = require("archiver");
const async = require("async"); 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 supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard)/i;
const passModelsDir = _configuration.models.dir; const passModelsDir = _configuration.models.dir;
const outputDir = _configuration.output.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) 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); 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 @function fileStreamToBuffer
@params {String} path - the path of the file to be read @params {String} path - the path of the file to be read
@@ -62,11 +119,11 @@ function fileStreamToBuffer(path, ...callbacks) {
function checkSignatureRequirements() { function checkSignatureRequirements() {
let checkCertificate = new Promise(function(available, notAvailable) { 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) { 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]); 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 @returns {Object} Promise
*/ */
function generateManifestSignature(manifestUUID) {
return new Promise(function(done, rejected) {
checkSignatureRequirements()
.then(function() {
let opensslError = false;
let opensslBuffer = [];
let opensslProcess = spawn("openssl", [ function generateManifestSignature(manifest) {
"smime", // return new Promise(function(done, rejected) {
"-binary", let signature = forge.pkcs7.createSignedData();
"-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"]}`
]);
opensslProcess.stdout.on("data", function(data) { if (typeof manifest === "object") {
opensslBuffer.push(data); 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}`);
}
opensslProcess.stderr.on("data", function(data) { signature.addCertificate(Certificates.wwdr);
opensslBuffer.push(data); signature.addCertificate(Certificates.signerCert);
opensslError = true;
});
opensslProcess.stdout.on("end", function() { signature.addSigner({
if (opensslError) { key: Certificates.signerKey,
return rejected(Buffer.concat(opensslBuffer)); certificate: Certificates.signerCert,
} digestAlgorithm: forge.pki.oids.sha1,
authenticatedAttributes: [{
return done(Buffer.concat(opensslBuffer)); type: forge.pki.oids.contentType,
}); value: forge.pki.oids.data
}) }, {
.catch(function(e) { type: forge.pki.oids.messageDigest,
return rejected(`Cannot fulfill signature requirements.\n${e}`); }, {
// 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) Generates a Buffer of JSON file (manifest)
@function generateManifest @function generateManifest
@@ -157,7 +272,8 @@ function generateManifest(fromObject, manifestUUID) {
manifestWS.write(source); manifestWS.write(source);
manifestWS.end(); 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) { function RequestHandler(request, response) {
if (!Certificates.status) {
throw new Error("passkit requires initialization by calling .init() method.");
}
if (!supportedTypesOfPass.test(request.params.type)) { if (!supportedTypesOfPass.test(request.params.type)) {
// 😊 // 😊
response.set("Content-Type", "application/json"); 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) { fileStreamToBuffer(`${passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) {
editPassStructure(filterPassOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) { editPassStructure(filterPassOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) {
// Manifest dictionary // Manifest dictionary
let manifestRaw = {}; let manifest = {};
let archive = archiver("zip"); let archive = archiver("zip");
archive.append(passFileBuffer, { name: "pass.json" }); 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) { async.each(list, function getHashAndArchive(file, callback) {
if (/(manifest|signature|pass)/ig.test(file)) { 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 // 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 }); 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}`) fs.createReadStream(`${passModelsDir}/${request.params.type}.pass/${file}`)
.on("data", function(data) { .on("data", function(data) {
hashFlow.update(data); hashFlow.update(data.toString("binary"));
}) })
.on("error", function(e) { .on("error", function(e) {
return callback(e); return callback(e);
}) })
.on("end", function() { .on("end", function() {
manifestRaw[file] = hashFlow.digest("hex").trim(); manifest[file] = hashFlow.digest().toHex().trim();
return callback(); return callback();
}); });
}, function end(error) { }, function end(error) {
@@ -316,47 +436,47 @@ function RequestHandler(request, response) {
throw new Error(`Unable to compile manifest. ${error}`); throw new Error(`Unable to compile manifest. ${error}`);
} }
let uuid = UUIDGen(); // let uuid = UUIDGen();
generateManifest(manifestRaw, uuid) // generateManifest(manifestRaw, uuid)
.then(function(manifestBuffer) { // .then(function(manifestBuffer) {
archive.append(manifestBuffer, { name: "manifest.json" }); archive.append(Buffer.from(JSON.stringify(manifest), "utf8"), { name: "manifest.json" });
let signatureBuffer = generateManifestSignature(manifest);
generateManifestSignature(uuid)
.then(function(signatureBuffer) {
if (!fs.existsSync("output")) { console.log(signatureBuffer)
fs.mkdirSync("output");
}
archive.append(signatureBuffer, { name: "signature" }); if (!fs.existsSync("output")) {
let outputWS = fs.createWriteStream(`${outputDir}/${request.params.type}.pkpass`); fs.mkdirSync("output");
}
archive.pipe(outputWS); archive.append(signatureBuffer, { name: "signature" });
archive.finalize(); let outputWS = fs.createWriteStream(`${outputDir}/${request.params.type}.pkpass`);
outputWS.on("close", function() { archive.pipe(outputWS);
response.status(201).download(`${outputDir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, { archive.finalize();
cacheControl: false,
headers: { outputWS.on("close", function() {
"Content-type": "application/vnd.apple.pkpass", response.status(201).download(`${outputDir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, {
"Content-length": fs.statSync(`${outputDir}/${request.params.type}.pkpass`).size cacheControl: false,
} headers: {
}); "Content-type": "application/vnd.apple.pkpass",
"Content-length": fs.statSync(`${outputDir}/${request.params.type}.pkpass`).size
}
}); });
})
.catch(function(buffer) {
throw buffer.toString();
}); });
}) // })
.catch(function(error) { // .catch(function(error) {
throw error; // throw error;
}); // });
}); });
}) })
.catch(function(err) { .catch(function(err) {
throw err;
// 😊 // 😊
response.set("Content-Type", "application/json"); response.set("Content-Type", "application/json");
response.status(418).send({ ecode: 418, status: false, message: `Got error while parsing pass.json file: ${err}` }); 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", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" "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": { "normalize-path": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",

View File

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