@sproutboat/runtime 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.41.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1eacf43: Secrets read from the environment (a standalone binary, a sproutd worker) are decoded as UTF-8 from the proposed compatibility date 2026-11-07. Before, the handler got the environment's bytes one char per byte, so `é` arrived as `é`. The broker transport is unchanged: its secrets already arrive as real strings. Bytes that are not valid UTF-8 pass through as before.
8
+
9
+ ### Patch Changes
10
+
11
+ - f74835a: `crypto.subtle` signs and verifies P-256 (ES256 and ECDH) with BearSSL's 64-bit `ec_p256_m64` where the target has a 128-bit multiply, and keeps the portable code everywhere else (and for P-384 and P-521). BearSSL never picks m64 by itself, so the C asks for it with `br_ec_p256_m64_get()`, which returns null on a target without one. Through the runtime on macOS arm64, ES256 signs 2.2x faster (2,563 to 5,621 per second) and verifies 3.1x faster (1,318 to 4,050 per second). Results are unchanged, so there is no new compatibility date.
12
+
13
+ This needs the BearSSL that the CLI pins from the release that ships with it: m64 is not in BearSSL 0.6, so a CLI that still links 0.6 cannot link this runtime.
14
+
3
15
  ## 0.40.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9397,6 +9397,14 @@ static const sbc_u8 sbc_oid_x25519[] = { 0x2b, 0x65, 0x6e };
9397
9397
  static int sbc_oid_eq(const sbc_u8* a, size_t al, const sbc_u8* b, size_t bl) { return al == bl && memcmp(a, b, al) == 0; }
9398
9398
 
9399
9399
  static int sbc_curve_br(int c) { return c == 1 ? BR_EC_secp256r1 : c == 2 ? BR_EC_secp384r1 : c == 3 ? BR_EC_secp521r1 : 0; }
9400
+
9401
+ static const br_ec_impl* sbc_ec(int br_curve) {
9402
+ if (br_curve == BR_EC_secp256r1) {
9403
+ const br_ec_impl* fast = br_ec_p256_m64_get();
9404
+ if (fast) return fast;
9405
+ }
9406
+ return br_ec_get_default();
9407
+ }
9400
9408
  static int sbc_curve_from_br(int c) { return c == BR_EC_secp256r1 ? 1 : c == BR_EC_secp384r1 ? 2 : c == BR_EC_secp521r1 ? 3 : 0; }
9401
9409
  static size_t sbc_curve_size(int c) { return c == 1 ? 32 : c == 2 ? 48 : c == 3 ? 66 : 0; }
9402
9410
  static void sbc_curve_oid(int c, const sbc_u8** o, size_t* n) {
@@ -9579,7 +9587,7 @@ static int sbc_decode(int pkcs8, const sbc_u8* der, size_t len, sbc_buf* out) {
9579
9587
  if (cv) {
9580
9588
  sbc_u8 pub[133];
9581
9589
  br_ec_public_key pk;
9582
- if (br_ec_compute_pub(br_ec_get_default(), &pk, pub, sk) != 0) {
9590
+ if (br_ec_compute_pub(sbc_ec(sk->curve), &pk, pub, sk) != 0) {
9583
9591
  sbc_put1(out, SBC_K_EC); sbc_put1(out, (sbc_u8)cv); sbc_put1(out, 1);
9584
9592
  sbc_field(out, pk.q, pk.qlen);
9585
9593
 
@@ -9862,7 +9870,7 @@ static int sbc_ecdsa_sign(int bits, const sbc_u8* key, size_t kl, const sbc_u8*
9862
9870
  sbc_u8 h[64], sig[132];
9863
9871
  rc = SBC_OPERATION;
9864
9872
  if (sbc_digest(bits, m, ml, h)) {
9865
- size_t n = br_ecdsa_sign_raw_get_default()(br_ec_get_default(), sbc_hash(bits), h, sk, sig);
9873
+ size_t n = br_ecdsa_sign_raw_get_default()(sbc_ec(sk->curve), sbc_hash(bits), h, sk, sig);
9866
9874
  if (n) { sbc_put(out, sig, n); rc = SBC_OK; }
9867
9875
  }
9868
9876
  free(dc);
@@ -9877,7 +9885,7 @@ static int sbc_ecdsa_verify(int bits, int curve, const sbc_u8* point, size_t pl,
9877
9885
  if (sl != 2 * fs) return SBC_OK;
9878
9886
  br_ec_public_key pk;
9879
9887
  pk.curve = sbc_curve_br(curve); pk.q = (sbc_u8*)point; pk.qlen = pl;
9880
- if (br_ecdsa_vrfy_raw_get_default()(br_ec_get_default(), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
9888
+ if (br_ecdsa_vrfy_raw_get_default()(sbc_ec(pk.curve), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
9881
9889
  return SBC_OK;
9882
9890
  }
9883
9891
 
@@ -9887,12 +9895,12 @@ static int sbc_ec_point_ok(int curve, const sbc_u8* point, size_t pl) {
9887
9895
  sbc_u8 q[133];
9888
9896
  memcpy(q, point, pl);
9889
9897
  sbc_u8 one[1] = { 1 };
9890
- return br_ec_get_default()->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
9898
+ return sbc_ec(sbc_curve_br(curve))->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
9891
9899
  }
9892
9900
  static int sbc_ec_keygen(int curve, sbc_buf* out) {
9893
9901
  br_ec_private_key sk; br_ec_public_key pk;
9894
9902
  sbc_u8 kb[BR_EC_KBUF_PRIV_MAX_SIZE], pb[BR_EC_KBUF_PUB_MAX_SIZE];
9895
- const br_ec_impl* impl = br_ec_get_default();
9903
+ const br_ec_impl* impl = sbc_ec(sbc_curve_br(curve));
9896
9904
  if (!br_ec_keygen(&sbc_prng, impl, &sk, kb, sbc_curve_br(curve))) return SBC_OPERATION;
9897
9905
  if (!br_ec_compute_pub(impl, &pk, pb, &sk)) return SBC_OPERATION;
9898
9906
  size_t need = br_encode_ec_pkcs8_der(0, &sk, &pk);
@@ -10059,8 +10067,9 @@ static int sbc_ecdh(int curve, const sbc_u8* pkcs8, size_t kl, const sbc_u8* pee
10059
10067
  if (sk->curve == sbc_curve_br(curve) && fs && pl == 1 + 2 * fs && peer[0] == 4) {
10060
10068
  sbc_u8 pt[133];
10061
10069
  memcpy(pt, peer, pl);
10062
- if (br_ec_get_default()->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
10063
- size_t xl; size_t xo = br_ec_get_default()->xoff(sk->curve, &xl);
10070
+ const br_ec_impl* impl = sbc_ec(sk->curve);
10071
+ if (impl->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
10072
+ size_t xl; size_t xo = impl->xoff(sk->curve, &xl);
10064
10073
  sbc_put(out, pt + xo, xl); rc = SBC_OK;
10065
10074
  }
10066
10075
  }
@@ -12,10 +12,10 @@ uses it.
12
12
 
13
13
  | Need | Source | Licence |
14
14
  | --- | --- | --- |
15
- | SHA-1/256/384/512, HMAC, HKDF | BearSSL 0.6 | MIT |
16
- | RSA (private and public operations, key generation, PKCS#1 v1.5 padding), RSA-OAEP | BearSSL 0.6 | MIT |
17
- | ECDSA and ECDH on P-256, P-384, P-521, EC key generation | BearSSL 0.6 | MIT |
18
- | AES (constant-time), CBC, CTR, GCM | BearSSL 0.6 | MIT |
15
+ | SHA-1/256/384/512, HMAC, HKDF | BearSSL (master 7bea48e) | MIT |
16
+ | RSA (private and public operations, key generation, PKCS#1 v1.5 padding), RSA-OAEP | BearSSL (master 7bea48e) | MIT |
17
+ | ECDSA and ECDH on P-256, P-384, P-521, EC key generation | BearSSL (master 7bea48e) | MIT |
18
+ | AES (constant-time), CBC, CTR, GCM | BearSSL (master 7bea48e) | MIT |
19
19
  | Ed25519, X25519 | Monocypher 4.0.2, vendored in `vendor/` | BSD-2-Clause or CC0-1.0 |
20
20
 
21
21
  BearSSL is already linked into every embedded sprout (standalone binaries and sproutd) for
@@ -183,6 +183,17 @@ static int sbc_oid_eq(const sbc_u8* a, size_t al, const sbc_u8* b, size_t bl) {
183
183
 
184
184
  /* Curve codes used on the wire to JS: 1 = P-256, 2 = P-384, 3 = P-521. */
185
185
  static int sbc_curve_br(int c) { return c == 1 ? BR_EC_secp256r1 : c == 2 ? BR_EC_secp384r1 : c == 3 ? BR_EC_secp521r1 : 0; }
186
+ /* BearSSL's default EC implementation is the portable m31 (m15 where there is no 64-bit multiply
187
+ result). P-256 has a 64-bit specialisation, m64, that is roughly 3x faster to sign and 5x to verify,
188
+ but BearSSL never selects it by itself. br_ec_p256_m64_get() returns null on a target without a
189
+ 128-bit multiply, and m64 only knows P-256, so every other curve keeps the default. */
190
+ static const br_ec_impl* sbc_ec(int br_curve) {
191
+ if (br_curve == BR_EC_secp256r1) {
192
+ const br_ec_impl* fast = br_ec_p256_m64_get();
193
+ if (fast) return fast;
194
+ }
195
+ return br_ec_get_default();
196
+ }
186
197
  static int sbc_curve_from_br(int c) { return c == BR_EC_secp256r1 ? 1 : c == BR_EC_secp384r1 ? 2 : c == BR_EC_secp521r1 ? 3 : 0; }
187
198
  static size_t sbc_curve_size(int c) { return c == 1 ? 32 : c == 2 ? 48 : c == 3 ? 66 : 0; }
188
199
  static void sbc_curve_oid(int c, const sbc_u8** o, size_t* n) {
@@ -380,7 +391,7 @@ static int sbc_decode(int pkcs8, const sbc_u8* der, size_t len, sbc_buf* out) {
380
391
  if (cv) {
381
392
  sbc_u8 pub[133];
382
393
  br_ec_public_key pk;
383
- if (br_ec_compute_pub(br_ec_get_default(), &pk, pub, sk) != 0) {
394
+ if (br_ec_compute_pub(sbc_ec(sk->curve), &pk, pub, sk) != 0) {
384
395
  sbc_put1(out, SBC_K_EC); sbc_put1(out, (sbc_u8)cv); sbc_put1(out, 1);
385
396
  sbc_field(out, pk.q, pk.qlen);
386
397
  /* The scalar, left padded to the field size as JWK d wants it. */
@@ -677,7 +688,7 @@ static int sbc_ecdsa_sign(int bits, const sbc_u8* key, size_t kl, const sbc_u8*
677
688
  sbc_u8 h[64], sig[132];
678
689
  rc = SBC_OPERATION;
679
690
  if (sbc_digest(bits, m, ml, h)) {
680
- size_t n = br_ecdsa_sign_raw_get_default()(br_ec_get_default(), sbc_hash(bits), h, sk, sig);
691
+ size_t n = br_ecdsa_sign_raw_get_default()(sbc_ec(sk->curve), sbc_hash(bits), h, sk, sig);
681
692
  if (n) { sbc_put(out, sig, n); rc = SBC_OK; }
682
693
  }
683
694
  free(dc);
@@ -692,7 +703,7 @@ static int sbc_ecdsa_verify(int bits, int curve, const sbc_u8* point, size_t pl,
692
703
  if (sl != 2 * fs) return SBC_OK;
693
704
  br_ec_public_key pk;
694
705
  pk.curve = sbc_curve_br(curve); pk.q = (sbc_u8*)point; pk.qlen = pl;
695
- if (br_ecdsa_vrfy_raw_get_default()(br_ec_get_default(), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
706
+ if (br_ecdsa_vrfy_raw_get_default()(sbc_ec(pk.curve), h, sbc_hlen(bits), &pk, sig, sl)) *ok = 1;
696
707
  return SBC_OK;
697
708
  }
698
709
  /* A public point is valid when it is on the curve (the group is prime order, cofactor 1). */
@@ -702,12 +713,12 @@ static int sbc_ec_point_ok(int curve, const sbc_u8* point, size_t pl) {
702
713
  sbc_u8 q[133];
703
714
  memcpy(q, point, pl);
704
715
  sbc_u8 one[1] = { 1 };
705
- return br_ec_get_default()->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
716
+ return sbc_ec(sbc_curve_br(curve))->mul(q, pl, one, 1, sbc_curve_br(curve)) != 0;
706
717
  }
707
718
  static int sbc_ec_keygen(int curve, sbc_buf* out) {
708
719
  br_ec_private_key sk; br_ec_public_key pk;
709
720
  sbc_u8 kb[BR_EC_KBUF_PRIV_MAX_SIZE], pb[BR_EC_KBUF_PUB_MAX_SIZE];
710
- const br_ec_impl* impl = br_ec_get_default();
721
+ const br_ec_impl* impl = sbc_ec(sbc_curve_br(curve));
711
722
  if (!br_ec_keygen(&sbc_prng, impl, &sk, kb, sbc_curve_br(curve))) return SBC_OPERATION;
712
723
  if (!br_ec_compute_pub(impl, &pk, pb, &sk)) return SBC_OPERATION;
713
724
  size_t need = br_encode_ec_pkcs8_der(0, &sk, &pk);
@@ -885,8 +896,9 @@ static int sbc_ecdh(int curve, const sbc_u8* pkcs8, size_t kl, const sbc_u8* pee
885
896
  if (sk->curve == sbc_curve_br(curve) && fs && pl == 1 + 2 * fs && peer[0] == 4) {
886
897
  sbc_u8 pt[133];
887
898
  memcpy(pt, peer, pl);
888
- if (br_ec_get_default()->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
889
- size_t xl; size_t xo = br_ec_get_default()->xoff(sk->curve, &xl);
899
+ const br_ec_impl* impl = sbc_ec(sk->curve);
900
+ if (impl->mul(pt, pl, sk->x, sk->xlen, sk->curve)) {
901
+ size_t xl; size_t xo = impl->xoff(sk->curve, &xl);
890
902
  sbc_put(out, pt + xo, xl); rc = SBC_OK;
891
903
  }
892
904
  }
@@ -9,7 +9,7 @@ import { assembleSubtleC } from "./assemble";
9
9
 
10
10
  const bear = join(
11
11
  process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? join(homedir(), ".cache/sproutboat"),
12
- "bearssl-0.6/bearssl-0.6",
12
+ "bearssl-7bea48e/BearSSL-7bea48e",
13
13
  );
14
14
  const lib = join(bear, "build/libbearssl.a");
15
15
  // SUBTLE_DRIVER: a prebuilt driver binary (a machine with no C compiler, or a cross-built one).
@@ -4962,8 +4962,13 @@ function __sbEmbeddedDispatch(msg) {
4962
4962
  if (op === "secret.get") {
4963
4963
  // Secrets reach a standalone binary through the environment, the same
4964
4964
  // channel the launcher and the supervisor use; there is nothing to decrypt.
4965
- const value = __sbEnv(String(msg.name));
4966
- if (!value) throw new Error("secret not set: " + msg.name);
4965
+ const raw = __sbEnv(String(msg.name));
4966
+ if (!raw) throw new Error("secret not set: " + msg.name);
4967
+ // __sbEnv hands back the environment's bytes as a bytestring, one char per byte, so a UTF-8 secret ("é" is
4968
+ // C3 A9) reached the app as "é". From compatibility date 2026-11-07 it is decoded as UTF-8, as workerd's
4969
+ // secrets are text. Bytes that are not valid UTF-8 pass through unchanged (__sbFromUtf8's rule). The broker
4970
+ // transport is not touched: its secrets arrive as JSON, already real strings.
4971
+ const value = __sbCompatDate() >= "2026-11-07" ? __sbFromUtf8(raw) : raw;
4967
4972
  return { ok: true, value };
4968
4973
  }
4969
4974
 
package/test/bearssl.ts CHANGED
@@ -7,9 +7,10 @@ import { join, resolve } from "node:path";
7
7
  // The prelude carries crypto.subtle's native half on every transport, so a test that compiles a
8
8
  // handler using crypto.subtle needs BearSSL on the include path and link line, as the CLI arranges
9
9
  // for a build. Same pinned source as the CLI's src/bearssl.ts; built once into the toolchain cache.
10
- const VERSION = "0.6";
11
- const URL_ = `https://bearssl.org/bearssl-${VERSION}.tar.gz`;
12
- const SHA256 = "6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139efc83ff14";
10
+ const COMMIT = "7bea48e5e850ab4cafbe68d3765cdaba13a86d6f";
11
+ const VERSION = COMMIT.slice(0, 7);
12
+ const URL_ = `https://bearssl.org/gitweb/?p=BearSSL;a=snapshot;h=${COMMIT};sf=tgz`;
13
+ const SHA256 = "ed397832b1613582737d9c1ce01c1af74ad6a9e9c52a341e24433987cda0f372";
13
14
 
14
15
  const walk = async (dir: string): Promise<string[]> =>
15
16
  (
@@ -28,7 +29,7 @@ export async function bearsslEnv(): Promise<{ SB_EXTRA_CFLAGS: string; SB_EXTRA_
28
29
  process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? join(homedir(), ".cache/sproutboat"),
29
30
  `bearssl-${VERSION}-test`,
30
31
  );
31
- const tree = join(root, `bearssl-${VERSION}`);
32
+ const tree = join(root, `BearSSL-${VERSION}`);
32
33
  const objects = join(root, "obj");
33
34
  const marker = join(objects, ".complete");
34
35
  if (!existsSync(marker)) {