@blockcast/mmt-alta-parse 0.1.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 ADDED
@@ -0,0 +1,181 @@
1
+ # @blockcast/mmt-alta-parse
2
+
3
+ Zero-dependency leaf package that extracts the detached **ALTA authenticator trailer** from an MMTP source packet. ALTA (Authenticated Lightweight TESLA-like Algorithm) is Blockcast's sender-authentication scheme for multicast streams: each MMTP source packet carries a detached signature trailer that downstream consumers (the FEC worker, the FFI core) verify against a catalog-published public key. This package is **the parser only** — it locates the trailer, returns the raw authenticator bytes, and tells the caller where the signed payload ends. Signature verification (Ed25519 / P-256) lives in the consumer that has crypto and the public key in hand.
4
+
5
+ It is split into a leaf package with zero `@blockcast/*` dependencies so both `@blockcast/mmt-container` (wire-path parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share one implementation without creating a package cycle.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @blockcast/mmt-alta-parse
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { extractAltaTrailer } from "@blockcast/mmt-alta-parse"
17
+
18
+ // `wirePacket` is a full MMTP packet as it came off the network (Uint8Array).
19
+ const trailer = extractAltaTrailer(wirePacket)
20
+
21
+ if (trailer === null) {
22
+ // Packet has no ALTA trailer. This is normal for:
23
+ // - non-MPU packets (repair, signaling)
24
+ // - unsigned streams
25
+ // - malformed / truncated packets
26
+ return
27
+ }
28
+
29
+ // `trailer.auth` — the authenticator bytes (no ext_type/ext_length prefix)
30
+ // `trailer.signedEnd` — byte offset where the signed payload ends inside wirePacket.
31
+ // The signer signed wirePacket.subarray(0, signedEnd).
32
+
33
+ const signedPayload = wirePacket.subarray(0, trailer.signedEnd)
34
+ // Hand off to a verifier with the catalog-published public key:
35
+ // ed25519.verify_detached(trailer.auth, signedPayload, publicKey)
36
+ ```
37
+
38
+ For FEC-recovered source symbols (where the upstream FEC pipeline has already stripped the 4-byte Source FEC Payload ID), pass `{ skipFecId: true }`:
39
+
40
+ ```ts
41
+ const trailer = extractAltaTrailer(recoveredSymbol, { skipFecId: true })
42
+ ```
43
+
44
+ ## Exports
45
+
46
+ Per `package.json` (single entry point, ESM only):
47
+
48
+ | Export | Kind | Purpose |
49
+ |--------|------|---------|
50
+ | `extractAltaTrailer(packet, options?)` | function | Parse a wire MMTP packet and return its `AltaTrailer` or `null`. |
51
+ | `AltaTrailer` | type | `{ auth: Uint8Array; signedEnd: number }` |
52
+ | `MMTP_EXT_TYPE_ALTA_AUTH` | const `0xf0a1` | The MMTP header-extension `ext_type` assigned to the detached ALTA trailer. |
53
+ | `MMTP_EXT_TRAILER_HEADER_SIZE` | const `4` | Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 bytes BE). |
54
+
55
+ The package has no runtime dependencies — `package.json` lists `"dependencies": {}`.
56
+
57
+ ## ALTA wire format
58
+
59
+ The detached ALTA authenticator is appended **after** the declared MPU payload bytes and **before** the optional 4-byte Source FEC Payload ID. ISO/IEC 23008-1 defines `payload_length` as a length-after-field value: it includes the six MPU-header bytes following the field, the MFU data-unit header when present, and the media bytes. The MMTP header-extension `X` bit stays `0` — non-ALTA receivers treat the trailing bytes as data past `payload_length` and discard them per ISO/IEC 23008-1 §9.2.2 ("may be discarded without impacting correct processing").
60
+
61
+ Wire layout for a signed source packet:
62
+
63
+ ```
64
+ | MMTP hdr | length (2) | remaining MPU hdr (6) | MFU hdr? | MFU data | TRAILER | SS_ID? (4) |
65
+ \_______________________________________________________/
66
+ payload_length bytes start after the field
67
+ ^
68
+ |
69
+ signedEnd
70
+ ```
71
+
72
+ In MPU-payload coordinates, `signedEnd` is exactly `2 + payload_length`; the returned packet-relative offset also includes the complete, possibly extended MMTP header.
73
+
74
+ The trailer itself is:
75
+
76
+ ```
77
+ | ext_type (2 BE) | ext_length (2 BE) | auth (ext_length bytes) |
78
+ 0xf0a1
79
+ ```
80
+
81
+ - `ext_type = 0xf0a1` identifies the trailer as ALTA. Anything else is rejected (`null` return).
82
+ - `ext_length` is the authenticator length in bytes — 64 for Ed25519, 64 for P-256 raw `(r,s)`, or whatever the signer minted.
83
+ - The signed range is everything from byte 0 through `signedEnd-1` of the wire packet — i.e. the MMTP header, MPU header, MFU header, and MFU payload, but **not** the trailer itself or the optional FEC payload ID.
84
+
85
+ This is the **detached trailer** form (a.k.a. "Path 2 / 08-07") from `draft-ramadan-moq-mmt` §12. It coexists with FEC: the signer signs the source-packet bytes **before** they enter the FEC encoder, so a receiver that recovers a missing source symbol from repair gets the same signed bytes the sender minted. This is the "sign-before-encode" rule — see the parent monorepo Phase 8 spec.
86
+
87
+ ### When `extractAltaTrailer` returns `null`
88
+
89
+ The function returns `null` (rather than throwing) for every non-trailer outcome — see `src/index.ts:69-115`:
90
+
91
+ | Condition | Why |
92
+ |-----------|-----|
93
+ | Packet shorter than `MMTP_HEADER_SIZE + MPU_HEADER_SIZE` (20 B) | Truncated/malformed |
94
+ | Packet type ≠ MPU (`0x00`) | Repair and signaling packets aren't signed per-packet |
95
+ | Fragment type ≠ 2 (MFU) | Only timed or non-timed MFU media packets carry trailers |
96
+ | `payload_length` cannot contain the MPU-header remainder, structurally required DU header, and media | The declaration ends inside headers or describes a header-only MFU |
97
+ | `2 + payload_length` exceeds the pre-FEC packet boundary | Truncated/malformed declaration |
98
+ | No bytes between MFU data and SS_ID for the 4-byte trailer header | Unsigned stream |
99
+ | `ext_type` ≠ `0xf0a1` | Some other header-extension lives in that slot |
100
+ | Declared `ext_length` does not end exactly at the SS_ID boundary | Malformed or ambiguous trailer suffix |
101
+
102
+ Callers should treat `null` as "no signature on this packet" and route based on stream policy (require-signed vs allow-unsigned).
103
+
104
+ ## Verification flow (where this package fits)
105
+
106
+ ```
107
+ catalog (CMSF)
108
+
109
+ alta.publicKey (SPKI bytes)
110
+
111
+
112
+ ┌─────────────────────────────────────────────────────┐
113
+ │ @blockcast/mmt-container :: mmtp-router │
114
+ │ wirePacket → extractAltaTrailer(packet) ──────────┼──► this package
115
+ │ ⇒ { auth, signedEnd } │
116
+ │ │
117
+ │ feedSourceSymbol(..., { auth, payloadEnd: signedEnd })
118
+ └────────────────────────┬────────────────────────────┘
119
+
120
+
121
+ ┌─────────────────────────────────────────────────────┐
122
+ │ @blockcast/fec-worker (configured with publicKey)│
123
+ │ ed25519.verify_detached(auth, │
124
+ │ symbol[0..payloadEnd], │
125
+ │ publicKey) │
126
+ │ → emits altaVerified per packet → FecTrackStats │
127
+ └─────────────────────────────────────────────────────┘
128
+ ```
129
+
130
+ Concretely, in `packages/container/src/mmtp-router.ts:798`, the router calls `extractAltaTrailer(packet)` for every signed source packet on the wire, packages the result as `{ auth, payloadEnd: signedEnd }`, and hands it to the FEC manager. The FEC worker (configured with `alta.publicKey` from the CMSF catalog at `configure()` time) does the actual Ed25519 / P-256 verification and increments per-track `altaVerified` / `altaFailed` counters in `FecTrackStats`. Those counters surface in CMCD as `x-bc-alta-verified` (see `packages/container/src/cmcd-producer.ts:93`).
131
+
132
+ ## Why not TESLA
133
+
134
+ TESLA (RFC 4082) is the IETF reference for sender-authenticated multicast: it uses delayed-disclosure of MAC keys to amortize signing cost, but at the protocol level it carries 10–20% authentication overhead per stream (depending on key chain depth and disclosure interval). ALTA is Blockcast's lighter variant — same threat model (verify each packet was minted by the legitimate sender, not a rogue injector on the multicast group), measurably lower overhead (~6.91% in production multicast tests). The parent monorepo's `CLAUDE.md` calls this out at `packages/alta/`. ALTA replaces the v2 stub verifier in v3.0 / Phase 8.
135
+
136
+ ## Relation to DRM
137
+
138
+ ALTA and DRM solve **different** problems and are orthogonal:
139
+
140
+ | | ALTA | DRM (Widevine / PlayReady / FairPlay / ClearKey) |
141
+ |---|---|---|
142
+ | Authenticates | the **sender** | the **receiver** |
143
+ | Question | "Did this packet come from the legitimate broadcaster?" | "Is this device authorized to play this content?" |
144
+ | Wire layer | Per-MMTP-packet detached trailer | Sample-level CENC encryption + license server |
145
+ | Catalog field | `alta.publicKey` | `contentProtection[]` (planned in v4 / M7) |
146
+
147
+ A stream can use both — ALTA on the multicast wire (so receivers reject injected packets), CENC + DRM at the sample level (so unauthorized devices can't play). They don't interact.
148
+
149
+ ## Specifications
150
+
151
+ - **ISO/IEC 23008-1:2023** §9.2.2 (MMTP packet header), §9.2.3 (MPU/MFU header) — the host packet structure
152
+ - **draft-ramadan-moq-mmt** §12 — detached authenticator trailer ("Path 2 / 08-07") wire format
153
+ - **RFC 8032** — Ed25519 signature algorithm (a typical inhabitant of the trailer)
154
+ - **NIST FIPS 186-4** — ECDSA over P-256 (alternative inhabitant)
155
+ - **RFC 4082** — TESLA (the heavier scheme ALTA replaces; included for context, not a dependency)
156
+ - ATSC A/331 §A — referenced by libmmt for catalog-style key/signaling distribution patterns
157
+
158
+ This package is agnostic to the signature algorithm — it only carries opaque bytes.
159
+
160
+ ## Related packages
161
+
162
+ | Package | Role |
163
+ |---------|------|
164
+ | `@blockcast/mmt-container` | Consumes this parser via `mmtp-router.ts`; provides the CMSF catalog that distributes `alta.publicKey` to the FEC worker. |
165
+ | `@blockcast/fec-worker` | Verifier — receives `{ auth, payloadEnd }` and the catalog public key at configure time, performs the actual Ed25519 / P-256 verification, increments `altaVerified` / `altaFailed` in `FecTrackStats`. |
166
+ | `@blockcast/mmt-fec` | Surfaces verification counters to the application via FEC stats snapshots. |
167
+
168
+ ## Testing
169
+
170
+ ```bash
171
+ pnpm test # vitest run
172
+ pnpm test:watch # vitest in watch mode
173
+ pnpm typecheck # tsc --noEmit
174
+ pnpm build # tsc → dist/
175
+ ```
176
+
177
+ Direct tests in `src/index.test.ts` cover canonical complete/fragmented declarations, ALTA-only and ALTA+FEC suffixes, malformed lengths, and rejection of obsolete boundary conventions. Coverage is also exercised through the re-exported parser in `@blockcast/mmt-container`.
178
+
179
+ ## License
180
+
181
+ Apache-2.0 — see `LICENSE`.
File without changes
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Zero-dependency helper for extracting the detached ALTA authenticator trailer
3
+ * from an MMTP source packet.
4
+ *
5
+ * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path
6
+ * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share
7
+ * one implementation without creating a package cycle between them.
8
+ *
9
+ * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt §12 (detached trailer, Path 2).
10
+ */
11
+ /**
12
+ * MMTP header-extension ext_type assigned to the detached ALTA authenticator
13
+ * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`
14
+ * block appended after the MPU's declared payload bytes, before the 4-byte
15
+ * Source FEC Payload ID. MMTP X-bit stays 0 — non-ALTA MMTP receivers treat
16
+ * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1
17
+ * §9.2.2 "may be discarded without impacting correct processing").
18
+ */
19
+ export declare const MMTP_EXT_TYPE_ALTA_AUTH = 61601;
20
+ /** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */
21
+ export declare const MMTP_EXT_TRAILER_HEADER_SIZE = 4;
22
+ /** Result of extracting the detached ALTA authenticator from an MMTP packet. */
23
+ export interface AltaTrailer {
24
+ /** Authenticator bytes (no ext_type/ext_length prefix, no trailing padding). */
25
+ auth: Uint8Array;
26
+ /**
27
+ * Wire-packet byte offset where the signed payload ends, i.e. where the
28
+ * trailer begins. Caller uses this to produce
29
+ * `payload = wirePacket.subarray(0, signedEnd)` for verify_detached.
30
+ */
31
+ signedEnd: number;
32
+ }
33
+ /**
34
+ * Extract the detached ALTA authenticator trailer from an MMTP source packet.
35
+ *
36
+ * Wire layout:
37
+ * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`
38
+ * `payload_length = 6 + DU bytes + media bytes`
39
+ *
40
+ * Returns `null` when:
41
+ * - the packet is too small / malformed,
42
+ * - `payload_length` cannot contain the remaining six MPU-header bytes,
43
+ * - the declared payload overruns the packet,
44
+ * - no bytes remain between `payload_length` and the SS_ID,
45
+ * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},
46
+ * - the declared `ext_length` would extend past the SS_ID boundary.
47
+ *
48
+ * Non-MPU packets (repair, signaling) always return `null`.
49
+ *
50
+ * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the
51
+ * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has
52
+ * already been stripped upstream.
53
+ */
54
+ export declare function extractAltaTrailer(mmtpPacket: Uint8Array, options?: {
55
+ skipFecId?: boolean;
56
+ }): AltaTrailer | null;
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAiBH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAE7C,8EAA8E;AAC9E,eAAO,MAAM,4BAA4B,IAAI,CAAA;AAE7C,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,IAAI,EAAE,UAAU,CAAA;IAChB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,WAAW,GAAG,IAAI,CA4DpB"}
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Zero-dependency helper for extracting the detached ALTA authenticator trailer
3
+ * from an MMTP source packet.
4
+ *
5
+ * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path
6
+ * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share
7
+ * one implementation without creating a package cycle between them.
8
+ *
9
+ * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt §12 (detached trailer, Path 2).
10
+ */
11
+ // --- Private MMTP/MPU/MFU constants (ISO/IEC 23008-1 §9.2.2–9.2.3) ---
12
+ // Duplicated here as immutable spec values so this package stays a leaf with
13
+ // zero `@blockcast/*` dependencies. Re-deriving from `mmt-container` would
14
+ // reintroduce the package cycle this split exists to avoid.
15
+ const MMTP_HEADER_SIZE = 12;
16
+ const MPU_HEADER_SIZE = 8;
17
+ const MPU_LENGTH_FIELD_SIZE = 2;
18
+ const MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE;
19
+ const TIMED_MFU_DU_HEADER_SIZE = 14;
20
+ const NONTIMED_MFU_DU_HEADER_SIZE = 4;
21
+ const PACKET_TYPE_MPU = 0x00;
22
+ // --- Public ALTA trailer constants ---
23
+ /**
24
+ * MMTP header-extension ext_type assigned to the detached ALTA authenticator
25
+ * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`
26
+ * block appended after the MPU's declared payload bytes, before the 4-byte
27
+ * Source FEC Payload ID. MMTP X-bit stays 0 — non-ALTA MMTP receivers treat
28
+ * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1
29
+ * §9.2.2 "may be discarded without impacting correct processing").
30
+ */
31
+ export const MMTP_EXT_TYPE_ALTA_AUTH = 0xf0a1;
32
+ /** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */
33
+ export const MMTP_EXT_TRAILER_HEADER_SIZE = 4;
34
+ /**
35
+ * Extract the detached ALTA authenticator trailer from an MMTP source packet.
36
+ *
37
+ * Wire layout:
38
+ * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`
39
+ * `payload_length = 6 + DU bytes + media bytes`
40
+ *
41
+ * Returns `null` when:
42
+ * - the packet is too small / malformed,
43
+ * - `payload_length` cannot contain the remaining six MPU-header bytes,
44
+ * - the declared payload overruns the packet,
45
+ * - no bytes remain between `payload_length` and the SS_ID,
46
+ * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},
47
+ * - the declared `ext_length` would extend past the SS_ID boundary.
48
+ *
49
+ * Non-MPU packets (repair, signaling) always return `null`.
50
+ *
51
+ * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the
52
+ * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has
53
+ * already been stripped upstream.
54
+ */
55
+ export function extractAltaTrailer(mmtpPacket, options) {
56
+ if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
57
+ return null;
58
+ // ISO/IEC 23008-1:2023 Figure 8 places V=0 packet_type in the low six bits.
59
+ const packetType = mmtpPacket[1] & 0x3f;
60
+ if (packetType !== PACKET_TYPE_MPU)
61
+ return null;
62
+ const fecType = (mmtpPacket[0] >> 3) & 0x03;
63
+ let headerLength = MMTP_HEADER_SIZE;
64
+ if (fecType < 2 && (mmtpPacket[0] & 0x20) !== 0)
65
+ headerLength += 4;
66
+ if (fecType < 2 && (mmtpPacket[0] & 0x02) !== 0) {
67
+ if (headerLength + 4 > mmtpPacket.length)
68
+ return null;
69
+ const extensionLength = new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + headerLength + 2, 2).getUint16(0, false);
70
+ headerLength += 4 + extensionLength;
71
+ }
72
+ if (headerLength + MPU_HEADER_SIZE > mmtpPacket.length)
73
+ return null;
74
+ const payload = mmtpPacket.subarray(headerLength);
75
+ // Inline a minimal MPU-header read — only the fields extractAltaTrailer needs.
76
+ // payloadLength: 16 BE at bytes 0-1.
77
+ // Byte 2 packs FT(4) | T(1) | FI(2) | A(1).
78
+ const payloadLength = (payload[0] << 8) | payload[1];
79
+ const flagsByte = payload[2];
80
+ const fragmentType = (flagsByte >> 4) & 0x0f;
81
+ const timed = ((flagsByte >> 3) & 0x01) === 1;
82
+ const fragmentationIndicator = (flagsByte >> 1) & 0x03;
83
+ // Only MFU packets (FT=2) carry the detached trailer.
84
+ if (fragmentType !== 2)
85
+ return null;
86
+ // FI=0/1 structurally carries a DU header; FI=2/3 does not. Require at
87
+ // least one media byte after that deterministic layout so the bytes at the
88
+ // declared boundary cannot impersonate ALTA for a header-only fragment.
89
+ const dataUnitHeaderSize = fragmentationIndicator <= 1
90
+ ? (timed ? TIMED_MFU_DU_HEADER_SIZE : NONTIMED_MFU_DU_HEADER_SIZE)
91
+ : 0;
92
+ if (payloadLength <= MPU_HEADER_REMAINDER_SIZE + dataUnitHeaderSize)
93
+ return null;
94
+ if (fecType > 1)
95
+ return null;
96
+ const hasFecId = !options?.skipFecId && fecType === 1;
97
+ const payloadEnd = hasFecId ? payload.length - 4 : payload.length;
98
+ const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength;
99
+ if (declaredEnd > payloadEnd)
100
+ return null;
101
+ if (declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE > payloadEnd)
102
+ return null;
103
+ const extType = (payload[declaredEnd] << 8) | payload[declaredEnd + 1];
104
+ if (extType !== MMTP_EXT_TYPE_ALTA_AUTH)
105
+ return null;
106
+ const extLen = (payload[declaredEnd + 2] << 8) | payload[declaredEnd + 3];
107
+ const authStart = declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE;
108
+ if (authStart + extLen !== payloadEnd)
109
+ return null;
110
+ return {
111
+ auth: payload.subarray(authStart, payloadEnd),
112
+ signedEnd: headerLength + declaredEnd,
113
+ };
114
+ }
115
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@blockcast/mmt-alta-parse",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency leaf: extract the detached ALTA authenticator trailer from an MMTP source packet",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsc --watch",
21
+ "clean": "rm -rf dist",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest"
25
+ },
26
+ "dependencies": {},
27
+ "devDependencies": {
28
+ "typescript": "^5.3.0",
29
+ "vitest": "^1.0.0"
30
+ },
31
+ "keywords": [
32
+ "mmt",
33
+ "mmtp",
34
+ "alta",
35
+ "authenticator",
36
+ "trailer",
37
+ "iso23008-1"
38
+ ],
39
+ "license": "Apache-2.0",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/Blockcast/libmmt.git",
43
+ "directory": "packages/mmt-alta-parse"
44
+ }
45
+ }
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ MMTP_EXT_TRAILER_HEADER_SIZE,
4
+ MMTP_EXT_TYPE_ALTA_AUTH,
5
+ extractAltaTrailer,
6
+ } from './index.js'
7
+
8
+ const MMTP_HEADER_SIZE = 12
9
+ const MPU_HEADER_SIZE = 8
10
+ const TIMED_MFU_DU_HEADER_SIZE = 14
11
+
12
+ function buildPacket(options?: {
13
+ auth?: Uint8Array
14
+ appendFecId?: boolean
15
+ fecType?: number
16
+ fi?: number
17
+ media?: Uint8Array
18
+ payloadLength?: number
19
+ packetType?: number
20
+ timed?: boolean
21
+ trailing?: Uint8Array
22
+ wireDuSize?: number
23
+ }): Uint8Array {
24
+ const auth = options?.auth
25
+ const appendFecId = options?.appendFecId ?? false
26
+ const fecType = options?.fecType ?? (appendFecId ? 1 : 0)
27
+ const fi = options?.fi ?? 0
28
+ const media = options?.media ?? new Uint8Array([0xaa, 0xbb, 0xcc])
29
+ const timed = options?.timed ?? true
30
+ const structuralDuSize = fi <= 1 ? (timed ? TIMED_MFU_DU_HEADER_SIZE : 4) : 0
31
+ const duSize = options?.wireDuSize ?? structuralDuSize
32
+ const payloadLength = options?.payloadLength ?? (MPU_HEADER_SIZE - 2 + duSize + media.length)
33
+ const bytes: number[] = []
34
+
35
+ bytes.push((fecType & 0x03) << 3)
36
+ bytes.push(options?.packetType ?? 0)
37
+ bytes.push(0, 1)
38
+ bytes.push(0, 0, 0, 1)
39
+ bytes.push(0, 0, 0, 2)
40
+
41
+ bytes.push((payloadLength >> 8) & 0xff, payloadLength & 0xff)
42
+ bytes.push((2 << 4) | (timed ? 1 << 3 : 0) | (fi << 1))
43
+ bytes.push(0)
44
+ bytes.push(0, 0, 0, 3)
45
+
46
+ for (let i = 0; i < duSize; i++) bytes.push(0)
47
+ for (const value of media) bytes.push(value)
48
+
49
+ if (auth) {
50
+ bytes.push((MMTP_EXT_TYPE_ALTA_AUTH >> 8) & 0xff, MMTP_EXT_TYPE_ALTA_AUTH & 0xff)
51
+ bytes.push((auth.length >> 8) & 0xff, auth.length & 0xff)
52
+ for (const value of auth) bytes.push(value)
53
+ }
54
+ if (options?.trailing) {
55
+ for (const value of options.trailing) bytes.push(value)
56
+ }
57
+ if (appendFecId) bytes.push(0, 0, 0, 9)
58
+
59
+ return new Uint8Array(bytes)
60
+ }
61
+
62
+ describe('extractAltaTrailer', () => {
63
+ it('locates ALTA after the canonical ISO-declared span and before Source FEC ID', () => {
64
+ const media = new Uint8Array([0x10, 0x20, 0x30, 0x40])
65
+ const auth = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
66
+ const packet = buildPacket({ media, auth, appendFecId: true })
67
+
68
+ expect(extractAltaTrailer(packet)).toEqual({
69
+ auth,
70
+ signedEnd: MMTP_HEADER_SIZE + MPU_HEADER_SIZE + TIMED_MFU_DU_HEADER_SIZE + media.length,
71
+ })
72
+ })
73
+
74
+ it('locates ALTA in a recovered source symbol with the FEC ID already stripped', () => {
75
+ const auth = new Uint8Array([1, 2, 3, 4, 5])
76
+ const packet = buildPacket({ auth, fecType: 1 })
77
+ expect(extractAltaTrailer(packet, { skipFecId: true })?.auth).toEqual(auth)
78
+ expect(extractAltaTrailer(packet)).toBeNull()
79
+ })
80
+
81
+ it.each([
82
+ { fi: 0, duSize: TIMED_MFU_DU_HEADER_SIZE },
83
+ { fi: 1, duSize: TIMED_MFU_DU_HEADER_SIZE },
84
+ { fi: 2, duSize: 0 },
85
+ { fi: 3, duSize: 0 },
86
+ ])('uses the same declared coordinate for FI=$fi', ({ fi, duSize }) => {
87
+ const media = new Uint8Array([0x55, 0x66])
88
+ const auth = new Uint8Array([0x77])
89
+ const result = extractAltaTrailer(buildPacket({ fi, media, auth }))
90
+ expect(result?.signedEnd).toBe(MMTP_HEADER_SIZE + MPU_HEADER_SIZE + duSize + media.length)
91
+ expect(result?.auth).toEqual(auth)
92
+ })
93
+
94
+ it.each([0, 5])('rejects payload_length=%i because it cannot contain the MPU header remainder', (payloadLength) => {
95
+ expect(extractAltaTrailer(buildPacket({ auth: new Uint8Array([1]), payloadLength }))).toBeNull()
96
+ })
97
+
98
+ it.each([
99
+ { fi: 0, timed: true, payloadLength: 6, wireDuSize: 0 },
100
+ { fi: 0, timed: true, payloadLength: 20, wireDuSize: TIMED_MFU_DU_HEADER_SIZE },
101
+ { fi: 0, timed: false, payloadLength: 10, wireDuSize: 4 },
102
+ { fi: 2, timed: true, payloadLength: 6, wireDuSize: 0 },
103
+ ])('rejects an FT=2/FI=$fi declaration with no media after its structural DU header', (options) => {
104
+ expect(extractAltaTrailer(buildPacket({
105
+ ...options,
106
+ auth: new Uint8Array([1]),
107
+ media: new Uint8Array(),
108
+ }))).toBeNull()
109
+ })
110
+
111
+ it('rejects a declaration that overruns the packet', () => {
112
+ expect(extractAltaTrailer(buildPacket({
113
+ auth: new Uint8Array([1]),
114
+ payloadLength: 0xffff,
115
+ }))).toBeNull()
116
+ })
117
+
118
+ it('rejects the obsolete media-only declaration without scanning for ALTA', () => {
119
+ const media = new Uint8Array([0xf0, 0xa1, 0, 1, 0x99])
120
+ const packet = buildPacket({
121
+ media,
122
+ auth: new Uint8Array([0xaa]),
123
+ payloadLength: media.length,
124
+ })
125
+ expect(extractAltaTrailer(packet)).toBeNull()
126
+ })
127
+
128
+ it('returns null when the exact declared end has no ALTA trailer', () => {
129
+ expect(extractAltaTrailer(buildPacket({ appendFecId: true }))).toBeNull()
130
+ })
131
+
132
+ it('rejects bytes after the authenticator instead of accepting a partial suffix', () => {
133
+ const packet = buildPacket({
134
+ auth: new Uint8Array([0xaa, 0xbb]),
135
+ appendFecId: true,
136
+ trailing: new Uint8Array([0xcc]),
137
+ })
138
+ expect(extractAltaTrailer(packet)).toBeNull()
139
+ })
140
+
141
+ it('exports the four-byte ALTA trailer header size', () => {
142
+ expect(MMTP_EXT_TRAILER_HEADER_SIZE).toBe(4)
143
+ })
144
+ })
package/src/index.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Zero-dependency helper for extracting the detached ALTA authenticator trailer
3
+ * from an MMTP source packet.
4
+ *
5
+ * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path
6
+ * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share
7
+ * one implementation without creating a package cycle between them.
8
+ *
9
+ * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt §12 (detached trailer, Path 2).
10
+ */
11
+
12
+ // --- Private MMTP/MPU/MFU constants (ISO/IEC 23008-1 §9.2.2–9.2.3) ---
13
+ // Duplicated here as immutable spec values so this package stays a leaf with
14
+ // zero `@blockcast/*` dependencies. Re-deriving from `mmt-container` would
15
+ // reintroduce the package cycle this split exists to avoid.
16
+
17
+ const MMTP_HEADER_SIZE = 12
18
+ const MPU_HEADER_SIZE = 8
19
+ const MPU_LENGTH_FIELD_SIZE = 2
20
+ const MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE
21
+ const TIMED_MFU_DU_HEADER_SIZE = 14
22
+ const NONTIMED_MFU_DU_HEADER_SIZE = 4
23
+ const PACKET_TYPE_MPU = 0x00
24
+
25
+ // --- Public ALTA trailer constants ---
26
+
27
+ /**
28
+ * MMTP header-extension ext_type assigned to the detached ALTA authenticator
29
+ * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`
30
+ * block appended after the MPU's declared payload bytes, before the 4-byte
31
+ * Source FEC Payload ID. MMTP X-bit stays 0 — non-ALTA MMTP receivers treat
32
+ * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1
33
+ * §9.2.2 "may be discarded without impacting correct processing").
34
+ */
35
+ export const MMTP_EXT_TYPE_ALTA_AUTH = 0xf0a1
36
+
37
+ /** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */
38
+ export const MMTP_EXT_TRAILER_HEADER_SIZE = 4
39
+
40
+ /** Result of extracting the detached ALTA authenticator from an MMTP packet. */
41
+ export interface AltaTrailer {
42
+ /** Authenticator bytes (no ext_type/ext_length prefix, no trailing padding). */
43
+ auth: Uint8Array
44
+ /**
45
+ * Wire-packet byte offset where the signed payload ends, i.e. where the
46
+ * trailer begins. Caller uses this to produce
47
+ * `payload = wirePacket.subarray(0, signedEnd)` for verify_detached.
48
+ */
49
+ signedEnd: number
50
+ }
51
+
52
+ /**
53
+ * Extract the detached ALTA authenticator trailer from an MMTP source packet.
54
+ *
55
+ * Wire layout:
56
+ * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`
57
+ * `payload_length = 6 + DU bytes + media bytes`
58
+ *
59
+ * Returns `null` when:
60
+ * - the packet is too small / malformed,
61
+ * - `payload_length` cannot contain the remaining six MPU-header bytes,
62
+ * - the declared payload overruns the packet,
63
+ * - no bytes remain between `payload_length` and the SS_ID,
64
+ * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},
65
+ * - the declared `ext_length` would extend past the SS_ID boundary.
66
+ *
67
+ * Non-MPU packets (repair, signaling) always return `null`.
68
+ *
69
+ * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the
70
+ * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has
71
+ * already been stripped upstream.
72
+ */
73
+ export function extractAltaTrailer(
74
+ mmtpPacket: Uint8Array,
75
+ options?: { skipFecId?: boolean },
76
+ ): AltaTrailer | null {
77
+ if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null
78
+ // ISO/IEC 23008-1:2023 Figure 8 places V=0 packet_type in the low six bits.
79
+ const packetType = mmtpPacket[1]! & 0x3f
80
+ if (packetType !== PACKET_TYPE_MPU) return null
81
+
82
+ const fecType = (mmtpPacket[0]! >> 3) & 0x03
83
+ let headerLength = MMTP_HEADER_SIZE
84
+ if (fecType < 2 && (mmtpPacket[0]! & 0x20) !== 0) headerLength += 4
85
+ if (fecType < 2 && (mmtpPacket[0]! & 0x02) !== 0) {
86
+ if (headerLength + 4 > mmtpPacket.length) return null
87
+ const extensionLength = new DataView(
88
+ mmtpPacket.buffer,
89
+ mmtpPacket.byteOffset + headerLength + 2,
90
+ 2,
91
+ ).getUint16(0, false)
92
+ headerLength += 4 + extensionLength
93
+ }
94
+ if (headerLength + MPU_HEADER_SIZE > mmtpPacket.length) return null
95
+
96
+ const payload = mmtpPacket.subarray(headerLength)
97
+
98
+ // Inline a minimal MPU-header read — only the fields extractAltaTrailer needs.
99
+ // payloadLength: 16 BE at bytes 0-1.
100
+ // Byte 2 packs FT(4) | T(1) | FI(2) | A(1).
101
+ const payloadLength = (payload[0]! << 8) | payload[1]!
102
+ const flagsByte = payload[2]!
103
+ const fragmentType = (flagsByte >> 4) & 0x0f
104
+ const timed = ((flagsByte >> 3) & 0x01) === 1
105
+ const fragmentationIndicator = (flagsByte >> 1) & 0x03
106
+
107
+ // Only MFU packets (FT=2) carry the detached trailer.
108
+ if (fragmentType !== 2) return null
109
+
110
+ // FI=0/1 structurally carries a DU header; FI=2/3 does not. Require at
111
+ // least one media byte after that deterministic layout so the bytes at the
112
+ // declared boundary cannot impersonate ALTA for a header-only fragment.
113
+ const dataUnitHeaderSize = fragmentationIndicator <= 1
114
+ ? (timed ? TIMED_MFU_DU_HEADER_SIZE : NONTIMED_MFU_DU_HEADER_SIZE)
115
+ : 0
116
+ if (payloadLength <= MPU_HEADER_REMAINDER_SIZE + dataUnitHeaderSize) return null
117
+
118
+ if (fecType > 1) return null
119
+ const hasFecId = !options?.skipFecId && fecType === 1
120
+ const payloadEnd = hasFecId ? payload.length - 4 : payload.length
121
+ const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength
122
+ if (declaredEnd > payloadEnd) return null
123
+
124
+ if (declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE > payloadEnd) return null
125
+ const extType = (payload[declaredEnd]! << 8) | payload[declaredEnd + 1]!
126
+ if (extType !== MMTP_EXT_TYPE_ALTA_AUTH) return null
127
+ const extLen = (payload[declaredEnd + 2]! << 8) | payload[declaredEnd + 3]!
128
+
129
+ const authStart = declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE
130
+ if (authStart + extLen !== payloadEnd) return null
131
+
132
+ return {
133
+ auth: payload.subarray(authStart, payloadEnd),
134
+ signedEnd: headerLength + declaredEnd,
135
+ }
136
+ }