@aicent/rttp 0.1.2 → 1.2.7
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 +60 -48
- package/package.json +2 -2
- package/src/conformance.d.mts +1 -1
- package/src/conformance.mjs +117 -4
- package/src/rttp-uri.d.mts +4 -4
- package/src/rttp-uri.mjs +29 -29
- package/src/vectors.json +103 -13
package/README.md
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
# @aicent/rttp
|
|
2
2
|
|
|
3
|
-
**RTTP for JavaScript: `rttp://` intent addressing, `ROUTE_SHARD` derivation
|
|
3
|
+
**RTTP for JavaScript: `rttp://` intent addressing, `ROUTE_SHARD` derivation --
|
|
4
4
|
and the published conformance vectors that prove an implementation is right.**
|
|
5
5
|
|
|
6
|
-
[RFC-002
|
|
6
|
+
[RFC-002 sec. 10](https://rttp.com/RFC-002/) - spec v1.2.6 - zero dependencies - no build step
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## Verify it in one command
|
|
10
|
+
## Verify it in one command -- no trust required
|
|
11
11
|
|
|
12
12
|
```console
|
|
13
13
|
$ npx @aicent/rttp
|
|
14
|
-
[PASS] all
|
|
14
|
+
[PASS] all 35 checks passed (node, independent implementation)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
That is the point of this package. A specification is worth exactly what an
|
|
18
18
|
independent implementation can reproduce from it, so instead of asking you to
|
|
19
19
|
believe a table of numbers, the vectors ship inside the package and replay
|
|
20
|
-
locally
|
|
20
|
+
locally -- offline, no account, no network. `[PASS]` or it is not. Every vector file shipped here is listed in `vectors.manifest.json` with its byte count and SHA-256, produced by a generator rather than typed by hand - recompute the hash yourself if you prefer.
|
|
21
21
|
|
|
22
22
|
The JavaScript and Python sides **share no code**. They are separate
|
|
23
23
|
implementations of the same published vectors, and they agree byte for byte.
|
|
24
24
|
|
|
25
|
-
### The other half
|
|
25
|
+
### The other half -- Python, on PyPI
|
|
26
26
|
|
|
27
27
|
```console
|
|
28
28
|
$ pip install rttp
|
|
29
29
|
$ python -m rttp.selftest
|
|
30
|
-
[PASS] all
|
|
30
|
+
[PASS] all 80 checks passed (3 skipped) # default install
|
|
31
|
+
|
|
32
|
+
$ pip install rttp[ed25519]
|
|
33
|
+
$ python -m rttp.selftest
|
|
34
|
+
[PASS] all 83 checks passed # with the optional Ed25519 backend
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
Published as **`rttp`**
|
|
37
|
+
Published as **`rttp`** -- the bare name, because PyPI has no scopes and the name
|
|
34
38
|
was free. That package ships a **byte-identical copy** of this same vector set and
|
|
35
39
|
replays it offline, exactly as this one does: two registries, two languages, one
|
|
36
40
|
specification.
|
|
@@ -71,38 +75,38 @@ $ node examples/quickstart.mjs
|
|
|
71
75
|
459e543b73d86005b72ba77d5756e83c
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
Its expected output is the shard printed above
|
|
78
|
+
Its expected output is the shard printed above -- the value the published
|
|
75
79
|
conformance vectors pin for that address, so the example can be **checked**
|
|
76
80
|
rather than believed. It is also the value the Python implementation derives,
|
|
77
81
|
from code that shares nothing with this one.
|
|
78
82
|
|
|
79
|
-
Addressing is **DNS-free** (RFC-002
|
|
83
|
+
Addressing is **DNS-free** (RFC-002 sec. 10.5): the routing hash is derived from the
|
|
80
84
|
authority by SHA-256, so no registry, resolver or network is involved. A URI
|
|
81
|
-
either yields a shard or throws
|
|
85
|
+
either yields a shard or throws -- there is no fallback, and no `rttps`.
|
|
82
86
|
|
|
83
87
|
---
|
|
84
88
|
|
|
85
89
|
## The one rule that surprises people
|
|
86
90
|
|
|
87
|
-
**Malformed input is rejected, not normalised.** `rttp
|
|
88
|
-
`rttp://F3B2A1C4
|
|
89
|
-
US-ASCII (RFC-002
|
|
91
|
+
**Malformed input is rejected, not normalised.** `rttp://.../VESSEL` and
|
|
92
|
+
`rttp://F3B2A1C4..../vessel` both throw, because the canonical form is lowercase
|
|
93
|
+
US-ASCII (RFC-002 sec. 10.3) and a case variant is not a spelling difference -- it is
|
|
90
94
|
a *different string*. Admitting both would give one address two spellings, which
|
|
91
|
-
is the ambiguity
|
|
95
|
+
is the ambiguity sec. 10.3 exists to remove.
|
|
92
96
|
|
|
93
97
|
Nothing here is ever accepted because a check could not be performed: this
|
|
94
98
|
package fails closed everywhere.
|
|
95
99
|
|
|
96
100
|
---
|
|
97
101
|
|
|
98
|
-
## The browser's registration prefix
|
|
102
|
+
## The browser's registration prefix -- accepted, never advertised
|
|
99
103
|
|
|
100
104
|
A browser lets a page register a protocol handler only for a scheme it already
|
|
101
105
|
knows, or for one carrying the browser's own **reserved registration prefix**. So
|
|
102
106
|
the string a web page hands to a handler is not the bare address.
|
|
103
107
|
|
|
104
108
|
This package **accepts** that form, because refusing a string a browser is about
|
|
105
|
-
to deliver would break the handler path
|
|
109
|
+
to deliver would break the handler path -- but it is a **tolerance, and the
|
|
106
110
|
specification defines no such form**. The canonical `rttp://` address is what
|
|
107
111
|
this package reports, and the only form it ever emits: same authority, same path,
|
|
108
112
|
same **route shard** either way.
|
|
@@ -116,7 +120,7 @@ The `scheme` field tells you which form arrived; everything else is identical.
|
|
|
116
120
|
|
|
117
121
|
---
|
|
118
122
|
|
|
119
|
-
## API
|
|
123
|
+
## API -- main entry
|
|
120
124
|
|
|
121
125
|
| Export | What it does |
|
|
122
126
|
|:---|:---|
|
|
@@ -124,14 +128,14 @@ The `scheme` field tells you which form arrived; everything else is identical.
|
|
|
124
128
|
| `parseAndDerive(uri)` | Identical to `parse`; the specification's own wording. |
|
|
125
129
|
| `deriveRouteShard(authority)` | `Uint8Array(16)` = `SHA-256(ASCII(authority))[0:16]`. |
|
|
126
130
|
| `deriveRouteShardHex(authority)` | The same, as 32 lowercase hex characters. |
|
|
127
|
-
| `isValidAction(value)` |
|
|
131
|
+
| `isValidAction(value)` | sec. 10.2 `action` form test. The verb set is **open**. |
|
|
128
132
|
| `RttpUriError` | The only error type thrown. |
|
|
129
133
|
| `SCHEME`, `BROWSER_HANDLER_SCHEME`, `ROUTE_SHARD_BYTES`, `HASH_INTENT_LEN`, `ACTION_MAX_LEN`, `SPEC_RELEASE` | Constants. |
|
|
130
134
|
|
|
131
135
|
### Returned fields
|
|
132
136
|
|
|
133
137
|
Field names are `snake_case` **on purpose**: the published conformance vectors use
|
|
134
|
-
those names, so this output and the Python package's output compare directly
|
|
138
|
+
those names, so this output and the Python package's output compare directly --
|
|
135
139
|
field for field, byte for byte.
|
|
136
140
|
|
|
137
141
|
| Field | Meaning |
|
|
@@ -140,7 +144,7 @@ field for field, byte for byte.
|
|
|
140
144
|
| `intent` | 8 lowercase hex digits (32-bit routing hash) or a readable organ token |
|
|
141
145
|
| `intent_is_hash` / `intent_hash32` | Whether it is a hash-intent, and its value |
|
|
142
146
|
| `pillar`, `root` | The other two authority labels |
|
|
143
|
-
| `action` | Empty string when omitted (RFC-002
|
|
147
|
+
| `action` | Empty string when omitted (RFC-002 sec. 10.4 default operation) |
|
|
144
148
|
| `action_omitted` | The same fact as a boolean |
|
|
145
149
|
| `authority` | `<intent>.<pillar>.<root>`, canonical |
|
|
146
150
|
| `canonical_uri` | Always begins `rttp://` |
|
|
@@ -148,7 +152,7 @@ field for field, byte for byte.
|
|
|
148
152
|
|
|
149
153
|
---
|
|
150
154
|
|
|
151
|
-
## The conformance vectors
|
|
155
|
+
## The conformance vectors -- `@aicent/rttp/conformance`
|
|
152
156
|
|
|
153
157
|
```js
|
|
154
158
|
import { loadVectors, runConformance } from '@aicent/rttp/conformance';
|
|
@@ -172,32 +176,34 @@ In CI:
|
|
|
172
176
|
|
|
173
177
|
| Export | What it does |
|
|
174
178
|
|:---|:---|
|
|
175
|
-
| `runConformance(vectors)` | Replay a vector set
|
|
179
|
+
| `runConformance(vectors)` | Replay a vector set -> `{ checked, failures, passed }` |
|
|
176
180
|
| `loadVectors(path?)` | Parse a vector set; defaults to the shipped one |
|
|
177
181
|
| `DEFAULT_VECTORS_PATH` | Absolute path of the shipped `vectors.json` |
|
|
178
|
-
| `parseUri(uri)` | The independent RFC-002
|
|
179
|
-
| `buildFrame(fields)` | The independent 128-byte frame builder
|
|
180
|
-
| `ACTION_MAX_LEN` | `16`
|
|
182
|
+
| `parseUri(uri)` | The independent RFC-002 sec. 10.2 implementation -> `{ authority, action, shardHex }` |
|
|
183
|
+
| `buildFrame(fields)` | The independent 128-byte frame builder -> `Buffer` |
|
|
184
|
+
| `ACTION_MAX_LEN` | `16` -- the ACTION field's capacity |
|
|
181
185
|
|
|
182
186
|
`vectors.json` is plain JSON and ships inside the package, also reachable as the
|
|
183
187
|
subpath export `@aicent/rttp/vectors.json`. Any language can read it; that is
|
|
184
188
|
what it is for.
|
|
185
189
|
|
|
186
|
-
### What the
|
|
190
|
+
### What the 35 checks are
|
|
187
191
|
|
|
188
192
|
| Group | Count | What it proves |
|
|
189
193
|
|:---|:---|---:|
|
|
190
|
-
| `positive_uris` | 6 | Accept, canonicalise and derive
|
|
191
|
-
| `negative_uris` | 15 | **Reject**
|
|
192
|
-
| `frame_vectors` | 3 | URI
|
|
194
|
+
| `positive_uris` | 6 | Accept, canonicalise and derive -- including a hash-intent and an omitted action. **Five are conformance claims; one is a `tolerance`** -- the browser-registration prefix -- and the vector set labels it as such |
|
|
195
|
+
| `negative_uris` | 15 | **Reject** -- uppercase, userinfo, query, fragment, port, multi-segment path, wrong authority shape, trailing slash, foreign scheme, surrounding whitespace |
|
|
196
|
+
| `frame_vectors` | 3 | URI -> `ROUTE_SHARD` -> the 128-byte `PulseHeader128`, bit for bit |
|
|
193
197
|
| `compat_vector_spec_rev_0` | 1 | A frame written before the v1.2.6 extension block still verifies |
|
|
198
|
+
| `negative_frames` | 9 | **Reject** -- fail-closed frames: ACTION length/padding/embedded NUL/grammar, unknown SPEC_REV, SPEC_REV=0 with a non-zero extension, non-zero RESERVED, unknown FLAGS bit |
|
|
199
|
+
| `positive_frames` | 1 | Accept -- SPEC_REV=1 with ACTION_LEN=0 is valid and equivalent to an omitted action |
|
|
194
200
|
|
|
195
201
|
Each vector is deterministic: fixed sequence numbers, fixed timestamp, fixed AID.
|
|
196
202
|
Two implementations either agree byte for byte or they do not.
|
|
197
203
|
|
|
198
204
|
### Why the checker lives in this package, and why that is still honest
|
|
199
205
|
|
|
200
|
-
`src/conformance.mjs` is written from RFC-002
|
|
206
|
+
`src/conformance.mjs` is written from RFC-002 sec. 10 alone and **must never import
|
|
201
207
|
`src/rttp-uri.mjs`**. A conformance suite that reuses the code under test only
|
|
202
208
|
proves the code equals itself. The two files share no code, so the rule is
|
|
203
209
|
checkable by reading them.
|
|
@@ -209,8 +215,8 @@ not a verification step.
|
|
|
209
215
|
|
|
210
216
|
`buildFrame` **refuses** an action longer than the field can hold rather than
|
|
211
217
|
truncating it: a silently shortened verb would be a routing decision made by the
|
|
212
|
-
codec, and the codec does not get to make that decision. (
|
|
213
|
-
limit on `action`
|
|
218
|
+
codec, and the codec does not get to make that decision. (sec. 10.2 sets no length
|
|
219
|
+
limit on `action` -- a *parser* must accept a long one; a frame builder cannot.)
|
|
214
220
|
|
|
215
221
|
The vectors are **generated, not written**. `generated_by` in the JSON names the
|
|
216
222
|
generator, and the file says so itself: change the cases in the generator, not the
|
|
@@ -219,19 +225,19 @@ agreeing with its own bugs.
|
|
|
219
225
|
|
|
220
226
|
---
|
|
221
227
|
|
|
222
|
-
## Scope
|
|
228
|
+
## Scope -- what this package is not
|
|
223
229
|
|
|
224
230
|
| | |
|
|
225
231
|
|:---|:---|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
232
|
+
| OK **Validation** | Real, and specified (RFC-002 sec. 10.2 / sec. 10.3). |
|
|
233
|
+
| OK **Canonicalisation** | Real, and specified. |
|
|
234
|
+
| OK **ROUTE_SHARD derivation** | Real, and specified. |
|
|
235
|
+
| OK **Conformance vectors + independent replay** | Published and generated. |
|
|
236
|
+
| NO **Framing, as a library API** | `buildFrame` is exported from `./conformance`, where it exists to replay frame vectors -- it is not offered as a general codec. |
|
|
237
|
+
| NO **Transport** | Not here. There is no `send()`. |
|
|
238
|
+
| NO **Routing service** | Not here. The shard is computed locally; delivering a frame to that hash is an operator's job. |
|
|
239
|
+
| NO **Sealing (sec. 11)** | Separate artifacts. |
|
|
240
|
+
| NO **Browsers** | Node only -- it uses `node:crypto`. The derivation is trivial to reproduce elsewhere, and the vectors let you prove you did. |
|
|
235
241
|
|
|
236
242
|
---
|
|
237
243
|
|
|
@@ -240,18 +246,24 @@ agreeing with its own bugs.
|
|
|
240
246
|
| Ecosystem | Name | Status |
|
|
241
247
|
|:---|:---|:---|
|
|
242
248
|
| crates.io | `rttp` | held by this project |
|
|
243
|
-
| PyPI | `rttp` | **published**
|
|
244
|
-
| npm | **`@aicent/rttp`** | the bare `rttp` name on npm is held by an unrelated, unmaintained 2017 REST helper, and the `rttp` scope cannot be created at all
|
|
249
|
+
| PyPI | `rttp` | **published** -- the Python reference implementation; `pip install rttp` |
|
|
250
|
+
| npm | **`@aicent/rttp`** | the bare `rttp` name on npm is held by an unrelated, unmaintained 2017 REST helper, and the `rttp` scope cannot be created at all -- a personal account already holds that name. This package is therefore published under the organization this project controls; the package name itself is still just `rttp`. |
|
|
245
251
|
|
|
246
252
|
---
|
|
247
253
|
|
|
248
254
|
## Specification status
|
|
249
255
|
|
|
250
|
-
* **`rttp` URI scheme**
|
|
251
|
-
Provisional, **under review**. It is **not yet registered**
|
|
256
|
+
* **`rttp` URI scheme** -- submitted to IANA under RFC 7595, ticket **#1459939**,
|
|
257
|
+
Provisional, **under review**. It is **not yet registered** -- as of 2026-09-20
|
|
252
258
|
the IANA "URI Schemes" registry contains no `rttp` entry. Please describe it
|
|
253
259
|
that way.
|
|
254
|
-
* **
|
|
260
|
+
* **Internet-Draft (IETF)** -- the protocol is under IETF review as the
|
|
261
|
+
Individual Submission Internet-Draft `draft-li-rttp-intent-addressing`
|
|
262
|
+
(revision -01, posted 2026-09-20, informational):
|
|
263
|
+
https://datatracker.ietf.org/doc/draft-li-rttp-intent-addressing/ .
|
|
264
|
+
An Internet-Draft is a working document -- it is not an IETF standard and
|
|
265
|
+
carries no IETF endorsement.
|
|
266
|
+
* **Frame layout** -- RFC-002 sec. 4.1, extended by `SPEC/RTTP-FRAME-EXT-v1.2.6.md`.
|
|
255
267
|
|
|
256
268
|
Where this package and the specification disagree, **the specification wins and
|
|
257
269
|
this package is wrong.** Please report it.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicent/rttp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "RTTP for JavaScript: rttp:// intent validation, canonicalisation and ROUTE_SHARD derivation (RFC-002
|
|
3
|
+
"version": "1.2.7",
|
|
4
|
+
"description": "RTTP for JavaScript: rttp:// intent validation, canonicalisation and ROUTE_SHARD derivation (RFC-002 sec. 10), plus the published conformance vectors and an independent replay of them, including the 128-byte frame. Zero dependencies, no build step.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rttp",
|
|
7
7
|
"uri",
|
package/src/conformance.d.mts
CHANGED
|
@@ -95,7 +95,7 @@ export declare const DEFAULT_VECTORS_PATH: string;
|
|
|
95
95
|
/** The ACTION field is 16 bytes. */
|
|
96
96
|
export declare const ACTION_MAX_LEN: 16;
|
|
97
97
|
|
|
98
|
-
/** Independent implementation of RFC-002
|
|
98
|
+
/** Independent implementation of RFC-002 sec. 10.2. Throws `Error`. */
|
|
99
99
|
export declare function parseUri(uri: string): ParsedUri;
|
|
100
100
|
|
|
101
101
|
/** Independent implementation of the 128-byte frame. Throws `Error`. */
|
package/src/conformance.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* imports nothing from `src/rttp-uri.mjs` and must not: a conformance suite that
|
|
14
14
|
* reuses the code it is checking only proves that the code equals itself.
|
|
15
15
|
*
|
|
16
|
-
* Authority boundary: URI syntax = RFC-002
|
|
16
|
+
* Authority boundary: URI syntax = RFC-002 sec. 10.2; frame layout = sec. 4.1 as
|
|
17
17
|
* extended by `SPEC/RTTP-FRAME-EXT-v1.2.6.md`.
|
|
18
18
|
*
|
|
19
19
|
* Zero dependencies: `node:crypto` and `node:fs`, nothing else.
|
|
@@ -31,7 +31,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
31
31
|
export const DEFAULT_VECTORS_PATH = fileURLToPath(new URL('./vectors.json', import.meta.url));
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// Frame constants (
|
|
34
|
+
// Frame constants (sec. 4.1)
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
|
|
37
37
|
const MAGIC = 0x52545450;
|
|
@@ -42,7 +42,14 @@ const SIZE = 128;
|
|
|
42
42
|
const OFF_ACTION_LEN = 0x68;
|
|
43
43
|
const OFF_ACTION = 0x69;
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** Offsets fixed by RTTP-FRAME-EXT-v1.2.6 sec. 2 (extension block at 0x66..0x7F). */
|
|
46
|
+
const OFF_SPEC_REV = 0x66;
|
|
47
|
+
const OFF_FLAGS = 0x67;
|
|
48
|
+
const OFF_RESERVED = 0x79;
|
|
49
|
+
const KNOWN_SPEC_REVS = new Set([0, 1]);
|
|
50
|
+
const FLAG_URI_ANCHORED = 0x01;
|
|
51
|
+
|
|
52
|
+
/** The ACTION field is 16 bytes; ACTION_LEN is one byte. See RFC-002 sec. 10.2. */
|
|
46
53
|
export const ACTION_MAX_LEN = 16;
|
|
47
54
|
|
|
48
55
|
const TOKEN = /^[a-z0-9-]+$/;
|
|
@@ -53,7 +60,7 @@ const HASH_INTENT = /^[0-9a-f]{8}$/;
|
|
|
53
60
|
// ---------------------------------------------------------------------------
|
|
54
61
|
|
|
55
62
|
/**
|
|
56
|
-
* Parse one `rttp` URI - an implementation written from RFC-002
|
|
63
|
+
* Parse one `rttp` URI - an implementation written from RFC-002 sec. 10.2 alone,
|
|
57
64
|
* sharing no code with `src/rttp-uri.mjs`.
|
|
58
65
|
*
|
|
59
66
|
* @param {string} uri
|
|
@@ -159,6 +166,79 @@ export function buildFrame({ sequenceId, ttl, priority, shardHex, aidHex, timest
|
|
|
159
166
|
return buf; // 0x79..0x80 RESERVED = 0
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Verify a `PulseHeader128`, mirroring the Python `pulse_header.verify`.
|
|
171
|
+
*
|
|
172
|
+
* Implements the extension-block rules of RTTP-FRAME-EXT-v1.2.6 sec. 2 / sec. 4
|
|
173
|
+
* (R2-R7) and the ACTION grammar of RFC-002 sec. 10.2. Every failure throws: a
|
|
174
|
+
* check that cannot be performed is a failure, never a pass.
|
|
175
|
+
*
|
|
176
|
+
* @param {Buffer} raw 128 bytes
|
|
177
|
+
* @param {{ expectedAidOrigin?: string }} [options]
|
|
178
|
+
* @returns {{ specRev: number, flags: number, uriAnchored: boolean,
|
|
179
|
+
* action: string, actionOmitted: boolean }}
|
|
180
|
+
*/
|
|
181
|
+
export function verifyFrame(raw, options = {}) {
|
|
182
|
+
if (!Buffer.isBuffer(raw) || raw.length !== SIZE) {
|
|
183
|
+
throw new Error(`frame must be ${SIZE} bytes`);
|
|
184
|
+
}
|
|
185
|
+
if (raw.readUInt32BE(0x00) !== MAGIC) throw new Error('bad RTTP_MAGIC');
|
|
186
|
+
if (raw.readBigUInt64BE(0x0C) !== VERSION_ID) throw new Error('unsupported VERSION_ID');
|
|
187
|
+
|
|
188
|
+
const specRev = raw[OFF_SPEC_REV];
|
|
189
|
+
if (!KNOWN_SPEC_REVS.has(specRev)) {
|
|
190
|
+
throw new Error(`unknown SPEC_REV: ${specRev} (fail closed)`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const flags = raw[OFF_FLAGS];
|
|
194
|
+
const actionLen = raw[OFF_ACTION_LEN];
|
|
195
|
+
|
|
196
|
+
if (specRev === 0) {
|
|
197
|
+
// R6: a pre-v1.2.6 frame must have an all-zero extension block.
|
|
198
|
+
for (let i = OFF_SPEC_REV; i < SIZE; i += 1) {
|
|
199
|
+
if (raw[i] !== 0) throw new Error('SPEC_REV=0 but extension block is not all-zero');
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// R7: unknown FLAGS bits and any RESERVED byte are rejections.
|
|
203
|
+
if ((flags & ~FLAG_URI_ANCHORED) !== 0) {
|
|
204
|
+
throw new Error(`unknown FLAGS bits set: 0x${(flags & ~FLAG_URI_ANCHORED).toString(16).padStart(2, '0')}`);
|
|
205
|
+
}
|
|
206
|
+
for (let i = OFF_RESERVED; i < SIZE; i += 1) {
|
|
207
|
+
if (raw[i] !== 0) throw new Error('RESERVED bytes must be zero in SPEC_REV=1');
|
|
208
|
+
}
|
|
209
|
+
// R4: ACTION_LEN is one byte, the field holds 16.
|
|
210
|
+
if (actionLen > ACTION_MAX_LEN) {
|
|
211
|
+
throw new Error(`ACTION_LEN ${actionLen} > ${ACTION_MAX_LEN}`);
|
|
212
|
+
}
|
|
213
|
+
for (let i = OFF_ACTION + actionLen; i < OFF_ACTION + ACTION_MAX_LEN; i += 1) {
|
|
214
|
+
if (raw[i] !== 0) throw new Error('ACTION padding must be zero');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let action = '';
|
|
219
|
+
if (specRev === 1 && actionLen > 0) {
|
|
220
|
+
action = raw.subarray(OFF_ACTION, OFF_ACTION + actionLen).toString('ascii');
|
|
221
|
+
if (!TOKEN.test(action)) {
|
|
222
|
+
throw new Error(`ACTION violates RFC-002 sec. 10.2: ${JSON.stringify(action)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (options.expectedAidOrigin !== undefined) {
|
|
227
|
+
const got = raw.subarray(0x46, 0x66).toString('hex');
|
|
228
|
+
if (got !== options.expectedAidOrigin) {
|
|
229
|
+
throw new Error('AID_ORIGIN does not match the expected origin');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
specRev,
|
|
235
|
+
flags,
|
|
236
|
+
uriAnchored: (flags & FLAG_URI_ANCHORED) !== 0,
|
|
237
|
+
action,
|
|
238
|
+
actionOmitted: action === '',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
162
242
|
// ---------------------------------------------------------------------------
|
|
163
243
|
// Vector loading and replay
|
|
164
244
|
// ---------------------------------------------------------------------------
|
|
@@ -219,6 +299,31 @@ export function runConformance(vectors) {
|
|
|
219
299
|
if (frame.toString('hex') !== c.header_hex) {
|
|
220
300
|
failures.push(`${c.uri} :: frame bytes mismatch`);
|
|
221
301
|
}
|
|
302
|
+
try {
|
|
303
|
+
verifyFrame(frame, { expectedAidOrigin: aidHex });
|
|
304
|
+
} catch (err) {
|
|
305
|
+
failures.push(`${c.uri} :: published frame rejected by verifyFrame: ${err.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const c of (vectors.negative_frames || [])) {
|
|
310
|
+
checked += 1;
|
|
311
|
+
try {
|
|
312
|
+
verifyFrame(Buffer.from(c.header_hex, 'hex'), { expectedAidOrigin: aidHex });
|
|
313
|
+
failures.push(`frame ${c.rule} :: ${c.label} :: expected REJECT but accepted`);
|
|
314
|
+
} catch { /* expected */ }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const c of (vectors.positive_frames || [])) {
|
|
318
|
+
checked += 1;
|
|
319
|
+
try {
|
|
320
|
+
const got = verifyFrame(Buffer.from(c.header_hex, 'hex'), { expectedAidOrigin: aidHex });
|
|
321
|
+
if (got.action !== c.action) {
|
|
322
|
+
failures.push(`frame ${c.rule} :: action ${got.action} != ${c.action}`);
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
failures.push(`frame ${c.rule} :: ${c.label} :: expected ACCEPT but rejected: ${err.message}`);
|
|
326
|
+
}
|
|
222
327
|
}
|
|
223
328
|
|
|
224
329
|
{
|
|
@@ -239,6 +344,14 @@ export function runConformance(vectors) {
|
|
|
239
344
|
if (frame.toString('hex') !== cv.header_hex) {
|
|
240
345
|
failures.push('compat vector :: frame bytes mismatch');
|
|
241
346
|
}
|
|
347
|
+
try {
|
|
348
|
+
const got = verifyFrame(Buffer.from(cv.header_hex, 'hex'), { expectedAidOrigin: aidHex });
|
|
349
|
+
if (got.specRev !== 0 || !got.actionOmitted) {
|
|
350
|
+
failures.push('compat vector :: SPEC_REV=0 semantics mismatch');
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
failures.push(`compat vector :: verifyFrame rejected an old frame: ${err.message}`);
|
|
354
|
+
}
|
|
242
355
|
}
|
|
243
356
|
|
|
244
357
|
return { checked, failures, passed: failures.length === 0 };
|
package/src/rttp-uri.d.mts
CHANGED
|
@@ -17,7 +17,7 @@ export interface RttpUri {
|
|
|
17
17
|
intent_hash32: number | null;
|
|
18
18
|
pillar: string;
|
|
19
19
|
root: string;
|
|
20
|
-
/** Empty string when omitted (
|
|
20
|
+
/** Empty string when omitted (sec. 10.4 default operation). */
|
|
21
21
|
action: string;
|
|
22
22
|
action_omitted: boolean;
|
|
23
23
|
/** `<intent>.<pillar>.<root>`, canonical. */
|
|
@@ -30,7 +30,7 @@ export interface RttpUri {
|
|
|
30
30
|
route_shard_hex: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** The URI does not conform. Always fail closed (
|
|
33
|
+
/** The URI does not conform. Always fail closed (sec. 10.5). */
|
|
34
34
|
export declare class RttpUriError extends Error {
|
|
35
35
|
constructor(message: string);
|
|
36
36
|
name: 'RttpUriError';
|
|
@@ -42,7 +42,7 @@ export declare const SCHEME: 'rttp';
|
|
|
42
42
|
/** The browser protocol-handler form - a property of the client, never of the target. */
|
|
43
43
|
export declare const BROWSER_HANDLER_SCHEME: 'web+rttp';
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** sec. 4.1: ROUTE_SHARD is a u128. */
|
|
46
46
|
export declare const ROUTE_SHARD_BYTES: 16;
|
|
47
47
|
|
|
48
48
|
/** Length of a hash-intent, in lowercase hex characters. */
|
|
@@ -60,7 +60,7 @@ export declare function parse(uri: string): RttpUri;
|
|
|
60
60
|
/** Identical to `parse`. */
|
|
61
61
|
export declare function parseAndDerive(uri: string): RttpUri;
|
|
62
62
|
|
|
63
|
-
/**
|
|
63
|
+
/** sec. 10.2 `action` syntax test. Judges form only - the verb set is open. */
|
|
64
64
|
export declare function isValidAction(value: unknown): boolean;
|
|
65
65
|
|
|
66
66
|
/** `authority` -> ROUTE_SHARD (16 bytes). Throws `RttpUriError`. */
|
package/src/rttp-uri.mjs
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aicent/rttp - RFC-002
|
|
2
|
+
* @aicent/rttp - RFC-002 sec. 10 `rttp` URI validation, canonicalisation and
|
|
3
3
|
* ROUTE_SHARD derivation.
|
|
4
4
|
*
|
|
5
|
-
* This module is the reference implementation of the mapping RFC-002
|
|
5
|
+
* This module is the reference implementation of the mapping RFC-002 sec. 10.4
|
|
6
6
|
* promises:
|
|
7
7
|
* "Dereferencing an `rttp` URI emits one pulse ... carrying `action` as the
|
|
8
8
|
* intent verb."
|
|
9
|
-
* The specification previously had syntax (
|
|
9
|
+
* The specification previously had syntax (sec. 10.2 ABNF) but no "URI -> frame
|
|
10
10
|
* field" mapping; this module and `SPEC/RTTP-FRAME-EXT-v1.2.6.md` supply that
|
|
11
11
|
* segment. It is an independent implementation of the same conformance vectors
|
|
12
12
|
* as the Python package `rttp` on PyPI - two implementations that share no code
|
|
13
13
|
* and agree byte for byte is what makes the vectors worth anything.
|
|
14
14
|
*
|
|
15
15
|
* Authority (important):
|
|
16
|
-
* * URI **syntax** - the sole authority is the RFC-002
|
|
16
|
+
* * URI **syntax** - the sole authority is the RFC-002 sec. 10.2 ABNF. This module
|
|
17
17
|
* adds no syntax.
|
|
18
18
|
* * This module does exactly two things: (1) validate and **canonicalise**
|
|
19
|
-
* per
|
|
19
|
+
* per sec. 10.2/sec. 10.3; (2) derive ROUTE_SHARD.
|
|
20
20
|
*
|
|
21
21
|
* Design constraints (each is a requirement in the specification):
|
|
22
|
-
* *
|
|
22
|
+
* * sec. 10.1 `intent` is either 8 lowercase hex digits (a 32-bit routing hash)
|
|
23
23
|
* or a readable organ token.
|
|
24
|
-
* *
|
|
25
|
-
* *
|
|
24
|
+
* * sec. 10.2 `action` is an **open set**: 1*( %x61-7A / DIGIT / "-" ).
|
|
25
|
+
* * sec. 10.3 The canonical form is **lowercase US-ASCII**; there is no userinfo,
|
|
26
26
|
* port, query or fragment.
|
|
27
|
-
* *
|
|
27
|
+
* * sec. 10.5 **No DNS**: ROUTE_SHARD is pure computation - no registry, no
|
|
28
28
|
* resolver, no network.
|
|
29
29
|
*
|
|
30
30
|
* Case is **not** normalised. A case variant is not a spelling difference, it is
|
|
31
|
-
* a different string, and it is rejected (
|
|
31
|
+
* a different string, and it is rejected (sec. 10.3). Nothing here is ever accepted
|
|
32
32
|
* because a check could not be performed: this module fails closed.
|
|
33
33
|
*
|
|
34
|
-
* ROUTE_SHARD derivation (see spec
|
|
34
|
+
* ROUTE_SHARD derivation (see spec sec. 5): the first 16 bytes of
|
|
35
35
|
* SHA-256(ASCII(canonical_authority)).
|
|
36
36
|
*
|
|
37
37
|
* Zero dependencies - `node:crypto` and nothing else.
|
|
@@ -54,22 +54,22 @@ export const SCHEME = 'rttp';
|
|
|
54
54
|
* **client**, never of the target: both spellings name the same address, and the
|
|
55
55
|
* canonical output of this module is always `rttp://`.
|
|
56
56
|
*
|
|
57
|
-
* Accepted for **tolerance only**
|
|
57
|
+
* Accepted for **tolerance only** -- the specification defines no such form. A
|
|
58
58
|
* conforming third-party implementation is not required to reproduce this
|
|
59
59
|
* tolerance; refusing the string a browser is about to deliver would, however,
|
|
60
60
|
* break the handler path.
|
|
61
61
|
*/
|
|
62
62
|
export const BROWSER_HANDLER_SCHEME = 'web+rttp';
|
|
63
63
|
|
|
64
|
-
/**
|
|
64
|
+
/** sec. 4.1: ROUTE_SHARD is a u128. */
|
|
65
65
|
export const ROUTE_SHARD_BYTES = 16;
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/** sec. 10.2: `lowhex = %x30-39 / %x61-66`; 8 of them is a 32-bit routing hash. */
|
|
68
68
|
export const HASH_INTENT_LEN = 8;
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* The capacity of the frame's ACTION field (`0x69`..`0x78`, one byte of
|
|
72
|
-
* ACTION_LEN at `0x68`). **Not** a syntax rule:
|
|
72
|
+
* ACTION_LEN at `0x68`). **Not** a syntax rule: sec. 10.2 places no length limit on
|
|
73
73
|
* `action`, so `parse()` accepts longer values. A frame builder must reject
|
|
74
74
|
* them, because the field cannot hold more.
|
|
75
75
|
*/
|
|
@@ -78,11 +78,11 @@ export const ACTION_MAX_LEN = 16;
|
|
|
78
78
|
/** The RTTP specification release this module was written against. */
|
|
79
79
|
export const SPEC_RELEASE = 'v1.2.6';
|
|
80
80
|
|
|
81
|
-
//
|
|
81
|
+
// sec. 10.2: name-intent / pillar / root / action = 1*( %x61-7A / DIGIT / "-" )
|
|
82
82
|
const TOKEN = /^[a-z0-9-]+$/;
|
|
83
83
|
const LOWHEX = /^[0-9a-f]+$/;
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// sec. 10.3: this scheme defines no userinfo / port / query / fragment
|
|
86
86
|
const FORBIDDEN_CHARS = ['@', '?', '#', '[', ']'];
|
|
87
87
|
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
@@ -90,7 +90,7 @@ const FORBIDDEN_CHARS = ['@', '?', '#', '[', ']'];
|
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* The URI does not conform. **Always fail closed** (
|
|
93
|
+
* The URI does not conform. **Always fail closed** (sec. 10.5: no fallback, and no
|
|
94
94
|
* `rttps`).
|
|
95
95
|
*/
|
|
96
96
|
export class RttpUriError extends Error {
|
|
@@ -105,7 +105,7 @@ export class RttpUriError extends Error {
|
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
108
|
+
* sec. 10.2 token check, with the case rule of sec. 10.3 kept separate so that an
|
|
109
109
|
* uppercase letter produces a message that says what is actually wrong.
|
|
110
110
|
*
|
|
111
111
|
* @param {string} value
|
|
@@ -118,10 +118,10 @@ function checkToken(value, what) {
|
|
|
118
118
|
for (const ch of value) {
|
|
119
119
|
if (TOKEN.test(ch)) continue;
|
|
120
120
|
if (ch !== ch.toLowerCase()) {
|
|
121
|
-
//
|
|
121
|
+
// sec. 10.3: the canonical form is lowercase. Uppercase is not a normalisable
|
|
122
122
|
// spelling difference - it is illegal input.
|
|
123
123
|
throw new RttpUriError(
|
|
124
|
-
`${what} contains uppercase '${ch}' - lowercase US-ASCII only (RFC-002
|
|
124
|
+
`${what} contains uppercase '${ch}' - lowercase US-ASCII only (RFC-002 sec. 10.3)`);
|
|
125
125
|
}
|
|
126
126
|
throw new RttpUriError(
|
|
127
127
|
`${what} contains illegal character ${JSON.stringify(ch)} (allowed: a-z, 0-9, '-')`);
|
|
@@ -142,7 +142,7 @@ function checkIntent(value) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
*
|
|
145
|
+
* sec. 10.2 `action` syntax test: 1*( %x61-7A / DIGIT / "-" ).
|
|
146
146
|
*
|
|
147
147
|
* `action` is an **open set** - this only judges the *form*, never whether the
|
|
148
148
|
* verb is one this specification knows. Frame builders reuse it so the rule is
|
|
@@ -168,10 +168,10 @@ export function isValidAction(value) {
|
|
|
168
168
|
*
|
|
169
169
|
* Properties:
|
|
170
170
|
* * **Deterministic** - one authority always yields one shard.
|
|
171
|
-
* * **Pure computation** - no DNS, no registry, no network (
|
|
171
|
+
* * **Pure computation** - no DNS, no registry, no network (sec. 10.5).
|
|
172
172
|
* * **One-way** - the authority cannot be recovered from the shard.
|
|
173
173
|
* * **Independent of `action`** - it routes *where*, never *what*, which is
|
|
174
|
-
* exactly why `action` must be a separate frame field (see spec
|
|
174
|
+
* exactly why `action` must be a separate frame field (see spec sec. 5.3).
|
|
175
175
|
*
|
|
176
176
|
* @param {string} canonicalAuthority
|
|
177
177
|
* @returns {Uint8Array} 16 bytes
|
|
@@ -181,7 +181,7 @@ export function deriveRouteShard(canonicalAuthority) {
|
|
|
181
181
|
throw new RttpUriError('canonical_authority is empty');
|
|
182
182
|
}
|
|
183
183
|
if (canonicalAuthority !== canonicalAuthority.toLowerCase()) {
|
|
184
|
-
throw new RttpUriError('canonical_authority must be lowercase (RFC-002
|
|
184
|
+
throw new RttpUriError('canonical_authority must be lowercase (RFC-002 sec. 10.3)');
|
|
185
185
|
}
|
|
186
186
|
const digest = createHash('sha256')
|
|
187
187
|
.update(Buffer.from(canonicalAuthority, 'ascii'))
|
|
@@ -206,7 +206,7 @@ export function deriveRouteShardHex(canonicalAuthority) {
|
|
|
206
206
|
*
|
|
207
207
|
* The validation order deliberately stays "reject before interpreting": any
|
|
208
208
|
* suspicious form throws immediately, and nothing is ever guessed at.
|
|
209
|
-
* (
|
|
209
|
+
* (sec. 10.5: user agents that do not implement this scheme fail closed.)
|
|
210
210
|
*
|
|
211
211
|
* Field names are `snake_case` on purpose: the published conformance vectors use
|
|
212
212
|
* those names, so the output of the Python and JavaScript implementations
|
|
@@ -241,7 +241,7 @@ export function parse(uri) {
|
|
|
241
241
|
throw new RttpUriError('uri must not contain leading/trailing whitespace');
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
// --- scheme (
|
|
244
|
+
// --- scheme (sec. 10.6 prefix whitelist) ---
|
|
245
245
|
let scheme;
|
|
246
246
|
let rest;
|
|
247
247
|
if (uri.startsWith(`${BROWSER_HANDLER_SCHEME}://`)) {
|
|
@@ -255,7 +255,7 @@ export function parse(uri) {
|
|
|
255
255
|
`not an rttp URI: must begin with '${SCHEME}://' or '${BROWSER_HANDLER_SCHEME}://'`);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// ---
|
|
258
|
+
// --- sec. 10.3 excluded characters ---
|
|
259
259
|
for (const ch of FORBIDDEN_CHARS) {
|
|
260
260
|
if (uri.includes(ch)) {
|
|
261
261
|
throw new RttpUriError(
|
|
@@ -273,7 +273,7 @@ export function parse(uri) {
|
|
|
273
273
|
throw new RttpUriError("path must be a single segment '/<action>'");
|
|
274
274
|
}
|
|
275
275
|
if (slash !== -1 && action === '') {
|
|
276
|
-
//
|
|
276
|
+
// sec. 10.2: path = "/" action, action = 1*(...). A trailing slash with an empty
|
|
277
277
|
// action is a different form from "no path", and the two must not be
|
|
278
278
|
// conflated.
|
|
279
279
|
throw new RttpUriError("trailing '/' with empty action: path must be '/<action>'");
|
package/src/vectors.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"spec": "RTTP-FRAME-EXT-v1.2.6",
|
|
3
3
|
"applies_to": [
|
|
4
|
-
"RFC-002
|
|
5
|
-
"RFC-002
|
|
4
|
+
"RFC-002 sec. 4.1",
|
|
5
|
+
"RFC-002 sec. 10"
|
|
6
6
|
],
|
|
7
7
|
"generated_by": "SPEC/tools/conformance.py",
|
|
8
8
|
"note": "Generated file - do not edit by hand. Change the cases in conformance.py instead.",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"action_omitted": false,
|
|
79
79
|
"route_shard_hex": "cdc32d19641c84aca3292de6276de056",
|
|
80
80
|
"tolerance": true,
|
|
81
|
-
"note": "tolerance - the specification defines no such form; accepted because it is the scheme name under which a handler is registered (RFC-002
|
|
81
|
+
"note": "tolerance - the specification defines no such form; accepted because it is the scheme name under which a handler is registered (RFC-002 sec. 10.6)"
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"uri": "rttp://a-b-c.d-e.f-g/audit-v2",
|
|
@@ -98,35 +98,35 @@
|
|
|
98
98
|
"negative_uris": [
|
|
99
99
|
{
|
|
100
100
|
"uri": "rttp://f3b2a1c4.rttp.aicent/VESSEL",
|
|
101
|
-
"reason": "uppercase action - lowercase only (RFC-002
|
|
101
|
+
"reason": "uppercase action - lowercase only (RFC-002 sec. 10.3)"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"uri": "rttp://F3B2A1C4.rttp.aicent/vessel",
|
|
105
|
-
"reason": "uppercase intent - lowercase only (RFC-002
|
|
105
|
+
"reason": "uppercase intent - lowercase only (RFC-002 sec. 10.3)"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"uri": "RTTP://f3b2a1c4.rttp.aicent/vessel",
|
|
109
|
-
"reason": "uppercase scheme prefix - canonical form is lowercase (RFC-002
|
|
109
|
+
"reason": "uppercase scheme prefix - canonical form is lowercase (RFC-002 sec. 10.3)"
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
"uri": "WEB+RTTP://f3b2a1c4.rttp.aicent/vessel",
|
|
113
|
-
"reason": "uppercase scheme prefix on the handler form (RFC-002
|
|
113
|
+
"reason": "uppercase scheme prefix on the handler form (RFC-002 sec. 10.3)"
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"uri": "rttp://subject@rttp.aicent/vessel",
|
|
117
|
-
"reason": "userinfo is not defined (RFC-002
|
|
117
|
+
"reason": "userinfo is not defined (RFC-002 sec. 10.3)"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"uri": "rttp://f3b2a1c4.rttp.aicent/vessel?x=1",
|
|
121
|
-
"reason": "query is not defined (RFC-002
|
|
121
|
+
"reason": "query is not defined (RFC-002 sec. 10.3)"
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
"uri": "rttp://f3b2a1c4.rttp.aicent/vessel#f",
|
|
125
|
-
"reason": "fragment is not defined (RFC-002
|
|
125
|
+
"reason": "fragment is not defined (RFC-002 sec. 10.3)"
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
"uri": "rttp://f3b2a1c4.rttp.aicent:443/vessel",
|
|
129
|
-
"reason": "port is not defined (RFC-002
|
|
129
|
+
"reason": "port is not defined (RFC-002 sec. 10.3)"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"uri": "rttp://f3b2a1c4.rttp.aicent/a/b",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"uri": "https://f3b2a1c4.rttp.aicent/vessel",
|
|
149
|
-
"reason": "not an rttp scheme - there is no fallback (RFC-002
|
|
149
|
+
"reason": "not an rttp scheme - there is no fallback (RFC-002 sec. 10.5)"
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
"uri": "rttp://f3b2a1c4.rttp.aicent/vessel ",
|
|
@@ -198,13 +198,103 @@
|
|
|
198
198
|
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000030000000000000000186cc6acd4b0000080ffbf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d0101000000000000000000000000000000000000000000000000"
|
|
199
199
|
}
|
|
200
200
|
],
|
|
201
|
+
"negative_frames": [
|
|
202
|
+
{
|
|
203
|
+
"label": "ACTION_LEN exceeds the 16-byte maximum (17)",
|
|
204
|
+
"rule": "R4",
|
|
205
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
|
|
206
|
+
"expected": "reject",
|
|
207
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01011176657373656c0000000000000000000000000000000000"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"label": "ACTION padding is non-zero",
|
|
211
|
+
"rule": "R4",
|
|
212
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
|
|
213
|
+
"expected": "reject",
|
|
214
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010676657373656c0078000000000000000000000000000000"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"label": "ACTION contains an embedded NUL",
|
|
218
|
+
"rule": "R4",
|
|
219
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
|
|
220
|
+
"expected": "reject",
|
|
221
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010676657300656c0000000000000000000000000000000000"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"label": "ACTION violates the sec. 10.2 grammar (uppercase)",
|
|
225
|
+
"rule": "R4",
|
|
226
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
|
|
227
|
+
"expected": "reject",
|
|
228
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010656455353454c0000000000000000000000000000000000"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"label": "SPEC_REV is outside the known set (2)",
|
|
232
|
+
"rule": "R5",
|
|
233
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
|
|
234
|
+
"expected": "reject",
|
|
235
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d02010676657373656c0000000000000000000000000000000000"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"label": "SPEC_REV is outside the known set (255)",
|
|
239
|
+
"rule": "R5",
|
|
240
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
|
|
241
|
+
"expected": "reject",
|
|
242
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789dff010676657373656c0000000000000000000000000000000000"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"label": "SPEC_REV=0 with a non-zero extension block",
|
|
246
|
+
"rule": "R6",
|
|
247
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
|
|
248
|
+
"expected": "reject",
|
|
249
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d00010676657373656c0000000000000000000000000000000000"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"label": "RESERVED is non-zero under SPEC_REV=1",
|
|
253
|
+
"rule": "R7",
|
|
254
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
|
|
255
|
+
"expected": "reject",
|
|
256
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010676657373656c0000000000000000000001000000000000"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"label": "unknown FLAGS bit set under SPEC_REV=1",
|
|
260
|
+
"rule": "R7",
|
|
261
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
|
|
262
|
+
"expected": "reject",
|
|
263
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01030676657373656c0000000000000000000000000000000000"
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
"positive_frames": [
|
|
267
|
+
{
|
|
268
|
+
"label": "SPEC_REV=1 with ACTION_LEN=0 is valid and equivalent to omission",
|
|
269
|
+
"rule": "R3",
|
|
270
|
+
"spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
|
|
271
|
+
"expected": "accept",
|
|
272
|
+
"action": "",
|
|
273
|
+
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d0101000000000000000000000000000000000000000000000000"
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
"envelope_vector": {
|
|
277
|
+
"spec": "RTTP-SEAL-ENVELOPE-v1.2.6 sec. 7",
|
|
278
|
+
"alg": "ed25519",
|
|
279
|
+
"seed_hex": "e722fed924c7f76b157faab674ea97bb7c3075f55ad7d3c6319418e22b86d611",
|
|
280
|
+
"aid": "3125ee20b2238281f70ca408192f82152647d365219184fa1fe22c08a42c035e",
|
|
281
|
+
"pub": "9c097c66ab4f82136ceddaef56854f740bf87da3eec42a249ba40bbb066f1e6a",
|
|
282
|
+
"ts": 1760000000,
|
|
283
|
+
"nonce": "0011223344556677",
|
|
284
|
+
"payload": {
|
|
285
|
+
"intent": "verify",
|
|
286
|
+
"task_id": "selftest-0001"
|
|
287
|
+
},
|
|
288
|
+
"signing_input_hex": "727474702d7365616c2d76310a7b22616964223a2233313235656532306232323338323831663730636134303831393266383231353236343764333635323139313834666131666532326330386134326330333565222c22616c67223a2265643235353139222c226e6f6e6365223a2230303131323233333434353536363737222c227061796c6f6164223a7b22696e74656e74223a22766572696679222c227461736b5f6964223a2273656c66746573742d30303031227d2c22707562223a2239633039376336366162346638323133366365646461656635363835346637343062663837646133656563343261323439626134306262623036366631653661222c227473223a313736303030303030307d",
|
|
289
|
+
"sig": "8d8e0d458b1defb6fd078343e7e46fc96cbaa05b6ead9e587d324e4c2358145790042f4a5001f3d902e7da670e2ea5700903363dd2825159a25dc262b25cb305"
|
|
290
|
+
},
|
|
201
291
|
"compat_vector_spec_rev_0": {
|
|
202
292
|
"uri": "rttp://f3b2a1c4.rttp.aicent/vessel",
|
|
203
293
|
"spec_rev": 0,
|
|
204
294
|
"action": "",
|
|
205
295
|
"action_omitted": true,
|
|
206
296
|
"uri_anchored": false,
|
|
207
|
-
"expected": "verify() MUST accept; action is treated as omitted (RFC-002
|
|
297
|
+
"expected": "verify() MUST accept; action is treated as omitted (RFC-002 sec. 10.4 default operation)",
|
|
208
298
|
"header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d0000000000000000000000000000000000000000000000000000"
|
|
209
299
|
}
|
|
210
300
|
}
|