diff --git a/packages/tsp-js/CHANGELOG.md b/packages/tsp-js/CHANGELOG.md index abfc143..7bee63d 100644 --- a/packages/tsp-js/CHANGELOG.md +++ b/packages/tsp-js/CHANGELOG.md @@ -8,6 +8,13 @@ For history before this file, see `git log` on `packages/tsp-js`. ## [Unreleased] +### Fixed + +- An XSCS/XCTL body that is not exactly one Bytes primitive is refused. It was + read as its first primitive, silently dropping the rest of the `-A##` stream, + and data after the stream was ignored. See + [tswg-tsp-specification#77](https://github.com/trustoverip/tswg-tsp-specification/issues/77). + ## [0.3.0] — Trust Spanning Protocol specification Rev 3 **Breaking. This package now packs Rev 3, and a Rev 2 peer cannot read what it diff --git a/packages/tsp-js/src/rev3/payload.ts b/packages/tsp-js/src/rev3/payload.ts index 528458f..0569d95 100644 --- a/packages/tsp-js/src/rev3/payload.ts +++ b/packages/tsp-js/src/rev3/payload.ts @@ -196,11 +196,17 @@ export function decodePayloadFrame( decodePadding(frame, cur); const streamQuadlets = wire.decodeCount(wire.TSP_GENERIC_STREAM, frame, cur); if (streamQuadlets === undefined) throw new Error("tsp: missing -A payload stream"); + // The body is an `-A##` stream that ends the frame and holds exactly one + // Bytes primitive — the form the spec's vectors and the ToIP reference + // use. Anything else is refused, never truncated to its first primitive + // (trustoverip/tswg-tsp-specification#77). const streamEnd = cur.pos + streamQuadlets * 3; - if (streamEnd > frameEnd) throw new Error("tsp: -A stream overruns the payload frame"); + if (streamEnd !== frameEnd) throw new Error("tsp: -A stream does not end the payload frame"); const body = wire.decodeVariableData(wire.TSP_PLAINTEXT, frame, cur); if (body === undefined) throw new Error("tsp: missing payload body"); - if (cur.pos > streamEnd) throw new Error("tsp: payload body overruns the -A stream"); + if (cur.pos !== streamEnd) { + throw new Error("tsp: -A stream must hold exactly one Bytes primitive"); + } // `XCTL` carries an upper-layer control payload — opaque to TSP, exactly // like `XSCS`. It is not a relationship-forming message and shares nothing // with one but the word "control". diff --git a/packages/tsp-js/tests/payload.app-stream.mjs b/packages/tsp-js/tests/payload.app-stream.mjs new file mode 100644 index 0000000..0fa167d --- /dev/null +++ b/packages/tsp-js/tests/payload.app-stream.mjs @@ -0,0 +1,43 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import * as wire from "../dist/cesr/wire.js"; +import { decodePayloadFrame } from "../dist/rev3/payload.js"; + +// An XSCS body is exactly one Bytes primitive (tswg-tsp-specification#77): +// an -H## group, a second primitive, or data after the stream is refused, +// never truncated to the first primitive. +const enc = new TextEncoder(); +const prim = (s) => { + const out = []; + wire.encodeVariableData(wire.TSP_PLAINTEXT, enc.encode(s), out); + return out; +}; +const frameOf = (stream, trailing = []) => { + const body = [...wire.XSCS, ...prim(""), ...prim("")]; // NULL sender, no padding + wire.encodeCount(wire.TSP_GENERIC_STREAM, stream.length / 3, body); + body.push(...stream, ...trailing); + const frame = []; + wire.encodeCount(wire.TSP_PAYLOAD, body.length / 3, frame); + return Uint8Array.from([...frame, ...body]); +}; +const open = (f) => decodePayloadFrame(f, "did:example:alice", new Uint8Array()); + +test("a single Bytes primitive is the application body", () => { + assert.equal(new TextDecoder().decode(open(frameOf(prim("hello world"))).body), "hello world"); +}); + +test("an -H## group is refused", () => { + const json = prim('{"hello":"world"}'); + const group = []; + wire.encodeCount(wire.cesrInt("H"), json.length / 3, group); + assert.throws(() => open(frameOf([...group, ...json]))); +}); + +test("a second primitive is refused, not dropped", () => { + assert.throws(() => open(frameOf([...prim("one"), ...prim("two")])), /exactly one Bytes primitive/); +}); + +test("data after the stream is refused", () => { + assert.throws(() => open(frameOf(prim("one"), prim("x"))), /does not end the payload frame/); +});