feat(check-bundle): Implement bundle integrity checks
ADD
packages/check-bundle/README.md
+# check-bundle
+
+`checkBundle` verifies the integrity of a bundle, inspects all of its internal hashes and its one external hash.
+`checkBundle` verifies the internal consistency, completeness, coherence, and conciseness (no extra files) of the bundle.
+The function returns a rejected promise if the bundle fails the check.
+
+```js
+const bundle = await bundleSource('path/to/bundle.js');
+// 'bundle' is JSON-serializable
+await checkBundle(bundle);
+```
+
+This must be run in an Endo environment. To run on Node.js, import `@endo/init` before importing `@endo/import-bundle`.

ADD
packages/check-bundle/index.js
+// @ts-check
+import * as fs from 'fs';
+import * as crypto from 'crypto';
+import { checkBundle as powerlessCheckBundle } from './lite.js';
+
+/** @param {Uint8Array} bytes */
+const computeSha512 = bytes => {
+ const hash = crypto.createHash('sha512');
+ hash.update(bytes);
+ return hash.digest().toString('hex');
+};
+
+/**
+ * @param {string} path
+ */
+export const checkBundle = async path => {
+ const bytes = await fs.promises.readFile(path);
+ return powerlessCheckBundle(bytes, computeSha512, path);
+};
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {string=} name
+ */
+export const checkBundleBytes = async (bytes, name = undefined) => {
+ return powerlessCheckBundle(bytes, computeSha512, name);
+};

ADD
packages/check-bundle/lite.js
+// @ts-check
+/// <reference types="ses"/>
+
+import { decodeBase64 } from '@endo/base64/decode.js';
+import { parseArchive } from '@endo/compartment-mapper/import-archive.js';
+
+const { details: d, quote: q } = assert;
+
+/**
+ * Verifies that a bundle passes its own integrity checks or rejects the
+ * returned promise with an error.
+ * Asserts that the bundle is frozen to guard against inconsistent accessors or
+ * get traps.
+ *
+ * @param {Record<any, any>} bundle
+ * @param {(bytes: Uint8Array) => string} computeSha512
+ * @param {string} name
+ * @returns {Promise<void>}
+ */
+export const checkBundle = async (
+ bundle,
+ computeSha512,
+ name = '<unknown-bundle>',
+) => {
+ assert(bundle !== null, d`checkBundle expects a bundle object`);
+ assert.typeof(
+ bundle,
+ 'object',
+ d`checkBundle cannot hash non-bundle, must be of type object, got ${q(
+ bundle,
+ )}`,
+ );
+ assert(
+ Object.isFrozen(bundle),
+ `checkBundle cannot vouch for the ongoing integrity of an unfrozen object, got ${q(
+ bundle,
+ )}`,
+ );
+
+ const { moduleFormat } = bundle;
+ assert.typeof(
+ moduleFormat,
+ 'string',
+ d`checkBundle cannot hash non-bundle, moduleFormat must be a string, got ${typeof moduleFormat}`,
+ );
+
+ if (moduleFormat === 'endoZipBase64') {
+ const { endoZipBase64, endoZipBase64Sha512 } = bundle;
+ assert.typeof(
+ endoZipBase64,
+ 'string',
+ d`checkBundle cannot hash non-bundle, property 'endoZipBase64' must be a string, got ${typeof endoZipBase64}`,
+ );
+ assert.typeof(
+ endoZipBase64Sha512,
+ 'string',
+ d`checkBundle cannot bundle without the property 'endoZipBase64Sha512', which must be a string, got ${typeof endoZipBase64Sha512}`,
+ );
+ const bytes = decodeBase64(endoZipBase64);
+ const { sha512: parsedSha512 } = await parseArchive(bytes, name, {
+ computeSha512,
+ expectedSha512: endoZipBase64Sha512,
+ });
+ assert(parsedSha512 !== undefined);
+ } else if (
+ moduleFormat === 'getExport' ||
+ moduleFormat === 'nestedEvaluate'
+ ) {
+ assert.fail(
+ `checkBundle cannot determine hash of bundle with ${moduleFormat} moduleFormat because it is not necessarily consistent`,
+ );
+ } else {
+ assert.fail(
+ d`checkBundle cannot determine hash of bundle with unrecognized moduleFormat ${q(
+ moduleFormat,
+ )}`,
+ );
+ }
+};

MODIFY
packages/check-bundle/package.json
packages/check-bundle/package.json
{
"name": "@endo/check-bundle",
"version": "0.1.0",
- "description": "Checks the integrity of an Endo bundle and returns a version string suitable for integrity verification.",
+ "description": "Checks the integrity of an Endo bundle.",
"keywords": [
"endo",
"bundle",
"lint-fix": "eslint --fix .",
"lint:js": "eslint .",
"lint:types": "tsc -p jsconfig.json",
- "test": "ava"
+ "test": "ava",
+ "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js"
},
"dependencies": {
"@endo/base64": "^0.2.19",
"@endo/init": "^0.5.35",
"ava": "^3.12.1",
"babel-eslint": "^10.0.3",
+ "c8": "^7.7.3",
"eslint": "^7.23.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.9.0",

ADD
packages/check-bundle/src/json.js
+// @ts-check
+
+/**
+ * Parses JSON and, if necessary, throws exceptions that include the location
+ * of the offending file.
+ *
+ * @param {string} source
+ * @param {string} location
+ */
+export const parseLocatedJson = (source, location) => {
+ try {
+ return JSON.parse(source);
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ throw new SyntaxError(`Cannot parse JSON from ${location}, ${error}`);
+ }
+ throw error;
+ }
+};

ADD
packages/check-bundle/test/fixture/main.js
+// For the purposes of hashing, running an infinite loop in a valid JavaScript
+// file indicates that the hash algorithm does not run the program.
+/* eslint-disable no-empty */
+// a c'thuloop runs
+for (;;) {} // ever

ADD
packages/check-bundle/test/fixture/package.json
+{
+ "name": "fixture",
+ "version": "1.0.0"
+}

ADD
packages/check-bundle/test/test-check-bundle.js
+// @ts-check
+import '@endo/init/pre-bundle-source.js';
+import '@endo/init';
+import test from 'ava';
+import * as url from 'url';
+import * as crypto from 'crypto';
+import bundleSource from '@endo/bundle-source';
+import { checkBundle } from '../lite.js';
+import { checkBundleBytes } from '../index.js';
+
+const fixture = url.fileURLToPath(
+ new URL('fixture/main.js', import.meta.url).toString(),
+);
+
+/** @param {Uint8Array} bytes */
+const computeSha512 = bytes => {
+ const hash = crypto.createHash('sha512');
+ hash.update(bytes);
+ return hash.digest().toString('hex');
+};
+
+test('bundle and check get export package', async t => {
+ const bundle = await bundleSource(fixture, 'getExport');
+ await t.throwsAsync(checkBundle(bundle, computeSha512, 'fixture/main.js'), {
+ message: /checkBundle cannot determine hash of bundle with getExport moduleFormat because it is not necessarily consistent/,
+ });
+});
+
+test('bundle and check nested evaluate package', async t => {
+ const bundle = await bundleSource(fixture, 'nestedEvaluate');
+ await t.throwsAsync(checkBundle(bundle, computeSha512, 'fixture/main.js'), {
+ message: /checkBundle cannot determine hash of bundle with nestedEvaluate moduleFormat because it is not necessarily consistent/,
+ });
+});
+
+test('bundle and check endo zip base64 package', async t => {
+ const bundle = await bundleSource(fixture, 'endoZipBase64');
+ await checkBundle(bundle, computeSha512, 'fixture/main.js');
+ t.pass();
+});
+
+test('bundle and check endo zip base64 package (lite)', async t => {
+ const bundle = await bundleSource(fixture, 'endoZipBase64');
+ await checkBundleBytes(bundle, 'fixture/main.js');
+ t.pass();
+});
+
+test('bundle and check endo zip base64 package absent hash', async t => {
+ const { endoZipBase64Sha512: _, ...lightBundle } = await bundleSource(
+ fixture,
+ 'endoZipBase64',
+ );
+ Object.freeze(lightBundle);
+ await t.throwsAsync(
+ checkBundle(lightBundle, computeSha512, 'fixture/main.js'),
+ {
+ message:
+ "checkBundle cannot bundle without the property 'endoZipBase64Sha512', which must be a string, got (a string)",
+ },
+ );
+});
+
+test('bundle and check corrupt endo zip base64 package', async t => {
+ const bundle = await bundleSource(fixture, 'endoZipBase64');
+ const corruptBundle = harden({
+ ...bundle,
+ endoZipBase64Sha512:
+ '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
+ });
+ await t.throwsAsync(
+ checkBundle(corruptBundle, computeSha512, 'fixture/main.js'),
+ {
+ message: /^Archive compartment map failed a SHA-512 integrity check/,
+ },
+ );
+});
+
+test('bundle and hash unfrozen object', async t => {
+ const bundle = {};
+ await t.throwsAsync(checkBundle(bundle, computeSha512, 'fixture/main.js'), {
+ message: `checkBundle cannot vouch for the ongoing integrity of an unfrozen object, got {}`,
+ });
+});
+
+test('bundle and hash bogus package', async t => {
+ const bundle = Object.freeze({ moduleFormat: 'bogus' });
+ await t.throwsAsync(checkBundle(bundle, computeSha512, 'fixture/main.js'), {
+ message: `checkBundle cannot determine hash of bundle with unrecognized moduleFormat "bogus"`,
+ });
+});

feat(check-bundle): Implement bundle integrity checks