@uuaid/pillar 2.0.2 → 2.0.3
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/HOSTING-LIMITS.md +16 -0
- package/bin/carrier.mjs +15 -2
- package/bin/pillar-seed.mjs +13 -2
- package/package.json +1 -1
- package/src/identity/jcs.mjs +4 -5
- package/src/identity/keychain.mjs +35 -0
- package/src/net/carrier-server.mjs +55 -2
- package/src/net/envelope.mjs +3 -1
- package/src/net/refusals.mjs +1 -0
package/HOSTING-LIMITS.md
CHANGED
|
@@ -310,6 +310,21 @@ front of each one.
|
|
|
310
310
|
clients retry after 2 s). Stored envelopes stay in SQLite. Leave disk for a
|
|
311
311
|
256 MiB store plus its WAL file.
|
|
312
312
|
|
|
313
|
+
**Secure delete (from 2.0.3).** Expired envelopes are zeroed on disk by
|
|
314
|
+
default. `vacuum()` runs with SQLite `secure_delete` on and follows each
|
|
315
|
+
expiry pass that deleted anything with `wal_checkpoint(TRUNCATE)`, so neither
|
|
316
|
+
the database file nor the WAL keeps a deleted envelope's bytes. Turn it off
|
|
317
|
+
per broker with `--secure-delete off` (`CARRIER_SECURE_DELETE=off` for
|
|
318
|
+
`pillar-carrier`, `SEED_SECURE_DELETE=off` for `pillar-seed`); any other value
|
|
319
|
+
than on/off refuses to start. It is one setting for the whole store: SQLite
|
|
320
|
+
sets it per connection, so it cannot differ by tenant. Two things it does not
|
|
321
|
+
do:
|
|
322
|
+
- Bytes deleted before you upgraded stay in free pages. Run
|
|
323
|
+
`sqlite3 <db> 'VACUUM;'` once, with the carrier stopped, to clear them.
|
|
324
|
+
- A checkpoint that stays busy (another process holding the database open)
|
|
325
|
+
is logged as `secure-delete: WAL checkpoint still busy` and retried at the
|
|
326
|
+
next hourly pass; until then the WAL can still hold those bytes.
|
|
327
|
+
|
|
313
328
|
**Relay host.** Back up `PUSH_RELAY_SEED_PATH`. Every poll token names the
|
|
314
329
|
relay's key, so losing that file invalidates all of them
|
|
315
330
|
(apps/pillar-push/src/config.ts:19-22). Watch `fcmKey.daysLeft` on
|
|
@@ -343,6 +358,7 @@ or an exception message. A parameterised reason such as
|
|
|
343
358
|
| `keyId-sender-mismatch` | 400 | Sign with `transportSignature.keyId` equal to the sender UUAID, or send under a delegation |
|
|
344
359
|
| `publicKey-sender-mismatch` | 400 | Send from the UUAID your signing key derives, or attach the principal's delegation |
|
|
345
360
|
| `bad-signature` | 400, 401 | Sign again with this identity's own key over the exact bytes; the signature does not verify |
|
|
361
|
+
| `weak-publicKey` (from 2.0.3) | 400 | The sender key is a small-order or non-canonical Ed25519 point, which can "sign" anything; use a real identity key |
|
|
346
362
|
| `not-object`, `bad-version <v>`, `missing-or-bad-alg`, `alg-not-active:<alg>`, `bad-publicKey`, `bad-signature-encoding`, `missing-enc`, `enc-alg-not-supported:<alg>`, `enc-alg-not-active:<alg>`, `bad-epk`, `bad-iv`, `bad-ct`, `bad-canonical`, `verify-threw` | 400 (`verify-threw` also 401) | Re-seal the envelope with `seal()` from `@uuaid/pillar` or `@uuaid/pillar-client`; this one does not verify |
|
|
347
363
|
| `delegation-invalid:<why>` | 403 (send), 401 (inbox) | Mint a fresh delegation from the principal, then resend |
|
|
348
364
|
| `missing-auth-headers` | 401 | Send `x-pillar-pubkey`, `x-pillar-ts` and `x-pillar-sig` |
|
package/bin/carrier.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* carrier — run a standalone carrier server (the 24/7 post office).
|
|
4
4
|
*
|
|
5
5
|
* carrier --port 8799 --host 127.0.0.1 --db /opt/pillar-carrier/carrier.db
|
|
6
|
+
* carrier ... --secure-delete off # keep deleted envelope bytes (default: on)
|
|
6
7
|
*
|
|
7
8
|
* Defaults to loopback (RedTeam F5) — expose only behind a TLS-terminating
|
|
8
9
|
* proxy (nginx/Caddy). Pass --host 0.0.0.0 deliberately only if the carrier
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
* about who exists — pure store-and-forward.
|
|
12
13
|
*/
|
|
13
14
|
import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
|
|
14
|
-
import { CarrierServer } from "../src/net/carrier-server.mjs";
|
|
15
|
+
import { CarrierServer, parseSecureDelete } from "../src/net/carrier-server.mjs";
|
|
15
16
|
|
|
16
17
|
function parseArgs(argv) {
|
|
17
18
|
const args = {};
|
|
@@ -35,17 +36,29 @@ const ttlDays = Number(args.ttl ?? process.env.CARRIER_TTL_DAYS ?? 14);
|
|
|
35
36
|
const tierIssuers = String(args["tier-issuers"] ?? process.env.CARRIER_TIER_ISSUERS ?? "")
|
|
36
37
|
.split(",").map(s => s.trim()).filter(s => /^[0-9a-f]{64}$/i.test(s));
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
// Secure delete of expired envelopes: on unless this broker turns it off.
|
|
40
|
+
let secureDelete;
|
|
41
|
+
try {
|
|
42
|
+
secureDelete = parseSecureDelete(args["secure-delete"] ?? process.env.CARRIER_SECURE_DELETE);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`carrier: ${e.message}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const server = new CarrierServer({ dbPath, ttlDays, trustedTierIssuers: tierIssuers, secureDelete });
|
|
39
49
|
const addr = await server.listen(port, host);
|
|
40
50
|
console.log(`carrier listening on ${addr.address}:${addr.port}`);
|
|
41
51
|
console.log(` db: ${dbPath}`);
|
|
42
52
|
console.log(` ttl: ${ttlDays} days`);
|
|
43
53
|
console.log(` tiers: ${tierIssuers.length ? `${tierIssuers.length} trusted issuer(s)` : "disabled (community budgets for all)"}`);
|
|
54
|
+
console.log(` secure-delete: ${secureDelete ? "on (expired envelopes are zeroed; WAL truncated after each expiry pass)" : "off (expired envelope bytes stay on disk until reused)"}`);
|
|
44
55
|
|
|
45
56
|
// Vacuum expired envelopes hourly.
|
|
46
57
|
const vacuumTimer = setInterval(() => {
|
|
47
58
|
const n = server.vacuum();
|
|
48
59
|
if (n > 0) console.log(`vacuumed ${n} expired envelopes`);
|
|
60
|
+
const v = server.lastVacuum;
|
|
61
|
+
if (v?.scrubbed === false) console.error(`secure-delete: WAL checkpoint still busy after ${v.attempts} tries (busy=${v.checkpoint?.busy}); deleted bytes may remain in the WAL until the next pass`);
|
|
49
62
|
}, 3600_000);
|
|
50
63
|
|
|
51
64
|
const shutdown = async () => {
|
package/bin/pillar-seed.mjs
CHANGED
|
@@ -43,7 +43,7 @@ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22
|
|
|
43
43
|
import { mkdirSync } from "node:fs";
|
|
44
44
|
import { homedir } from "node:os";
|
|
45
45
|
import { join } from "node:path";
|
|
46
|
-
import { CarrierServer } from "../src/net/carrier-server.mjs";
|
|
46
|
+
import { CarrierServer, parseSecureDelete } from "../src/net/carrier-server.mjs";
|
|
47
47
|
import { Keychain } from "../src/identity/keychain.mjs";
|
|
48
48
|
|
|
49
49
|
function parseArgs(argv) {
|
|
@@ -68,6 +68,14 @@ const publicUrl = args["public-url"] ?? process.env.SEED_PUBLIC_URL ?? null;
|
|
|
68
68
|
const registryUrl = typeof args.announce === "string" ? args.announce.replace(/\/+$/, "") : null;
|
|
69
69
|
const operator = args.operator ?? process.env.SEED_OPERATOR ?? null;
|
|
70
70
|
const region = args.region ?? process.env.SEED_REGION ?? null;
|
|
71
|
+
// Secure delete of expired envelopes: on unless this seed turns it off.
|
|
72
|
+
let secureDelete;
|
|
73
|
+
try {
|
|
74
|
+
secureDelete = parseSecureDelete(args["secure-delete"] ?? process.env.SEED_SECURE_DELETE);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error(`pillar-seed: ${e.message}`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
71
79
|
|
|
72
80
|
/**
|
|
73
81
|
* The seed's identity. Distinct from any agent keychain on the box: it names
|
|
@@ -114,12 +122,13 @@ if (args.status) {
|
|
|
114
122
|
process.exit(0);
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl });
|
|
125
|
+
const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl, secureDelete });
|
|
118
126
|
const addr = await server.listen(port, host);
|
|
119
127
|
console.log(`pillar-seed listening on http://${addr.address}:${addr.port}`);
|
|
120
128
|
console.log(` uuaid: ${keychain._identity.uuaid}`);
|
|
121
129
|
console.log(` db: ${dbPath}`);
|
|
122
130
|
console.log(` ttl: ${ttlDays} days`);
|
|
131
|
+
console.log(` secure-delete: ${secureDelete ? "on (expired envelopes are zeroed; WAL truncated after each expiry pass)" : "off (expired envelope bytes stay on disk until reused)"}`);
|
|
123
132
|
if (publicUrl) console.log(` public url: ${publicUrl}`);
|
|
124
133
|
|
|
125
134
|
// Boot self-check on the address we actually serve — verify before claim.
|
|
@@ -171,6 +180,8 @@ if (registryUrl) {
|
|
|
171
180
|
const vacuumTimer = setInterval(() => {
|
|
172
181
|
const n = server.vacuum();
|
|
173
182
|
if (n > 0) console.log(`vacuumed ${n} expired envelopes`);
|
|
183
|
+
const v = server.lastVacuum;
|
|
184
|
+
if (v?.scrubbed === false) console.error(`secure-delete: WAL checkpoint still busy after ${v.attempts} tries (busy=${v.checkpoint?.busy}); deleted bytes may remain in the WAL until the next pass`);
|
|
174
185
|
}, 3600_000);
|
|
175
186
|
|
|
176
187
|
const shutdown = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uuaid/pillar",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Pillar (Protocol for Inter-agent Linked Ledger And Relay) — the secure delivery layer of UACP, the Universal Agent Communication Protocol. UACP = UUAID (identity) + Envelope (signed, encrypted container) + Pillar (relay, mailbox, routing, store-and-forward). Reference implementation of the UACP delivery layer; includes VDA-1 (IAASO-3901, in ballot) delivery attestation and a blind escrow for the Zilligon ZGN / EVM rails.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/identity/jcs.mjs
CHANGED
|
@@ -24,11 +24,10 @@ function _ser(v) {
|
|
|
24
24
|
if (typeof v === "bigint") return v.toString();
|
|
25
25
|
if (Array.isArray(v)) return "[" + v.map(_ser).join(",") + "]";
|
|
26
26
|
if (typeof v === "object") {
|
|
27
|
-
// UTF-16 code
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
27
|
+
// RFC 8785 §3.2.3: keys sort by their UTF-16 code units, which is exactly
|
|
28
|
+
// JavaScript's default string order. (Comparing UTF-16LE *bytes* is not: it
|
|
29
|
+
// puts "Ā" (U+0100) before "a", and disagreed with @uuaid/pillar-client.)
|
|
30
|
+
const keys = Object.keys(v).sort(); // @rule jcs.code-unit-order
|
|
32
31
|
return "{" + keys.map(k => JSON.stringify(k) + ":" + _ser(v[k])).join(",") + "}";
|
|
33
32
|
}
|
|
34
33
|
throw new Error("JCS: unsupported type " + typeof v);
|
|
@@ -12,9 +12,43 @@
|
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "node:fs";
|
|
13
13
|
import { dirname } from "node:path";
|
|
14
14
|
import { randomBytes, createHash, generateKeyPairSync, sign, verify, createPublicKey, createPrivateKey, hkdfSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
15
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
15
16
|
|
|
16
17
|
const KEYCHAIN_VERSION = 1;
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* RFC 8032 / FIPS 186-5 rule for an Ed25519 public key: the canonical encoding
|
|
21
|
+
* of a point that is not of small order and lies in the prime-order subgroup.
|
|
22
|
+
*
|
|
23
|
+
* OpenSSL's Ed25519 verify (node `crypto.verify`) does not check this: it uses
|
|
24
|
+
* the non-cofactored equation, and 5 of the 8 small-order points verify as
|
|
25
|
+
* keys. With the identity point as the key, R = identity and S = 0 verifies on
|
|
26
|
+
* every message, so anyone could forge a signature "from" that key's uuaid
|
|
27
|
+
* (found 2026-10-06). The same rule as `isStrictEd25519PublicKey` in
|
|
28
|
+
* @uuaid/pillar-client.
|
|
29
|
+
*
|
|
30
|
+
* The answer depends on the key alone, so it is cached (bounded).
|
|
31
|
+
*/
|
|
32
|
+
const STRICT_KEY_CACHE_MAX = 4096;
|
|
33
|
+
const strictKeyCache = new Map();
|
|
34
|
+
|
|
35
|
+
export function isStrictEd25519PublicKey(publicKeyHex) {
|
|
36
|
+
if (typeof publicKeyHex !== "string" || !/^[0-9a-f]{64}$/i.test(publicKeyHex)) return false;
|
|
37
|
+
const hex = publicKeyHex.toLowerCase();
|
|
38
|
+
const cached = strictKeyCache.get(hex);
|
|
39
|
+
if (cached !== undefined) return cached;
|
|
40
|
+
let ok = false;
|
|
41
|
+
try {
|
|
42
|
+
const point = ed25519.Point.fromHex(hex, false);
|
|
43
|
+
ok = point.toHex() === hex && !point.isSmallOrder() && point.isTorsionFree();
|
|
44
|
+
} catch (_e) {
|
|
45
|
+
ok = false;
|
|
46
|
+
}
|
|
47
|
+
if (strictKeyCache.size >= STRICT_KEY_CACHE_MAX) strictKeyCache.delete(strictKeyCache.keys().next().value);
|
|
48
|
+
strictKeyCache.set(hex, ok);
|
|
49
|
+
return ok;
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
/**
|
|
19
53
|
* Derive a stable, publishable local id from a raw Ed25519 public key. This
|
|
20
54
|
* makes the UUAID self-authenticating: given the UUAID, anyone can
|
|
@@ -149,6 +183,7 @@ export class Keychain {
|
|
|
149
183
|
}
|
|
150
184
|
|
|
151
185
|
static verifyDetached(publicKeyHex, data, signatureBytes) {
|
|
186
|
+
if (!isStrictEd25519PublicKey(publicKeyHex)) return false; // @rule keychain.strict-key
|
|
152
187
|
const spkiPrefix = Buffer.from("302a300506032b6570032100", "hex");
|
|
153
188
|
const der = Buffer.concat([spkiPrefix, Buffer.from(publicKeyHex, "hex")]);
|
|
154
189
|
const pub = createPublicKey({ key: der, format: "der", type: "spki" });
|
|
@@ -81,11 +81,39 @@ const RATE_MAP_HARD_CAP = 10_000; // F2: bound memory under key spray
|
|
|
81
81
|
const COMMUNITY_DB_BYTES = Math.floor(MAX_DB_BYTES * 0.85); // last 15% of the store is reserved
|
|
82
82
|
const COMMUNITY_CONCURRENT_WAITS = 48; // of MAX_CONCURRENT_WAITS
|
|
83
83
|
const MAX_PRIORITY_GOOD_PER_MIN = 600; // priority senders bill to their own per-IP bucket
|
|
84
|
+
const CHECKPOINT_ATTEMPTS = 3; // TRUNCATE checkpoint tries per vacuum pass
|
|
85
|
+
const CHECKPOINT_BUSY_MS = 250; // per try; a busy pass is retried next hour
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Secure delete for the envelope store: one switch per broker (--secure-delete
|
|
89
|
+
* on|off, CARRIER_SECURE_DELETE / SEED_SECURE_DELETE). ON by default.
|
|
90
|
+
*
|
|
91
|
+
* A plain DELETE leaves an expired envelope's bytes readable in the database
|
|
92
|
+
* file, and, while the database is open, in the WAL (PC-4 delete-residue probe,
|
|
93
|
+
* 2026-10-08). ON sets PRAGMA secure_delete, which zeroes deleted content, and
|
|
94
|
+
* vacuum() follows every expiry delete with wal_checkpoint(TRUNCATE), which
|
|
95
|
+
* empties the WAL of the frames that still hold it. OFF keeps SQLite's default:
|
|
96
|
+
* deleted bytes stay until the page is reused. secure_delete is per connection,
|
|
97
|
+
* so this is a per-broker setting, never a per-tenant one.
|
|
98
|
+
*
|
|
99
|
+
* Returns true or false; throws on anything that is not a recognised spelling,
|
|
100
|
+
* so a typo never silently turns the scrub off.
|
|
101
|
+
*/
|
|
102
|
+
export function parseSecureDelete(value) {
|
|
103
|
+
if (value === undefined || value === null || value === true) return true;
|
|
104
|
+
const v = String(value).trim().toLowerCase();
|
|
105
|
+
if (["on", "1", "true", "yes"].includes(v)) return true;
|
|
106
|
+
if (["off", "0", "false", "no"].includes(v)) return false;
|
|
107
|
+
throw new Error(`secure-delete must be on or off, not "${value}"`);
|
|
108
|
+
}
|
|
84
109
|
|
|
85
110
|
export class CarrierServer {
|
|
86
|
-
constructor({ dbPath = "carrier.db", ttlDays = 14, cryptoInventory, trustedTierIssuers = [], keychain = null, publicUrl = null } = {}) {
|
|
111
|
+
constructor({ dbPath = "carrier.db", ttlDays = 14, cryptoInventory, trustedTierIssuers = [], keychain = null, publicUrl = null, secureDelete = true } = {}) {
|
|
87
112
|
this.db = new Database(dbPath);
|
|
88
113
|
this.db.pragma("journal_mode = WAL");
|
|
114
|
+
this.secureDelete = secureDelete !== false;
|
|
115
|
+
this.db.pragma(`secure_delete = ${this.secureDelete ? "ON" : "OFF"}`);
|
|
116
|
+
this.lastVacuum = null;
|
|
89
117
|
this.db.exec(`
|
|
90
118
|
create table if not exists envelopes (
|
|
91
119
|
seq integer primary key autoincrement,
|
|
@@ -235,10 +263,35 @@ export class CarrierServer {
|
|
|
235
263
|
return true;
|
|
236
264
|
}
|
|
237
265
|
|
|
238
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* Remove envelopes older than TTL. Call periodically. Returns the number
|
|
268
|
+
* deleted. With secure delete on, a pass that deleted anything then runs
|
|
269
|
+
* wal_checkpoint(TRUNCATE) until it reports busy=0 (at most
|
|
270
|
+
* CHECKPOINT_ATTEMPTS tries); `lastVacuum` records the outcome, and
|
|
271
|
+
* `lastVacuum.scrubbed` is false when the checkpoint never completed.
|
|
272
|
+
*/
|
|
239
273
|
vacuum() {
|
|
240
274
|
const cutoff = new Date(Date.now() - this.ttlDays * 86400_000).toISOString();
|
|
241
275
|
const r = this.db.prepare("delete from envelopes where created_at < ?").run(cutoff);
|
|
276
|
+
const report = { deleted: r.changes, secureDelete: this.secureDelete, checkpoint: null, attempts: 0, scrubbed: null };
|
|
277
|
+
if (this.secureDelete && r.changes > 0) {
|
|
278
|
+
// vacuum() runs on the event loop: wait at most CHECKPOINT_BUSY_MS per
|
|
279
|
+
// try for a reader to finish, never the connection's 5 s default.
|
|
280
|
+
const busyTimeout = this.db.pragma("busy_timeout", { simple: true });
|
|
281
|
+
this.db.pragma(`busy_timeout = ${CHECKPOINT_BUSY_MS}`);
|
|
282
|
+
try {
|
|
283
|
+
while (report.attempts < CHECKPOINT_ATTEMPTS) {
|
|
284
|
+
report.attempts++;
|
|
285
|
+
const [cp] = this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
286
|
+
report.checkpoint = cp;
|
|
287
|
+
if (cp && cp.busy === 0) break;
|
|
288
|
+
}
|
|
289
|
+
} finally {
|
|
290
|
+
this.db.pragma(`busy_timeout = ${busyTimeout}`);
|
|
291
|
+
}
|
|
292
|
+
report.scrubbed = report.checkpoint?.busy === 0;
|
|
293
|
+
}
|
|
294
|
+
this.lastVacuum = report;
|
|
242
295
|
return r.changes;
|
|
243
296
|
}
|
|
244
297
|
|
package/src/net/envelope.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { createHash, randomBytes } from "node:crypto";
|
|
30
30
|
import { jcs } from "../identity/jcs.mjs";
|
|
31
|
-
import { Keychain } from "../identity/keychain.mjs";
|
|
31
|
+
import { Keychain, isStrictEd25519PublicKey } from "../identity/keychain.mjs";
|
|
32
32
|
import { sealPayload, openPayload, E2E_ALG } from "../crypto/e2e.mjs";
|
|
33
33
|
import { verifyDelegationDoc, delegationCoversKind, CAPABILITY_ONLY_KINDS } from "./delegation.mjs";
|
|
34
34
|
|
|
@@ -112,6 +112,7 @@ export function open(envelope, { cryptoInventory } = {}) {
|
|
|
112
112
|
}
|
|
113
113
|
if (sig.keyId !== envelope.sender) return { ok: false, reason: "keyId-sender-mismatch" };
|
|
114
114
|
if (typeof sig.publicKey !== "string" || !/^[0-9a-f]{64}$/i.test(sig.publicKey)) return { ok: false, reason: "bad-publicKey" };
|
|
115
|
+
if (!isStrictEd25519PublicKey(sig.publicKey)) return { ok: false, reason: "weak-publicKey" }; // @rule envelope.open-strict-key
|
|
115
116
|
if (typeof sig.signature !== "string" || !/^[0-9a-f]{128}$/i.test(sig.signature)) return { ok: false, reason: "bad-signature-encoding" };
|
|
116
117
|
// Structural checks on the encryption block — an envelope without one is invalid.
|
|
117
118
|
const enc = envelope.enc;
|
|
@@ -172,6 +173,7 @@ function openDelegated(envelope, { cryptoInventory } = {}) {
|
|
|
172
173
|
const reject = (reason) => ({ ok: false, reason: `delegation-invalid:${reason}`, code: 403 });
|
|
173
174
|
const sig = envelope.transportSignature;
|
|
174
175
|
if (typeof sig.publicKey !== "string" || !/^[0-9a-f]{64}$/i.test(sig.publicKey)) return { ok: false, reason: "bad-publicKey" };
|
|
176
|
+
if (!isStrictEd25519PublicKey(sig.publicKey)) return { ok: false, reason: "weak-publicKey" }; // @rule envelope.open-delegated-strict-key
|
|
175
177
|
if (typeof sig.signature !== "string" || !/^[0-9a-f]{128}$/i.test(sig.signature)) return { ok: false, reason: "bad-signature-encoding" };
|
|
176
178
|
// Structural checks on the encryption block — same rules as the legacy path.
|
|
177
179
|
const enc = envelope.enc;
|
package/src/net/refusals.mjs
CHANGED
|
@@ -32,6 +32,7 @@ export const REFUSAL_FIXES = Object.freeze({
|
|
|
32
32
|
"keyId-sender-mismatch": "sign with transportSignature.keyId equal to the sender UUAID, or send under a delegation",
|
|
33
33
|
"publicKey-sender-mismatch": "send from the UUAID your signing key derives, or attach the principal's delegation",
|
|
34
34
|
"bad-signature": "sign again with this identity's own key over the exact bytes; the signature does not verify",
|
|
35
|
+
"weak-publicKey": "use a real Ed25519 identity key; this key is a small-order or non-canonical point that can sign anything, so no signature under it counts",
|
|
35
36
|
"delegation-invalid": "mint a fresh delegation from the principal, then resend",
|
|
36
37
|
// Carrier, envelope shape and crypto (envelope.mjs open()): one fix for all
|
|
37
38
|
"not-object": RESEAL,
|