Add managed TLS interoperability and persisted credentials
This commit is contained in:
+62
-2
@@ -31,8 +31,8 @@ Higher layers decide which values they support.
|
||||
|
||||
`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 from TLS exporter keys when TLS is implemented. Raw-key
|
||||
constructors support conformance tests and the future TLS integration.
|
||||
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
|
||||
@@ -55,3 +55,63 @@ allocates per packet; audio and relay allocation guarantees are later checkpoint
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Porting VoiceCat to pure .NET / C#
|
||||
|
||||
**Status:** initial wire/crypto slice implemented under `dotnet/`; later phases remain planned.
|
||||
**Status:** wire/media crypto and TLS/exporter foundations implemented under `dotnet/`,
|
||||
including C++ interoperability, persisted TOFU, and compatible server credentials.
|
||||
Codec/audio, managed server/client state, and UI phases remain planned.
|
||||
See `dotnet/README.md`, `docs/api-dotnet.md`, and `PROGRESS.md` for verification and next steps.
|
||||
**Target runtime:** .NET 10 LTS (in-service to Nov 2028), with .NET 11 as the follow-on.
|
||||
**Scope:** replace the C++ core (`libvoicecat`), the C++ server, the C++ `vccli`, and the
|
||||
@@ -676,6 +678,16 @@ not assumed.
|
||||
**Exit criterion:** C# client completes a TLS 1.3 handshake with the C++ server, derives
|
||||
matching media keys, and pins the leaf fingerprint.
|
||||
|
||||
**Checkpoint (2026-09-15):** implemented nonblocking managed TLS, handshake-time
|
||||
exporters, explicit certificate acceptance, persisted TOFU, and native-compatible
|
||||
credentials. The C++ TLS oracle authenticates a media challenge in both directions
|
||||
over an actual socket, proving exporter compatibility. Tests also cover managed
|
||||
fragmented loopback, first-connect acceptance, changed-pin rejection, TLS 1.2 rejection,
|
||||
close_notify/abrupt EOF, restart persistence, and import of C++ credential files.
|
||||
Socket orchestration remains a transport-owner responsibility; the complete managed
|
||||
server and client are later phases. See `dotnet/README.md` for the required native
|
||||
interoperability test command.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — Codec + DSP (est. 1 week)
|
||||
|
||||
@@ -49,6 +49,16 @@ This is a known limitation of the current design. Closing it properly requires b
|
||||
Ed25519 key into the TLS cert (e.g. as a SubjectAltName or extension), which is a planned
|
||||
future improvement. Until then, clients display both values but gate on the cert fingerprint.
|
||||
|
||||
**Managed rewrite checkpoint:** `dotnet/` uses nonblocking BouncyCastle TLS 1.3 and
|
||||
captures directional exporters during handshake completion. Its client requires an
|
||||
explicit leaf-fingerprint acceptance callback; PKI validation remains unimplemented.
|
||||
New managed server certificates include the Ed25519 public key in SAN URI
|
||||
`urn:voicecat:identity:ed25519:<lowercase-public-key-hex>`. Existing C++ credentials
|
||||
are imported unchanged. Verifying that URI against the declared ServerHello identity
|
||||
is still deferred to the managed session layer; leaf-certificate TOFU remains the
|
||||
trust gate. Missing members of a persisted credential set cause startup rejection
|
||||
rather than automatic identity rotation. See [api-dotnet.md](api-dotnet.md).
|
||||
|
||||
Client certificates are reserved for a future "key-based identity" option (see roadmap) but
|
||||
are not required in v1.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user