[Fix] Harden MTP codec, transport, and SDK security
This commit is contained in:
parent
188caf56cc
commit
a7e804c603
73 changed files with 11892 additions and 5756 deletions
|
|
@ -142,7 +142,7 @@ let conn = MTPClient::auth_register(config, &keyring, &host_pk).await?;
|
|||
|
||||
// Save for next session
|
||||
let id = conn.client_id;
|
||||
let keyring_bytes = keyring.to_bytes();
|
||||
let keyring_bytes = keyring.try_to_bytes()?;
|
||||
```
|
||||
|
||||
When callers already know whether a saved client ID exists, the convenience helper uses `Some(id)` for login and `None` for registration:
|
||||
|
|
@ -175,7 +175,7 @@ pub struct Keyring {
|
|||
}
|
||||
```
|
||||
|
||||
- Serialise: `keyring.to_bytes()` -> `Vec<u8>`
|
||||
- Serialise: `keyring.try_to_bytes()` -> `Result<Zeroizing<Vec<u8>>, CryptoError>`
|
||||
- Deserialise: `Keyring::from_bytes(&bytes)` -> `Result<Keyring, CryptoError>`
|
||||
- Get public half: `keyring.public_key_bundle()` -> `PublicKeyBundle`
|
||||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ let (kem_sk, kem_pk) = HybridKem::generate_keypair();
|
|||
let host_keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);
|
||||
|
||||
// Save to disk
|
||||
let bytes = host_keyring.to_bytes();
|
||||
let bytes = host_keyring.try_to_bytes()?;
|
||||
std::fs::write("host_keys.bin", bytes)?;
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,28 @@ Verified SDK results expose the authenticated `protectedVersion` and
|
|||
`finalRecipientId` alongside the application content.
|
||||
|
||||
Native applications use the same schema through `ProtectedMessageBuilder` and
|
||||
`open_protected`; language bindings delegate envelope construction and opening
|
||||
to this codec boundary.
|
||||
the replay-explicit `open_protected_checked` or `open_protected_without_replay`
|
||||
APIs; language bindings delegate envelope construction and opening to this
|
||||
codec boundary.
|
||||
|
||||
Message processing uses the replay-required native APIs
|
||||
`open_protected_checked` and `open_relay_metadata_checked` (or the equivalent
|
||||
browser client path). Stored-message or forensic tooling must opt into the
|
||||
explicit `*_without_replay` APIs. Native in-memory guards are bounded and
|
||||
configurable; durable guards must perform an atomic insert-if-absent on
|
||||
`(signer ID, MessageId)`.
|
||||
|
||||
Protected identifiers have semantic limits separate from the generic codec
|
||||
blob limit. The default maximum `MessageId` is 256 UTF-8 bytes and relay
|
||||
metadata is limited to 1 MiB of encoded metadata. Deployments can provide
|
||||
stricter limits through the receive policy. Limits are checked after
|
||||
authentication and before retained values enter replay or application state.
|
||||
|
||||
Transport-derived resource policies use a conservative decoder allocation
|
||||
factor of `4 * max_message_size`, in addition to the frame-size output limit.
|
||||
This factor accounts for owned wrapper, recipient, ciphertext, and decoded
|
||||
value copies; it is an implementation admission policy rather than a wire
|
||||
field.
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
|
|
@ -77,6 +97,15 @@ Client Host
|
|||
|
||||
Login proof binds the protocol version, client ID, host challenge, and client nonce. Registration proof binds the protocol version, public key bundle, host challenge, and client nonce. The host challenge is generated per connection.
|
||||
|
||||
Authentication attempts pass through a deployment-configurable limiter before
|
||||
client lookup, key validation, challenge signing, or registration callbacks.
|
||||
The default host configuration uses a bounded in-memory window. Hosts may key
|
||||
limits by connection, peer identity, claimed client ID, or registration flow.
|
||||
When identity concealment is enabled, an unknown client ID follows a dummy
|
||||
challenge/proof path and receives the same generic authentication failure as a
|
||||
known client with an invalid proof; disabling concealment restores the legacy
|
||||
identity-specific response for deployments where IDs are public.
|
||||
|
||||
`ForceAuthentication` requires login or registration. `AllowAuthentication` accepts authenticated and unauthenticated clients. `Unauthenticated` rejects authentication attempts. The connection states are `Pending`, `Authenticated`, `Unauthenticated`, and `Failed`.
|
||||
|
||||
## Version Negotiation
|
||||
|
|
|
|||
|
|
@ -160,6 +160,14 @@ process boundaries. A guard should atomically record a new ID before
|
|||
dispatching application content. Transport frame IDs must not be used for
|
||||
this purpose.
|
||||
|
||||
Native message-processing boundaries require a replay guard through the
|
||||
checked opening APIs. Reopening stored or forensic frames without a guard is
|
||||
available only through an explicitly named `without_replay` API. The reference
|
||||
in-memory guard is bounded and FIFO-evicts old entries, so it is a duplicate
|
||||
suppression cache rather than durable replay protection. A durable deployment
|
||||
must use an atomic insert-if-absent operation keyed by `(signer ID, MessageId)`;
|
||||
a separate read followed by insert is race-prone.
|
||||
|
||||
`VerifiedRelayMetadata` is an authenticated capability rather than a caller
|
||||
constructed data transfer object. Rust fields are private and the browser
|
||||
implementation keeps authenticated state behind a branded class. Content
|
||||
|
|
@ -174,9 +182,11 @@ fallback.
|
|||
Verification takes a receiver-side `SignaturePolicy`/`ProtectionPolicy`.
|
||||
`AnySupported` is useful for compatibility at the low-level codec boundary,
|
||||
but protocol receivers should select `Ed25519` or `Dual`. The browser SDK uses
|
||||
an explicit `ed25519` default and permits an operation or client override. It
|
||||
never derives receive policy from the recipient keyring. The sender's
|
||||
signature suite remains a separate choice. Signature policy must be applied
|
||||
an explicit `ed25519` default and permits an operation or client override. Its
|
||||
`MTPSecurityProfile` resolves protected-message sender/receiver suites,
|
||||
encrypted-pipe suites, and the authentication PQ requirement together;
|
||||
`any-supported` remains an explicit compatibility value. It never derives
|
||||
receive policy from the recipient keyring. Signature policy must be applied
|
||||
independently to relay metadata, relay content, and pipe session establishment.
|
||||
|
||||
### Key history and rotation
|
||||
|
|
@ -261,16 +271,43 @@ to proceed with an invalid local decryption key.
|
|||
Applications remain responsible for storage at rest. The `files` feature writes passphrase-protected keyrings to `.mk` files and public bundles to `.mpkb` files. Protected `.mk` files store the Argon2id identifier, parameters, salt, and AEAD ciphertext; they do not derive their key with HKDF. On Unix, keyring files are created with owner-only `0600` permissions.
|
||||
Restrict those files to the owning account and protect backups. Browser applications should treat the configured credential storage as sensitive application data.
|
||||
|
||||
Key-material parsing is explicit in the SDK: use the hex, Base64, or byte
|
||||
helpers for encoded key material. Arbitrary strings are no longer treated as
|
||||
passphrases by the compatibility `secretKeyFromString` helper. Applications
|
||||
migrating data written by the old implicit-HKDF behavior can use the explicitly
|
||||
named, deprecated `legacySecretKeyFromStringV1` helper only for that migration;
|
||||
new data must not use it. Passwords must use the explicit Argon2id passphrase
|
||||
API with a stored per-record salt and versioned parameters. The SDK's
|
||||
`deriveKeyFromPassphrase` uses a worker when browser workers are available;
|
||||
the explicitly named `deriveKeyFromPassphraseSync` form is for workers and
|
||||
command-line migrations. HKDF helpers are for high-entropy key material and
|
||||
are not password-hardening functions.
|
||||
|
||||
## Resource Limits and Operational Controls
|
||||
|
||||
`Policy::default()` sets a 16 MiB application message limit and a 64 KiB handshake message limit. It also sets a 30 second read timeout, a 30 second maximum idle timeout, a receiver queue capacity of 1000, and a maximum of 128 concurrent stream tasks. Tune these values for the deployment and peer trust level.
|
||||
|
||||
The recursive codec applies additional defaults while parsing untrusted values:
|
||||
maximum nesting depth 64, 65,536 value nodes, 16 MiB per blob or envelope,
|
||||
and 64 encrypted recipients. Decrypted values are parsed with the same limits.
|
||||
64 encrypted recipients, and a 64 MiB cumulative decoder allocation budget.
|
||||
Decrypted values are parsed with the same limits. Transport derives the blob,
|
||||
allocation, and encoder output budgets from its admitted frame size rather than
|
||||
serializing an unrestricted recursive value first. The default transport
|
||||
allocation budget is four times the admitted frame size to cover conservative
|
||||
owned-copy and crypto-buffer accounting; deployments may choose another
|
||||
factor with `DecodeLimits::for_transport_message_size_with_allocation_factor`.
|
||||
|
||||
The host does not provide a general authentication-attempt rate limiter.
|
||||
Deploy authentication endpoints behind a rate-limiting proxy or add admission control through the host callbacks, including `GuestIdGenerator` where guest connections are permitted.
|
||||
The host applies an authentication-attempt limiter before storage lookups,
|
||||
public-key validation, challenge signing, and registration callbacks. The
|
||||
default limiter is a bounded in-memory sliding window; configure a durable or
|
||||
distributed limiter when limits must coordinate across host instances. Unknown
|
||||
client IDs are sent through a fixed dummy challenge/proof path by default, so
|
||||
they receive a generic authentication failure instead of an enumeration hint.
|
||||
Deployments that intentionally publish client IDs can disable this concealment.
|
||||
|
||||
Keepalive Pong observation is bounded and accepts only the currently pending
|
||||
ping ID. Unsolicited Pongs are dropped before they can consume application
|
||||
receiver capacity.
|
||||
|
||||
## Security Limitations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Type Map
|
||||
|
||||
This file documents the Type Map & Registry configuration used by the MTP protocol. It will assume you are working with the [example-type-maps.yaml](./../example-type-maps.yaml).
|
||||
A type musn't be the version of MTP, it stays independant.
|
||||
MTP version defines the codec. The Type-Map version defines the available Types.
|
||||
|
||||
## Binary Frame Format
|
||||
|
||||
|
|
@ -85,6 +87,15 @@ The envelope length counts the bytes after the length field. A recipient entry i
|
|||
|
||||
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.
|
||||
|
||||
### Container ordering and signatures
|
||||
|
||||
Container entries are ordered sequences in the current format. Insertion order
|
||||
is therefore semantic: two containers with the same field/value pairs in a
|
||||
different order have different serialized bytes and different signatures. The
|
||||
decoder rejects duplicate field IDs. Applications that need map semantics must
|
||||
canonicalize their own input before signing; a future canonical map encoding
|
||||
requires a protocol-format version and cannot be inferred by a receiver.
|
||||
|
||||
## TypeMap & Compile-Time Type Safety
|
||||
|
||||
A `TypeMap` maps Communication-Types and Data-Types to their wire IDs. Each protocol version has its own `TypeMap` because the same type name may use different wire IDs in different versions.
|
||||
|
|
@ -175,17 +186,33 @@ mtp = { path = "..", features = ["host"] }
|
|||
|
||||
```rust
|
||||
use mtp::codec::registry::{Registry, VersionedCodec};
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataValue};
|
||||
use mtp_type_map::Version;
|
||||
|
||||
let registry = Registry::builtin();
|
||||
let codec = VersionedCodec::new(registry);
|
||||
let codec = VersionedCodec::for_version(registry, Version(3, 0)).unwrap();
|
||||
let value = CommunicationValue::new_with_type_map(
|
||||
CommunicationType::Ping,
|
||||
codec.type_map(),
|
||||
).with_payload(DataValue::Null);
|
||||
|
||||
// Encode with a specific version
|
||||
let bytes = codec.encode(&value, Version(3, 0)).unwrap();
|
||||
// The value must retain the negotiated map used to construct it.
|
||||
let bytes = codec.encode(&value).unwrap();
|
||||
|
||||
// Decode with a specific version
|
||||
let decoded = codec.decode(&bytes, Version(3, 0)).unwrap();
|
||||
let decoded = codec.decode(&bytes).unwrap();
|
||||
|
||||
// A clear value can be migrated explicitly when the application has chosen
|
||||
// that behavior. Protected values are not silently remapped.
|
||||
let migrated = codec.encode_migrating(&value).unwrap();
|
||||
```
|
||||
|
||||
`VersionedCodec::encode` compares the retained map identity (its protocol
|
||||
version) and returns `CodecError::MissingTypeMap` or
|
||||
`CodecError::TypeMapMismatch` on failure. `reply_to` retains the request's
|
||||
map, while `try_merge` rejects frames from different maps before copying any
|
||||
fields. The deprecated `merge` method records the error for compatibility; new
|
||||
code should migrate to `try_merge` and handle the result.
|
||||
|
||||
## Customizing Type Maps in Downstream Projects
|
||||
|
||||
External projects must provide their own type map configuration. Browser projects use the Vite plugin from [Defining Type Maps](#defining-type-maps) and do not need to publish, fork, or copy a generated WASM package.
|
||||
|
|
|
|||
Loading…
Reference in a new issue