GoogleChromeLabs / webbundle-plugins

A Webpack plugin for generating Web Bundles output.
https://www.npmjs.com/package/webbundle-webpack-plugin
Apache License 2.0
59 stars 11 forks source link

What specifcally needs to be changed to support Integrity Block V2? #80

Closed guest271314 closed 2 months ago

guest271314 commented 2 months ago

My code using V1 has been working for some time now. What specifcally needs to be changed to support Integrity Block V2?

robbiemc commented 2 months ago

Are you using the js (1) or go (2) tool to create and sign the bundles, or did you write your own? If you wrote your own, it will need to be updated, details on the v2 format here, diff

  1. https://github.com/WICG/webpackage/tree/main/js/sign (wbn-sign)
  2. https://github.com/WICG/webpackage/tree/main/go/integrityblock (not yet updated it looks like)
guest271314 commented 2 months ago

Thanks for the links.

I wrote my own based on the Rollup version, that does not depend on Rollup or Webpack, mostly by hand to substitute Web Cryptography API and Node.js-specific code for runtime agnostic code.

It is JavaScript runtime agnostic, the same code can be run using node, deno, and bun; and uses Web Cryptography API instead of Node.js-specific node:crypto module, which cannot be polyfilled.

Did you consider that all existing SWBN's break at the slightest change in the integrity block signing algorithm, particularly when uploading the file that worked the previous day in Chromium 129?

Are you expecting all existing SWBN to not work, be broken immediately, with no backward compatibility, every time integrity block signers change?

guest271314 commented 2 months ago

@robbiemc What is the purpose of getMajorType()?

function getMajorType(b) {
  return (b & 255) >> 5;
}

Is there any technical reason const { webcrypto } = "node:crypto" is not being used in the source code?

That would make it much simpler to port this library to Deno and Bun.

guest271314 commented 2 months ago

This is completely avoidable by library authors using Web Cryptography API to implement secure curve algorithms. Nothing is lost, functionality in two JavaScript runtimes other than Node.js is gained.

$ bun run webpack.config.js
HookWebpackError: [
  {
    "code": "unrecognized_keys",
    "keys": [
      "integrityBlockSign"
    ],
    "path": [],
    "message": "Unrecognized key(s) in object: 'integrityBlockSign'"
  }
]
$ deno run -A webpack.config.js
error: Uncaught (in promise) TypeError: Unsupported algorithm
    at Object.createPrivateKey (ext:deno_node/internal/crypto/keys.ts:118:19)
    at parsePemKey
guest271314 commented 2 months ago

This is what I have so far re-writing wbn-sign, utils.js, etc. to use Web Cryptography API, again. For some reason the code doesn't get past deterministicRec() webpackage-bundle.js.tar.gz

guest271314 commented 2 months ago

@robbiemc Alright, I made the changes by hand to update to Integrity Block V2, and supporting Web Cryptography API so this library can be run by deno, bun, and node.

How I did this.

  1. git clone https://github.com/GoogleChromeLabs/webbundle-plugins
  2. cd webbundle-plugins/packages/rollup-plugin-webbundle
  3. bun install -p
  4. In src/index.ts comment line 18, : EnforcedPlugin, line 32 const opts = await getValidatedOptionsWithDefaults(rawOpts); and lines 65-121, because I will not be using Rollup
  5. Bundle with Bun bun build --target=node --format=esm --sourcemap=none --outfile=webpackage-bundle.js ./webbundle-plugins/packages/rollup-plugin-webbundle/src/index.ts
  6. Create reference to Web Cryptography API that will be used in the code in the bundled script instead of node:crypto directly import { webcrypto } from "node:crypto";
  7. In /node_modules/wbn-sign/lib/utils/utils.js use switch (key.algorithm.name) {
  8. getRawPublicKey becomes an async function for substituting const exportedKey = await webcrypto.subtle.exportKey("spki", publicKey); for publicKey.export({ type: "spki", format: "der" });
  9. In /node_modules/wbn-sign/lib/signers/integrity-block-signer.js use const publicKey = await signingStrategy.getPublicKey(); and [getPublicKeyAttributeName(publicKey)]: await getRawPublicKey(publicKey); verifySignature() also becomes an async function where const algorithm = { name: "Ed25519" }; const isVerified = await webcrypto.subtle.verify(algorithm, publicKey, signature, data); is substituted for const isVerified = crypto2.verify(undefined, data, publicKey, signature);
  10. In /node_modules/wbn-sign/lib/web-bundle-id.js serialize() function becomes async for return base32Encode(new Uint8Array([...await getRawPublicKey(this.key), ...this.typeSuffix]), "RFC4648", { padding: false }).toLowerCase();; and serializeWithIsolatedWebAppOrigin() becomes an async function for return${this.scheme}${await this.serialize()}/;; toString() becomes an async function for return \ Web Bundle ID: ${await this.serialize()} Isolated Web App Origin: ${await this.serializeWithIsolatedWebAppOrigin()}`;`
  11. In src/index.ts export {WebBundleId, bundleIsolatedWebApp};
  12. In index.js, the entry point for how I am creating the SWBN and IWA I get the public and private keys created with Web Cryptography API, and use Web Cryptography API to sign and verify
globalThis.Buffer ??= (await import("node:buffer")).Buffer; // For Deno
import { bundleIsolatedWebApp, WebBundleId } from "./webpackage-bundle.js";
// import { WebBundleId } from "wbn-sign";
import * as fs from "node:fs";
import * as path from "node:path";
import * as crypto from "node:crypto";
const { webcrypto } = crypto;
const algorithm = { name: "Ed25519" };
const decoder = new TextDecoder();
const controller = fs.readFileSync("./direct-sockets/direct-socket-controller.js");
const script = fs.readFileSync("./assets/script.js");
const privateKey = fs.readFileSync("./privateKey.json");
const publicKey = fs.readFileSync("./publicKey.json");
// https://github.com/tQsW/webcrypto-curve25519/blob/master/explainer.md
const cryptoKey = {
  privateKey: await webcrypto.subtle.importKey(
    "jwk",
    JSON.parse(decoder.decode(privateKey)),
    algorithm.name,
    true,
    ["sign"],
  ),
  publicKey: await webcrypto.subtle.importKey(
    "jwk",
    JSON.parse(decoder.decode(publicKey)),
    algorithm.name,
    true,
    ["verify"],
  ),
};

const webBundleId = await new WebBundleId(
    cryptoKey.publicKey,
  ).serialize();

const isolatedWebAppURL = await new WebBundleId(
    cryptoKey.publicKey,
  ).serializeWithIsolatedWebAppOrigin();

fs.writeFileSync(
  "./direct-sockets/direct-socket-controller.js", 
  decoder.decode(controller).replace(
    "IWA_URL", 
    `isolated-app://${new URL(isolatedWebAppURL).hostname}`
  )
);

fs.writeFileSync(
  "./assets/script.js",
  decoder.decode(script).replace(
     /USER_AGENT\s=\s"?.+"/g,
    `USER_AGENT = "Built with ${navigator.userAgent}"`,
  ),
);

const { fileName, source } = await bundleIsolatedWebApp({
  baseURL: isolatedWebAppURL,
  static: { dir: "assets" },
  formatVersion: "b2",
  output: "signed.swbn",
  integrityBlockSign: {
    webBundleId,
    isIwa: true,
    // https://github.com/GoogleChromeLabs/webbundle-plugins/blob/d251f6efbdb41cf8d37b9b7c696fd5c795cdc231/packages/rollup-plugin-webbundle/test/test.js#L408
    // wbn-sign/lib/signers/node-crypto-signing-strategy.js
    strategies: [new (class CustomSigningStrategy {
      async sign(data) {
        return new Uint8Array(
          await webcrypto.subtle.sign(algorithm, cryptoKey.privateKey, data),
        );
      }
      async getPublicKey() {
        return cryptoKey.publicKey;
      }
    })()],
  },
  headerOverride: {
    "cross-origin-embedder-policy": "require-corp",
    "cross-origin-opener-policy": "same-origin",
    "cross-origin-resource-policy": "same-origin",
    "content-security-policy":
      "base-uri 'none'; default-src 'self'; object-src 'none'; frame-src 'self' https: blob: data:; connect-src 'self' https: wss:; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' https: blob: data:; media-src 'self' https: blob: data:; font-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; require-trusted-types-for 'script';",
  },
});
fs.writeFileSync(fileName, source);
console.log(`\x1b[38;5;220m${fileName} ${source.byteLength} bytes.`);

The above changes don't change Node.js behaviour.

Now the same code can be used by node, deno, and bun. The bytes of the generated .swbn are different between the runtimes only because I dynamically write the user agent of the runtime in the script that is bundled and run in the IWA.

$ deno run -A --unstable-byonm index.js
isolated-app://yoihnto6u24xgcuwc374ffu44koa6tsmdsi7giqqoinwcyjhxxnqaaic/

signed.swbn, 18093 bytes.
$ bun run index.js
isolated-app://yoihnto6u24xgcuwc374ffu44koa6tsmdsi7giqqoinwcyjhxxnqaaic/

signed.swbn, 18092 bytes.
$ node --experimental-default-type=module index.js
(node:71318) ExperimentalWarning: The Ed25519 Web Crypto API algorithm is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
isolated-app://yoihnto6u24xgcuwc374ffu44koa6tsmdsi7giqqoinwcyjhxxnqaaic/

signed.swbn, 18092 bytes.

webpackage-bundle.js.tar.gz