WitRPC secures communication on two independent axes: encryption protects data in transit, and authorization controls who may connect. Both are enabled with builder options and require no key or certificate management for the common cases.
End-to-end encryption
Encryption is switched on with one call on each side:
// Server
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithEncryption();
});
server.StartWaitingForConnection();
// Client
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("localhost", 5000);
options.WithEncryption();
});
await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None);During the handshake, the two sides perform an RSA key exchange, and separate AES-256-GCM keys, one per direction, are derived from the exchanged master key via HKDF-SHA256. From then on every message (calls, responses, and events alike) is encrypted and authenticated: each frame carries a strictly ordered counter, so a tampered, replayed, reordered, or dropped frame raises WitExceptionEncryption instead of silently producing garbage. Key generation, exchange, and derivation are handled by the framework. (3.0 replaced the earlier AES-CBC scheme with GCM; besides authentication, the new path benchmarks 4.5–6× faster on large payloads.)
Encryption settings must match. If one side enables encryption and the other does not, the handshake fails and no connection is established. To run deliberately unencrypted, call WithoutEncryption() on both sides; this is reasonable when the transport layer already provides TLS. On local transports (memory-mapped files, named pipes), where there is no TLS, message-layer encryption is the natural way to protect the channel from other local processes.
Transport-level TLS
Message-layer encryption and transport TLS are independent tools, and either satisfies the "encrypt on untrusted networks" rule:
- TCP with TLS:
WithTcpSecure(port, maxNumberOfClients, certificate)on the server andWithTcpSecure(host, port, targetHost, sslValidationCallback)on the client, wheretargetHostmatches the certificate name and the callback can benullfor standard validation. - Secure WebSocket: an
https://listener with a certificate on the server,wss://on the client.
TLS also gives non-WitRPC clients (browsers, REST callers) transport security. For the REST layer it is the only encryption, since each REST call is a bare HTTP request with no handshake. Message-layer encryption adds protection that survives TLS termination points such as reverse proxies. On network transports, treat TLS as the primary protection and message-layer encryption as defence in depth beneath it.
Handshake hardening
Independently of the two axes above, the server guards the connection before a client is trusted:
- Protocol version handshake. Client and server negotiate the protocol version; a client from an older major is refused with a readable reason in the server log (encrypted for the client when it offered a key) rather than a decode failure.
- Handshake timeout. An incoming connection must complete its handshake within a bound (30 seconds by default,
WithHandshakeTimeout(...)), so a stalled or hostile peer cannot hold a slot open. - Frame-size caps. Length prefixes are validated before any allocation, and each transport enforces a maximum message size (256 MB by default), closing the unbounded-buffer surface on public endpoints.
- Authorization boundary. A client that fails token validation has its transport closed outright, and server events are delivered only to authorized connections.
Token-based authorization
Access control uses tokens presented during the handshake:
// Server: require a token
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService());
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithEncryption();
options.WithAccessToken("Secr3tToken");
});
// Client: present the token
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("localhost", 5000);
options.WithEncryption();
options.WithAccessToken("Secr3tToken");
});The client sends the token during connection; the server accepts the connection only if it matches. A client with a missing or wrong token is rejected before any method call can happen. If no authorization is needed, WithoutAuthorization() (or simply not configuring a token) leaves the server open.
Dynamic tokens and custom validation
A single static token is the simplest scheme; both sides can go further.
On the client, WithAccessToken has overloads that take a callback (Func<string> or Func<Task<string>>), and WithAccessTokenProvider accepts an IAccessTokenProvider implementation. Use these when tokens expire or come from an identity provider: the framework requests a fresh token when it needs one instead of caching a stale string.
On the server, WithAccessTokenValidator accepts an IAccessTokenValidator implementation with your own logic: look tokens up in a database, verify JWT signatures, enforce per-client keys or roles. Whatever the validator decides during the handshake determines whether the connection is accepted.
Encryption in Blazor WebAssembly: BouncyCastle
The standard encryption relies on .NET cryptography APIs that are not fully available inside the browser sandbox, so Blazor WebAssembly clients need an alternative. Two exist: the OutWit.Communication.Client.Blazor channel factory implements the scheme on the browser's own Web Crypto API and pairs with a server's standard WithEncryption() (see Blazor WebAssembly & AOT →), and the BouncyCastle packages described here implement it in pure C#. BouncyCastle runs everywhere .NET runs, browser included; the security scheme stays the same (RSA-OAEP key exchange, authenticated AES-256-GCM data encryption since 3.0).
Install OutWit.Communication.Client.Encryption.BouncyCastle and OutWit.Communication.Server.Encryption.BouncyCastle, then configure both sides with WithBouncyCastleEncryption() instead of WithEncryption():
// Server
var server = WitServerBuilder.Build(options =>
{
options.WithWebSocket("http://localhost:5000", maxNumberOfClients: 100);
options.WithJson();
options.WithBouncyCastleEncryption();
options.WithService(new MyService());
});
// Client (Blazor WebAssembly)
var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket("ws://localhost:5000");
options.WithJson();
options.WithBouncyCastleEncryption();
});The two encryption modes are not interoperable: a BouncyCastle server talks only to BouncyCastle clients and vice versa. Pick one mode per connection and configure it on both ends. For ordinary .NET-to-.NET communication the standard WithEncryption() is sufficient and avoids the extra dependency; reach for BouncyCastle when a WebAssembly (or otherwise restricted) client is involved. More on the WASM environment in Blazor WebAssembly & AOT →.
Practices worth following
Encrypt by default on any network. Enable encryption (or TLS) for every connection that leaves the machine. Skipping it is defensible only inside a controlled local environment, and the performance cost of AES is small enough that "just in case" is usually the right call.
Keep configurations symmetric. Every security option mirrors: encryption mode, token, validator expectations. A mismatch on any of them fails the handshake. Sourcing the shared values from one configuration point on both sides prevents drift.
Treat tokens like passwords. Keep them out of source code; supply them through configuration or environment variables, and rotate them on a schedule or on suspicion of exposure.
Grow into custom validation when one token stops being enough. Per-client keys, expiring tokens, and integration with an existing identity system all fit the IAccessTokenValidator / IAccessTokenProvider pair without changing anything else in the setup.
Test the failure paths. Verify that a client with a wrong token is rejected, and that mismatched encryption fails to connect. WithLogger(...) on the server shows exactly which handshake step refused a client, which turns security debugging from guesswork into reading a log line.