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

Plugin that serves webbundles? #11

Open bitdivine opened 2 years ago

bitdivine commented 2 years ago

Thank you for this plugin. This looks good, but is there a plugin for eager web-bundle consumers rather than creators?

My Goal

A user can click on https://somedomain.org and the web page loads, using a signed https exchange, without any web server being entrusted with a certificate for https://somedomain.org. Entrusting a webserver with a certificate is deeply problematic in some use cases.

I would like the solution to be generic, so that the solution (plugin or otherwise) can be audited once, then used with many domains, rather than special casing every single domain that wants to "go serverless with SXG". If this is too optimistic, I could start with a root domain such as "we-love-sxg.org" and each subdomain could be served with its own SXG file.

Plan

Have a generic way of serving SXG files if there is no https server but there is a DNS record indicating the location of an SXG file.

This could be a browser plugin that, if an https request fails, does a DNS lookup looking for a txt record with the URL of an SXG file. If one exists, get that instead. If such a plugin already exists, super, I'd love to play with it. If not, I would like to explore the space. It looks as if manifest3 plugins support redirects but not necessarily in a way compatible with this idea.

guest271314 commented 9 months ago

Working on something similar https://github.com/guest271314/webbundle. My goal is to bundle the scripts to be used in the browser, bring your own .pem file. What I've found is this library is not simple to bundle. Really does depend on Node.js. Deno throws no supported algorithm when signing.

sonkkeli commented 9 months ago

Yes. Node support is unfortunately required as long as Ed25519 keys are not supported by SubtleCrypto so that JS-only library would be possible. So node is needed in order to sign a web bundle, as Node's Crypto API supports Ed25519 keys.

guest271314 commented 9 months ago

@sonkkeli That's rather restrictive. Means the source code is importable using Deno though throws. Have you reached out to the Deno people about supporting your library?

FWIW I couldn't get wbn and wbn-sign to generate an IWA standalone.

import webpack from "npm:webpack";
import WebBundlePlugin from "npm:webbundle-webpack-plugin";
import {NodeCryptoSigningStrategy,
  parsePemKey,
  readPassphrase,
  WebBundleId} from "npm:wbn-sign@^0.1.0"
import { merge } from "npm:webpack-merge";
import HtmlWebpackPlugin from "npm:html-webpack-plugin";
import CopyPlugin from "npm:copy-webpack-plugin";
import fs from "node:fs";
import path from "node:path";

// ...

const privateKeyFile = "ed25519key.pem";
const privateKey = fs.readFileSync(privateKeyFile);
const parsedPrivateKey = parsePemKey(privateKey);
const baseURL = new WebBundleId(
  parsedPrivateKey,
).serializeWithIsolatedWebAppOrigin();
console.log(baseURL);
const webBundlePlugin = new WebBundlePlugin({
  baseURL,
  output: "signed.swbn",
  integrityBlockSign: {
    key: parsedPrivateKey,
  },
});
deno run -A webpack.wbn.latest.js
error: Uncaught TypeError: Unsupported algorithm
    at Object.createPrivateKey (ext:deno_node/internal/crypto/keys.ts:104:23)
    at parsePemKey (file:///home/user/webbundle/node_modules/.deno/wbn-sign@0.1.2/node_modules/wbn-sign/lib/utils/utils.js:24:19)
    at file:///home/user/webbundle/webpack.wbn.latest.js:61:26
sonkkeli commented 9 months ago

No I haven't. To me it looks like ed25519 should be supported by deno https://deno.land/std@0.166.0/node/internal/crypto/types.ts?s=KeyType but I'm using the Node.js so haven't tried myself.

guest271314 commented 9 months ago

Is a goal of webbundle to be used exclusively in a Node.js environment?

Bun has issues with worker_threads not being implemented https://github.com/oven-sh/bun/issues/7479.

I usually try to write source code that can be used in Node.js, Deno, Bun, and other JavaScript runtimes in the spirit of proposal-common-minimum-api.

sonkkeli commented 9 months ago

It's not limited to node-only as there's also golang version of the package. Whenever the support for ed25519 keys is added to SubtleCrypto, it'll be rather simple to create a browser-only version of the wbn-sign package. Web bundles are still experimental and I'm sure the tooling coverage improves when use of IWAs increases.

guest271314 commented 9 months ago

It looks like file:///home/user/webbundle/node_modules/.deno/wbn-sign@0.1.2/node_modules/wbn-sign/lib/utils/utils.js:24:19 is importing from keys.ts at Object.createPrivateKey (ext:deno_node/internal/crypto/keys.ts:104:23) not types.ts. Is that a simple fix to adjust the library source code for the case of using Deno to sign the bundle?

guest271314 commented 9 months ago

Looks like this is supported in Deno https://github.com/denoland/deno/blob/ca64771257d23ceee97e882965269702c359f6aa/cli/tests/node_compat/test/parallel/test-webcrypto-sign-verify.js#L115-L133


// Test Sign/Verify Ed25519
{
    const { publicKey, privateKey } = await subtle.generateKey({
      name: 'Ed25519',
    }, true, ['sign', 'verify']);
sonkkeli commented 9 months ago

You can create a pull request here https://github.com/WICG/webpackage/blob/main/js/sign/src/utils/utils.ts

guest271314 commented 9 months ago

I'm not sure yet how to incorporate that code into utils.js.

    const signature = await subtle.sign({
      name: 'Ed25519',
    }, privateKey, ec.encode(data));

or

    const { publicKey, privateKey } = await subtle.generateKey({
      name: 'Ed25519',
    }, true, ['sign', 'verify']);

?

I'll do some tests and if I can get it working will file a PR.

guest271314 commented 9 months ago

@sonkkeli Almost forgot. WICG banned me so I can't file a PR over there.

guest271314 commented 9 months ago

Yes. Node support is unfortunately required as long as Ed25519 keys are not supported by SubtleCrypto

Node support is not required then, if we are talking about Ed25519, self.crypto.subtle.

This deno test code modified to run the in the browser logs true on Chromium Version 122.0.6170.0 (Developer Build) (64-bit) in DevTools console

// Test Sign/Verify Ed25519
{
  async function test(data) {
    const ec = new TextEncoder();
    const { publicKey, privateKey } = await subtle.generateKey({
      name: 'Ed25519',
    }, true, ['sign', 'verify']);

    const signature = await subtle.sign({
      name: 'Ed25519',
    }, privateKey, ec.encode(data));

    assert(await subtle.verify({
      name: 'Ed25519',
    }, publicKey, signature, ec.encode(data)));
  }

  test('hello world').then(common.mustCall());
}
guest271314 commented 9 months ago

This returns true in the browser.

// Test Sign/Verify Ed25519
{

  async function test(data) {
    const {subtle} = self.crypto;
    const ec = new TextEncoder();
    const { publicKey, privateKey } = await subtle.generateKey({
      name: 'Ed25519',
    }, true, ['sign', 'verify']);

    const signature = await subtle.sign({
      name: 'Ed25519',
    }, privateKey, ec.encode(data));

    console.log(await subtle.verify({
      name: 'Ed25519',
    }, publicKey, signature, ec.encode(data)));
  }

  test('hello world').catch(console.error);
}

We can read and write files to the local file system with WICG File System Access API.

So we should be able to create Signed Web Bundles and Isolated Web Apps in the browser.

guest271314 commented 9 months ago

Secure Curves in the Web Cryptography API

The Implementation of the Ed25519 algorithm landed Chromium in Nov 2022 and shipped in Chrome 110.0.5424.0. The X25519 key sharing algorithm took more time due to the review process, but it finally landed in March 2023 and has been shipped in Chrome since 113.0.5657.0.

guest271314 commented 9 months ago

@sonkkeli

It's not limited to node-only as there's also golang version of the package. Whenever the support for ed25519 keys is added to SubtleCrypto, it'll be rather simple to create a browser-only version of the wbn-sign package.

Alright, a slightly modified version of this https://stackblitz.com/edit/single-node-js-bundle?file=src%2Findex.js bundles wbn and wbn-sign into a single script.

Now I just need to know what I'm doing incorrectly here. WIth this signed.swbn is not being loaded as an Isolated Web App from file

const wbn = require("wbn");
const wbnSign = require("wbn-sign");
const fs = require("fs");
const path = require("path");
// const { merge } = require("webpack-merge");
// const HtmlWebpackPlugin = require("html-webpack-plugin");
// const CopyPlugin = require("copy-webpack-plugin");
// const CopyPlugin = require("copy-webpack-plugin");
// use imported module anywhere to avoid tree shaking of unused modules
console.log(
  ">> app",
  wbn,
  wbnSign,
);

const headers = {
  "content-security-policy":
    "base-uri 'none'; default-src 'self'; object-src 'none'; frame-src 'self' https: blob: data:; connect-src 'self' https:; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' https: blob: data:; media-src 'self' https: blob: data:; font-src 'self' blob: data:; require-trusted-types-for 'script'; frame-ancestors 'self';",
  "cross-origin-embedder-policy": "require-corp",
  "cross-origin-opener-policy": "same-origin",
  "cross-origin-resource-policy": "same-origin",
};

const privateKey = wbnSign.parsePemKey(
  fs.readFileSync(path.resolve(__dirname, "../src/ed25519key.pem"), "utf-8"),
);

// Web Bundle ID only:
const webBundleId = new wbnSign.WebBundleId(privateKey).serialize();

// With origin, meaning "isolated-app://" combined with Web Bundle ID:
const webBundleIdWithIWAOrigin = new wbnSign.WebBundleId(
  privateKey,
).serializeWithIsolatedWebAppOrigin();

console.log(webBundleId, webBundleIdWithIWAOrigin);

const builder = new wbn.BundleBuilder();

builder.addExchange(
  webBundleIdWithIWAOrigin, // URL
  200, // response code
  { "Content-Type": "text/html", ...headers }, // response headers
  fs.readFileSync(path.resolve(__dirname, "../src/index.html")), // response body (string or Uint8Array)
);

builder.addExchange(
  webBundleIdWithIWAOrigin + "script.js",
  200,
  { "Content-Type": "text/javascript", ...headers },
  fs.readFileSync(path.resolve(__dirname, "../src/script.js")),
);

builder.addExchange(
  webBundleIdWithIWAOrigin + "manifest.webmanifest",
  200,
  { "Content-Type": "application/manifest+json", ...headers },
  fs.readFileSync(path.resolve(__dirname, "../src/manifest.webmanifest")),
);

builder.setPrimaryURL(webBundleIdWithIWAOrigin);

fs.writeFileSync("out.wbn", builder.createBundle());

const buf = fs.readFileSync("out.wbn");
const bundle = new wbn.Bundle(buf);
const exchanges = [];
for (const url of bundle.urls) {
  const resp = bundle.getResponse(url);
  exchanges.push({
    url,
    status: resp.status,
    headers: resp.headers,
    body: new TextDecoder("utf-8").decode(resp.body),
  });
}
console.log(
  JSON.stringify(
    {
      version: bundle.version, // format version
      exchanges,
    },
    null,
    2,
  ),
);

(async () => {
  // Option 1: With the default (`NodeCryptoSigningStrategy`) signing strategy.
  const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
    buf,
    new wbnSign.NodeCryptoSigningStrategy(privateKey),
  ).sign();

  fs.writeFileSync("signed.swbn", signedWebBundle);
})();
guest271314 commented 9 months ago

@sonkkeli I stopped asking questions and for help and got to work yesterday. Subtituted webcrypto for node:crypto usages in the Rollup version; and Uint8Array for Buffer. The same code can now be run using node, deno, and bun to produce the same Signed Web Bundle and Isolated Web App. I still have to clean up the code before I file a PR.