[WIP] Security work While on holiday

This commit is contained in:
Alex 2026-08-12 22:45:28 +02:00
commit 7f0231e3f1
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
109 changed files with 19694 additions and 5210 deletions

View file

@ -6,21 +6,84 @@ This file documents the Type Map & Registry configuration used by the MTP protoc
Every transport frame is a four-byte big-endian length followed by one `CommunicationValue`. The length counts all bytes after the length field.
This is the only transport frame length prefix. Transports write the
`CommunicationValue` bytes directly and do not add another length before this
field. The close-frame sentinel occupies the same four-byte position.
```text
u32 length
u16 communication_type
u8 flags
u32 id if flag 0x04 is set
u48 sender if flag 0x01 is set
u48 receiver if flag 0x02 is set
u8 signature_type if flag 0x10 is set
... signature if flag 0x10 is set, length depends on signature_type
... data container or encrypted payload
[4 bytes total length]
[2 bytes communication type]
[1 byte flags]
bit 0 = has ID
bit 1 = has sender ID
bit 2 = has receiver ID
bits 3-7 must be zero
[4 bytes ID] if bit 0
[8 bytes sender ID] if bit 1
[8 bytes receiver ID] if bit 2
[DataValue payload]
```
The flag values are `0x01` for sender, `0x02` for receiver, `0x04` for frame ID, `0x08` for encrypted data, `0x10` for a frame signature, and `0x20` for a signed encrypted container. Sender and receiver IDs are six-byte unsigned big-endian values. The `communication_type` and every container field use IDs from the negotiated `TypeMap`.
The only defined flag values are `0x01` for ID, `0x02` for sender, and `0x04` for receiver. Unknown flag bits are rejected. IDs are full-width unsigned big-endian values: the correlation ID is `u32`, while sender and receiver IDs are `u64`. Encryption and signing are properties of the `DataValue` payload, never of the frame header.
Data values begin with a one-byte kind marker. MTP assigns `0x01` and `0x02` to boolean true and false, `0x03` to signed integers, `0x04` to unsigned integers, `0x05` to floats, `0x06` to UTF-8 strings, `0x07` to bytes, `0x08` to arrays, `0x09` to containers, `0x0A` through `0x0C` to crypto containers, and `0xFF` to null. Length-prefixed values use a four-byte big-endian payload length; container and array counts use two-byte big-endian counts.
`Relay` is the reserved opaque application communication type. Relay frames
omit the outer sender, expose only the next-hop receiver and transport
correlation data, and carry the actual operation and application metadata in
their protected payload.
## DataValue Wire Format
Every `DataValue` begins with a one-byte kind marker. MTP assigns `0x01` and `0x02` to boolean true and false, `0x03` to signed `i128`, `0x04` to unsigned `u128`, `0x05` to `f64`, `0x06` to UTF-8 strings, `0x07` to bytes, `0x08` to arrays, `0x09` to containers, `0x0A` to `Encrypted<Value>`, `0x0B` to `Signed<Value>`, and `0xFF` to null. Kind `0x0C` is reserved and rejected. All multibyte numeric values, counts, and lengths are big-endian.
Strings and bytes have a four-byte byte length. Arrays have a two-byte element count followed by that many self-delimiting values. The protection wrappers have the following canonical layouts.
```text
Container
09
[2 bytes element count]
repeat for each element:
[2 bytes DataTypeId]
[DataValue]
```
Container field IDs must be unique. Each nested value is self-delimiting, so container elements have no generic per-element payload length.
```text
Signed
0B
[4 bytes wrapper length]
[1 byte signature algorithm]
[1 byte purpose]
[8 bytes signer ID]
[signature]
[DataValue]
```
The wrapper length counts the bytes after the length field. Signature length is determined by the signature algorithm. The signature covers `MTP-DATA-SIGN-1 || algorithm || purpose || signer ID || serialized inner value`.
```text
Encrypted
0A
[4 bytes envelope length]
[1 byte encryption suite]
[1 byte purpose]
[2 bytes recipient count]
[recipient entry]
...
[encrypted DataValue bytes]
```
The envelope length counts the bytes after the length field. A recipient entry is an unlabeled fixed-size KEM ciphertext and wrapped content-encryption key; both lengths are determined by the selected suite. The encrypted bytes are the AEAD output for the complete serialized inner `DataValue`.
Protection nesting directly represents both signer-visibility choices: `Encrypted(Signed(Container))` keeps signer metadata private, while `Signed(Encrypted(Container))` exposes it. A frame with no outer sender and an `Encrypted(Signed(Container))` payload uses sealed sender. Sealed sender adds no flag or distinct wire type.
## TypeMap & Compile-Time Type Safety
@ -43,6 +106,12 @@ export default defineConfig({
Rust and manual WASM builds can set `MTP_TYPE_MAPS` directly (see [Customizing Type Maps in Downstream Projects](#customizing-type-maps-in-downstream-projects)).
For browser builds, `protocol_version` selects the one application map compiled
into that WASM client. The Vite-generated `mtp/type-map` module contains the
reserved MTP names and the application names from that selected version only;
the selected version must be present in `type_maps`. This keeps its TypeScript
unions aligned with the client runtime.
### Using Generated Enums
After editing the config and rebuilding, `CommunicationType` and `DataType` enums are generated automatically. Use them in code:
@ -50,11 +119,17 @@ After editing the config and rebuilding, `CommunicationType` and `DataType` enum
```rust
use mtp::type_map::{CommunicationType, DataType, TypeMap};
let tm = TypeMap::v2_0();
let tm = TypeMap::v3_0();
let id = tm.data_id_enum(DataType::SomeType).unwrap();
```
The enums are a **union across all versions**; every type name from every version is a variant. The version-specific `TypeMap` maps each variant to the correct wire ID for that version. For a type absent from a selected version, the lookup returns `None`.
For native builds with the `registry` feature, the enums are a **union across
all versions**; every type name from every version is a variant. The
version-specific `TypeMap` maps each variant to the correct wire ID for that
version. For a type absent from a selected version, the lookup returns `None`.
Browser-generated TypeScript unions intentionally differ: they contain only
the selected `protocol_version` plus reserved names, matching the WASM client
compiled by the Vite plugin.
Encoding/decoding uses a `TypeMap` to resolve type names to wire IDs:
@ -70,23 +145,19 @@ let decoded = decode(&bytes, &tm).unwrap();
```
```rust
let tm_v2 = TypeMap::v2_0();
assert!(tm_v2.data_id_enum(DataType::SomeType).is_some()); // defined in v2.0
assert!(tm_v2.data_id_enum(DataType::ExampleType).is_none()); // NOT in v2.0
let tm_v1 = TypeMap::v1_0();
assert!(tm_v1.data_id_enum(DataType::ExampleType).is_some()); // defined in v1.0
let tm_v3 = TypeMap::v3_0();
assert!(tm_v3.data_id_enum(DataType::SomeType).is_some());
```
When communicating with a peer on another version, encode only variants that map in the negotiated version. If an incoming frame names a type absent from the selected map, reject it as a protocol or type-map compatibility error; do not reinterpret its wire ID using another version's map. Keep old IDs stable, register both versions during migration, and remove a version only after its clients have moved.
When communicating with a peer on another version, encode only variants that map in the negotiated version. If an incoming frame names a type absent from the selected map, reject it as a protocol or type-map compatibility error; do not reinterpret its wire ID using another version's map. The self-delimiting codec begins at protocol version `3.0`; older versions are not codec fallbacks.
### Forward/Backward Compatibility Between Versions
Because enums are a union of all types across versions, a variant might exist that has no wire mapping in the *negotiated* version:
```
v2.0 client sends DataType::SomeType → host encodes with v2.0 TypeMap → wire ID 32
v2.0 host receives DataType::ExampleType (from v1.0 client) → not in v2.0 TypeMap → None → Error
v3.0 client sends DataType::SomeType → host encodes with v3.0 TypeMap → wire ID 32
v3.0 host receives an unsupported pre-v3.0 peer → version negotiation error
```
Encoding a frame with an unmapped communication or data type returns `CodecError::UnknownCommunicationType` or `CodecError::UnknownDataType`. Select a mapped variant from the compiled-in version before sending it.
@ -109,10 +180,10 @@ let registry = Registry::builtin();
let codec = VersionedCodec::new(registry);
// Encode with a specific version
let bytes = codec.encode(&value, Version(2, 0)).unwrap();
let bytes = codec.encode(&value, Version(3, 0)).unwrap();
// Decode with a specific version
let decoded = codec.decode(&bytes, Version(2, 0)).unwrap();
let decoded = codec.decode(&bytes, Version(3, 0)).unwrap();
```
## Customizing Type Maps in Downstream Projects