Set security.mode = .aead for authenticated encryption, replay protection, bounded
pre-authentication work, and traffic-key updates. With .none (the default) all crypto
compiles out. See the security model before deployment.
const Cfg = magnet.Config{
.channels = Schema,
.protocol_id = 0xC0FFEE,
.capabilities = 0b111,
.required_capabilities = 0b001,
.security = .{
.mode = .aead,
.forward_secrecy = true,
},
};The server replies to a valid hello with a context-bound stateless cookie and allocates no connection until the client echoes it. The cookie binds the source address, protocol/app versions, capability offer, and ephemeral public key. The endpoint also applies a fixed, per-source token bucket before emitting challenges and never sends beyond the 3× anti-amplification budget of an unvalidated address.
server.secSetup(psk, challenge_secret);
var prng = std.Random.DefaultPrng.init(seed); // use an OS CSPRNG in production
_ = client.connectToForwardSecret(server_addr, psk, prng.random());Without forward_secrecy, call connectTo. With it, a fresh client X25519 key and the
server's stateless ephemeral key are mixed into the PSK-derived schedule. Server ephemeral
keys are recoverable only while the current/previous cookie secret is retained, so rotating
and erasing those secrets provides delayed forward secrecy without half-open connection
state.
capabilities is an authenticated supported-feature bitset. Negotiation returns the
intersection, exposed as session.negotiatedCapabilities(). Either peer's required bits
must survive or the handshake fails, preventing downgrade of a critical extension.
app_version remains a simple equality gate when nonzero.
Directional keys update automatically according to key_update_packets and
key_update_ms, or explicitly with session.requestKeyUpdate(). The receiver authenticates
the next phase before committing it and retains the preceding receive key for reordered old
packets. session.keyEpoch() exposes the local epoch.
AEAD sessions derive an independent packet-number skip schedule from a sender-local secret
that is never transmitted or derived from peer-known traffic keys. A peer sees a gap only
after it has occurred and is disconnected if it acknowledges that never-sent number.
delivery.pn_skip_period controls the frequency and defaults to 32.
For connectTo and connectToWithToken, generate a fresh secret for every connection
from OS entropy (io is any std.Io instance, such as init.io in main or a driver's
Threaded instance):
var local_secret: [16]u8 = undefined;
try std.Io.randomSecure(io, &local_secret);
_ = client.connectTo(server_addr, psk, local_secret);The forward-secret connect methods draw this value from their existing std.Random
argument. A server derives a unique local secret from its private challenge secret and a
non-repeating endpoint counter. Low-level Session users must call
setPacketNumberFilterKey after secSetup; a missing key fails closed before transmission.
CID handshakes deliver a CID-bound stateless-reset token. A server that lost connection state can emit a small reset for an unknown CID without allocating; only a peer holding the authenticated token accepts it. Reset packets cannot create reset loops.
A backend signs short-lived connect tokens authorizing a client for specific servers. Keys
derive from the token; expired, forged, replayed, or wrong-server tokens are rejected.
Enable with security.tokens = true:
const tok = magnet.proto.conn.token.issue(csprng, issuer_key, protocol_id, expire_s, .{ … });
server.secSetupTokens(issuer_key, challenge_secret, own_addr);
var local_secret: [16]u8 = undefined;
try std.Io.randomSecure(io, &local_secret);
_ = client.connectToWithToken(server_addr, &tok, local_secret);When forward secrecy is also enabled, use connectToWithTokenForwardSecret. Serialize a
token for backend delivery with tok.encodeForClient and restore it with
Token.decodeFromBackend; the UDP form deliberately omits the client's key copies. Real
drivers supply Unix time and rotate challenge secrets. Deterministic drivers should call
setTokenTime and rotateChallengeSecret themselves.
Tokens use XChaCha20-Poly1305 and random 192-bit nonces. Production issuers must pass a
CSPRNG; issueWithNonce exists only for deterministic tooling.
Accepted token MACs remain in the fixed replay table until their authenticated expiry.
The table never evicts an unexpired entry: new token admissions fail closed when
limits.token_dedup_cap is full. A retransmitted response for the same live session is
recognized separately and only re-arms the encrypted proof packet.
proto.conn.resumption issues opaque, short-lived XChaCha tickets containing a fresh
resumption secret, protocol/capability binding, and an early-data ceiling. The application
sends Offer(max_early_data) through its existing signaling/admission path. The server calls
Server.accept before allocating a connection; this authenticates the ticket, checks
expiry/capabilities, enforces a bounded one-time replay cache, and invokes a mandatory
Policy callback. Build it with Policy.init(Context, &context, callback) so the policy
context and callback parameter are checked as one concrete type before being erased for
storage.
Only replay-safe operations belong in early data: reads, idempotent state replacement, or
requests carrying an application deduplication key. Never accept purchases, increments,
one-time grants, or other non-idempotent mutations. After acceptance, the client/server call
resumeTo / acceptResumed with the returned secret, capability set, and a fresh
sender-local 16-byte secret on each side. CID resumption is currently intentionally
unsupported; perform a full CID handshake instead.
Size the replay cache for all valid tickets in the ticket lifetime. Once an entry is evicted, the network layer cannot remember it; applications protecting valuable actions need their own durable idempotency store as well.
Use acceptWithKeys(.{ .current = new, .previous = old }, ...) during ticket-key rotation.
Persist Server.saveReplayState with a separate checkpoint-authentication key and restore
it with restoreReplayState before accepting early data after restart; the checkpoint is
HMAC-authenticated and can be written atomically through runtime.checkpoint. Do not persist
live traffic keys/packet numbers as a restart mechanism; resume with a fresh session instead.
Ed25519 certificates plus X25519 ECDH provide identity without a shared password:
const id = magnet.proto.conn.identity;
const master = try id.agree(my_x25519_kp, &my_cert, &peer_cert, ca_pubkey, now_s, initiator);
// Feed `master` to secSetup/connectTo like any other 32-byte master key.security.connection_ids = true addresses a session by an opaque ID instead of its IP.
A port rebind keeps the path state; an IP change must answer a path challenge before full
sending resumes. Combine CID migration with the validated multipath wrapper when multiple
simultaneous interfaces should remain available.
Runnable: encrypted.zig,
connect_tokens.zig,
cert_identity.zig,
migration.zig.