Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/tsp-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions packages/tsp-js/src/rev3/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
43 changes: 43 additions & 0 deletions packages/tsp-js/tests/payload.app-stream.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});