cafe 1.0.138
dotnet add package cafe --version 1.0.138
NuGet\Install-Package cafe -Version 1.0.138
<PackageReference Include="cafe" Version="1.0.138" />
<PackageVersion Include="cafe" Version="1.0.138" />
<PackageReference Include="cafe" />
paket add cafe --version 1.0.138
#r "nuget: cafe, 1.0.138"
#:package cafe@1.0.138
#addin nuget:?package=cafe&version=1.0.138
#tool nuget:?package=cafe&version=1.0.138
Readme
- BouncyCastle TLS 1.3 Handshake Flow
- HTTP/2 Frame Handling
- HTTP Server Core Components
BouncyCastle TLS 1.3 Handshake Flow
Based on BouncyCastle C# source (bc-csharp-master/crypto/src/tls/).
State Machine
Key states defined in TlsProtocol.cs:
| State | Value | Description |
|---|---|---|
| CS_START | 0 | Initial, waiting for ClientHello |
| CS_CLIENT_HELLO | 1 | ClientHello received |
| CS_SERVER_HELLO_RETRY_REQUEST | 2 | HelloRetryRequest sent |
| CS_CLIENT_HELLO_RETRY | 3 | Second ClientHello received |
| CS_SERVER_HELLO | 4 | ServerHello sent |
| CS_SERVER_ENCRYPTED_EXTENSIONS | 5 | EncryptedExtensions sent |
| CS_SERVER_CERTIFICATE | 7 | Certificate sent |
| CS_SERVER_FINISHED | 20 | Server Finished sent |
| CS_CLIENT_FINISHED | 18 | Client Finished received |
| CS_END | 21 | Handshake complete |
Full Handshake Sequence (No HelloRetryRequest)
Client Server
| |
| 1. ClientHello |
|------------------------------>| CS_START -> CS_CLIENT_HELLO
| | ReceiveClientHelloMessage()
| | Generate13ServerHello()
| | - Negotiate version/cipher suite
| | - ECDHE key exchange (embedded)
| | - Derive earlySecret, handshakeSecret, masterSecret
| |
| 2. ServerHello |
|<------------------------------| CS_SERVER_HELLO
| | SendServerHelloMessage()
| |
| [ChangeCipherSpec] |
|<------------------------------| Middlebox compatibility (RFC 8446 D.4)
| |
| === Encrypted from here === |
| | Send13ServerHelloCoda()
| | Establish13PhaseHandshake()
| | -> derive "c hs traffic", "s hs traffic"
| | EnablePendingCipherWrite()
| |
| 3. EncryptedExtensions |
|<------------------------------| CS_SERVER_ENCRYPTED_EXTENSIONS
| | Send13EncryptedExtensionsMessage()
| |
| 4. [CertificateRequest] |
|<------------------------------| (optional, client auth)
| |
| 5. Certificate |
|<------------------------------| CS_SERVER_CERTIFICATE
| | Establish13ServerCredentials() -> GetCredentials()
| | Send13CertificateMessage()
| | Certificate.Encode() requires:
| | TLS 1.3: certificateRequestContext = EmptyBytes (not null)
| | TLS 1.2: certificateRequestContext = null
| |
| 6. CertificateVerify |
|<------------------------------| Generate13CertificateVerify()
| | TLS 1.3 only allows RSA-PSS (not PKCS#1 v1.5)
| | SignatureAndHashAlgorithm must use Intrinsic hash
| |
| 7. Finished |
|<------------------------------| CS_SERVER_FINISHED
| | Send13FinishedMessage()
| | Establish13PhaseApplication()
| | -> derive "c ap traffic", "s ap traffic"
| | EnablePendingCipherWrite()
| |
| 8. [Certificate] |
|------------------------------>| (only if CertificateRequest was sent)
| 9. [CertificateVerify] |
|------------------------------>|
| |
| 10. Finished |
|------------------------------>| CS_CLIENT_FINISHED
| | Receive13ClientFinished()
| | Verify client's verify_data
| | EnablePendingCipherRead()
| | CompleteHandshake()
| |
| <==== Application Data ====> | CS_END
Key Exchange (ECDHE)
TLS 1.3 key exchange happens inside Generate13ServerHello(), NOT via the m_keyExchange object:
SelectKeyShareGroup()- negotiate ECDH group from client/server supported groupsFindEarlyKeyShare()- check if client sent a share for the negotiated group- If no match → trigger HelloRetryRequest
- If match → create
TlsAgreement:agreement.ReceivePeerValue(clientShare.KeyExchange)- feed client's public keyagreement.GenerateEphemeral()- generate server ephemeral key pairagreement.CalculateSecret()- compute shared secret
Secret Derivation
Three phases of key derivation via HKDF:
0-RTT: earlySecret = HKDF-Extract(0, PSK_or_0)
|
Handshake: handshakeSecret = HKDF-Extract(derivedSecret, sharedSecret)
|
App: masterSecret = HKDF-Extract(derivedSecret, 0)
Establish13PhaseSecrets()- derives all three phase secretsEstablish13PhaseHandshake()- derives handshake traffic keys from handshakeSecretEstablish13PhaseApplication()- derives application traffic keys from masterSecret
TLS 1.3 vs TLS 1.2 Differences in BouncyCastle
| Aspect | TLS 1.2 | TLS 1.3 |
|---|---|---|
| KeyExchangeAlgorithm | ECDHE_RSA(19), DHE_RSA(5), etc. | NULL(0) |
| Certificate.RequestContext | must be null | must be non-null (EmptyBytes for server) |
| CertificateVerify signature | RSA PKCS#1 v1.5 allowed | RSA-PSS only |
| SignatureAndHashAlgorithm for PSS | GetInstance(sha256, rsa_pss_rsae_sha256) | Use static fields: SignatureAndHashAlgorithm.rsa_pss_rsae_sha256 (hash=Intrinsic) |
| Key exchange object | m_keyExchange | embedded in Generate13ServerHello() |
| Encryption start | after ChangeCipherSpec | after ServerHello (all subsequent messages encrypted) |
Pitfalls Encountered
1. KeyExchangeAlgorithm.NULL (0)
TLS 1.3 cipher suites map to KeyExchangeAlgorithm.NULL = 0. DefaultTlsServer.GetCredentials() doesn't handle this case, throws internal_error. Fix: override GetCredentials() to handle NULL.
2. Certificate.Encode() certificateRequestContext
Certificate constructed via new Certificate(TlsCertificate[]) sets certificateRequestContext = null. TLS 1.3's Encode() asserts it must be non-null. Fix: for TLS 1.3, reconstruct with new Certificate(TlsUtilities.EmptyBytes, entries).
3. RSA-PSS SignatureAndHashAlgorithm
GetInstance(HashAlgorithm.sha256, SignatureAlgorithm.rsa_pss_rsae_sha256) creates a wrong instance (hash=4). The correct instance has hash=8 (Intrinsic). Fix: use SignatureAndHashAlgorithm.rsa_pss_rsae_sha256 static field. TLS 1.3 clients reject PKCS#1 v1.5 in CertificateVerify with illegal_parameter(47).
HTTP/2 Frame Handling
Overview
HTTP/2 (RFC 9113) is a binary framing protocol that multiplexes multiple requests/responses over a single TCP connection. Unlike HTTP/1.1's text-based request/response model, HTTP/2 uses binary frames (HEADERS, DATA, SETTINGS, etc.) carried on numbered streams.
Key HTTP/2 Concepts
Connection Preface
The client begins by sending a 24-byte magic string (connection preface):
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
This is immediately followed by a SETTINGS frame (which may be empty). The server must send its own SETTINGS frame in response.
Frame Format
Every HTTP/2 frame has a fixed 9-byte header followed by a variable-length payload:
+-----------------------------------------------+
| Length (24 bits) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
- Length: 24-bit unsigned integer, max 2^24-1 (16,777,215) bytes
- Type: 8-bit frame type identifier
- Flags: 8-bit field, semantics depend on frame type
- R: 1-bit reserved, must be 0
- Stream Identifier: 31-bit unsigned integer, 0 for connection-level frames
Frame Types
| Type | Value | Description |
|---|---|---|
| DATA | 0x0 | Request/response body data |
| HEADERS | 0x1 | Opens a stream with header fields |
| PRIORITY | 0x2 | Stream priority (deprecated in RFC 9113) |
| RST_STREAM | 0x3 | Abrupt stream termination |
| SETTINGS | 0x4 | Connection-level configuration parameters |
| PUSH_PROMISE | 0x5 | Server push notification |
| PING | 0x6 | Liveness check / RTT measurement |
| GOAWAY | 0x7 | Graceful connection shutdown |
| WINDOW_UPDATE | 0x8 | Flow control window increment |
| CONTINUATION | 0x9 | Continuation of a fragmented HEADERS block |
Common Flags
| Flag | Bit | Applicable Frames | Meaning |
|---|---|---|---|
| END_STREAM | 0x1 | DATA, HEADERS | Last frame on this stream |
| END_HEADERS | 0x4 | HEADERS, PUSH_PROMISE, CONTINUATION | Last frame of a header block |
| PADDED | 0x8 | DATA, HEADERS, PUSH_PROMISE | Frame has padding |
| PRIORITY | 0x20 | HEADERS | Frame carries priority info |
| ACK | 0x1 | SETTINGS, PING | Acknowledgement frame |
Stream Lifecycle
Streams in HTTP/2 have well-defined states:
+--------+
send PP | | recv PP
,-----------| idle |-----------.
/ | | \
v +--------+ v
+----------+ +----------+
| | send H / recv H | |
| reserved | | reserved |
| (local) | | (remote) |
| | | |
+----------+ +----------+
| send H / recv H |
| ,----------------------'
v v
+--------+
| |
| open |
| |
+--------+
| ^
send ES / recv ES |
`-----------'
A stream transitions from idle → open when the first HEADERS frame is sent/received. The stream moves to half-closed (local) or half-closed (remote) when END_STREAM is sent or received, respectively. When both sides have sent/received END_STREAM, the stream is closed.
HPACK Header Compression
HTTP/2 mandates HPACK (RFC 7541) for header compression to reduce overhead from repeated header fields. HPACK uses:
- Static Table: 61 predefined header field name-value pairs (e.g.,
:method: GETat index 2,:path: /at index 4) - Dynamic Table: Built incrementally during the connection, populated by literal header fields with indexing
- Huffman Coding: Optional Huffman-encoded string representation for compact transmission
HPACK Representations
| Type | Bit Pattern | Description |
|---|---|---|
| Indexed Header Field | 1xxxxxxx |
References static/dynamic table by index |
| Literal with Incremental Indexing | 01xxxxxx |
New header, added to dynamic table |
| Literal without Indexing | 0000xxxx |
New header, not stored |
| Literal never Indexed | 0001xxxx |
New header, never stored (sensitive) |
| Dynamic Table Size Update | 001xxxxx |
Changes max dynamic table size |
Pseudo-Headers
HTTP/2 requests use pseudo-headers (prefixed with :) instead of a request line:
| Pseudo-Header | Description |
|---|---|
:method |
HTTP method (GET, POST, etc.) |
:scheme |
URI scheme (http, https) |
:authority |
Host and port |
:path |
Request path and query string |
:status |
Response status code (response only) |
Http2FrameHandler Usage
Class Overview
Http2FrameHandler (cafe.network/http/Http2FrameHandler.cs) is an abstract base class that incrementally parses HTTP/2 frames from a byte stream and dispatches them to type-specific virtual handler methods.
Key Methods
| Method | Description |
|---|---|
ProcessBytes(ReadOnlySpan<byte> data) |
Feeds raw bytes into the internal buffer. Complete frames are parsed and dispatched automatically. Unconsumed partial frames are retained. |
DispatchFrame(Http2Frame frame) |
Routes a parsed frame to its type-specific handler (OnData, OnHeaders, etc.) |
Virtual Handler Methods
Override these in a subclass to implement frame processing logic:
protected virtual void OnData(Http2Frame frame) { }
protected virtual void OnHeaders(Http2Frame frame) { }
protected virtual void OnPriority(Http2Frame frame) { }
protected virtual void OnRstStream(Http2Frame frame) { }
protected virtual void OnSettings(Http2Frame frame) { }
protected virtual void OnPushPromise(Http2Frame frame) { }
protected virtual void OnPing(Http2Frame frame) { }
protected virtual void OnGoAway(Http2Frame frame) { }
protected virtual void OnWindowUpdate(Http2Frame frame) { }
protected virtual void OnContinuation(Http2Frame frame) { }
protected virtual void OnUnknownFrame(Http2Frame frame) { }
Helper Methods for Constructing Frames
| Static Method | Description |
|---|---|
CreateSettingsFrame(ids?, values?, ack) |
Builds a SETTINGS frame with optional parameters |
CreateSettingsAckFrame() |
Builds a SETTINGS ACK frame |
CreatePingFrame(opaqueData, ack) |
Builds a PING frame (8-byte opaque data) |
CreateGoAwayFrame(lastStreamId, errorCode, debugData?) |
Builds a GOAWAY frame |
CreateWindowUpdateFrame(streamId, increment) |
Builds a WINDOW_UPDATE frame |
CreateRstStreamFrame(streamId, errorCode) |
Builds a RST_STREAM frame |
Usage Pattern
// 1. Subclass Http2FrameHandler
class MyHandler : Http2FrameHandler
{
public bool IsComplete { get; private set; }
protected override void OnSettings(Http2Frame frame)
{
// Acknowledge settings
var ack = Http2FrameHandler.CreateSettingsAckFrame();
// ... send ack frame bytes back to client
}
protected override void OnHeaders(Http2Frame frame)
{
// Parse pseudo-headers (:method, :path, etc.) from payload
// Check END_STREAM flag to determine if body follows
}
protected override void OnData(Http2Frame frame)
{
// Accumulate request body from payload
if (frame.HasFlag(Http2FrameFlags.EndStream))
IsComplete = true;
}
}
// 2. Feed bytes into the handler
var handler = new MyHandler();
while (moreDataAvailable)
{
int bytesRead = await stream.ReadAsync(buffer);
handler.ProcessBytes(new ReadOnlySpan<byte>(buffer, 0, bytesRead));
}
Integration with HttpParser.HandleHttp2
The HttpParser class (cafe.network/http/HttpParser.cs) uses an internal Http2RequestHandler subclass to handle HTTP/2 request parsing:
- After detecting the HTTP/2 connection preface, the remaining bytes are fed to the handler
- Data is read from the stream in a loop, processed incrementally via
ProcessBytes() - The handler collects HEADERS frames (parsing
:method,:path, and regular headers) and DATA frames (accumulating the body) - When END_STREAM is received, parsing completes and
BuildRequest()assembles anHttpRequestobject
Request Parsing Flow
Client Server
| |
| Connection Preface |
| "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" |
|------------------------------>| ValidateHttp2()
| |
| SETTINGS frame (0, Ack=0) |
|------------------------------>| OnSettings() - marks received
| |
| HEADERS frame (Stream 1) |
| :method: GET |
| :path: /api/test |
| END_STREAM=1 | <- No body
|------------------------------>| OnHeaders() → ParseHeadersPayload()
| | → _streamMethod[1] = "GET"
| | → _streamPath[1] = "/api/test"
| | MarkStreamEnded(1)
| | CheckStreamComplete(1) → IsComplete
| |
| <======== BuildRequest() ======== |
| HttpRequest { Method="GET", Path="/api/test", Version="HTTP/2" }
With Request Body
| |
| HEADERS frame (Stream 1) |
| :method: POST |
| :path: /api/submit |
| END_HEADERS=1 |
|------------------------------>| OnHeaders()
| | (END_STREAM not set, body will follow)
| |
| DATA frame (Stream 1) |
| Payload: {"key":"val"} |
| END_STREAM=1 |
|------------------------------>| OnData() → accumulate body
| | MarkStreamEnded(1)
| | CheckStreamComplete(1) → IsComplete
| |
| <======== BuildRequest() ======== |
| HttpRequest { Body = {"key":"val"}, ... }
Limitations
- HPACK Decoding is simplified: indexed header fields (those referencing the static/dynamic tables) are skipped. A full HPACK decoder with dynamic table support is needed for production use.
- Huffman Decoding is not implemented; Huffman-encoded string literals will produce garbled output.
- Server Push (
PUSH_PROMISEframes) are not handled. - Flow Control is not enforced; WINDOW_UPDATE processing is not implemented in the request handler.
- Continuation Fragmentation: Headers split across HEADERS + CONTINUATION frames are supported, but the frame ordering constraints are not validated.
HTTP Server Core Components
TcpHost
TcpHost Class Overview
TcpHost (cafe.network/TcpHost.cs) is the TCP server entry point. It listens on a specified port, accepts incoming connections, and dispatches each connection to HttpHandler for processing. It supports both plain TCP and TLS (via BouncyCastle) connections.
TcpHost Constructors
| Constructor | Description |
|---|---|
TcpHost() |
Creates a TcpHost with default keep-alive timeout (10 seconds) |
TcpHost(int keepAliveTimeoutSeconds = 10) |
Creates a TcpHost with a custom keep-alive timeout |
TcpHost(Certificate certificate, AsymmetricKeyParameter privateKey, int keepAliveTimeoutSeconds = 10) |
Creates a TLS-enabled TcpHost with the given certificate and private key |
TcpHost Key Methods
| Method | Description |
|---|---|
Listen(int port) |
Starts listening on the specified port. Each accepted connection is dispatched to a thread pool task. |
Stop() |
Stops the listener and sets the exit flag. |
TcpHost Usage Pattern
// 1. Plain HTTP server
var host = new TcpHost(keepAliveTimeoutSeconds: 30);
host.Listen(8080);
// 2. HTTPS server (TLS)
var cert = LoadCertificate(); // Org.BouncyCastle.Tls.Certificate
var key = LoadPrivateKey(); // Org.BouncyCastle.Crypto.AsymmetricKeyParameter
var tlsHost = new TcpHost(cert, key, keepAliveTimeoutSeconds: 30);
tlsHost.Listen(443);
// 3. Stop the server
host.Stop();
Internal Flow
Client Server
| |
| TCP Connect |
|------------------------------>| AcceptTcpClientAsync()
| | HttpSessionContext.Instance.Clear()
| | Taskpool.Start("stream", HandleRequestAsync)
| |
| [TLS Handshake if cert/key] |
|<----------------------------->| TlsServerStream wrap
| |
| HTTP Request |
|------------------------------>| HttpHandler.HandleRequestAsync(stream, ct)
| |
| HTTP Response |
|<------------------------------| Response.WriteAsync(stream)
| |
| [Keep-Alive loop or close] |
HttpHandler
HttpHandler Class Overview
HttpHandler (cafe.network/http/HttpHandler.cs) handles an HTTP connection within a keep-alive loop. For each request iteration, it clears the session context, parses the request via HttpParser, routes it through Router.Process(), and writes the response back. It is designed to be subclassed for custom handling logic.
HttpHandler Key Methods
| Method | Description |
|---|---|
HandleRequestAsync(Stream stream, CancellationToken ct) |
Main entry point. Runs a keep-alive loop that repeatedly parses requests and sends responses. |
HttpHandler Request Processing Flow
┌─────────────────────────────────────────────────┐
│ Keep-Alive Loop │
│ │
│ 1. HttpSessionContext.Instance.Clear() │
│ 2. HttpSessionContext.Instance.Setup() │
│ → Creates new HttpRequest & HttpResponse │
│ 3. request = await HttpParser.ParseRequest() │
│ 4. response = Router.Process(request, itid) │
│ 5. Determine keep-alive from request headers │
│ 6. If status >= 400, force close connection │
│ 7. await response.WriteAsync(stream) │
│ 8. If not keep-alive → break loop │
│ │
└─────────────────────────────────────────────────┘
HttpHandler Keep-Alive Logic
| Condition | Result |
|---|---|
Connection: keep-alive header present |
Keep connection open |
No Connection header + HTTP/1.1 |
Keep connection open (default) |
No Connection header + HTTP/1.0 |
Close connection |
| Response status code >= 400 | Force close connection |
HttpParser
HttpParser Class Overview
HttpParser (cafe.network/http/HttpParser.cs) is a static utility class that parses raw byte streams into HttpRequest objects. It automatically detects HTTP/1.1 vs HTTP/2 by checking for the HTTP/2 connection preface, and delegates to the appropriate internal handler.
HttpParser Key Methods
| Method | Description |
|---|---|
ParseRequest(Stream stream, TimeSpan idleTimeout) |
Static entry point. Reads initial bytes, detects HTTP version, and delegates to HandleHttp1_1() or HandleHttp2(). |
HttpParser HTTP/1.1 Parsing Flow
- Read bytes until
\r\n\r\ndelimiter is found (end of headers) - Parse the request line:
METHOD PATH VERSION - Parse headers line by line (
Name: Value) - Parse query string from URL if
?is present - Read body using one of two strategies:
ChunkedBodyParser: whenTransfer-Encoding: chunkedis presentDirectBodyParser: whenContent-Lengthis present (or no body)
- Returns the populated
HttpRequest
Stream bytes → Find \r\n\r\n → Parse request line + headers → Parse body → HttpRequest
HttpParser HTTP/2 Parsing Flow
- Validate the 24-byte HTTP/2 connection preface (
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n) - Create an internal
Http2RequestHandler(subclass ofHttp2FrameHandler) - Feed remaining bytes (after preface) into the handler
- Continue reading from stream and feeding into
handler.ProcessBytes()untilhandler.IsComplete - Call
handler.BuildRequest()to assemble the finalHttpRequest
Stream bytes → Validate preface → Http2RequestHandler.ProcessBytes() → BuildRequest() → HttpRequest
The Http2RequestHandler internal class:
- Collects HEADERS and CONTINUATION frames, parsing pseudo-headers (
:method,:path) and regular headers - Accumulates DATA frame payloads as the request body
- Tracks stream completion via END_STREAM flag
- Handles RST_STREAM (cleanup) and GOAWAY (throw IOException)
HttpSessionContext
HttpSessionContext Class Overview
HttpSessionContext (cafe.network/http/HttpSessionContext.cs) provides per-request context storage using AsyncLocal<T>. It holds the current HttpRequest and HttpResponse objects, making them accessible throughout the async call chain without explicit parameter passing.
HttpSessionContext Key Properties
| Property | Type | Description |
|---|---|---|
Request |
HttpRequest? |
The current HTTP request object |
Response |
HttpResponse? |
The current HTTP response object |
RequestId |
string |
Shorthand for Request.RequestId, or empty string if Request is null |
Instance |
HttpSessionContext |
Static property that gets/creates the context for the current async flow |
HttpSessionContext Key Methods
| Method | Description |
|---|---|
Setup() |
Creates a new HttpSessionContext with fresh HttpRequest and HttpResponse instances, and sets it as the current async context. |
Clear() |
Resets the async context to a new empty HttpSessionContext. |
HttpSessionContext Usage Pattern
// The context is automatically managed by HttpHandler in the keep-alive loop.
// Access it anywhere during request processing:
var request = HttpSessionContext.Instance.Request;
var response = HttpSessionContext.Instance.Response;
// Read request info
string method = request.Method;
string path = request.Path;
// Write response
response.StatusCode = 200;
response.Body = Encoding.UTF8.GetBytes("Hello World");
HttpSessionContext Integration with Other Components
| Component | How it uses HttpSessionContext |
|---|---|
TcpHost |
Calls Clear() when a new connection is accepted |
HttpHandler |
Calls Clear() and Setup() at the start of each keep-alive iteration; calls Clear() in the finally block |
HttpParser |
Populates Instance.Request with parsed method, path, headers, body, etc. |
Router |
Reads from Instance.Request and writes to Instance.Response for static file serving and BizUnit routing |
Context Lifecycle per Connection
Connection Accepted
│
├─ TcpHost: Clear()
│
└─ Keep-Alive Loop (HttpHandler):
│
├─ Clear() ──→ resets to empty context
├─ Setup() ──→ creates new Request + Response
├─ HttpParser.ParseRequest() ──→ populates Request
├─ Router.Process() ──→ populates Response
├─ Response.WriteAsync() ──→ sends to client
│
└─ finally: Clear()
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- BouncyCastle.Cryptography (>= 2.7.0-beta.58)
- Mimosa (>= 1.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
初始版本