@cello-protocol/crypto 0.0.58 → 0.0.60
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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest-test-fixture.d.ts.map +1 -1
- package/dist/manifest-test-fixture.js +12 -1
- package/dist/manifest-test-fixture.js.map +1 -1
- package/dist/session-content-seal.d.ts +86 -0
- package/dist/session-content-seal.d.ts.map +1 -0
- package/dist/session-content-seal.js +124 -0
- package/dist/session-content-seal.js.map +1 -0
- package/dist/session-ephemeral-auth.d.ts +96 -0
- package/dist/session-ephemeral-auth.d.ts.map +1 -0
- package/dist/session-ephemeral-auth.js +137 -0
- package/dist/session-ephemeral-auth.js.map +1 -0
- package/dist/session-key-agreement.d.ts +207 -0
- package/dist/session-key-agreement.d.ts.map +1 -0
- package/dist/session-key-agreement.js +320 -0
- package/dist/session-key-agreement.js.map +1 -0
- package/dist/session-salt.d.ts +129 -0
- package/dist/session-salt.d.ts.map +1 -0
- package/dist/session-salt.js +205 -0
- package/dist/session-salt.js.map +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export { ed25519_FROST } from "@noble/curves/ed25519.js";
|
|
|
15
15
|
export { buildCheckpointTbs, computeCheckpointHash } from "./checkpoint.js";
|
|
16
16
|
export { buildRelayRegistrationTbs, verifyRelayRegistrationSignature } from "./relay-registration.js";
|
|
17
17
|
export { sealToRecipient, openSealed, CONTENT_SEAL_OVERHEAD_BYTES } from "./content-seal.js";
|
|
18
|
+
export { generateSaltContribution, deriveSessionSalt, saltFingerprint, saltedContentHash, SALT_CONTRIBUTION_BYTES, SALT_FINGERPRINT_BYTES, SESSION_SALT_BYTES, } from "./session-salt.js";
|
|
19
|
+
export { generateSessionEphemeral, destroySessionEphemeral, deriveSessionSecrets, SESSION_KEY_BYTES, type SessionEphemeral, type SessionSecrets, } from "./session-key-agreement.js";
|
|
20
|
+
export { ephemeralSigningMessage, signSessionEphemeral, verifySessionEphemeral, EPHEMERAL_AUTH_REFUSALS, EPHEMERAL_SIG_BYTES, EPHEMERAL_PUBLIC_BYTES, type EphemeralAuthRefusal, type EphemeralAuthResult, } from "./session-ephemeral-auth.js";
|
|
21
|
+
export { sealSessionContent, openSessionContent, SESSION_CONTENT_SEAL_V1, SESSION_CONTENT_SEAL_OVERHEAD_BYTES, } from "./session-content-seal.js";
|
|
18
22
|
export type { ManifestVerifyResult, ManifestVerifyDiagnostics, ManifestVerifySkipReason, ManifestVerifySkippedEntry, ConsortiumManifestInput } from "./manifest.js";
|
|
19
23
|
export { canonicalManifestBody, verifyManifest } from "./manifest.js";
|
|
20
24
|
export type { PreAuthCapability, PreAuthCapabilityBody, CapabilityVerifyResult, CapabilityVerifyReason } from "./preauth-capability.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACpI,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClG,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACxI,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3F,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,0BAA0B,EAC1B,YAAY,EACZ,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,6BAA6B,EAC7B,YAAY,EACZ,uBAAuB,EACvB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAIzE,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,YAAY,GACb,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,mCAAmC,CAAC;AAK3C,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGzD,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG5E,OAAO,EAAE,yBAAyB,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAGtG,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,21 @@ export { buildCheckpointTbs, computeCheckpointHash } from "./checkpoint.js";
|
|
|
20
20
|
export { buildRelayRegistrationTbs, verifyRelayRegistrationSignature } from "./relay-registration.js";
|
|
21
21
|
// M7-MSG-001: recipient content sealed box (E2E encryption for parked content)
|
|
22
22
|
export { sealToRecipient, openSealed, CONTENT_SEAL_OVERHEAD_BYTES } from "./content-seal.js";
|
|
23
|
+
// Decisions Carried #8/#9/#10: the SESSION SALT — agreed in the same exchange as the envelope key
|
|
24
|
+
// but NOT derived from it, because the key must be destroyed at close and the salt must survive the
|
|
25
|
+
// session. Both sides contribute; HMAC for the salted content hash; a fingerprint for the open check.
|
|
26
|
+
export { generateSaltContribution, deriveSessionSalt, saltFingerprint, saltedContentHash, SALT_CONTRIBUTION_BYTES, SALT_FINGERPRINT_BYTES, SESSION_SALT_BYTES, } from "./session-salt.js";
|
|
27
|
+
// DOD-M15-KEYAGREE-1: CELLO's own per-session key agreement — ephemeral-ephemeral X25519 with the
|
|
28
|
+
// hybrid-PQ hook built in. ONE output, the envelope key: the salt is agreed separately above
|
|
29
|
+
// (Decisions Carried #8), because the key is destroyed at close and the salt outlives the session.
|
|
30
|
+
export { generateSessionEphemeral, destroySessionEphemeral, deriveSessionSecrets, SESSION_KEY_BYTES, } from "./session-key-agreement.js";
|
|
31
|
+
// DOD-M15-EPHEMERAL-AUTH-1: the throwaway key is SIGNED with the agent's identity and the peer's is
|
|
32
|
+
// verified BEFORE anything is derived from it — without this a relay substitutes both sides' keys
|
|
33
|
+
// and reads everything while both ends see a working conversation. Plus the AEAD that encrypts a
|
|
34
|
+
// message body under the agreed key: distinct from `content-seal.ts`, which seals PARKED content to
|
|
35
|
+
// a long-term identity key and deliberately has no forward secrecy.
|
|
36
|
+
export { ephemeralSigningMessage, signSessionEphemeral, verifySessionEphemeral, EPHEMERAL_AUTH_REFUSALS, EPHEMERAL_SIG_BYTES, EPHEMERAL_PUBLIC_BYTES, } from "./session-ephemeral-auth.js";
|
|
37
|
+
export { sealSessionContent, openSessionContent, SESSION_CONTENT_SEAL_V1, SESSION_CONTENT_SEAL_OVERHEAD_BYTES, } from "./session-content-seal.js";
|
|
23
38
|
export { canonicalManifestBody, verifyManifest } from "./manifest.js";
|
|
24
39
|
export { canonicalCapabilityBody, signCapability, verifyCapability, encodeCapability, decodeCapability } from "./preauth-capability.js";
|
|
25
40
|
export { CONSORTIUM_ROOT_KEYS, CONSORTIUM_THRESHOLD, TEST_CONSORTIUM_ROOT_KEYS, TEST_CONSORTIUM_THRESHOLD, } from "./consortium-keys.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEpI,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAExI,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAU3F,OAAO,EACL,6BAA6B,EAC7B,YAAY,EACZ,uBAAuB,EACvB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAE1B,iGAAiG;AACjG,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,uGAAuG;AACvG,0FAA0F;AAC1F,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,YAAY,GACb,MAAM,4BAA4B,CAAC;AAEpC,8FAA8F;AAC9F,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,mCAAmC,CAAC;AAE3C,qGAAqG;AACrG,mGAAmG;AACnG,qEAAqE;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,8EAA8E;AAC9E,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,oEAAoE;AACpE,OAAO,EAAE,yBAAyB,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAEtG,+EAA+E;AAC/E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEpI,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAExI,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAU3F,OAAO,EACL,6BAA6B,EAC7B,YAAY,EACZ,uBAAuB,EACvB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAE1B,iGAAiG;AACjG,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,uGAAuG;AACvG,0FAA0F;AAC1F,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,YAAY,GACb,MAAM,4BAA4B,CAAC;AAEpC,8FAA8F;AAC9F,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,iCAAiC,EACjC,wBAAwB,GACzB,MAAM,mCAAmC,CAAC;AAE3C,qGAAqG;AACrG,mGAAmG;AACnG,qEAAqE;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,8EAA8E;AAC9E,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,oEAAoE;AACpE,OAAO,EAAE,yBAAyB,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAEtG,+EAA+E;AAC/E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAC7F,kGAAkG;AAClG,oGAAoG;AACpG,sGAAsG;AACtG,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAE3B,kGAAkG;AAClG,6FAA6F;AAC7F,mGAAmG;AACnG,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,oBAAoB,EACpB,iBAAiB,GAGlB,MAAM,4BAA4B,CAAC;AAEpC,oGAAoG;AACpG,kGAAkG;AAClG,iGAAiG;AACjG,oGAAoG;AACpG,oEAAoE;AACpE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GAGvB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mCAAmC,GACpC,MAAM,2BAA2B,CAAC;AAInC,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGtE,OAAO,EAAE,uBAAuB,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACxI,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAE3F,wEAAwE;AACxE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-test-fixture.d.ts","sourceRoot":"","sources":["../src/manifest-test-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAM3F,CAAC;AAEX,sGAAsG;AACtG,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,IAAI,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,kBAAkB,EAAE,EAC3B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,uBAAuB,
|
|
1
|
+
{"version":3,"file":"manifest-test-fixture.d.ts","sourceRoot":"","sources":["../src/manifest-test-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,CAM3F,CAAC;AAEX,sGAAsG;AACtG,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,IAAI,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,kBAAkB,EAAE,EAC3B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,uBAAuB,CA+BzB;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B;;;CAG9B,CAAC"}
|
|
@@ -34,7 +34,18 @@ export function makeTestManifest(nodes, opts) {
|
|
|
34
34
|
const manifest = {
|
|
35
35
|
version: opts?.version ?? 1,
|
|
36
36
|
not_before: opts?.notBefore ?? "2026-01-01T00:00:00Z",
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* ⚠️ A FIXTURE THAT EXPIRES IS A TEST SUITE WITH A FUSE. This defaulted to `2027-01-01`, four
|
|
39
|
+
* months out when it was found (2026-08-24) — after which every default-fixture manifest becomes
|
|
40
|
+
* EXPIRED and every test that needs an in-window one goes red **on a date, with no code change**.
|
|
41
|
+
* It would also have silently switched on the lapsed-manifest branches added by
|
|
42
|
+
* `DOD-M15-EXPIRY-CONSUMER-POLICY-1` for every existing verifier test at the same moment.
|
|
43
|
+
*
|
|
44
|
+
* Far-future and FIXED, not `Date.now() + 1y`: a rolling default makes the fixture's window
|
|
45
|
+
* depend on the clock, and the tests that care about window boundaries pass their own dates
|
|
46
|
+
* anyway — which is the reason this default only ever needs to mean "in window".
|
|
47
|
+
*/
|
|
48
|
+
expires: opts?.expires ?? "2099-01-01T00:00:00Z",
|
|
38
49
|
nodes: nodes,
|
|
39
50
|
signatures: [],
|
|
40
51
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-test-fixture.js","sourceRoot":"","sources":["../src/manifest-test-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA0E;IACvG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;CACrB,CAAC;AAqBX;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAA2B,EAC3B,IAA2B;IAE3B,MAAM,QAAQ,GAA4B;QACxC,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,CAAC;QAC3B,UAAU,EAAE,IAAI,EAAE,SAAS,IAAI,sBAAsB;QACrD,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,sBAAsB;QAChD,KAAK,EAAE,KAA2C;QAClD,UAAU,EAAE,EAAE;KACf,CAAC;IAEF,qCAAqC;IACrC,MAAM,IAAI,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAE7C,sDAAsD;IACtD,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACzC,YAAY,EAAE,GAAG;QACjB,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;KACpF,CAAC,CAAC,CAAC;IAEJ,QAAQ,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,aAAa,EAAE,kEAAkE;IACjF,YAAY,EAAE,kEAAkE;CACxE,CAAC"}
|
|
1
|
+
{"version":3,"file":"manifest-test-fixture.js","sourceRoot":"","sources":["../src/manifest-test-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA0E;IACvG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;CACrB,CAAC;AAqBX;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAA2B,EAC3B,IAA2B;IAE3B,MAAM,QAAQ,GAA4B;QACxC,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,CAAC;QAC3B,UAAU,EAAE,IAAI,EAAE,SAAS,IAAI,sBAAsB;QACrD;;;;;;;;;;WAUG;QACH,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,sBAAsB;QAChD,KAAK,EAAE,KAA2C;QAClD,UAAU,EAAE,EAAE;KACf,CAAC;IAEF,qCAAqC;IACrC,MAAM,IAAI,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAE7C,sDAAsD;IACtD,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACzC,YAAY,EAAE,GAAG;QACjB,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;KACpF,CAAC,CAAC,CAAC;IAEJ,QAAQ,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,aAAa,EAAE,kEAAkE;IACjF,YAAY,EAAE,kEAAkE;CACxE,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M15-EPHEMERAL-AUTH-1 — encrypting a message body under the AGREED SESSION KEY.
|
|
3
|
+
*
|
|
4
|
+
* ─── What this is for, and how it differs from `content-seal.ts` ───────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Two encryption schemes exist in this codebase and they are NOT interchangeable. Which one applies
|
|
7
|
+
* is decided by the ROUTE a message takes, and getting that wrong is silent:
|
|
8
|
+
*
|
|
9
|
+
* **This file — LIVE messages.** Encrypted under the per-session key both sides agreed from
|
|
10
|
+
* throwaway keypairs (`session-key-agreement.ts`). Those keypairs are destroyed when the session
|
|
11
|
+
* ends, so a recording of the traffic cannot be decrypted later even if both agents' long-term
|
|
12
|
+
* identity keys are eventually compromised. That is forward secrecy, and it is the whole point.
|
|
13
|
+
*
|
|
14
|
+
* **`content-seal.ts` — PARKED messages.** Encrypted to the recipient's LONG-TERM identity key,
|
|
15
|
+
* because the recipient is offline and cannot take part in a handshake. It has no forward secrecy
|
|
16
|
+
* by construction: whoever holds that identity key can open every message ever sealed to it. That
|
|
17
|
+
* is an accepted trade for the mailbox and must not be copied here.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ A message that fails live delivery falls back to the mailbox, so the SAME message can take
|
|
20
|
+
* either scheme. The caller must encrypt the copy that goes on the wire and leave the plaintext it
|
|
21
|
+
* holds for the park backstop alone — sealing an already-encrypted body to the identity key gives
|
|
22
|
+
* the recipient bytes locked under a session key that is about to be destroyed.
|
|
23
|
+
*
|
|
24
|
+
* ─── Construction (SPARC Phase P) ──────────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* sealSessionContent(key, plaintext):
|
|
27
|
+
* 1. iv = random 12 bytes # NIST SP 800-38D §8.2.2
|
|
28
|
+
* 2. ct||tag = AES-256-GCM(key, iv, plaintext) # NIST SP 800-38D
|
|
29
|
+
* 3. blob = VERSION(1) || iv(12) || ct || tag(16)
|
|
30
|
+
*
|
|
31
|
+
* openSessionContent(key, blob) -> plaintext | null
|
|
32
|
+
*
|
|
33
|
+
* AES-256-GCM matches `content-seal.ts` rather than introducing a second AEAD, so there is one
|
|
34
|
+
* primitive to reason about. RFCs: HKDF — RFC 5869 (upstream, in the key agreement); AES-GCM — NIST
|
|
35
|
+
* SP 800-38D.
|
|
36
|
+
*
|
|
37
|
+
* ─── Why a RANDOM IV rather than a counter ─────────────────────────────────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* A counter is the stronger construction and it needs state that survives a restart. This session
|
|
40
|
+
* key does not: the secret is deliberately never persisted, and a revived session RE-KEYS
|
|
41
|
+
* (Decisions Carried #5), so a counter would restart at zero under a NEW key — which is safe, and
|
|
42
|
+
* would also have to be proven safe at every future change to the revival rule. A 96-bit random IV
|
|
43
|
+
* under a key that lives for one session, with a birthday bound around 2^32 messages, is not the
|
|
44
|
+
* limiting factor here and needs no state at all.
|
|
45
|
+
*
|
|
46
|
+
* ─── The VERSION byte is FRAMING, not compatibility ────────────────────────────────────────────
|
|
47
|
+
*
|
|
48
|
+
* There are no other builds to be compatible with — CELLO is pre-launch with no external installs —
|
|
49
|
+
* so this byte is not here to interoperate with anything. It is here because the alternative is a
|
|
50
|
+
* blob whose layout is implied by its length, and a reader that meets bytes it cannot frame would
|
|
51
|
+
* then hand attacker-chosen input to a decryptor under a guessed layout. One byte, one comparison,
|
|
52
|
+
* and an unknown value REFUSES rather than parsing.
|
|
53
|
+
*
|
|
54
|
+
* That is the only justification, and it is worth stating because the tempting one — "so a future
|
|
55
|
+
* version can change the format" — would be exactly the speculative scaffolding this codebase has
|
|
56
|
+
* ruled against building.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* The only version this build writes, and the only one it reads.
|
|
60
|
+
*
|
|
61
|
+
* `0x01` rather than `0x00`: a zero first byte is what an all-zero buffer and a truncated read both
|
|
62
|
+
* look like, so a version of zero cannot be told from an absent one.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SESSION_CONTENT_SEAL_V1 = 1;
|
|
65
|
+
/** Bytes added to every message: version + IV + tag. */
|
|
66
|
+
export declare const SESSION_CONTENT_SEAL_OVERHEAD_BYTES: number;
|
|
67
|
+
/**
|
|
68
|
+
* Encrypt a message body under the agreed session key.
|
|
69
|
+
*
|
|
70
|
+
* Throws on a wrong-width key rather than deriving from whatever it was given: a short key silently
|
|
71
|
+
* accepted is encryption that appears to work under something that is not the agreed secret.
|
|
72
|
+
*/
|
|
73
|
+
export declare function sealSessionContent(key: Uint8Array, plaintext: Uint8Array): Uint8Array;
|
|
74
|
+
/**
|
|
75
|
+
* Decrypt a message body under the agreed session key.
|
|
76
|
+
*
|
|
77
|
+
* **Returns `null` on every failure and never throws** — wrong key, tampered ciphertext, truncated
|
|
78
|
+
* blob, unknown version. The caller cannot tell those apart and must not try: GCM's tag check is the
|
|
79
|
+
* only thing that distinguishes "not for us" from "modified in flight", and a caller that branched
|
|
80
|
+
* on the difference would be branching on attacker-controlled input.
|
|
81
|
+
*
|
|
82
|
+
* The caller's job on `null` is to refuse the message loudly, not to retry or to fall back to
|
|
83
|
+
* reading it as plaintext.
|
|
84
|
+
*/
|
|
85
|
+
export declare function openSessionContent(key: Uint8Array, blob: Uint8Array): Uint8Array | null;
|
|
86
|
+
//# sourceMappingURL=session-content-seal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-content-seal.d.ts","sourceRoot":"","sources":["../src/session-content-seal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAWH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAO,CAAC;AAE5C,wDAAwD;AACxD,eAAO,MAAM,mCAAmC,QAA2B,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,UAAU,CAerF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAiBvF"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M15-EPHEMERAL-AUTH-1 — encrypting a message body under the AGREED SESSION KEY.
|
|
3
|
+
*
|
|
4
|
+
* ─── What this is for, and how it differs from `content-seal.ts` ───────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Two encryption schemes exist in this codebase and they are NOT interchangeable. Which one applies
|
|
7
|
+
* is decided by the ROUTE a message takes, and getting that wrong is silent:
|
|
8
|
+
*
|
|
9
|
+
* **This file — LIVE messages.** Encrypted under the per-session key both sides agreed from
|
|
10
|
+
* throwaway keypairs (`session-key-agreement.ts`). Those keypairs are destroyed when the session
|
|
11
|
+
* ends, so a recording of the traffic cannot be decrypted later even if both agents' long-term
|
|
12
|
+
* identity keys are eventually compromised. That is forward secrecy, and it is the whole point.
|
|
13
|
+
*
|
|
14
|
+
* **`content-seal.ts` — PARKED messages.** Encrypted to the recipient's LONG-TERM identity key,
|
|
15
|
+
* because the recipient is offline and cannot take part in a handshake. It has no forward secrecy
|
|
16
|
+
* by construction: whoever holds that identity key can open every message ever sealed to it. That
|
|
17
|
+
* is an accepted trade for the mailbox and must not be copied here.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ A message that fails live delivery falls back to the mailbox, so the SAME message can take
|
|
20
|
+
* either scheme. The caller must encrypt the copy that goes on the wire and leave the plaintext it
|
|
21
|
+
* holds for the park backstop alone — sealing an already-encrypted body to the identity key gives
|
|
22
|
+
* the recipient bytes locked under a session key that is about to be destroyed.
|
|
23
|
+
*
|
|
24
|
+
* ─── Construction (SPARC Phase P) ──────────────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* sealSessionContent(key, plaintext):
|
|
27
|
+
* 1. iv = random 12 bytes # NIST SP 800-38D §8.2.2
|
|
28
|
+
* 2. ct||tag = AES-256-GCM(key, iv, plaintext) # NIST SP 800-38D
|
|
29
|
+
* 3. blob = VERSION(1) || iv(12) || ct || tag(16)
|
|
30
|
+
*
|
|
31
|
+
* openSessionContent(key, blob) -> plaintext | null
|
|
32
|
+
*
|
|
33
|
+
* AES-256-GCM matches `content-seal.ts` rather than introducing a second AEAD, so there is one
|
|
34
|
+
* primitive to reason about. RFCs: HKDF — RFC 5869 (upstream, in the key agreement); AES-GCM — NIST
|
|
35
|
+
* SP 800-38D.
|
|
36
|
+
*
|
|
37
|
+
* ─── Why a RANDOM IV rather than a counter ─────────────────────────────────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* A counter is the stronger construction and it needs state that survives a restart. This session
|
|
40
|
+
* key does not: the secret is deliberately never persisted, and a revived session RE-KEYS
|
|
41
|
+
* (Decisions Carried #5), so a counter would restart at zero under a NEW key — which is safe, and
|
|
42
|
+
* would also have to be proven safe at every future change to the revival rule. A 96-bit random IV
|
|
43
|
+
* under a key that lives for one session, with a birthday bound around 2^32 messages, is not the
|
|
44
|
+
* limiting factor here and needs no state at all.
|
|
45
|
+
*
|
|
46
|
+
* ─── The VERSION byte is FRAMING, not compatibility ────────────────────────────────────────────
|
|
47
|
+
*
|
|
48
|
+
* There are no other builds to be compatible with — CELLO is pre-launch with no external installs —
|
|
49
|
+
* so this byte is not here to interoperate with anything. It is here because the alternative is a
|
|
50
|
+
* blob whose layout is implied by its length, and a reader that meets bytes it cannot frame would
|
|
51
|
+
* then hand attacker-chosen input to a decryptor under a guessed layout. One byte, one comparison,
|
|
52
|
+
* and an unknown value REFUSES rather than parsing.
|
|
53
|
+
*
|
|
54
|
+
* That is the only justification, and it is worth stating because the tempting one — "so a future
|
|
55
|
+
* version can change the format" — would be exactly the speculative scaffolding this codebase has
|
|
56
|
+
* ruled against building.
|
|
57
|
+
*/
|
|
58
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
59
|
+
/** AES-256. */
|
|
60
|
+
const KEY_BYTES = 32;
|
|
61
|
+
/** GCM's standard nonce length (NIST SP 800-38D §8.2). */
|
|
62
|
+
const IV_BYTES = 12;
|
|
63
|
+
/** GCM tag. Full length, never truncated. */
|
|
64
|
+
const TAG_BYTES = 16;
|
|
65
|
+
/**
|
|
66
|
+
* The only version this build writes, and the only one it reads.
|
|
67
|
+
*
|
|
68
|
+
* `0x01` rather than `0x00`: a zero first byte is what an all-zero buffer and a truncated read both
|
|
69
|
+
* look like, so a version of zero cannot be told from an absent one.
|
|
70
|
+
*/
|
|
71
|
+
export const SESSION_CONTENT_SEAL_V1 = 0x01;
|
|
72
|
+
/** Bytes added to every message: version + IV + tag. */
|
|
73
|
+
export const SESSION_CONTENT_SEAL_OVERHEAD_BYTES = 1 + IV_BYTES + TAG_BYTES;
|
|
74
|
+
/**
|
|
75
|
+
* Encrypt a message body under the agreed session key.
|
|
76
|
+
*
|
|
77
|
+
* Throws on a wrong-width key rather than deriving from whatever it was given: a short key silently
|
|
78
|
+
* accepted is encryption that appears to work under something that is not the agreed secret.
|
|
79
|
+
*/
|
|
80
|
+
export function sealSessionContent(key, plaintext) {
|
|
81
|
+
if (key.length !== KEY_BYTES) {
|
|
82
|
+
throw new Error(`SESSION CONTENT SEAL: the session key must be ${KEY_BYTES} bytes, got ${key.length}. This is a ` +
|
|
83
|
+
"LOCAL defect — the key comes from this side's own agreement, not from the peer. Refusing " +
|
|
84
|
+
"rather than encrypting under something that is not the agreed secret.");
|
|
85
|
+
}
|
|
86
|
+
const iv = new Uint8Array(randomBytes(IV_BYTES));
|
|
87
|
+
const cipher = createCipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(iv));
|
|
88
|
+
const ct = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
|
|
89
|
+
const tag = cipher.getAuthTag();
|
|
90
|
+
return new Uint8Array(Buffer.concat([Buffer.from([SESSION_CONTENT_SEAL_V1]), Buffer.from(iv), ct, tag]));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Decrypt a message body under the agreed session key.
|
|
94
|
+
*
|
|
95
|
+
* **Returns `null` on every failure and never throws** — wrong key, tampered ciphertext, truncated
|
|
96
|
+
* blob, unknown version. The caller cannot tell those apart and must not try: GCM's tag check is the
|
|
97
|
+
* only thing that distinguishes "not for us" from "modified in flight", and a caller that branched
|
|
98
|
+
* on the difference would be branching on attacker-controlled input.
|
|
99
|
+
*
|
|
100
|
+
* The caller's job on `null` is to refuse the message loudly, not to retry or to fall back to
|
|
101
|
+
* reading it as plaintext.
|
|
102
|
+
*/
|
|
103
|
+
export function openSessionContent(key, blob) {
|
|
104
|
+
if (key.length !== KEY_BYTES)
|
|
105
|
+
return null;
|
|
106
|
+
// A blob must have room for version + IV + tag, and one byte cannot be assumed present: an empty
|
|
107
|
+
// frame would otherwise index past the end and read `undefined` as a version.
|
|
108
|
+
if (blob.length < SESSION_CONTENT_SEAL_OVERHEAD_BYTES)
|
|
109
|
+
return null;
|
|
110
|
+
if (blob[0] !== SESSION_CONTENT_SEAL_V1)
|
|
111
|
+
return null;
|
|
112
|
+
const iv = blob.subarray(1, 1 + IV_BYTES);
|
|
113
|
+
const ct = blob.subarray(1 + IV_BYTES, blob.length - TAG_BYTES);
|
|
114
|
+
const tag = blob.subarray(blob.length - TAG_BYTES);
|
|
115
|
+
try {
|
|
116
|
+
const decipher = createDecipheriv("aes-256-gcm", Buffer.from(key), Buffer.from(iv));
|
|
117
|
+
decipher.setAuthTag(Buffer.from(tag));
|
|
118
|
+
return new Uint8Array(Buffer.concat([decipher.update(Buffer.from(ct)), decipher.final()]));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=session-content-seal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-content-seal.js","sourceRoot":"","sources":["../src/session-content-seal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M15-EPHEMERAL-AUTH-1 — binding the throwaway key to the agent's identity.
|
|
3
|
+
*
|
|
4
|
+
* ─── The attack this closes, step by step ──────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The key agreement mixes two throwaway keys into a shared secret, and the secret itself never
|
|
7
|
+
* crosses the wire. But if nobody SIGNS the throwaway key, an arriving one carries no evidence of
|
|
8
|
+
* who sent it:
|
|
9
|
+
*
|
|
10
|
+
* 1. Alice sends her throwaway public key. The relay is carrying the traffic.
|
|
11
|
+
* 2. The relay keeps Alice's and forwards its OWN key to Bob instead.
|
|
12
|
+
* 3. It does the same in the other direction.
|
|
13
|
+
* 4. It now shares one secret with Alice and a different one with Bob.
|
|
14
|
+
* 5. It decrypts everything, reads it, re-encrypts it, and passes it on. Neither side sees
|
|
15
|
+
* anything wrong, because both halves of the conversation decrypt perfectly.
|
|
16
|
+
*
|
|
17
|
+
* So the unauthenticated layer stops a PASSIVE recorder — which is what harvest-now-decrypt-later
|
|
18
|
+
* is about — and does nothing against the party actually carrying the messages. **And we run the
|
|
19
|
+
* relays**, so without this the guarantee reduces to "trust us", which is the one thing CELLO exists
|
|
20
|
+
* so nobody has to do.
|
|
21
|
+
*
|
|
22
|
+
* ─── What is signed, and why the session id is in it ───────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* message = "cello/session/v1/ephemeral" || sessionId || ephemeralPublic
|
|
25
|
+
*
|
|
26
|
+
* The LABEL makes this signature unusable as any other kind of CELLO signature, and vice versa: an
|
|
27
|
+
* agent signs many things, and a signature that is valid in two contexts is a signature an attacker
|
|
28
|
+
* can move between them.
|
|
29
|
+
*
|
|
30
|
+
* The SESSION ID binds the ephemeral to the conversation it was minted for. Without it, a signed
|
|
31
|
+
* ephemeral captured from one session could be replayed into another between the same two agents —
|
|
32
|
+
* the signature verifies, both sides derive, and the relay that replayed it knows the secret from
|
|
33
|
+
* the session it harvested. The session id is length-prefixed so that `sessionId ‖ ephemeral` cannot
|
|
34
|
+
* be re-split at a different point to produce the same bytes from different inputs.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ WHAT THIS DOES NOT DO. It proves the ephemeral came from the holder of that identity key. It
|
|
37
|
+
* does not prove the identity key is the agent you meant to talk to — that is the counterparty
|
|
38
|
+
* identity the caller passes in, and the caller must take it from what the OPERATOR asked for, never
|
|
39
|
+
* from a value the directory or the relay handed back. Verifying against a key the attacker chose is
|
|
40
|
+
* relocating the trust rather than closing it.
|
|
41
|
+
*/
|
|
42
|
+
/** Ed25519 signatures are 64 bytes; X25519 publics are 32. */
|
|
43
|
+
export declare const EPHEMERAL_SIG_BYTES = 64;
|
|
44
|
+
export declare const EPHEMERAL_PUBLIC_BYTES = 32;
|
|
45
|
+
/**
|
|
46
|
+
* The exact bytes both sides sign and verify.
|
|
47
|
+
*
|
|
48
|
+
* Written once and used by BOTH directions on purpose. A signer and a verifier that each build the
|
|
49
|
+
* message from their own reading of a spec is how two implementations end up disagreeing about a
|
|
50
|
+
* byte and rejecting each other for a reason neither can see.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ephemeralSigningMessage(sessionId: Uint8Array, ephemeralPublic: Uint8Array): Uint8Array;
|
|
53
|
+
/** Sign this side's throwaway public key with the agent's long-term identity key. */
|
|
54
|
+
export declare function signSessionEphemeral(signer: {
|
|
55
|
+
sign(data: Uint8Array): Promise<Uint8Array>;
|
|
56
|
+
}, sessionId: Uint8Array, ephemeralPublic: Uint8Array): Promise<Uint8Array>;
|
|
57
|
+
/**
|
|
58
|
+
* Why a peer's ephemeral was refused. A CLOSED set — a free-form string is what lets a new code
|
|
59
|
+
* reach a caller with nothing to act on (`refusal-reasons.ts` records what that cost).
|
|
60
|
+
*/
|
|
61
|
+
export declare const EPHEMERAL_AUTH_REFUSALS: {
|
|
62
|
+
/** No signature at all. */
|
|
63
|
+
readonly SIGNATURE_MISSING: "ephemeral_signature_missing";
|
|
64
|
+
/** Present but the wrong width, or the public key is the wrong width. */
|
|
65
|
+
readonly MALFORMED: "ephemeral_malformed";
|
|
66
|
+
/** Verified against the expected identity and did not match. */
|
|
67
|
+
readonly SIGNATURE_MISMATCH: "ephemeral_signature_mismatch";
|
|
68
|
+
};
|
|
69
|
+
export type EphemeralAuthRefusal = (typeof EPHEMERAL_AUTH_REFUSALS)[keyof typeof EPHEMERAL_AUTH_REFUSALS];
|
|
70
|
+
export type EphemeralAuthResult = {
|
|
71
|
+
ok: true;
|
|
72
|
+
} | {
|
|
73
|
+
ok: false;
|
|
74
|
+
reason: EphemeralAuthRefusal;
|
|
75
|
+
detail: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* VERIFY A PEER'S EPHEMERAL BEFORE ANYTHING IS DERIVED FROM IT.
|
|
79
|
+
*
|
|
80
|
+
* 🚨 MISSING, MALFORMED AND MISMATCHED ALL FAIL. They are separate REASONS because they send an
|
|
81
|
+
* operator to different places, and they are the same OUTCOME because an attacker evading a mismatch
|
|
82
|
+
* check simply supplies no signature at all. "We could not tell" must never be more forgiving than
|
|
83
|
+
* "we proved it wrong" — that is the hole, not the mismatch.
|
|
84
|
+
*
|
|
85
|
+
* Called BEFORE `deriveSessionSecrets`, never alongside it. Deriving first and checking after means
|
|
86
|
+
* a key exists, briefly, that was agreed with someone unproven — and every "briefly" in this
|
|
87
|
+
* codebase eventually turns out to be long enough for something to read it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function verifySessionEphemeral(opts: {
|
|
90
|
+
/** The identity we EXPECT — from what the operator asked for, never from the directory or relay. */
|
|
91
|
+
expectedIdentityPublic: Uint8Array;
|
|
92
|
+
sessionId: Uint8Array;
|
|
93
|
+
peerEphemeralPublic: Uint8Array | undefined;
|
|
94
|
+
peerSignature: Uint8Array | undefined;
|
|
95
|
+
}): EphemeralAuthResult;
|
|
96
|
+
//# sourceMappingURL=session-ephemeral-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-ephemeral-auth.d.ts","sourceRoot":"","sources":["../src/session-ephemeral-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AASH,8DAA8D;AAC9D,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,GAAG,UAAU,CActG;AAED,qFAAqF;AACrF,wBAAsB,oBAAoB,CACxC,MAAM,EAAE;IAAE,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;CAAE,EACvD,SAAS,EAAE,UAAU,EACrB,eAAe,EAAE,UAAU,GAC1B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB;IAClC,2BAA2B;;IAE3B,yEAAyE;;IAEzE,gEAAgE;;CAExD,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAC9B,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAC;AAEzE,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,oGAAoG;IACpG,sBAAsB,EAAE,UAAU,CAAC;IACnC,SAAS,EAAE,UAAU,CAAC;IACtB,mBAAmB,EAAE,UAAU,GAAG,SAAS,CAAC;IAC5C,aAAa,EAAE,UAAU,GAAG,SAAS,CAAC;CACvC,GAAG,mBAAmB,CAwCtB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M15-EPHEMERAL-AUTH-1 — binding the throwaway key to the agent's identity.
|
|
3
|
+
*
|
|
4
|
+
* ─── The attack this closes, step by step ──────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The key agreement mixes two throwaway keys into a shared secret, and the secret itself never
|
|
7
|
+
* crosses the wire. But if nobody SIGNS the throwaway key, an arriving one carries no evidence of
|
|
8
|
+
* who sent it:
|
|
9
|
+
*
|
|
10
|
+
* 1. Alice sends her throwaway public key. The relay is carrying the traffic.
|
|
11
|
+
* 2. The relay keeps Alice's and forwards its OWN key to Bob instead.
|
|
12
|
+
* 3. It does the same in the other direction.
|
|
13
|
+
* 4. It now shares one secret with Alice and a different one with Bob.
|
|
14
|
+
* 5. It decrypts everything, reads it, re-encrypts it, and passes it on. Neither side sees
|
|
15
|
+
* anything wrong, because both halves of the conversation decrypt perfectly.
|
|
16
|
+
*
|
|
17
|
+
* So the unauthenticated layer stops a PASSIVE recorder — which is what harvest-now-decrypt-later
|
|
18
|
+
* is about — and does nothing against the party actually carrying the messages. **And we run the
|
|
19
|
+
* relays**, so without this the guarantee reduces to "trust us", which is the one thing CELLO exists
|
|
20
|
+
* so nobody has to do.
|
|
21
|
+
*
|
|
22
|
+
* ─── What is signed, and why the session id is in it ───────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* message = "cello/session/v1/ephemeral" || sessionId || ephemeralPublic
|
|
25
|
+
*
|
|
26
|
+
* The LABEL makes this signature unusable as any other kind of CELLO signature, and vice versa: an
|
|
27
|
+
* agent signs many things, and a signature that is valid in two contexts is a signature an attacker
|
|
28
|
+
* can move between them.
|
|
29
|
+
*
|
|
30
|
+
* The SESSION ID binds the ephemeral to the conversation it was minted for. Without it, a signed
|
|
31
|
+
* ephemeral captured from one session could be replayed into another between the same two agents —
|
|
32
|
+
* the signature verifies, both sides derive, and the relay that replayed it knows the secret from
|
|
33
|
+
* the session it harvested. The session id is length-prefixed so that `sessionId ‖ ephemeral` cannot
|
|
34
|
+
* be re-split at a different point to produce the same bytes from different inputs.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ WHAT THIS DOES NOT DO. It proves the ephemeral came from the holder of that identity key. It
|
|
37
|
+
* does not prove the identity key is the agent you meant to talk to — that is the counterparty
|
|
38
|
+
* identity the caller passes in, and the caller must take it from what the OPERATOR asked for, never
|
|
39
|
+
* from a value the directory or the relay handed back. Verifying against a key the attacker chose is
|
|
40
|
+
* relocating the trust rather than closing it.
|
|
41
|
+
*/
|
|
42
|
+
import { verify as edVerify } from "./ed25519.js";
|
|
43
|
+
const ENC = new TextEncoder();
|
|
44
|
+
/** Domain separation. Versioned, so a future change is a new label rather than a reinterpretation. */
|
|
45
|
+
const EPHEMERAL_SIG_LABEL = ENC.encode("cello/session/v1/ephemeral");
|
|
46
|
+
/** Ed25519 signatures are 64 bytes; X25519 publics are 32. */
|
|
47
|
+
export const EPHEMERAL_SIG_BYTES = 64;
|
|
48
|
+
export const EPHEMERAL_PUBLIC_BYTES = 32;
|
|
49
|
+
/**
|
|
50
|
+
* The exact bytes both sides sign and verify.
|
|
51
|
+
*
|
|
52
|
+
* Written once and used by BOTH directions on purpose. A signer and a verifier that each build the
|
|
53
|
+
* message from their own reading of a spec is how two implementations end up disagreeing about a
|
|
54
|
+
* byte and rejecting each other for a reason neither can see.
|
|
55
|
+
*/
|
|
56
|
+
export function ephemeralSigningMessage(sessionId, ephemeralPublic) {
|
|
57
|
+
// The session id is LENGTH-PREFIXED (4 bytes, big-endian). Concatenating two variable-length
|
|
58
|
+
// fields lets a crafted pair re-split at a different boundary and produce identical bytes from
|
|
59
|
+
// different inputs — the signature would then cover something other than what it appears to.
|
|
60
|
+
const out = new Uint8Array(EPHEMERAL_SIG_LABEL.length + 4 + sessionId.length + ephemeralPublic.length);
|
|
61
|
+
let o = 0;
|
|
62
|
+
out.set(EPHEMERAL_SIG_LABEL, o);
|
|
63
|
+
o += EPHEMERAL_SIG_LABEL.length;
|
|
64
|
+
out[o++] = (sessionId.length >>> 24) & 0xff;
|
|
65
|
+
out[o++] = (sessionId.length >>> 16) & 0xff;
|
|
66
|
+
out[o++] = (sessionId.length >>> 8) & 0xff;
|
|
67
|
+
out[o++] = sessionId.length & 0xff;
|
|
68
|
+
out.set(sessionId, o);
|
|
69
|
+
o += sessionId.length;
|
|
70
|
+
out.set(ephemeralPublic, o);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** Sign this side's throwaway public key with the agent's long-term identity key. */
|
|
74
|
+
export async function signSessionEphemeral(signer, sessionId, ephemeralPublic) {
|
|
75
|
+
return signer.sign(ephemeralSigningMessage(sessionId, ephemeralPublic));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Why a peer's ephemeral was refused. A CLOSED set — a free-form string is what lets a new code
|
|
79
|
+
* reach a caller with nothing to act on (`refusal-reasons.ts` records what that cost).
|
|
80
|
+
*/
|
|
81
|
+
export const EPHEMERAL_AUTH_REFUSALS = {
|
|
82
|
+
/** No signature at all. */
|
|
83
|
+
SIGNATURE_MISSING: "ephemeral_signature_missing",
|
|
84
|
+
/** Present but the wrong width, or the public key is the wrong width. */
|
|
85
|
+
MALFORMED: "ephemeral_malformed",
|
|
86
|
+
/** Verified against the expected identity and did not match. */
|
|
87
|
+
SIGNATURE_MISMATCH: "ephemeral_signature_mismatch",
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* VERIFY A PEER'S EPHEMERAL BEFORE ANYTHING IS DERIVED FROM IT.
|
|
91
|
+
*
|
|
92
|
+
* 🚨 MISSING, MALFORMED AND MISMATCHED ALL FAIL. They are separate REASONS because they send an
|
|
93
|
+
* operator to different places, and they are the same OUTCOME because an attacker evading a mismatch
|
|
94
|
+
* check simply supplies no signature at all. "We could not tell" must never be more forgiving than
|
|
95
|
+
* "we proved it wrong" — that is the hole, not the mismatch.
|
|
96
|
+
*
|
|
97
|
+
* Called BEFORE `deriveSessionSecrets`, never alongside it. Deriving first and checking after means
|
|
98
|
+
* a key exists, briefly, that was agreed with someone unproven — and every "briefly" in this
|
|
99
|
+
* codebase eventually turns out to be long enough for something to read it.
|
|
100
|
+
*/
|
|
101
|
+
export function verifySessionEphemeral(opts) {
|
|
102
|
+
if (opts.peerSignature === undefined) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: EPHEMERAL_AUTH_REFUSALS.SIGNATURE_MISSING,
|
|
106
|
+
detail: "the peer sent a session key with no signature over it, so there is nothing to tie it to " +
|
|
107
|
+
"your counterparty. An unsigned key is exactly what a relay substituting its own would send, " +
|
|
108
|
+
"and it is refused on the same path as a wrong signature — a check that is lenient about a " +
|
|
109
|
+
"missing proof is a check an attacker skips.",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (opts.peerSignature.length !== EPHEMERAL_SIG_BYTES ||
|
|
113
|
+
opts.peerEphemeralPublic === undefined ||
|
|
114
|
+
opts.peerEphemeralPublic.length !== EPHEMERAL_PUBLIC_BYTES) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
reason: EPHEMERAL_AUTH_REFUSALS.MALFORMED,
|
|
118
|
+
detail: `the peer's session key material is the wrong shape — signature ${opts.peerSignature.length} ` +
|
|
119
|
+
`bytes (expected ${EPHEMERAL_SIG_BYTES}), key ${opts.peerEphemeralPublic?.length ?? 0} bytes ` +
|
|
120
|
+
`(expected ${EPHEMERAL_PUBLIC_BYTES}). Refusing rather than padding: a short value silently ` +
|
|
121
|
+
"zero-extended is an agreement with something that is not the peer.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const message = ephemeralSigningMessage(opts.sessionId, opts.peerEphemeralPublic);
|
|
125
|
+
if (!edVerify(opts.expectedIdentityPublic, message, opts.peerSignature)) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: EPHEMERAL_AUTH_REFUSALS.SIGNATURE_MISMATCH,
|
|
129
|
+
detail: "the peer's session key is signed, but not by the counterparty this session is with. That " +
|
|
130
|
+
"is what a relay substituting its own key looks like: it would then share one secret with " +
|
|
131
|
+
"you and another with them, and read everything in between while both sides saw a working " +
|
|
132
|
+
"conversation. Refusing.",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=session-ephemeral-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-ephemeral-auth.js","sourceRoot":"","sources":["../src/session-ephemeral-auth.ts"],"names":[],"mappings":"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"}
|