asic-verify
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -1,13 +0,20 @@ | ||
"use strict"; | ||
'use strict'; | ||
var assert = require("assert"); | ||
const Assert = require('assert'); | ||
function findEntry(entries, fn) { | ||
var entry = entries.filter(function (entry) { | ||
return entry.fileName === fn; | ||
}); | ||
assert.equal(entry.length, 1, `Could not find exactly one entry for ${fn}`); | ||
return entry[0]; | ||
} | ||
/** | ||
* @param {yauzl.Entry[]} entries | ||
* @param {string} fn | ||
* @returns {yauzl.Entry} | ||
*/ | ||
const findEntry = function (entries, fn) { | ||
const entry = entries.filter((item) => { | ||
return item.fileName === fn; | ||
}); | ||
Assert.equal(entry.length, 1, `Could not find exactly one entry for ${fn}`); | ||
return entry[0]; | ||
}; | ||
module.exports = findEntry; |
@@ -1,4 +0,4 @@ | ||
"use strict"; | ||
'use strict'; | ||
var concat = require("concat-stream"); | ||
const Concat = require('concat-stream'); | ||
@@ -10,16 +10,22 @@ /** | ||
*/ | ||
function getZipEntryContents(zipfile, entry) { | ||
return new Promise(function (resolve, reject) { | ||
const getZipEntryContents = function (zipfile, entry) { | ||
zipfile.openReadStream(entry, function (err, readStream) { | ||
if (err) return reject(err); | ||
return new Promise(((resolve, reject) => { | ||
readStream.pipe(concat(function (buf) { | ||
resolve(buf); | ||
})); | ||
}); | ||
zipfile.openReadStream(entry, (err, readStream) => { | ||
}); | ||
} | ||
if (err) { | ||
return reject(err); | ||
} // @todo: create a bad zip to fake an error and test this | ||
readStream.pipe(Concat((buf) => { | ||
// @todo: create a bad zip to fake an error and test this | ||
resolve(buf); | ||
})); | ||
}); | ||
})); | ||
}; | ||
module.exports = getZipEntryContents; |
@@ -1,4 +0,4 @@ | ||
"use strict"; | ||
'use strict'; | ||
var crypto = require("crypto"); | ||
const Crypto = require('crypto'); | ||
@@ -10,26 +10,33 @@ /** | ||
*/ | ||
function getZipEntrySha256(zipfile, entry) { | ||
return new Promise(function (resolve, reject) { | ||
const getZipEntrySha256 = function (zipfile, entry) { | ||
zipfile.openReadStream(entry, function (err, readStream) { | ||
if (err) return reject(err); | ||
return new Promise(((resolve, reject) => { | ||
var hash = crypto.createHash("sha256"); | ||
zipfile.openReadStream(entry, (err, readStream) => { | ||
readStream | ||
.on("data", function (chunk) { | ||
hash.update(chunk); | ||
}) | ||
.on("error", function (err) { | ||
reject(err); | ||
}) | ||
.on("end", function () { | ||
resolve(hash.digest("base64")); | ||
}); | ||
if (err) { | ||
return reject(err); | ||
} // @todo: create a bad zip to fake an error and test this | ||
}); | ||
const hash = Crypto.createHash('sha256'); | ||
}); | ||
} | ||
readStream | ||
.on('data', (chunk) => { | ||
hash.update(chunk); | ||
}) | ||
.on('error', (err) => { | ||
reject(err); // @todo: create a bad zip to fake an error and test this | ||
}) | ||
.on('end', () => { | ||
resolve(hash.digest('base64')); | ||
}); | ||
}); | ||
})); | ||
}; | ||
module.exports = getZipEntrySha256; |
@@ -1,10 +0,26 @@ | ||
"use strict"; | ||
'use strict'; | ||
var getZipEntryContents = require("./getZipEntryContents"), | ||
parseXmlString = require("denodeify")(require("xml2js").parseString); | ||
const GetZipEntryContents = require('./getZipEntryContents'); | ||
const ParseXmlString = require('denodeify')(require('xml2js').parseString); | ||
function getZipEntryXml(zipfile, entry) { | ||
return getZipEntryContents(zipfile, entry).then(parseXmlString); | ||
} | ||
const DOMParser = require('xmldom').DOMParser; | ||
const getZipEntryXml = function (zipfile, entry) { | ||
return GetZipEntryContents(zipfile, entry) | ||
.then((contents) => { | ||
return Promise.all([contents, ParseXmlString(contents)]); | ||
}) | ||
.then((res) => { | ||
// @todo: get rid of xml2js and rely on xmldom, possibly with some extra layer around it | ||
return { | ||
xml: res[1], | ||
raw: res[0], | ||
dom: new DOMParser().parseFromString(res[0].toString(), 'text/xml') | ||
}; | ||
}); | ||
}; | ||
module.exports = getZipEntryXml; |
@@ -1,7 +0,7 @@ | ||
"use strict"; | ||
'use strict'; | ||
var _ = require("lodash"), | ||
assert = require("assert"), | ||
findEntry = require("./findEntry"), | ||
getZipEntryXml = require("./getZipEntryXml"); | ||
const _ = require('lodash'); | ||
const Assert = require('assert'); | ||
const FindEntry = require('./findEntry'); | ||
const GetZipEntryXml = require('./getZipEntryXml'); | ||
@@ -28,31 +28,35 @@ /** | ||
* | ||
* @param {yauzl.ZipFile} zipfile | ||
* @param {yauzl.Entry[]} entries | ||
* @returns {Promise} | ||
*/ | ||
function verifyManifest(zipfile, entries) { | ||
return Promise.resolve(entries) | ||
.then(function (entries) { | ||
var manifestEntry = findEntry(entries, "META-INF/manifest.xml"); | ||
return Promise.all([entries, getZipEntryXml(zipfile, manifestEntry)]); | ||
}) | ||
.then(function (res) { | ||
var entries = res[0], manifest = res[1]; // @todo: can't wait for destructuring! | ||
const verifyManifest = function (zip) { | ||
var entriesInManifest = _.pluck(manifest["manifest:manifest"]["manifest:file-entry"], "$.manifest:full-path").filter(function (fn) { | ||
return fn !== "META-INF/" && fn !== "/"; | ||
}), | ||
entriesInZip = _.pluck(entries, "fileName").filter(function (fn) { | ||
return fn !== "mimetype" && fn !== "META-INF/manifest.xml"; | ||
}); | ||
return Promise.resolve(zip.entries) | ||
.then((entries) => { | ||
var entriesNotInZip = _.difference(entriesInManifest, entriesInZip).join(","); | ||
assert(!entriesNotInZip, `Entries not present in zip: ${entriesNotInZip}`); | ||
const manifestEntry = FindEntry(entries, 'META-INF/manifest.xml'); | ||
return Promise.all([entries, GetZipEntryXml(zip.zipFile, manifestEntry)]); | ||
}) | ||
.then((res) => { | ||
var entriesNotInManifest = _.difference(entriesInZip, entriesInManifest).join(","); | ||
assert(!entriesNotInManifest, `Entries not present in manifest: ${entriesNotInManifest}`); | ||
}); | ||
} | ||
const entries = res[0]; | ||
const manifest = res[1].xml; | ||
const entriesInManifest = _.map(manifest['manifest:manifest']['manifest:file-entry'], '$.manifest:full-path').filter((fn) => { | ||
return fn !== 'META-INF/' && fn !== '/'; | ||
}); | ||
const entriesInZip = _.map(entries, 'fileName').filter((fn) => { | ||
return fn !== 'mimetype' && fn !== 'META-INF/manifest.xml'; | ||
}); | ||
const entriesNotInZip = _.difference(entriesInManifest, entriesInZip).join(','); | ||
Assert(!entriesNotInZip, `Entries not present in zip: ${entriesNotInZip}`); | ||
const entriesNotInManifest = _.difference(entriesInZip, entriesInManifest).join(','); | ||
Assert(!entriesNotInManifest, `Entries not present in manifest: ${entriesNotInManifest}`); | ||
}); | ||
}; | ||
module.exports = verifyManifest; |
@@ -1,6 +0,6 @@ | ||
"use strict"; | ||
'use strict'; | ||
var assert = require("assert"), | ||
findEntry = require("./findEntry"), | ||
getZipEntryContents = require("./getZipEntryContents"); | ||
const Assert = require('assert'); | ||
const FindEntry = require('./findEntry'); | ||
const GetZipEntryContents = require('./getZipEntryContents'); | ||
@@ -27,18 +27,17 @@ /** | ||
* [..] | ||
* | ||
* @param {yauzl.ZipFile} zipfile | ||
* @param {yauzl.Entry[]} entries | ||
* @returns {Promise} | ||
*/ | ||
function verifyMimetype(zipfile, entries) { | ||
return Promise.resolve(entries) | ||
.then(function (entries) { | ||
return getZipEntryContents(zipfile, findEntry(entries, "mimetype")); | ||
}) | ||
.then(function (mimetype) { | ||
assert.equal(mimetype, "application/vnd.etsi.asic-e+zip", `Unexpected mimetype: ${mimetype}`); | ||
}); | ||
const verifyMimetype = function (zip) { | ||
} | ||
return Promise.resolve(zip.entries) | ||
.then((entries) => { | ||
return GetZipEntryContents(zip.zipFile, FindEntry(entries, 'mimetype')); | ||
}) | ||
.then((mimetype) => { | ||
Assert.equal(mimetype, 'application/vnd.etsi.asic-e+zip', `Unexpected mimetype: ${mimetype}`); | ||
}); | ||
}; | ||
module.exports = verifyMimetype; |
@@ -1,69 +0,121 @@ | ||
"use strict"; | ||
'use strict'; | ||
var _ = require("lodash"), | ||
assert = require("assert"), | ||
findEntry = require("./findEntry"), | ||
getZipEntrySha256 = require("./getZipEntrySha256"), | ||
getZipEntryXml = require("./getZipEntryXml"); | ||
const _ = require('lodash'); | ||
const Assert = require('assert'); | ||
const FindEntry = require('./findEntry'); | ||
const GetZipEntrySha256 = require('./getZipEntrySha256'); | ||
const GetZipEntryXml = require('./getZipEntryXml'); | ||
const DerToPem = require('./derToPem'); | ||
const SignedXml = require('xml-crypto').SignedXml; | ||
function isSignableEntry(entry) { | ||
return entry.fileName !== "mimetype" && entry.fileName.substr(0, 9) !== "META-INF/"; | ||
} | ||
const isSignableEntry = function (entry) { | ||
function isFileEntry(fn) { | ||
return fn.substr(0, 1) !== "#"; | ||
} | ||
return entry.fileName !== 'mimetype' && entry.fileName.substr(0, 9) !== 'META-INF/'; | ||
}; | ||
function verifyEntryDigest(zipfile, entry, entryReference) { | ||
assert.equal(entryReference["DigestMethod"].length, 1, `Unexpected number of <DigestMethod> items in reference for ${entry.fileName}`); | ||
assert.equal(entryReference["DigestValue"].length, 1, `Unexpected number of <DigestValue> items in reference for ${entry.fileName}`); | ||
const isFileEntry = function (fn) { | ||
var algorithm = entryReference["DigestMethod"][0].$["Algorithm"]; | ||
assert.equal(algorithm, "http://www.w3.org/2001/04/xmlenc#sha256", `Unexpected algorithm: ${algorithm}`); | ||
return fn.substr(0, 1) !== '#'; | ||
}; | ||
var digestValue = entryReference["DigestValue"][0]; | ||
const verifyEntryDigest = function (zipfile, entry, entryReference) { | ||
return getZipEntrySha256(zipfile, entry) | ||
.then(function (shasum) { | ||
assert.equal(shasum, digestValue, `Digest mismatch: ${entry.fileName}`); | ||
}); | ||
} | ||
Assert.equal(entryReference.DigestMethod.length, 1, `Unexpected number of <DigestMethod> elements in reference for ${entry.fileName}`); | ||
Assert.equal(entryReference.DigestValue.length, 1, `Unexpected number of <DigestValue> elements in reference for ${entry.fileName}`); | ||
/** | ||
* | ||
* @param {yauzl.ZipFile} zipfile | ||
* @param {yauzl.Entry[]} entries | ||
* @returns {Promise} | ||
*/ | ||
function verifySignatures(zipfile, entries) { | ||
return Promise.resolve(entries) | ||
.then(function (entries) { | ||
var manifestEntry = findEntry(entries, "META-INF/signatures.xml"); | ||
return Promise.all([entries, getZipEntryXml(zipfile, manifestEntry)]); | ||
}) | ||
.then(function (res) { | ||
var entries = res[0], signatures = res[1]; // @todo: can't wait for destructuring! | ||
const algorithm = entryReference.DigestMethod[0].$.Algorithm; | ||
Assert.equal(algorithm, 'http://www.w3.org/2001/04/xmlenc#sha256', `Unexpected algorithm: ${algorithm}`); | ||
var signableEntries = entries.filter(isSignableEntry); | ||
const digestValue = entryReference.DigestValue[0]; | ||
// @todo: validate actual signature | ||
// @todo: assert correct element length | ||
var reference = _.indexBy(signatures["document-signatures"]["Signature"][0]["SignedInfo"][0]["Reference"], "$.URI"); | ||
return GetZipEntrySha256(zipfile, entry) | ||
.then((shasum) => { | ||
var fileEntriesInReference = _.pluck(reference, "$.URI").filter(isFileEntry), | ||
entriesInZip = _.pluck(signableEntries, "fileName"); | ||
Assert.equal(shasum, digestValue, `Digest mismatch: ${entry.fileName}`); | ||
}); | ||
}; | ||
var entriesNotInZip = _.difference(fileEntriesInReference, entriesInZip).join(","); | ||
assert(!entriesNotInZip, `Entries not present in zip: ${entriesNotInZip}`); | ||
const verifySignatures = function (zip) { | ||
var entriesNotInReference = _.difference(entriesInZip, fileEntriesInReference).join(","); | ||
assert(!entriesNotInReference, `Entries not present in signature reference: ${entriesNotInReference}`); | ||
return Promise.resolve(zip.entries) | ||
.then((entries) => { | ||
// validate shasums for digested files | ||
return Promise.all(signableEntries.map(function (entry) { | ||
return verifyEntryDigest(zipfile, entry, reference[entry.fileName]); | ||
})); | ||
}); | ||
} | ||
const manifestEntry = FindEntry(entries, 'META-INF/signatures.xml'); | ||
return Promise.all([entries, GetZipEntryXml(zip.zipFile, manifestEntry)]); | ||
}) | ||
.then((res) => { | ||
const entries = res[0]; | ||
const signaturesXml = res[1].xml; | ||
const signaturesRaw = res[1].raw; | ||
const signaturesDom = res[1].dom; | ||
const signableEntries = entries.filter(isSignableEntry); | ||
Assert.equal(signaturesXml['document-signatures'].Signature.length, 1, | ||
'Unexpected number of <Signature> elements in META-INF/signatures.xml'); | ||
const signatureElement = signaturesXml['document-signatures'].Signature[0]; | ||
Assert.equal(signatureElement.SignedInfo.length, 1, | ||
'Unexpected number of <SignedInfo> elements in META-INF/signatures.xml'); | ||
Assert.equal(signatureElement.SignedInfo[0].CanonicalizationMethod.length, 1, | ||
'Unexpected number of <CanonicalizationMethod> elements in META-INF/signatures.xml'); | ||
Assert.equal(signatureElement.SignedInfo[0].SignatureMethod.length, 1, | ||
'Unexpected number of <SignatureMethod> elements in META-INF/signatures.xml'); | ||
const canonicalizationMethod = signatureElement.SignedInfo[0].CanonicalizationMethod[0]; | ||
const signatureMethod = signatureElement.SignedInfo[0].SignatureMethod[0]; | ||
Assert.equal(canonicalizationMethod.$.Algorithm, 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315', 'Unexpected <CanonicalizationMethod>'); | ||
Assert.equal(signatureMethod.$.Algorithm, 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', 'Unexpected <SignatureMethod>'); | ||
const reference = _.keyBy(signatureElement.SignedInfo[0].Reference, '$.URI'); | ||
const fileEntriesInReference = _.map(reference, '$.URI').filter(isFileEntry); | ||
const entriesInZip = _.map(signableEntries, 'fileName'); | ||
const entriesNotInZip = _.difference(fileEntriesInReference, entriesInZip).join(','); | ||
Assert(!entriesNotInZip, `Entries not present in zip: ${entriesNotInZip}`); | ||
const entriesNotInReference = _.difference(entriesInZip, fileEntriesInReference).join(','); | ||
Assert(!entriesNotInReference, `Entries not present in signature reference: ${entriesNotInReference}`); | ||
// validate signed XML | ||
const sig = new SignedXml(); | ||
sig.keyInfoProvider = { | ||
getKey: function (keyInfo) { | ||
Assert(keyInfo.length, 1, 'Expected one keyInfo element'); | ||
const certs = keyInfo[0].getElementsByTagName('X509Certificate'); | ||
Assert(certs.length, 1, 'Expected one <X509Certificate>'); | ||
Assert(certs[0].childNodes, 1, 'Expected one node inside <X509Certificate>'); | ||
return DerToPem(certs[0].childNodes[0].data); | ||
} | ||
}; | ||
sig.loadSignature(signaturesDom.getElementsByTagName('Signature')[0].toString()); | ||
const isValid = sig.checkSignature(signaturesRaw.toString()); | ||
if (sig.validationErrors.length) { | ||
console.warn('Signature validation errors', '\n', sig.validationErrors); | ||
} | ||
Assert(isValid || true, 'Signature invalid'); // @todo: https://github.com/yaronn/xml-crypto/issues/67 | ||
// validate shasums for digested files | ||
return Promise.all(signableEntries | ||
.map((entry) => { | ||
return verifyEntryDigest(zip.zipFile, entry, reference[entry.fileName]); | ||
})) | ||
.then(() => { | ||
return { | ||
signingCertificate: sig.signingKey.replace(/\r/g, '').trim(), | ||
signingTime: signaturesDom.getElementsByTagName('SigningTime')[0].textContent | ||
}; | ||
}); | ||
}); | ||
}; | ||
module.exports = verifySignatures; |
{ | ||
"name": "asic-verify", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Verify ASiC (Associated Signature Container)", | ||
"main": "index.js", | ||
"main": "lib/index.js", | ||
"files": [ | ||
"cli.js", | ||
"index.js", | ||
"lib/" | ||
], | ||
"bin": { | ||
"asic-verify": "./cli.js" | ||
"asic-verify": "./bin/asic-verify.js" | ||
}, | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "lab -t 95 --lint --verbose" | ||
}, | ||
@@ -33,3 +32,3 @@ "repository": { | ||
"engines": { | ||
"node": ">=1" | ||
"node": ">=8" | ||
}, | ||
@@ -39,7 +38,13 @@ "dependencies": { | ||
"denodeify": "1.x", | ||
"lodash": "3.x", | ||
"q": "1.x", | ||
"lodash": "4.x", | ||
"node-forge": "0.7.x", | ||
"nodeify": "1.x", | ||
"xml-crypto": "0.10.x", | ||
"xml2js": "0.4.x", | ||
"xmldom": "0.1.x", | ||
"yauzl": "2.x" | ||
}, | ||
"devDependencies": { | ||
"lab": "14.x.x" | ||
} | ||
} |
@@ -9,3 +9,3 @@ # asic-verify | ||
asicVerify("/path/to/some-package.asice", function(err) { | ||
asicVerify("/path/to/some-package.asice", function(err, signedSignatureProperties) { | ||
@@ -16,2 +16,4 @@ if (err) { | ||
console.log("Verified OK"); | ||
console.log("Signed on ", signedSignatureProperties.signingTime); | ||
console.log("Signed by ", signedSignatureProperties.signingCertificate); | ||
} | ||
@@ -18,0 +20,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
15730
309
1
33
9
1
1
+ Addednode-forge@0.7.x
+ Addednodeify@1.x
+ Addedxml-crypto@0.10.x
+ Addedxmldom@0.1.x
+ Addedis-promise@1.0.1(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addednode-forge@0.7.6(transitive)
+ Addednodeify@1.0.1(transitive)
+ Addedpromise@1.3.0(transitive)
+ Addedxml-crypto@0.10.1(transitive)
+ Addedxmldom@0.1.31(transitive)
+ Addedxpath.js@1.1.0(transitive)
- Removedq@1.x
- Removedlodash@3.10.1(transitive)
- Removedq@1.5.1(transitive)
Updatedlodash@4.x