@ai-ecoverse/slicc-kernel 1.126.1 → 1.127.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/README.md CHANGED
@@ -430,7 +430,7 @@ The module's default export (or its `main` export) is called with a context and
430
430
  - **`servername`** must be a host name (letters, digits and `-` in labels of at most 63, at most 253 in all, one trailing dot allowed) or an IP literal, else `EINVAL` before anything is dialled. An IP literal sends no SNI and must match an iPAddress subjectAltName.
431
431
  - **The `tls` property** is the handshake's: `{ path: 'kernel', substitutes: false, verified, protocol: 'TLSv1.3' or 'TLSv1.2', alpnProtocol (the agreed protocol, or false), authorized, authorizationError?, servername, peerCertificates, cipher, clientCertificate, sni, hostnameChecked, hostnameMatches? }`.
432
432
  - `peerCertificates` is the server's chain as DER, leaf first.
433
- - `verified` is the kernel's own verdict on the session, whatever the caller's flags: the chain verifies against the kernel's **bundled roots** (never a `ca` the caller passed, which only decides `authorized`) and the leaf names `servername`, which is a host name, not an IP literal. `rejectUnauthorized: false`, `checkHostname: false` and `sni: false` don't change it; a custom `ca` makes it false even when the chain would pass both. It's what sealed substitution in an A2 session will be gated on ([#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)); until then `substitutes` stays false.
433
+ - `verified` is the kernel's own verdict on the session, whatever the caller's flags: the chain verifies against the kernel's **bundled roots** (never a `ca` the caller passed, which only decides `authorized`) and the leaf names `servername`, which is a host name, not an IP literal. `rejectUnauthorized: false`, `checkHostname: false` and `sni: false` don't change it; a custom `ca` makes it false even when the chain would pass both. Sealed substitution in an A2 session is gated on it ([#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).
434
434
  - `cipher` is the negotiated suite as Node's `getCipher()` gives it: `{ name, standardName, version }`, where `name` is OpenSSL's (`ECDHE-RSA-AES128-GCM-SHA256`) and `standardName` IANA's. As in Node, `version` is the suite's own version, not the session's: `TLSv1.0` for a CBC-SHA suite on a TLS 1.2 session.
435
435
  - `clientCertificate` says whether a client certificate was offered. `sni` says whether the server name was sent: it's false with `sni: false` and for an IP-literal `servername`, which TLS never sends.
436
436
  - `hostnameChecked` says whether the kernel checked the leaf against `servername`. `hostnameMatches` says whether it names it.
@@ -454,7 +454,24 @@ The module's default export (or its `main` export) is called with a context and
454
454
  - **The caller must then check the server's identity itself**, as slicc-jsh does with Node's `checkServerIdentity` and `peerCertificates[0]`; pass it only when the program supplied one.
455
455
  - With `rejectUnauthorized: false` it's `EINVAL`.
456
456
  - **STARTTLS:** `net.connect({ socket: conn, host, tls })` upgrades an open plain connection to an uplink destination in place, as Node's `tls.connect({ socket })` does. It resolves with a connection whose streams now carry the decrypted bytes and whose `tls` is the session. Don't hold a pending read on the plain connection while it upgrades. Pass `host` (or `tls.servername`): the plain connection only knows the address it reached, so without a name the certificate is checked against that IP, and a certificate for the host name fails.
457
- - **Sealed values aren't substituted.** The kernel encrypts end to end, so a placeholder written into an A2 connection reaches the server as written, unsubstituted (it isn't the secret, so nothing leaks). A caller that relies on substitution checks `conn.tls.substitutes` and uses `fetch`, or an A1 connection, for sealed values.
457
+ - **Sealed values in an A2 session** ([#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)) are substituted only at a credential position, only in a session the kernel verified itself, and only as the vault allows. `substitutes` is true when all of these hold:
458
+ - `verified` is true;
459
+ - ALPN didn't agree `h2`;
460
+ - a vault is connected;
461
+ - the uplink said which exit the connection leaves by (`UplinkConn.exit`: `tailscale` or `device:<id>`, the only exits an uplink connection takes). Without one, or with any other value, nothing is substituted.
462
+
463
+ Then:
464
+ - **HTTP/1.1:** if the **first request head** the program writes on the session (it starts with a standard method) has placeholders (`fnox_proxy_<24 hex>`) in exactly one header line, either in the value or in the `user:password` of `Basic` credentials, the kernel asks the vault once (`sessionMaterialize`, below) for that header's position.
465
+ - **What goes out:** the placeholders the vault answers with a value of at least 8 bytes go out as that value (`Basic` decoded, substituted and re-encoded). Everything else goes out as written:
466
+ - the request line and the body;
467
+ - other headers;
468
+ - a head with placeholders in two header lines;
469
+ - later requests on the same connection;
470
+ - anything once the head passes 16 KiB, or once the program pauses for 2 s in the middle of it;
471
+ - SMTP and pg, which come next.
472
+ - **What's read back:** once a process has had a value substituted for a host, everything its whole process tree reads from any A2 session to that host is scrubbed: the process itself, its ancestors and every descendant (forked, spawned or exec'd, before or after the substitution; a forked child's own new session included) with [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub), as S2 does: the value comes back as its placeholder, and a fragment or an encoded form (JSON, percent, HTML, base64, hex) as `[redacted:sealed]`. The scrub stays until the whole tree has ended. A process outside the tree isn't covered. A value shorter than 8 bytes is never substituted, as it couldn't be scrubbed.
473
+ - **The bound:** the server the value goes to is the one the vault rule names, verified against the kernel's own roots. Substitution trusts that server not to reflect the value back in a form the scrub doesn't know (encrypted under a key the program chose, say). That's the same residual as A1's.
474
+ - **Elsewhere:** a placeholder written into any other A2 session reaches the server as written. It isn't the secret, so nothing leaks.
458
475
  - **Limits:** no revocation checking (neither OCSP nor CRLs, as with Node's default), no session resumption, and the clock is the page's `Date.now()`.
459
476
  - `net.resolve(name, { family, signal })` asks the kernel's resolver, which WASIX `resolve` and Emscripten `getaddrinfo` use too: entries in a real `/etc/hosts`, the kernel's own names (`localhost`, the node name, `host.slicc.internal`) and then the uplink's DNS ([#913](https://github.com/ai-ecoverse/slicc-kernel/issues/913)). It answers the addresses, in the resolver's order, for `family` `4`, `6` or `0` (both, IPv4 first; the default). A name with no answer rejects with a `JsCallError` of code `ENOTFOUND`, as Node's `dns.lookup` does, another family with `EAI_FAMILY`, and an aborted `signal` with its reason.
460
477
  - `websocket(url, { protocols, headers, signal })`, a WebSocket over the kernel's transport (its `websocket`, so `fetchTransport({ webSocket: true })` in a browser), shaped like `WebSocketStream`. It resolves once the socket is open, with `url`, the `protocol` the far end chose (always one the program offered: a transport that hands back a socket with another fails the open with `EPROTO`, while browsers and Node's `WebSocket`, which `fetchTransport()` uses, fail such a handshake themselves without saying why, so the open fails with `ECONNREFUSED`), a `readable` of messages (`string` for text, `Uint8Array` for binary), a `writable` for sending them, `closed` (the `{ code, reason }` of the close, from either side, whether or not the program reads) and `close({ code, reason })`. An `http(s):` URL is taken as `ws(s):`. Sending waits while more than 1 MiB is buffered. A signal aborts the open, and after it the socket. A transport without WebSockets rejects with a `TypeError` whose `cause.code` is `ENOTSUP`. One that cannot connect rejects with the `code` its error carries (an errno, such as `EAI_NONAME`, `ECONNREFUSED`, `ETIMEDOUT` or `EPROTO`), else `ECONNREFUSED`. `ws://host.slicc.internal:port` reaches the real machine's loopback, as it does through the proxy;
@@ -940,8 +957,8 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
940
957
 
941
958
  **The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. Under `O_NONBLOCK` it gives `EAGAIN` when the pipe or terminal under it would. A write that finds room returns at once, taking at most 64 KiB, and the scrubbed bytes the file couldn't take yet go out before anything written later. Until they have, `poll` reports it unwritable and a nonblocking write gets `EAGAIN`, taking nothing. A blocking write that waits for them, or for room, ends with `EINTR` at a signal, taking nothing; one interrupted after it began writing returns its whole count, and the rest follows. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
942
959
 
943
- **A2 sessions (`sessionMaterialize`, [#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).** The request is in place but not sent yet: the next #1090 steps send it from the http1, SMTP and pg writers. For a kernel-terminated TLS session, the kernel will ask the hook `{ v: 1, op: 'sessionMaterialize', caller, stamp, session, host, port, protocol, position, verified, handshakeAt, exit, placeholders, head? }` (slicc-vault 1.10.0, Sessions), once per session:
944
- - `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by (`page`, `proxy`, `tailscale` or `device:<name>`);
960
+ **A2 sessions (`sessionMaterialize`, [#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).** For a kernel-terminated TLS session (see **Sealed values in an A2 session** under JS programs' `ctx.net`), the kernel asks the hook `{ v: 1, op: 'sessionMaterialize', caller, stamp, session, host, port, protocol, position, verified, handshakeAt, exit, placeholders, head? }` (slicc-vault 1.10.0, Sessions), once per session:
961
+ - `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by, as the uplink reported it (`tailscale` or `device:<id>`, the id of `[A-Za-z0-9._-]`);
945
962
  - `protocol` is `http1`, `smtp` or `pg`, and `position` one of its own: `header:<lower-cased name>`, `auth-plain` or `auth-login`, or `password-cleartext`; `head` (`{ method, path }`) is there for `http1` only, and required there;
946
963
  - `placeholders` are the 1 to 16 distinct `fnox_proxy_<24 hex>` placeholders found at that position.
947
964