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

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:

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:

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

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

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.