118 lines
7.1 KiB
Markdown
118 lines
7.1 KiB
Markdown
# Initial managed API contract
|
|
|
|
Status: initial port slice, API revision 1. No change to protobuf or media wire formats.
|
|
These are shared infrastructure APIs; the client-facing API follows with the client core.
|
|
|
|
## Protocol
|
|
|
|
`VoiceCat.Protocol` generates `Voicecat.V1` protobuf messages from the existing schema.
|
|
|
|
`ControlFraming.TryReadFrame(ref ReadOnlySequence<byte>, out ReadOnlySequence<byte>)`
|
|
extracts a payload and advances input only when a full frame exists. Returned memory
|
|
borrows the input's lifetime. Lengths above 16 MiB throw `InvalidDataException`.
|
|
Empty payloads are valid. `WriteFrame` and `WriteEnvelope` target `IBufferWriter<byte>`;
|
|
oversized outgoing payloads throw before output is written.
|
|
|
|
`ReadEnvelopesAsync(PipeReader, CancellationToken)` produces parsed envelopes and
|
|
advances consumed pipe data. It does not complete or dispose the caller's reader.
|
|
Clean EOF ends enumeration; partial EOF and oversized frames throw
|
|
`InvalidDataException`; malformed protobuf throws `InvalidProtocolBufferException`.
|
|
Cancellation propagates. A connection owner must close on protocol errors or
|
|
cancellation partway through a frame; partial frame bytes may already be consumed.
|
|
Fragments are consumed as they arrive so frames larger than pipe backpressure
|
|
thresholds make progress. Stopping enumeration between envelopes preserves the next frame.
|
|
|
|
`VoiceFrameHeader` is an immutable value with type, flags, codec, SSRC, sequence,
|
|
and timestamp. `Write(Span<byte>)` writes its 20-byte big-endian representation;
|
|
`TryRead` accepts at least 20 bytes and preserves unknown type/flag/codec values.
|
|
Higher layers decide which values they support.
|
|
|
|
## Media encryption
|
|
|
|
`MediaEncryptor` and `MediaDecryptor` each own one directional 32-byte session key
|
|
and mutable packet state. Use one owner at a time; they provide no synchronization.
|
|
Production constructs them through `TlsSession` media factories after its handshake.
|
|
Raw-key constructors support conformance tests.
|
|
|
|
`MediaEncryptor.Encrypt(VoiceFrameHeader, ReadOnlySpan<byte>, Span<byte>)` writes
|
|
the full header plus ciphertext and 16-byte tag and returns packet length. It replaces
|
|
the supplied sequence with its own counter, starting at zero. Capacity and overlap
|
|
errors throw before reserving a counter. Reserved counters are never reused after
|
|
encryption failure. At `ulong.MaxValue`, encryption throws and requires a new session.
|
|
|
|
`MediaDecryptor.TryDecrypt(ReadOnlySpan<byte>, Span<byte>, out VoiceFrameHeader,
|
|
out int)` authenticates and decrypts a complete packet. Short packets, failed tags,
|
|
replays, and packets outside the 64-packet window return false with default header
|
|
and zero bytes written. Authentication failure clears the attempted plaintext region;
|
|
structural/replay rejection leaves storage untouched. Callers must only consume
|
|
output after success. Invalid storage capacity and overlapping buffers throw.
|
|
|
|
The nonce is four zero bytes plus the big-endian header counter. All 20 header bytes
|
|
are authenticated associated data. The replay window advances after authentication.
|
|
The platform ChaCha20-Poly1305 implementation is preferred; BouncyCastle is used when
|
|
platform support is absent. Both produce the same wire bytes. The fallback currently
|
|
allocates per packet; audio and relay allocation guarantees are later checkpoints.
|
|
|
|
Dispose both objects to clear their owned key arrays and release platform crypto
|
|
resources. Use after disposal throws `ObjectDisposedException`.
|
|
|
|
## TLS sessions
|
|
|
|
`TlsSession` is a single-owner, nonblocking BouncyCastle TLS 1.3 state machine.
|
|
It owns no socket or worker thread. The transport owner feeds `ReceiveCiphertext`,
|
|
fully drains `DrainCiphertext` to its socket (including partial sends), and reads
|
|
application data through `ReadPlaintext`. Reads and drains return a byte count and
|
|
may require repeated calls. `WritePlaintext` requires `IsReady`. Socket cancellation,
|
|
backpressure, and connection lifetime belong to the transport owner.
|
|
|
|
`CreateClient(Func<string, bool>)` requires an explicit certificate acceptance
|
|
callback. It receives the uppercase SHA-256 fingerprint of the leaf certificate's
|
|
DER bytes during the handshake. Returning false rejects the session before application
|
|
data or media keys are available. This is TOFU certificate pinning; there is no PKI
|
|
chain or hostname validation. The synchronous callback must have the trust decision
|
|
available; an asynchronous first-connect prompt requires a subsequent connection
|
|
after explicit acceptance. Never automatically accept or persist an unknown pin.
|
|
|
|
`CreateServer(certificatePem, privateKeyPem)` supports ECDSA credentials; use
|
|
`ServerCredentials.CreateTlsSession()` to import persisted credentials. TLS 1.2 is
|
|
rejected. Handshake completion captures two 32-byte exporter keys using label
|
|
`voicecat media v1` and one-byte contexts 0 (client to server) and 1 (server to client).
|
|
BouncyCastle discards its exporter secrets after that callback. Media factories
|
|
select the correct direction for each role and require a ready session.
|
|
|
|
Create one encryptor and decryptor per connection and retain them for the connection's
|
|
lifetime: constructing a second encryptor resets its counter and would reuse nonces.
|
|
Dispose media objects separately from the TLS session. `Close()` queues close_notify;
|
|
drain it before disposal. On socket EOF call `CompleteInput()`; missing close_notify
|
|
throws `IOException`. TLS/protocol errors require closing the connection. Disposal
|
|
clears the session's owned exporter arrays and scratch buffer.
|
|
|
|
## Persisted trust and credentials
|
|
|
|
`TofuStore` uses the existing UTF-8 `host:port lowercase-hex-fingerprint` format.
|
|
Host matching is ordinal and case sensitive, matching native behavior. `Check`
|
|
returns `FirstConnect`, `Matched`, or `Mismatch` without changing persistence.
|
|
Only explicit `Pin` or `Remove` changes the file. Pin replacement requires an
|
|
explicit caller decision; malformed files fail closed. Changes replace the file
|
|
atomically before updating memory. Use one owner per store/file.
|
|
|
|
`ServerIdentity` reads and writes the native 96-byte Ed25519 format:
|
|
`public-key[32] || seed[32] || public-key[32]`. Loading verifies both public-key
|
|
copies against the seed. Disposal clears the owned seed.
|
|
|
|
`ServerCredentials.LoadOrCreate(directory, serverName)` imports `identity.key`,
|
|
`server.crt`, and `server.key` unchanged. If all are absent it creates an ECDSA-P256
|
|
self-signed certificate and identity. If only some exist it rejects startup rather
|
|
than rotating identity. Restore the missing files. New certificates include SAN URI
|
|
`urn:voicecat:identity:ed25519:<lowercase-public-key-hex>`; legacy certificates are
|
|
accepted unchanged. Checking this URI against ServerHello's identity is deferred
|
|
until the managed handshake/session layer is implemented; trust currently pins the
|
|
leaf certificate. Dispose credentials after their TLS sessions are created/finished
|
|
as required by the application lifetime.
|
|
|
|
Private file writes use a same-directory temporary file, flush, and atomic replacement.
|
|
On Unix new files use owner read/write permissions; Windows inherits directory ACLs.
|
|
The credential directory must have one provisioning owner. PEM strings and crypto
|
|
library internal copies are managed memory; owned-array clearing does not promise
|
|
erasure of every runtime/library copy.
|