sealed-channel 0.1.0

Transport-agnostic, forward-secret authenticated record channel from a PSK plus an externally-supplied ephemeral DH shared secret (ChaCha20-Poly1305 + HKDF-SHA256).
Documentation
# sealed-channel

`sealed-channel` is a small, transport-agnostic authenticated record channel.
It derives directional ChaCha20-Poly1305 record keys from:

- a high-entropy pre-shared secret (PSK),
- an externally supplied ephemeral Diffie-Hellman shared secret,
- the exact wire bytes of the client and server handshake messages.

The crate is pure compute. It performs no networking, no I/O, and no random
number generation. Callers own the transport, the ephemeral key exchange, and
all randomness.

## What It Provides

- HKDF-SHA256 key schedule with transcript binding.
- Directional client-to-server and server-to-client AEAD keys.
- Strict in-order record opening with replay and reordering rejection.
- Deterministic nonce construction that fails closed on counter exhaustion.
- `#![no_std]` with `alloc`.
- `#![forbid(unsafe_code)]`.

## Usage

```rust
use sealed_channel::schedule;

fn main() -> Result<(), sealed_channel::Error> {
    let psk = b"a-high-entropy-pre-shared-secret";
    let dh_shared_secret = [7_u8; 32];
    let client_hello = b"client hello wire bytes";
    let server_challenge = b"server challenge wire bytes";

    let client_keys = schedule::derive(
        psk,
        &dh_shared_secret,
        client_hello,
        server_challenge,
    )?;
    let server_keys = schedule::derive(
        psk,
        &dh_shared_secret,
        client_hello,
        server_challenge,
    )?;

    let (mut client_seal, _client_open) = client_keys.into_client();
    let (_server_seal, mut server_open) = server_keys.into_server();

    let frame = client_seal.seal(b"hello")?;
    let plaintext = server_open.open(&frame)?;

    assert_eq!(plaintext.as_slice(), b"hello");
    Ok(())
}
```

In a real protocol, each side should use fresh ephemeral keys per connection,
derive the same 32-byte DH shared secret, and pass the exact handshake bytes
seen on the wire.

## Construction

The key schedule computes:

```text
transcript = SHA256(domain || len(client_hello) || client_hello
                          || len(server_challenge) || server_challenge)
ikm        = dh_shared_secret || psk
hk         = HKDF-SHA256(salt = transcript, ikm)
```

Four domain-separated labels expand to:

- client-to-server AEAD key,
- server-to-client AEAD key,
- client-to-server nonce prefix,
- server-to-client nonce prefix.

Records are encoded as:

```text
[0xE0] || seq:u64be || ChaCha20-Poly1305(ciphertext || tag)
```

The clear `0xE0 || seq` header is authenticated as AEAD additional data. The
nonce is:

```text
nonce_prefix:4 || seq:u64be
```

The sequence number is strict and monotonic per direction. Replays,
reordering, malformed frames, and authentication failures return errors
without advancing opener state.

## Security Boundary

Authentication strength is the entropy of the PSK.

An active relay can run its own Diffie-Hellman exchange with each peer. The
PSK is therefore the secret that authenticates the channel against that active
attacker. Use a high-entropy PSK, at least 128 bits and preferably 256 bits.

Do not use a PIN, passphrase, or other low-entropy human secret as the PSK.
Authenticating weak secrets against active attackers requires a PAKE such as
SPAKE2+ or OPAQUE. This crate is not a PAKE.

Forward secrecy depends on the caller using fresh ephemeral DH keys and
discarding the ephemeral private keys after the handshake.

## Non-Goals

- No transport security or metadata confidentiality.
- No TLS replacement.
- No PAKE.
- No randomness.
- No handshake format.
- No network protocol.

## Status

This is a pre-1.0 crate and has not had an external security audit. Treat the
API and wire format as subject to change until the crate reaches a stable
release.

## License

Licensed under either of Apache-2.0 or MIT at your option.