[WIP] Security work While on holiday
This commit is contained in:
parent
a81ac4efca
commit
7f0231e3f1
109 changed files with 19694 additions and 5210 deletions
146
docs/PIPES.md
146
docs/PIPES.md
|
|
@ -1,6 +1,14 @@
|
|||
# MTP Pipes
|
||||
|
||||
Pipes are unidirectional QUIC streams for raw bytes. The creator sends a `PipeRequest` communication value, the peer accepts or rejects it, and the stream then carries bytes without an MTP frame around every write.
|
||||
Pipes are unidirectional QUIC/WebTransport streams. The transport primitive is
|
||||
byte-oriented, but raw pipe bytes are not confidential or authenticated by
|
||||
MTP. The creator sends a `PipeRequest` communication value, the peer accepts
|
||||
or rejects it, and an application that carries sensitive data must place the
|
||||
encrypted record layer described below on top of the accepted stream.
|
||||
|
||||
The request's `Description` and `PipeRequest` type remain clear transport
|
||||
metadata. Do not put identities, call details, file names, or other sensitive
|
||||
protocol information in them.
|
||||
|
||||
The creator owns the writer. The accepting peer owns the reader. A writer finishes with a stream FIN or aborts with a stream reset. A reader returns EOF after FIN and reports a connection or stream error when the peer closes unexpectedly.
|
||||
|
||||
|
|
@ -11,6 +19,88 @@ The creator calls `create_pipe` or the corresponding SDK `createPipe` method wit
|
|||
The request description is application metadata. It does not grant access to the stream, authenticate the creator, or negotiate an application protocol.
|
||||
Use the authenticated MTP connection and the host's admission policy when a pipe carries sensitive data.
|
||||
|
||||
The browser SDK's `createEncryptedPipe` and `acceptEncryptedPipe` convenience
|
||||
methods derive the local identity, actual pipe ID, random session ID, and
|
||||
default application purpose from MTP state. Use the lower-level session
|
||||
functions only when integrating a custom pipe transport. The low-level API
|
||||
checks that a supplied pipe ID matches the actual pipe; it does not infer a
|
||||
caller-provided sender or recipient identity.
|
||||
|
||||
The convenience methods intentionally require registered client credentials
|
||||
because their endpoint identity is the transport client's registered MTP
|
||||
identity. An application that needs a cryptographic identity independent from
|
||||
transport registration must use the lower-level session functions and provide
|
||||
the endpoint IDs and key material explicitly.
|
||||
|
||||
## Endpoint Encryption
|
||||
|
||||
`initiate_pipe_session`/`accept_pipe_session` in the native transport, or
|
||||
`initiateMTPPipeSession`/`acceptMTPPipeSession` in the browser SDK, perform the
|
||||
pipe-establishment step. The initiator sends an
|
||||
`Encrypted(Signed(Array<...>))` offer containing a fresh 32-byte initial chain key,
|
||||
session ID, pipe ID, direction, purpose, and both endpoint IDs. The recipient
|
||||
decrypts it with its keyring, resolves the expected sender bundle, verifies
|
||||
the signature, and checks every expected field before returning the record
|
||||
reader. The offer is bounded and separately framed from application records.
|
||||
|
||||
The helpers then return `EncryptedPipeWriter`/`EncryptedPipeReader` (or their
|
||||
browser equivalents) without changing the raw QUIC/WebTransport adapter. The
|
||||
context contains the unique pipe/session identity, endpoint identities,
|
||||
direction, and application protocol purpose. Do not derive the initial chain
|
||||
key from the clear description or pipe ID alone.
|
||||
|
||||
The receiver's signature verification policy is explicit and independent from
|
||||
its decryption keyring. Configure `signaturePolicy` on the browser accept
|
||||
helper, or use the client's `defaultSignatureVerificationPolicy`. The
|
||||
initiator and responder signing `signatureSuite` remain separate from this
|
||||
receive policy. Both sides default to Ed25519; choose `signatureSuite: "dual"`
|
||||
and a matching `signaturePolicy: "dual"` explicitly when hybrid signatures
|
||||
are required.
|
||||
|
||||
Each record is encoded as:
|
||||
|
||||
```text
|
||||
[4-byte big-endian ciphertext length]
|
||||
[1-byte record type: DATA=0, FINAL=1]
|
||||
[XChaCha20-Poly1305 nonce || ciphertext || tag]
|
||||
```
|
||||
|
||||
The AEAD associated data is `MTP-PIPE-E2EE-1 || purpose || direction ||
|
||||
transcript-hash || sequence || record length || record type`. The transcript
|
||||
hash binds the session ID, pipe ID, sender, recipient, purpose, and direction.
|
||||
The sequence starts at zero and advances only after successful authentication.
|
||||
A missing, duplicated, reordered, or modified record causes authentication to
|
||||
fail. Each record derives a one-use message key and the next chain key with
|
||||
HKDF using the authenticated context and sequence number; the bootstrap key is
|
||||
never used directly as an AEAD key. The record layer caps one encoded record at
|
||||
16 MiB.
|
||||
|
||||
`FINAL` is an authenticated empty record. A reader returns clean EOF only
|
||||
after validating it; transport EOF before `FINAL` is truncation.
|
||||
Authentication, framing, sequence, and I/O failures permanently poison the
|
||||
encrypted reader or writer and erase its current chain key. This is a one-way
|
||||
chain, not a Diffie-Hellman ratchet, so the ordinary offer does not provide
|
||||
forward secrecy.
|
||||
|
||||
The wrapper exposes `writeRecord`/`readRecord`. Callers that already have an
|
||||
independently authenticated session may still construct it directly with a
|
||||
key and context; otherwise use the establishment helpers.
|
||||
|
||||
For more than two members, native `initiate_group_pipe_session` and the browser
|
||||
`initiateMTPPipeSession` recipient-array form encrypt one fresh session key to
|
||||
each current member. Membership changes are rekeys: create a new session ID
|
||||
and offer with the new recipient set, and stop using the old record chain. A
|
||||
removed member must never receive a later session key; an added member must
|
||||
not receive historical records.
|
||||
|
||||
When a live call needs forward secrecy, use the duplex handshake
|
||||
`initiate_forward_secure_pipe_session`/`accept_forward_secure_pipe_session` or
|
||||
the browser `initiateMTPForwardSecurePipeSession`/
|
||||
`acceptMTPForwardSecurePipeSession`. The responder contributes a fresh
|
||||
ephemeral hybrid-KEM key, while long-term signing keys authenticate the
|
||||
exchange. These helpers require a bidirectional stream and bind the handshake
|
||||
transcript into the record context.
|
||||
|
||||
## Accepting or Rejecting a Pipe
|
||||
|
||||
The receiving side reads pipe requests through `receive_pipe`, the host dispatcher, or the browser pipe callback. It calls `accept` to obtain a reader or `deny` to reject the request. A rejected request completes the creator's handle with `Rejected` and no raw byte stream becomes available.
|
||||
|
|
@ -20,7 +110,12 @@ Normal messages and pipe requests share the transport and must pass through the
|
|||
|
||||
## Closing a Pipe
|
||||
|
||||
The creator closes a successful pipe with `PipeWriter::finish` or the browser writer's `close`; this sends a QUIC FIN and lets the reader observe EOF. Use `abort` when the peer should discard the stream immediately; this resets the stream and the reader receives an error instead of a clean EOF. Dropping the connection closes all active pipes.
|
||||
The creator closes a successful encrypted pipe with `EncryptedPipeWriter::finish`
|
||||
or the browser writer's `close`; this authenticates `FINAL` and then sends a
|
||||
QUIC FIN. Use `abort` when the peer should discard the stream immediately; this
|
||||
resets the stream and the reader receives an error instead of a clean EOF.
|
||||
Dropping the connection closes all active pipes. Raw pipe FIN is not an
|
||||
authenticated application completion signal.
|
||||
|
||||
The accepting side closes its reader by consuming it or dropping it. A reader does not send an application-level acknowledgement for EOF. If the application needs completion metadata, send an ordinary MTP message before finishing the pipe.
|
||||
|
||||
|
|
@ -38,23 +133,40 @@ Native applications use the pipe APIs on `MTPConnection`; browser applications u
|
|||
|
||||
## Native File Upload and Processing
|
||||
|
||||
The creator streams a file in chunks. The accepting side processes each chunk without buffering the complete file:
|
||||
The creator streams a file in encrypted records. The accepting side processes
|
||||
each decrypted chunk without buffering the complete file. The `session_key`
|
||||
below is obtained from the authenticated pipe-establishment protocol:
|
||||
|
||||
```rust
|
||||
// Client
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use mtp_transport::{PipeSessionParameters, initiate_pipe_session};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let handle = conn.create_pipe("file-upload").await?;
|
||||
if let Some(mut writer) = handle.wait().await? {
|
||||
let pipe_id = handle.pipe_id();
|
||||
if let Some(writer) = handle.wait().await? {
|
||||
let params = PipeSessionParameters::new(
|
||||
format!("file-upload/{pipe_id}"), pipe_id, own_client_id, host_client_id, 0x40, 0,
|
||||
)?;
|
||||
let mut writer = initiate_pipe_session(
|
||||
writer.into_inner(), params, &own_keyring, &host_public_bundle,
|
||||
).await?;
|
||||
let mut file = tokio::fs::File::open("input.bin").await?;
|
||||
tokio::io::copy(&mut file, &mut writer).await?;
|
||||
let mut buffer = [0u8; 64 * 1024];
|
||||
loop {
|
||||
let count = file.read(&mut buffer).await?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
writer.write_record(&buffer[..count]).await?;
|
||||
}
|
||||
writer.finish().await?;
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// Host
|
||||
use tokio::io::AsyncReadExt;
|
||||
use mtp_transport::{PipeSessionParameters, accept_pipe_session};
|
||||
|
||||
while let Ok(request) = conn.receive_pipe().await {
|
||||
if request.description() != "file-upload" {
|
||||
|
|
@ -62,16 +174,18 @@ while let Ok(request) = conn.receive_pipe().await {
|
|||
continue;
|
||||
}
|
||||
|
||||
let mut reader = request.accept().await?;
|
||||
let pipe_id = request.id();
|
||||
let reader = request.accept().await?;
|
||||
let params = PipeSessionParameters::new(
|
||||
format!("file-upload/{pipe_id}"), pipe_id, client_id, own_client_id, 0x40, 0,
|
||||
)?;
|
||||
let mut reader = accept_pipe_session(
|
||||
reader.into_inner(), ¶ms, &own_keyring, &client_public_bundle,
|
||||
).await?;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
let mut buffer = [0u8; 64 * 1024];
|
||||
loop {
|
||||
let count = reader.read(&mut buffer).await?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..count]);
|
||||
process_chunk(&buffer[..count]).await?;
|
||||
while let Some(chunk) = reader.read_record().await? {
|
||||
hasher.update(&chunk);
|
||||
process_chunk(&chunk).await?;
|
||||
}
|
||||
let digest = hasher.finalize();
|
||||
println!("processed upload with digest {digest:x}");
|
||||
|
|
|
|||
Loading…
Reference in a new issue