@ai-ecoverse/slicc-vault 1.10.2 → 1.12.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 +20 -7
- package/dist/types/contract.d.ts +57 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -111,8 +111,9 @@ Otherwise the vault keeps the policy it last accepted for that file, so the rule
|
|
|
111
111
|
- `[[proxy.rules]]`: `secret` (a seven entry of `[secrets]`), `domain` (one exact host, lower-cased), `header` (lower-cased, default `authorization`), `methods` (upper-cased; empty means any) and `paths` (each starting with `/`; empty means any). A rule that breaks any of this is dropped with a warning; the rest of the file still counts. Rules are numbered from 1 in file order, dropped ones included; a `session` audit entry names its rule by that number.
|
|
112
112
|
- **Session rules** (§3.19) add `protocol` and `port` (an integer from 1 to 65535), and are used only by `sessionMaterialize`, never by `forward`; a rule without `protocol` is used only by `forward`:
|
|
113
113
|
- `protocol = "http1"`: `header`, `methods` and `paths` as above; the position is the header. A `position` is a warning.
|
|
114
|
-
- `protocol = "smtp"`, with `position = "auth-plain"` or `"auth-login"`, or `protocol = "pg"`, with `position = "password-cleartext"
|
|
115
|
-
- `
|
|
114
|
+
- `protocol = "smtp"`, with `position = "auth-plain"` or `"auth-login"`, or `protocol = "pg"`, with `position = "password-cleartext"` or `"scram-sha-256"` (each allowed only where a rule names it): no `header`, `methods` or `paths` (a warning otherwise). Its `header` reads `''`.
|
|
115
|
+
- **Trust anchors** (§3.19.6), on any session rule: `anchors` (base64 DER certificates, each starting with a SEQUENCE and at most 8 KB), `pins` (base64 SHA-256 of a public key, 32 bytes) and `roots` (`true` or `false`), at most 8 anchors and 8 pins. A rule with anchors or pins accepts only sessions the kernel verified against them, never the public roots, unless it says `roots = true`; a rule without them accepts the public roots, and `roots = false` there is a warning. The vault checks only their format: the kernel checks that an anchor is a CA (CA:true and keyCertSign) and the chain. Like every rule, they come only from the attested root-owned file, so only an approved `sudo` write sets them: no program, agent or page op can.
|
|
116
|
+
- `port`, `position`, `anchors`, `pins` or `roots` without `protocol`, another `protocol`, or a missing or wrong `port`, `position`, `anchors`, `pins` or `roots` is a warning, and the rule is dropped.
|
|
116
117
|
- A record of kind `secret` is `active` when the file's `[secrets]` entry of its name holds the record's own placeholder and at least one rule names it. A placeholder from another vault or an old record doesn't activate anything.
|
|
117
118
|
- Nothing else in the file counts: no imports, profiles or other providers.
|
|
118
119
|
|
|
@@ -184,7 +185,8 @@ iframe.contentWindow.postMessage({ type: 'slicc-vault', v: 1, op: 'hello', role:
|
|
|
184
185
|
| `policy` | `files` | the attested policy files, below (Policy) |
|
|
185
186
|
| `forward` | `head`, `body?`, `owner`, `exit`, `rawTraits`, with a `MessagePort` transferred alongside | the injected head, below (Forward) |
|
|
186
187
|
| `materializeConsume` | `ticket`, `stamp`, `path`, `resolved`, `argv`, `cwd`, `interpreters`, `trusted` | the values for the child's environment, below (Exec) |
|
|
187
|
-
| `sessionMaterialize` | `stamp`, `session`, `host`, `port`, `protocol`, `position`, `verified`, `handshakeAt`, `exit`, `placeholders
|
|
188
|
+
| `sessionMaterialize` | `stamp`, `session`, `host`, `port`, `protocol`, `position`, `verified`, `handshakeAt`, `exit`, `verifiedBy?`, `anchor?`, then `placeholders` (and `head` for http1) at every position but `scram-sha-256`, which takes `scram` and no `placeholders` | `{ values }` for a kernel-verified TLS session, below (Sessions) |
|
|
189
|
+
| `sessionAnchors` | `stamp`, `session`, `host`, `port`, `protocol`, `exit` | `{ anchors, pins }`, below (Sessions) |
|
|
188
190
|
| `import` | `kind`, `bytes`, `password?`, `pick?` | below (Import and export) |
|
|
189
191
|
| `export` | `passphrase` | `{ bundle, count }`, below |
|
|
190
192
|
| anything else | | `ENOTSUP` |
|
|
@@ -317,7 +319,7 @@ port.postMessage({ v: 1, id, op: 'forward', head, body, owner, exit, rawTraits }
|
|
|
317
319
|
|
|
318
320
|
## Sessions
|
|
319
321
|
|
|
320
|
-
**`sessionMaterialize`** (§3.19, a page op that bios relays unchanged from the kernel) hands the kernel's TLS writer the secrets for one kernel-terminated TLS session (A2), for protocols `forward` can't carry: an http1 header on a kernel-made connection, SMTP AUTH, or pg's cleartext password. The kernel asks only after its own handshake verified the host (SNI, chain and hostname, whatever the caller's flags), never on a plaintext leg; it substitutes only at the position, uses each value once within 60 s, keeps it out of every process, fd, file, log and telemetry, and scrubs the session's read side (S2) for the values and their encodings. The vault picks the rules, so the kernel never parses `/etc/fnox/config.toml`.
|
|
322
|
+
**`sessionMaterialize`** (§3.19, a page op that bios relays unchanged from the kernel) hands the kernel's TLS writer the secrets for one kernel-terminated TLS session (A2), for protocols `forward` can't carry: an http1 header on a kernel-made connection, SMTP AUTH, or pg's cleartext password or SCRAM-SHA-256. The kernel asks only after its own handshake verified the host (SNI, chain and hostname, whatever the caller's flags), never on a plaintext leg; it substitutes only at the position, uses each value once within 60 s, keeps it out of every process, fd, file, log and telemetry, and scrubs the session's read side (S2) for the values and their encodings. The vault picks the rules, so the kernel never parses `/etc/fnox/config.toml`.
|
|
321
323
|
|
|
322
324
|
```js
|
|
323
325
|
port.postMessage({ v: 1, id, op: 'sessionMaterialize', stamp, session, host, port: 587, protocol: 'smtp', position: 'auth-plain', verified: true, handshakeAt, exit, placeholders });
|
|
@@ -327,10 +329,11 @@ The fields are the kernel's session attestation:
|
|
|
327
329
|
- `stamp`: the kernel's stamp of the process that owns the socket; `users` must be `enforce` (else `EACCES` "kernel users must be enforce").
|
|
328
330
|
- `session`: the kernel's session id, 16 to 128 characters of `A-Z`, `a-z`, `0-9`, `_` and `-`.
|
|
329
331
|
- `host`: the name the kernel verified the leaf against, compared lower-cased without one trailing dot; `port`; `protocol` (`http1`, `smtp` or `pg`).
|
|
330
|
-
- `position`: `header:<name>` (lower-cased) for http1, else `auth-plain`, `auth-login` or `
|
|
332
|
+
- `position`: `header:<name>` (lower-cased) for http1, else `auth-plain`, `auth-login`, `password-cleartext` or `scram-sha-256` (below).
|
|
331
333
|
- `verified`: the kernel's own verdict on the chain and the name; `handshakeAt`: when the handshake finished (epoch ms).
|
|
334
|
+
- `verifiedBy`: `roots` (the default; the kernel's bundled public roots) or `anchored` (a rule's anchors or pins, below), with `anchor`: base64 of the SHA-256 of the whole DER of the anchor that verified the chain, as `sessionAnchors` gave it, or the pin itself. `anchor` with `roots`, or a missing or malformed one with `anchored`, is `EINVAL`.
|
|
332
335
|
- `exit`: the exit the session leaves by (`page`, `proxy`, `tailscale` or `device:<id>`, with the device's `DeviceId`).
|
|
333
|
-
- `placeholders
|
|
336
|
+
- `placeholders` (every position but `scram-sha-256`, which takes `scram` instead, below): 1 to 16 placeholders, every one found at the position, in one request. `head` (http1 only, required there): `{ method, path }`, the request line's method and target.
|
|
334
337
|
- A malformed field is `EINVAL`, and so is a `protocol` other than these or a `position` that isn't one of its protocol's; neither burns the session.
|
|
335
338
|
|
|
336
339
|
Once the fields check out, the vault burns the session in IndexedDB, in one transaction, so no vault frame of the namespace, now or after a reload, answers it again until 60 s after the later of the request and `handshakeAt`. Then it refuses the whole request with `EACCES` "session refused: …" when:
|
|
@@ -344,6 +347,15 @@ Each placeholder then gets its value only when it is a `secret` record's, holdin
|
|
|
344
347
|
|
|
345
348
|
It answers `{ values: { [placeholder]: value } }` for those, and leaves the others out, so the kernel sends them as written. When none gets a value, it's `EACCES` with the first placeholder's reason: "NAME isn't a secret", "a placeholder of no record of this vault", "NAME's placeholder isn't the one /etc/fnox/config.toml names", "NAME was removed", "NAME expired", "no session rule in /etc/fnox/config.toml lets NAME be used", "no session rule lets NAME at PROTOCOL POSITION on HOST:PORT" (with " for METHOD PATH" for http1), "NAME isn't allowed on EXIT", or "the secret or its rule changed meanwhile". Each value handed out adds a `session` audit entry `{ name, session, domain, port, protocol, position, rule, exit, uid, tag }`, `rule` being the number of the rule that allowed it, and sets the record's `usedAt`. It never includes the value. Stored values never hold CR, LF or NUL (Records), so a value can't break out of its position.
|
|
346
349
|
|
|
350
|
+
**`sessionAnchors`** (§3.19.6) is value-free: when the kernel's bundled roots fail a session only on trust (the leaf names the host and the chain is otherwise valid), it asks with `{ stamp, session, host, port, protocol, exit }` (checked as above; `users` must be `enforce`), and the vault answers `{ anchors, pins }`: every anchor and pin of the session rules for that `protocol`, `domain` and `port` whose secret's `[exits]` line, if any, lists `exit`, without duplicates, or empty lists. It burns nothing and needs no unlocked vault. The kernel then verifies the chain against those anchors only (never the program's `ca`), and asks `sessionMaterialize` with `verifiedBy: 'anchored'` and the `anchor` it used. A placeholder gets its value only by a rule that trusts the session: a rule with anchors or pins needs an `anchored` session whose `anchor` is one of that rule's own ("the anchor that verified the session isn't one of NAME's session rule" otherwise), or `roots = true` for a `roots` session ("NAME's session rule accepts only its anchors and pins, not public roots"); a rule without them takes a `roots` session only. So one rule's anchor never opens another rule's secret, and a pinned host can't fall back to a public CA.
|
|
351
|
+
|
|
352
|
+
**pg SCRAM-SHA-256** (`position: 'scram-sha-256'`, §3.19.5). The program's pg client computes its `ClientProof` from the password it holds, a placeholder, so the kernel can't see which one it used; the vault finds it from the proof. The request carries `scram: { clientFirstBare, serverFirst, clientFinalBare, proof }` and no `placeholders` (`EINVAL` otherwise, and `scram` at any other position is `EINVAL` too):
|
|
353
|
+
- `clientFirstBare` is `n=<user>,r=<client nonce>` and `serverFirst` is `r=<nonce>,s=<salt>,i=<iterations>`, with no extensions; the server's nonce must extend the client's, and `i` must be 1 to 100000.
|
|
354
|
+
- `clientFinalBare` is `c=biws,r=<the server's nonce>`, without `p=`: `biws` is the GS2 header `n,,`, so channel binding (`-PLUS`, `p=…`, `y,,`) is refused. The kernel offers the program only `SCRAM-SHA-256`, because the program's binding data couldn't match the kernel's TLS.
|
|
355
|
+
- `proof` is base64 of the program's 32-byte `ClientProof`. Each message is at most 1024 characters. Anything else is `EINVAL`, before the burn.
|
|
356
|
+
|
|
357
|
+
The candidates are the placeholders that would pass the per-placeholder checks above for this session (a live `secret` with a `scram-sha-256` session rule for this host and port, whose `[exits]` allow `exit`), never other records; more than 16 is `EACCES` "more than 16 session rules' placeholders to try for SCRAM", before any hashing. For each, the vault computes RFC 5802's `ClientProof` with the placeholder as the password (PBKDF2-SHA-256 over the salt and iterations, then the Client Key, Stored Key and signature over `clientFirstBare,serverFirst,clientFinalBare`) and compares it in constant time. Only placeholders are hashed, never values, so it reveals nothing the program doesn't hold. Exactly one match is handed out as above (re-checked at release, audited with `position: 'scram-sha-256'`); none is `EACCES` "no session rule's placeholder matches the SCRAM proof", and more than one is `EACCES` "more than one placeholder matches the SCRAM proof". The kernel computes the real proof from the value, checks the server's real `v=` before giving the program one made from the placeholder, never passes on a server `e=` derived from the value, and wipes the value and every derived key.
|
|
358
|
+
|
|
347
359
|
## Model and MCP endpoints
|
|
348
360
|
|
|
349
361
|
The vault never injects a secret into a model or MCP endpoint, whatever a rule says.
|
|
@@ -445,13 +457,14 @@ The button writes its intent to `intents` under a random 128-bit nonce and opens
|
|
|
445
457
|
The npm package `@ai-ecoverse/slicc-vault` holds only TypeScript types, built from `src/contract.ts`: the port API (`Hello`, `Greeting`, `Request`, `Reply`, `VaultEvent`, `Role`), the button (`ButtonConfig`, `ButtonMessage`, `ButtonEvent`), the popup (`PopupIntent`, `PopupState`), the page's results (`SealResult`, `ImportResult`), and the data Settings shows (`SecretView`, `VaultStatus`, `ScrubState`, `PendingRequest`, `AuditEntry`, `ExitPolicyView`, `VaultList`). `Availability` (`connecting`, `waiting`, `locked`, `ready`) is for bios to tell spectrum "waiting for the vault" and "unlock the vault to continue". They mean:
|
|
446
458
|
|
|
447
459
|
- **`EpochMs`**: milliseconds since the epoch. **`DeviceId`**: the stable id of a linked device's link certificate, the same id bios uses for linked devices and trays, in `sourceDevice`, `device:<id>` exits (and `[exits]` lines) and `ExitPolicyView.devices[].device`; never a display name.
|
|
448
|
-
- **`Rule`**: `domain` is an exact lower-case host; `header` a lower-case header name; **empty `methods` or `paths` mean any**. A rule with `protocol` (`http1`, `smtp`, `pg`) is a session rule, with its `port` and, for smtp and pg, its `position`; show it as "PROTOCOL on host:port".
|
|
460
|
+
- **`Rule`**: `domain` is an exact lower-case host; `header` a lower-case header name; **empty `methods` or `paths` mean any**. A rule with `protocol` (`http1`, `smtp`, `pg`) is a session rule, with its `port` and, for smtp and pg, its `position`; show it as "PROTOCOL on host:port". `anchors`, `pins` and `roots` are its trust anchors (Policy).
|
|
449
461
|
- **`exec`**: `false` means the value is never revealed to a program; `any`, `argv` (with `execArgv`, the allowed argv prefixes) and `shell` are the `[exec]` modes under which `fnox exec` may hand it to a program.
|
|
450
462
|
- **`exits`** is present only when `/etc/slicc/secrets-policy.toml` restricts that secret; absent means every exit (`page`, `proxy`, `tailscale`, `device:<id>`).
|
|
451
463
|
- **Bindings in `list`:** read under one scrub version (retried, then `EAGAIN`), `provider`, `server` and `settings` are scrubbed over every value (a setting whose name holds one is dropped), and left out while the vault is locked before its first unlock.
|
|
452
464
|
- **`kind`**: `secret` (a credential programs use through rules), **`grant`** (a sign-in's refresh token, never injected; show it as "Sign-in"), `model` (a model provider key), `mcp` (an MCP server credential). `provider` and `account` name the service and the signed-in account where one is known; a `model` record always has its `provider`, an `mcp` record may have its `server`, and either may carry non-secret `settings`.
|
|
453
465
|
- **Who did it:** `uid` and `tag` are the kernel stamp for entries a program or the agent caused (`/dev/sealed` seals, `forward`, `mcp`, `materialize`, `session`). Every entry from a page-role op (`put`, `seal`, `remove`, `restore`, `import`, `policy`) and from the vault itself (`refresh`) has `uid: null, tag: null`, older entries included. Spectrum shows page ops as "by you", `refresh` as "automatically", and `policy` entries with no "by …".
|
|
454
466
|
- **`AuditEntry`**: `forward` is a use of a value in a request, with the exit that carried it; `mcp` a kernel program's call to a registered MCP server; `session` a value handed to the kernel for a verified TLS session (show it as "Sent over PROTOCOL to host"); **`materialize` is shown as "Revealed to a program"**; `seal`, `remove`, `restore` and `policy` (with its `changes`) record changes. Every entry has a `seq` for paging.
|
|
467
|
+
- **`SessionTarget`**: a `sessionMaterialize` request's protocol, position and what goes with it: `placeholders` (and `head` for http1) for every position but `scram-sha-256`, which takes `scram` (a `ScramExchange`) and no `placeholders`.
|
|
455
468
|
- **`VaultList.refused`** lists rules the vault dropped because they name a model or MCP endpoint.
|
|
456
469
|
- **`PendingRequest.prompt`** is untrusted, agent-written text of at most 200 characters.
|
|
457
470
|
- **`ExitPolicyView`** is read-only: the secrets `/etc/slicc/secrets-policy.toml` restricts to certain exits.
|
package/dist/types/contract.d.ts
CHANGED
|
@@ -26,7 +26,41 @@ export interface VaultError {
|
|
|
26
26
|
message: string;
|
|
27
27
|
}
|
|
28
28
|
export type SessionProtocol = 'http1' | 'smtp' | 'pg';
|
|
29
|
-
export type SessionPosition = 'auth-plain' | 'auth-login' | 'password-cleartext';
|
|
29
|
+
export type SessionPosition = 'auth-plain' | 'auth-login' | 'password-cleartext' | 'scram-sha-256';
|
|
30
|
+
export interface ScramExchange {
|
|
31
|
+
clientFirstBare: string;
|
|
32
|
+
serverFirst: string;
|
|
33
|
+
clientFinalBare: string;
|
|
34
|
+
proof: string;
|
|
35
|
+
}
|
|
36
|
+
export type SessionTarget = {
|
|
37
|
+
protocol: 'http1';
|
|
38
|
+
position: `header:${string}`;
|
|
39
|
+
placeholders: string[];
|
|
40
|
+
head: {
|
|
41
|
+
method: string;
|
|
42
|
+
path: string;
|
|
43
|
+
};
|
|
44
|
+
scram?: never;
|
|
45
|
+
} | {
|
|
46
|
+
protocol: 'smtp';
|
|
47
|
+
position: 'auth-plain' | 'auth-login';
|
|
48
|
+
placeholders: string[];
|
|
49
|
+
head?: never;
|
|
50
|
+
scram?: never;
|
|
51
|
+
} | {
|
|
52
|
+
protocol: 'pg';
|
|
53
|
+
position: 'password-cleartext';
|
|
54
|
+
placeholders: string[];
|
|
55
|
+
head?: never;
|
|
56
|
+
scram?: never;
|
|
57
|
+
} | {
|
|
58
|
+
protocol: 'pg';
|
|
59
|
+
position: 'scram-sha-256';
|
|
60
|
+
scram: ScramExchange;
|
|
61
|
+
placeholders?: never;
|
|
62
|
+
head?: never;
|
|
63
|
+
};
|
|
30
64
|
export interface Rule {
|
|
31
65
|
domain: string;
|
|
32
66
|
header: string;
|
|
@@ -35,6 +69,9 @@ export interface Rule {
|
|
|
35
69
|
protocol?: SessionProtocol;
|
|
36
70
|
position?: SessionPosition;
|
|
37
71
|
port?: number;
|
|
72
|
+
anchors?: string[];
|
|
73
|
+
pins?: string[];
|
|
74
|
+
roots?: boolean;
|
|
38
75
|
}
|
|
39
76
|
export interface RefusedRule {
|
|
40
77
|
secret: string;
|
|
@@ -449,22 +486,29 @@ export type Op = {
|
|
|
449
486
|
cwd: string;
|
|
450
487
|
interpreters: string[];
|
|
451
488
|
trusted: boolean;
|
|
452
|
-
} | {
|
|
489
|
+
} | ({
|
|
453
490
|
op: 'sessionMaterialize';
|
|
454
491
|
stamp: Stamp;
|
|
455
492
|
session: string;
|
|
456
493
|
host: string;
|
|
457
494
|
port: number;
|
|
458
|
-
protocol: SessionProtocol;
|
|
459
|
-
position: string;
|
|
460
495
|
verified: boolean;
|
|
461
496
|
handshakeAt: EpochMs;
|
|
462
497
|
exit: ExitName;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
498
|
+
} & SessionTarget & ({
|
|
499
|
+
verifiedBy?: 'roots';
|
|
500
|
+
anchor?: never;
|
|
501
|
+
} | {
|
|
502
|
+
verifiedBy: 'anchored';
|
|
503
|
+
anchor: string;
|
|
504
|
+
})) | {
|
|
505
|
+
op: 'sessionAnchors';
|
|
506
|
+
stamp: Stamp;
|
|
507
|
+
session: string;
|
|
508
|
+
host: string;
|
|
509
|
+
port: number;
|
|
510
|
+
protocol: SessionProtocol;
|
|
511
|
+
exit: ExitName;
|
|
468
512
|
};
|
|
469
513
|
export type AgentOp = {
|
|
470
514
|
op: 'redact';
|
|
@@ -607,6 +651,10 @@ export type OpReply = {
|
|
|
607
651
|
} | {
|
|
608
652
|
op: 'sessionMaterialize';
|
|
609
653
|
values: Record<string, string>;
|
|
654
|
+
} | {
|
|
655
|
+
op: 'sessionAnchors';
|
|
656
|
+
anchors: string[];
|
|
657
|
+
pins: string[];
|
|
610
658
|
};
|
|
611
659
|
export type Request = Op & {
|
|
612
660
|
v: Protocol;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-vault",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "The separate-origin vault at vault.sliccy.ai: sealed secrets, OAuth tokens, and model and MCP credentials for SLICC seven, used only by its injector",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@ai-ecoverse/jerk-oauth": "1.0.0",
|
|
50
|
-
"@ai-ecoverse/slicc-kernel": "1.
|
|
50
|
+
"@ai-ecoverse/slicc-kernel": "1.126.0",
|
|
51
51
|
"@ai-ecoverse/slicc-scrub": "1.0.0",
|
|
52
52
|
"@ai-ecoverse/slicc-shared-web": "1.24.2",
|
|
53
53
|
"@ai-ecoverse/wasm-bash": "5.3.0-20",
|