From c30022b558e8bd0c86f6533e2644748807c188c6 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 18 Aug 2024 21:43:39 +0200 Subject: [PATCH 1/4] WIP --- wit/tls.wit | 309 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 wit/tls.wit diff --git a/wit/tls.wit b/wit/tls.wit new file mode 100644 index 0000000..9a53f7c --- /dev/null +++ b/wit/tls.wit @@ -0,0 +1,309 @@ +interface tls { + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/poll@0.2.0.{pollable}; + + /// TLS protocol version. + /// + /// At the time of writing, these are the existing TLS versions: + /// - 0x0200: SSLv2 (Deprecated) + /// - 0x0300: SSLv3 (Deprecated) + /// - 0x0301: TLSv1.0 (Deprecated) + /// - 0x0302: TLSv1.1 (Deprecated) + /// - 0x0303: TLSv1.2 + /// - 0x0304: TLSv1.3 + /// + /// TODO: Want to use regular WIT `enum` type, but then adding a new protocol is backwards incompatible. + type protocol-version = u16; + + /// Application-Layer Protocol Negotiation (ALPN) protocol ID. + /// + /// ALPN IDs are between 1 and 255 bytes long. Typically, they represent the + /// binary encoding of an ASCII string (e.g. `[0x68 0x32]` for `"h2"`), + /// though this is not required. + type alpn-id = list; + + /// A X509 certificate chain; starting with the end-entity's certificate + /// followed by 0 or more intermediate certificates. + resource public-identity { + export-X509-chain: func() -> list>; + } + + /// The combination of a private key with its public certificate(s). + /// The private key data can not be exported. + resource private-identity { + /// TODO: find a way to "preopen" these private-identity resources, so that the sensitive private key data never has to flow through the guest. + parse: static func(private-key: list, x509-chain: list>) -> result; + + public-identity: func() -> public-identity; + } + + /// Resource to configure, control & observe a TLS client stream. + /// + /// # Transform stream + /// A TLS client resource does not perform any I/O on its own. It is a pure + /// stream transformer that takes cleartext data on one side and emits + /// secured TLS data on the other side and vice-versa. + /// The user of this resource is responsible for continually + /// "pumping" the data from the underlying socket into the TLS client's + /// `public-output` stream and the `public-input` back into the socket. + /// + /// # Usage + /// The general usage pattern looks something like this: + /// - Instantiate new `client`. + /// - (Optional) Further refine the settings using the various `configure-*` methods. + /// - Forward the "public" streams acquired in the previous step into the underlying socket. + /// - Call `resume`. + /// - Read & write application data into the "private" streams. + /// + /// # Suspend / resume + /// A `client` always starts out suspended. During this initial suspension + /// the various `configure-*` methods may be called. After configuration, + /// the TLS handshake must be manually initiated using the `resume` method. + /// + /// Many TLS libraries let users provide custom behavior through the + /// registration of callbacks. The WebAssembly Component Model does not + /// support callbacks. Instead, these customization points are modeled as + /// additional suspensions. + /// + /// The `suspend-at` parameter of the `connect` constructor controls at which + /// moments during the lifetime of the TLS stream the client should + /// automatically suspend itself. + /// + /// The consumer drives this transition using the `suspend` method. If the + /// client is not ready to be suspended, the pollable returned by `subscribe` + /// can be used to wait for its readiness. When `suspend` succeeds, the client + /// is suspended and some settings are open for configuration again. Once + /// ready to continue the connection, the consumer should call `resume`. + /// + /// In general: + /// - While a client is suspended, no data flows through the I/O streams. + /// - A client may only be configured while it is suspended. + /// + /// # Secure by default + /// Implementations should pick reasonably safe defaults for all security related + /// settings. Users of this interface should be able to confidently instantiate + /// a new `client` and then, without further configuration, immediately + /// initiate the handshake. + resource client { + /// Create a new suspended TLS client. + constructor(server-name: string, suspend-at: client-suspension-points); + + /// Obtain the I/O streams associated with this client. + /// These must be obtained exactly once, before the first call to `resume`. + /// + /// Returns an error if they were already obtained before. + /// + /// The I/O streams are child resources of the client. They must be + /// dropped before the client is dropped. + streams: func() -> result; + + + + + + /// Configure the ALPN IDs for this client to adertise to the server, + /// in descending order of preference. + /// + /// This may only be configured while suspended in the `constructed` phase. + configure-alpn-ids: func(value: list) -> result; + + /// Configure the client certificates, in descending order of preference. + /// + /// If the server requests a certificate from the client, + /// the TLS implementation will consult this list of configured identities + /// in the provided order and pick the first one that satisfies the + /// constraints sent by the server. If there was no match, then by default + /// it is up to the server to decide whether to continue or abort the connection. + /// + /// This may only be configured while suspended in the `constructed`, + /// `verify-server-identity` or `select-client-identity` phases. + configure-identities: func(value: list>) -> result; + + + + + + /// The server name that was provided during construction of this client. + server-name: func() -> string; + + /// The negotiated ALPN ID, if any. + /// + /// Returns `none` when: + /// - the handshake did not take place yet, + /// - the client did not advertise any ALPN IDs, + /// - a successful handshake has occurred, but there was no intersection + /// between the IDs advertised by the client and the IDs supported by + /// the server. + alpn-id: func() -> option; + + /// The negotiated TLS protocol version. + /// + /// Returns `none` if the handshake did not take place yet. + protocol-version: func() -> option; + + /// The client's identity advertised to the server, if any. + /// This will be one of the identities configured using `configure-identities`. + /// + /// This becomes available _after_ the `select-client-identity` phase. + /// + /// Returns `none` when: + /// - the handshake did not take place yet, + /// - the server did not request a client certificate, + /// - the server did request a client certificate, but there was no match + /// with the configured identities. + client-identity: func() -> option; + + /// The validated certificate of the server. + /// + /// This becomes available _after_ the `verify-server-identity` phase. + server-identity: func() -> option; + + + + + + /// Attempt to suspend the client at one of the places specified by + /// `suspend-at` during construction of the client. + /// + /// Returns `error(not-ready)` if the client is not ready to be suspended. + /// Use the pollable returned by `subscribe` to wait for its readiness. + /// + /// Returns `error(already-suspended)` is already suspended. + /// + /// Returns `error(closed)` if the connection has shut down either + /// successfully or abornmally. + /// + /// The suspension resource is a child resource of the client. Dropping + /// the `client` while it still has an active suspension resource may trap. + suspend: func() -> result; + + /// Resume the suspended client. Returns an error if the client is not + /// suspended. + /// + /// If the client was suspended though `suspend` (i.e. it is not the + /// initial resumption), this method will trap if the suspension + /// resource hasn't been dropped yet. + /// + /// Returns an error if the client is not suspended. + resume: func() -> result; + + /// Create a `pollable` which can be used to poll for + /// the client to be suspendable or closed. + /// + /// `subscribe` only has to be called once per client and can be (re)used + /// for the remainder of the client's lifetime. + subscribe: func() -> pollable; + } + + /// The I/O streams that represent both sides of the transform. + /// + /// The application side interacts with the cleartext "private" streams. + /// The network side interacts with the encrypted "public" streams. + /// + /// A typical setup looks something like this: + /// + /// ```text + /// : TCP Socket TLS Client/Server + /// +-----------------+ +---------------------------------------------+ + /// | | splice | decryption | read + /// | `input-stream` | ========>> | `public-output` =========>> `private-input` | ======>> your + /// | | | | app + /// | | | | lives + /// | `output-stream` | <<======== | `public-input` <<========= `private-output` | <<====== here + /// | | splice | encryption | write + /// +-----------------+ +---------------------------------------------+ + /// ``` + /// + /// The user of this interface is responsible for continually forwarding + /// data from the socket into the `public-output` stream and + /// data from the `public-input` into the socket. + /// + /// # Caution + /// Because the guest acts as both the producer and the consumer for these + /// streams, do not use the `blocking_*` methods as that will deadlock yourself. + record io-streams { + public-input: input-stream, + public-output: output-stream, + private-output: output-stream, + private-input: input-stream, + } + + flags client-suspension-points { + /// When the client received the server's certificate. + verify-server-identity, + + /// When the client received a certificate request from the server. + select-client-identity, + + /// When the initial handshake was successful. + connected, + } + + resource client-suspension { + /// At which point the TLS stream is suspended. + /// Exactly one flag is set in the return value. + at: func() -> client-suspension-points; + + /// Only for select-client-identity: + // TODO: acceptable-authorities: func() -> result>; + + /// Only for verify-server-identity: + // TODO: unverified-identity: func() -> result; + } + + enum suspend-error { + not-ready, + already-suspended, + closed, + } + + + + + + + + + + // TODO + resource server { + constructor(suspend-at: server-suspension-points); + streams: func() -> result; + + configure-alpn-ids: func(value: list) -> result; + configure-identities: func(value: list>) -> result; + + server-name: func() -> option; + alpn-id: func() -> option; + protocol-version: func() -> option; + client-identity: func() -> option; + server-identity: func() -> option; + + suspend: func() -> result; + resume: func() -> result; + subscribe: func() -> pollable; + } + + flags server-suspension-points { + /// When the server received the initial message from the client. + client-hello, + + /// When the server received the client's certificate. + verify-client-identity, + + /// When the initial handshake was successful. + accepted, + } + + resource server-suspension { + at: func() -> server-suspension-points; + + /// Only for client-hello: + // TODO: requested-protocol-versions: func() -> result>; + // TODO: requested-server-name: func() -> result>; + // TODO: requested-alpn-ids: func() -> result>; + + /// Only for verify-client-identity: + // TODO: unverified-identity: func() -> result; + } +} From c5054109510bc2f044839fe802934a110096957f Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Mon, 19 Aug 2024 22:44:01 +0200 Subject: [PATCH 2/4] Map out relationships with .NET types --- TLS.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 TLS.md diff --git a/TLS.md b/TLS.md new file mode 100644 index 0000000..54becb6 --- /dev/null +++ b/TLS.md @@ -0,0 +1,92 @@ +## Mapping to .NET types + +### `SslStream` + +| Member | WASI equivalent | +|------------------------------------|--| +| `CheckCertRevocationStatus` | ⛔ Not supported. Can be faked to return `false`. | +| `CipherAlgorithm` | ⛔ Not supported. | +| `CipherStrength` | ⛔ Not supported. | +| `HashAlgorithm` | ⛔ Not supported. | +| `HashStrength` | ⛔ Not supported. | +| `IsAuthenticated` | ✅ `true` after the `connected`/`accepted` suspension occurred. | +| `IsEncrypted` | ✅ Alias for `IsAuthenticated` | +| `IsMutuallyAuthenticated` | ✅ Check that the connection `IsAuthenticated`, and that both `client-identity` and `server-identity` are not null. | +| `IsServer` | ✅ To be maintained in userland | +| `IsSigned` | ✅ Alias for `IsAuthenticated` | +| `KeyExchangeAlgorithm` | ⛔ Not supported. | +| `KeyExchangeStrength` | ⛔ Not supported. | +| `LocalCertificate` | ✅ `tls-client::client-identity` / `tls-server::server-identity` | +| `NegotiatedApplicationProtocol` | ✅ `tls-client::alpn-id` / `tls-server::alpn-id` | +| `NegotiatedCipherSuite` | ⛔ Not supported. | +| `RemoteCertificate` | ✅ `tls-client::server-identity` / `tls-server::client-identity` | +| `SslProtocol` | ✅ `tls-client::protocol-version` / `tls-server::protocol-version` | +| `TargetHostName` | ✅ `tls-client::server-name` / `tls-server::server-name` | +| `TransportContext` | ❔ Unknown | +| `AuthenticateAsClient`, `AuthenticateAsClientAsync`, `BeginAuthenticateAsClient`, `EndAuthenticateAsClient` | ✅ Construct `tls-client` with at least the `connected` suspension enabled, configure it (see `SslClientAuthenticationOptions` table below), call `resume`, wait for the `connected` suspension. | +| `AuthenticateAsServer`, `AuthenticateAsServerAsync`, `BeginAuthenticateAsServer`, `EndAuthenticateAsServer` | ✅ Construct `tls-server` with at least the `accepted` suspension enabled, configure it (see `SslServerAuthenticationOptions` table below), call `resume`, wait for the `accepted` suspension. For the `ServerOptionsSelectionCallback` overload, enable `client-hello` suspension (see `SslClientHelloInfo` table below). | +| `NegotiateClientCertificateAsync` | ⛔ Not supported. | +| `Read`, `ReadAsync`, `BeginRead`, `EndRead`, `ReadByte`, `ReadAtLeast`, `ReadAtLeastAsync`, `ReadExactly`, `ReadExactlyAsync` | ✅ `private-input::read` | +| `Write`, `WriteAsync`, `BeginWrite`, `EndWrite`, `WriteByte` | ✅ `private-output::write` | +| `CopyTo`, `CopyToAsync` | ✅ Currently implemented in user space. Could be specialized as `output-stream::splice` in case both sides are WASI streams. | +| `Flush`, `FlushAsync` | ✅ `private-output::flush` | +| `Dispose`, `DisposeAsync`, `Close`, `Finalize`, `ShutdownAsync` | ✅ Fully flush and drop the `private-output` | +| `CanRead` | ✅ Implemented in user space | +| `ReadTimeout` | ✅ Implemented in user space | +| `CanWrite` | ✅ Implemented in user space | +| `WriteTimeout` | ✅ Implemented in user space | +| `CanTimeout` | ✅ Implemented in user space | +| `Position` | ✅ Implemented in user space | +| `InnerStream` | ✅ Implemented in user space | +| `LeaveInnerStreamOpen` | ✅ Implemented in user space | +| `CanSeek` | ✅ Not applicable to network streams. `false` | +| `Seek` | ✅ Not applicable to network streams. `throw new NotSupportedException()` | +| `Length` | ✅ Not applicable to network streams. `throw new NotSupportedException()` | +| `SetLength` | ✅ Not applicable to network streams. `throw new NotSupportedException()` | +| `ToString`, `Equals`, `GetHashCode`, `GetLifetimeService`, `GetType`, `InitializeLifetimeService`, `MemberwiseClone`, `ObjectInvariant`, `CreateObjRef`, `CreateWaitHandle` | ✅ Generic .NET methods. Not specific to TLS. Implemented in user space | + + +### `SslClientAuthenticationOptions` + +| Member | WASI equivalent | +|---------------------------------------|--| +| `AllowRenegotiation` | ⛔ Not supported. | +| `AllowTlsResume` | ⛔ Not supported. | +| `ApplicationProtocols` | ✅ `tls-client::configure-alpn-ids` | +| `CertificateChainPolicy` | ❔ Unknown | +| `CertificateRevocationCheckMode` | ⛔ Not supported. Or effectively: only `NoCheck` is supported. | +| `CipherSuitesPolicy` | ⛔ Not supported. Or effectively: only `null` (== OS default) is supported. | +| `ClientCertificateContext` | ❔ Unknown | +| `ClientCertificates` | ✅ `tls-client::configure-identities` | +| `EnabledSslProtocols` | ⛔ Not supported. Or effectively: only `None` (== OS default) is supported. | +| `EncryptionPolicy` | ⛔ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | +| `LocalCertificateSelectionCallback` | ✅ Enable `select-client-identity` suspension, while suspended call `tls-client::configure-identities`, and then `resume` | +| `RemoteCertificateValidationCallback` | ✅ Enable `verify-server-identity` suspension, perform validation and then either `resume` or abort the connection. | +| `TargetHost` | ✅ The `server-name` parameter of the `tls-client` constructor. | + + +### `SslServerAuthenticationOptions` + +| Member | WASI equivalent | +|---------------------------------------|--| +| `AllowRenegotiation` | ⛔ Not supported. | +| `AllowTlsResume` | ⛔ Not supported. | +| `ApplicationProtocols` | ✅ `tls-server::configure-alpn-ids` | +| `CertificateChainPolicy` | ❔ Unknown | +| `CertificateRevocationCheckMode` | ⛔ Not supported. Or effectively: only `NoCheck` is supported. | +| `CipherSuitesPolicy` | ⛔ Not supported. Or effectively: only `null` (== OS default) is supported. | +| `ClientCertificateRequired` | ⛔ Not supported. | +| `EnabledSslProtocols` | ⛔ Not supported. Or effectively: only `None` (== OS default) is supported. | +| `EncryptionPolicy` | ⛔ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | +| `RemoteCertificateValidationCallback` | ✅ Enable `verify-client-identity` suspension, perform validation and then either `resume` or abort the connection. | +| `ServerCertificate` | ✅ `tls-server::configure-identities` | +| `ServerCertificateContext` | ❔ Unknown | +| `ServerCertificateSelectionCallback` | ✅ Enable `client-hello` suspension, while suspended call `tls-server::configure-identities`, and then `resume` | + + +### `SslClientHelloInfo` + +| Member | WASI equivalent | +|----------------|--| +| `ServerName` | ✅ `server-suspension::requested-server-name` | +| `SslProtocols` | ✅ `server-suspension::requested-protocol-versions` | \ No newline at end of file From bf29dda73e9ab925a481ab20a05663f3304610ce Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Wed, 21 Aug 2024 12:56:58 +0200 Subject: [PATCH 3/4] Map out Node.JS `tls` interface --- TLS.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/TLS.md b/TLS.md index 54becb6..aaa2020 100644 --- a/TLS.md +++ b/TLS.md @@ -54,12 +54,12 @@ | `AllowTlsResume` | ⛔ Not supported. | | `ApplicationProtocols` | ✅ `tls-client::configure-alpn-ids` | | `CertificateChainPolicy` | ❔ Unknown | -| `CertificateRevocationCheckMode` | ⛔ Not supported. Or effectively: only `NoCheck` is supported. | -| `CipherSuitesPolicy` | ⛔ Not supported. Or effectively: only `null` (== OS default) is supported. | +| `CertificateRevocationCheckMode` | ⚠️ Not supported. Or effectively: only `NoCheck` is supported. | +| `CipherSuitesPolicy` | ⚠️ Not supported. Or effectively: only `null` (== OS default) is supported. | | `ClientCertificateContext` | ❔ Unknown | | `ClientCertificates` | ✅ `tls-client::configure-identities` | -| `EnabledSslProtocols` | ⛔ Not supported. Or effectively: only `None` (== OS default) is supported. | -| `EncryptionPolicy` | ⛔ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | +| `EnabledSslProtocols` | ⚠️ Not supported. Or effectively: only `None` (== OS default) is supported. | +| `EncryptionPolicy` | ⚠️ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | | `LocalCertificateSelectionCallback` | ✅ Enable `select-client-identity` suspension, while suspended call `tls-client::configure-identities`, and then `resume` | | `RemoteCertificateValidationCallback` | ✅ Enable `verify-server-identity` suspension, perform validation and then either `resume` or abort the connection. | | `TargetHost` | ✅ The `server-name` parameter of the `tls-client` constructor. | @@ -73,11 +73,11 @@ | `AllowTlsResume` | ⛔ Not supported. | | `ApplicationProtocols` | ✅ `tls-server::configure-alpn-ids` | | `CertificateChainPolicy` | ❔ Unknown | -| `CertificateRevocationCheckMode` | ⛔ Not supported. Or effectively: only `NoCheck` is supported. | -| `CipherSuitesPolicy` | ⛔ Not supported. Or effectively: only `null` (== OS default) is supported. | +| `CertificateRevocationCheckMode` | ⚠️ Not supported. Or effectively: only `NoCheck` is supported. | +| `CipherSuitesPolicy` | ⚠️ Not supported. Or effectively: only `null` (== OS default) is supported. | | `ClientCertificateRequired` | ⛔ Not supported. | -| `EnabledSslProtocols` | ⛔ Not supported. Or effectively: only `None` (== OS default) is supported. | -| `EncryptionPolicy` | ⛔ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | +| `EnabledSslProtocols` | ⚠️ Not supported. Or effectively: only `None` (== OS default) is supported. | +| `EncryptionPolicy` | ⚠️ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | | `RemoteCertificateValidationCallback` | ✅ Enable `verify-client-identity` suspension, perform validation and then either `resume` or abort the connection. | | `ServerCertificate` | ✅ `tls-server::configure-identities` | | `ServerCertificateContext` | ❔ Unknown | @@ -89,4 +89,99 @@ | Member | WASI equivalent | |----------------|--| | `ServerName` | ✅ `server-suspension::requested-server-name` | -| `SslProtocols` | ✅ `server-suspension::requested-protocol-versions` | \ No newline at end of file +| `SslProtocols` | ✅ `server-suspension::requested-protocol-versions` | + + +## Mapping to Node.js `tls` module + +### APIs + +| API | WASI equivalent | +|------------------------------------|--| +| `TLSSocket.localAddress`, `TLSSocket.localPort`, `TLSSocket.remoteAddress`, `TLSSocket.remoteFamily`, `TLSSocket.remotePort`, `TLSSocket.address`, `Server.address`, `Server.listen`, `Server: 'connection' event`, `Server.close` | ✅ These APIs can be implemented using [wasi-sockets](https://github.com/WebAssembly/wasi-sockets). The WASI TLS interface does not do any I/O. | +| `connect`, `createSecureContext`, `createSecurePair`, `createServer`, `TLSSocket.constructor` | ✅ Use the `tls-client` or `tls-server` resource. See the connection options table below for a more detailed breakdown. | +| `checkServerIdentity` | ✅ Implementable in user space. | +| `getCiphers` | ⛔ Not supported. | +| `rootCertificates` | ⛔ Not supported. | +| `CLIENT_RENEG_LIMIT` | ⛔ Not supported. | +| `CLIENT_RENEG_WINDOW` | ⛔ Not supported. | +| `DEFAULT_MAX_VERSION` | ⛔ Not supported. Can be faked to return the highest known protocol of the compiled Node.JS version. | +| `DEFAULT_MIN_VERSION` | ⛔ Not supported. Can be faked to return the lowest known protocol of the compiled Node.JS version. | +| `DEFAULT_ECDH_CURVE` | ⚠️ Not supported. Or effectively: only `'auto'` is supported. | +| `DEFAULT_CIPHERS` | ⛔ Not supported. | +| `TLSSocket: 'keylog' event`, `Server: 'keylog' event` | ⛔ Not supported. | +| `TLSSocket: 'OCSPResponse' event` | ⛔ Not supported. | +| `TLSSocket: 'secureConnect' event`, `Server: 'secureConnection' event` | ✅ Use the `connected` or `accepted` suspension points. | +| `TLSSocket: 'session' event` | ⛔ Not supported. | +| `TLSSocket.alpnProtocol` | ✅ `tls-client::alpn-id` / `tls-server::alpn-id` | +| `TLSSocket.authorizationError` | ⚠️ See `TLSSocket.authorized`. | +| `TLSSocket.authorized` | ⚠️ Peer certificate validation can not be disabled. So if the `tls-client::server-identity` or `tls-server::client-identity` is not null, the connection can be considered "authorized". | +| `TLSSocket.disableRenegotiation` | ⛔ Not supported. | +| `TLSSocket.enableTrace` | ⚠️ Technically, the raw TLS data can be captured from the `public-input/output` streams and re-parsed into whatever format Node.JS/OpenSSL wants. However, it is unlikely this is worth the effort. | +| `TLSSocket.encrypted` | ✅ Always `true`. | +| `TLSSocket.exportKeyingMaterial` | ⛔ Not supported. | +| `TLSSocket.getCertificate` | ✅ `tls-client::client-identity` / `tls-server::server-identity` | +| `TLSSocket.getCipher` | ⛔ Not supported. | +| `TLSSocket.getEphemeralKeyInfo` | ⛔ Not supported. | +| `TLSSocket.getFinished` | ⛔ Not supported. | +| `TLSSocket.getPeerCertificate` | ✅ `tls-client::server-identity` / `tls-server::client-identity` | +| `TLSSocket.getPeerFinished` | ⛔ Not supported. | +| `TLSSocket.getPeerX509Certificate` | ✅ `tls-client::server-identity` / `tls-server::client-identity` | +| `TLSSocket.getProtocol` | ✅ `tls-client::protocol-version` / `tls-server::protocol-version` | +| `TLSSocket.getSession` | ⛔ Not supported. Can be faked to return `undefined`. | +| `TLSSocket.getSharedSigalgs` | ⛔ Not supported. | +| `TLSSocket.getTLSTicket` | ⛔ Not supported. Can be faked to return `undefined`. | +| `TLSSocket.getX509Certificate` | ✅ `tls-client::client-identity` / `tls-server::server-identity` | +| `TLSSocket.isSessionReused` | ⛔ Not supported. Can be faked to return `false`. | +| `TLSSocket.renegotiate` | ⛔ Not supported. | +| `TLSSocket.servername` | ✅ `tls-client::server-name` / `tls-server::server-name` | +| `TLSSocket.setKeyCert` | ✅ `tls-server::configure-identities` | +| `TLSSocket.setMaxSendFragment` | ⛔ Not supported. | +| `Server: 'newSession' event` | ⛔ Not supported. | +| `Server: 'OCSPRequest' event` | ⛔ Not supported. | +| `Server: 'resumeSession' event` | ⛔ Not supported. | +| `Server: 'tlsClientError' event` | ✅ If the `tls-server` is closed prematurely (i.e.: before receiving the `accepted` event), then that's a 'tlsClientError'. | +| `Server.addContext` | ✅ To be implemented in user space. Activate the `client-hello` hook on the `tls-server`, and upon arrival of a client hello configure the `tls-server` based on the registered contexts. | +| `Server.getTicketKeys` | ⛔ Not supported. | +| `Server.setSecureContext` | ✅ See `Server.addContext`. | +| `Server.setTicketKeys` | ⛔ Not supported. | + + +### Connection options + +| Option | WASI equivalent | +|------------------------|--| +| `ALPNProtocols` | ✅ `tls-client::configure-alpn-ids` / `tls-server::configure-alpn-ids` | +| `ALPNCallback` | ✅ Activate `client-hello` hook. Use `server-suspension::requested-server-name` & `server-suspension::requested-alpn-ids` as parameters to the callback. The return value of the callback can be passed as single item list into `tls-server::configure-alpn-ids`. | +| `SNICallback` | ✅ Activate `client-hello` hook. Use `server-suspension::requested-server-name` as parameter to the callback. | +| `ca` | ⛔ Not supported. | +| `cert`, `key`, `pfx` | ✅ `tls-client::configure-identities` / `tls-server::configure-identities` | +| `passphrase` | ⚠️ Not applicable. Only raw private key data is accepted by `private-identity::parse`. | +| `checkServerIdentity` | ✅ Enable `verify-server-identity` suspension, perform validation and then either `resume` or abort the connection. | +| `ciphers` | ⛔ Not supported. | +| `clientCertEngine` | ⛔ (Deprecated) Not supported. | +| `crl` | ⛔ Not supported. | +| `dhparam` | ⛔ Not supported. | +| `ecdhCurve` | ⛔ Not supported. | +| `enableTrace` | ⚠️ See `TLSSocket.enableTrace` | +| `handshakeTimeout` | ✅ Implementable in user space | +| `honorCipherOrder` | ⛔ Not supported. | +| `isServer` | ✅ Indicates whether a `tls-client` or `tls-server` should be constructed. | +| `key` | ✅ `tls-client::configure-identities` / `tls-server::configure-identities` | +| `minDHSize` | ⛔ Not supported. | +| `minVersion`, `maxVersion`, `secureProtocol` | ⛔ Not supported. | +| `privateKeyEngine` | ⛔ (Deprecated) Not supported. | +| `privateKeyIdentifier` | ⛔ (Deprecated) Not supported. | +| `pskCallback` | ⛔ Not supported. | +| `pskIdentityHint` | ⛔ Not supported. | +| `rejectUnauthorized` | ⚠️ Not supported. Certificates are always validated. Or effectively: only `true` is supported. | +| `requestCert` | ⛔ Not supported. | +| `requestOCSP` | ⛔ Not supported. | +| `secureOptions` | ⛔ Not supported. | +| `servername` | ✅ The `server-name` parameter of the `tls-client` constructor. | +| `session` | ⛔ Not supported. | +| `sessionIdContext` | ⛔ Not supported. | +| `sessionTimeout` | ⛔ Not supported. | +| `sigalgs` | ⛔ Not supported. | +| `ticketKeys` | ⛔ Not supported. | + From 8f3b082e6ead0eebbcedf10d8a06d16dffbaa68d Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Tue, 3 Sep 2024 11:55:10 +0200 Subject: [PATCH 4/4] WIP --- TLS.md | 300 ++++++++++++++++++++++----- wit/tls.wit | 567 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 617 insertions(+), 250 deletions(-) diff --git a/TLS.md b/TLS.md index aaa2020..23fec74 100644 --- a/TLS.md +++ b/TLS.md @@ -1,3 +1,195 @@ +## Examples (in pseudocode) + +### Minimal client + +```rs +// TCP setup: +let ip = wasi_sockets::resolve_addresses("example.com").await?[0]; +let tcp_client = wasi_sockets::TcpSocket::new(); +let (tcp_input, tcp_output) = tcp_client.connect(ip, 443).await; + +// TLS setup: +let (tls_input, tls_output) = wasi_tls::ClientConnection::new(tcp_input, tcp_output) + .connect("example.com")? + .finish().await?; + +// Usage: +tls_output.blocking_write_and_flush("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"); +let http_response = tls_input.blocking_read(); + +println!(http_response); +``` + +### Minimal server + +```rs +// Prepare certificates + private key: +let id = wasi_tls::PrivateIdentity::parse( + fs::read("private.key"), + fs::read("public.crt"), +)?; + +// TCP setup: +let tcp_server = wasi_sockets::TcpSocket::new(); +tcp_server.bind(443); +tcp_server.listen(); +loop { + let (tcp_client, tcp_input, tcp_output) = tcp_server.accept().await?; + + // TLS setup: + let handshake = wasi_tls::ServerConnection::new(tcp_input, tcp_output).accept(); + handshake.configure_server_identities([id]); + let (tls_input, tls_output) = handshake.finish().await?; + + // Usage: + let http_request = tls_input.blocking_read(); + println!(http_request); + tls_output.blocking_write_and_flush("HTTP/1.1 200 OK\r\n\r\n"); +} +``` + +### Client features showcase + +```rs +let client_cert = wasi_tls::PrivateIdentity::parse( + fs::read("private.key"), + fs::read("public.crt"), +)?; + +let ip = wasi_sockets::resolve_addresses("example.com").await?[0]; + +let tcp_client = wasi_sockets::TcpSocket::new(); +let (tcp_input, tcp_output) = tcp_client.connect(ip, 443).await; + +let tls_connection = wasi_tls::ClientConnection::new(tcp_input, tcp_output); +let handshake = tls_connection.connect("example.com")?; + +// Configure settings: +{ + handshake.configure_alpn_ids(["h2"]); +} + +// Receive and validate server certificate: +{ + let server_cert = handshake.verify_server_identity().await?; + let parsed_cert = parse_der(server_cert.export_X509_chain()); // Note: certificate parsing must be done by the guest. + println!(parsed_cert); + + // At the time of writing, validations performed here are always *in addition* + // to the TLS implementation's default validation. + + if (/* custom logic */) { + handshake.abort(); + return; + } +} + +// Handle client certificate request: +match handshake.receive_client_identity_request().await? { + Some(certificate_request) => certificate_request.respond([client_cert]), // TODO: add showcase on how to select a client cert based on server-indicated authorities. + None => { /* No client certificate requested. */ }, +} + +let (tls_input, tls_output) = handshake.finish().await?; + +// Display TLS connection status: +println!(tls_connection.server_name()); +println!(tls_connection.alpn_id()); +println!(tls_connection.client_identity()); +println!(tls_connection.server_identity()); +println!(tls_connection.protocol_version()); +println!(tls_connection.cipher_suite()); + +tls_output.blocking_write_and_flush("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"); +let http_response = tls_input.blocking_read(); + +println!(http_response); +``` + +### Server features showcase + +```rs +let id1 = wasi_tls::PrivateIdentity::parse( + fs::read("private1.key"), + fs::read("public1.crt"), +)?; +let id2 = wasi_tls::PrivateIdentity::parse( + fs::read("private2.key"), + fs::read("public2.crt"), +)?; + +let tcp_server = wasi_sockets::TcpSocket::new(); +tcp_server.bind(443); +tcp_server.listen(); + +loop { + let (tcp_client, tcp_input, tcp_output) = tcp_server.accept().await?; + + let tls_connection = wasi_tls::ServerConnection::new(tcp_input, tcp_output); + let handshake = tls_connection.accept(); + + // Configure connection properties based on ClientHello: + { + let client_hello = handshake.receive_client_hello().await?; + println!(client_hello.server_name()); + println!(client_hello.alpn_ids()); + println!(client_hello.cipher_suites()); + + match client_hello.server_name() { + Some("example.com") => { + handshake.configure_alpn_ids(["h2"]); + handshake.configure_server_identities([id1]); + } + _ => { + handshake.configure_alpn_ids(["h2", "http/1.1"]); + handshake.configure_server_identities([id2]); + } + } + } + + // Request client certificate: + { + match handshake.request_client_identity().await? { + Some(client_cert) => { + let parsed_cert = parse_der(client_cert.export_X509_chain()); // Note: certificate parsing must be done by the guest. + println!(parsed_cert); + + // At the time of writing, validations performed here are always *in addition* + // to the TLS implementation's default validation. + + if (/* custom logic */) { + handshake.abort(); + return; + } + } + None => { + // Client didn't provide a certificate. + }, + } + } + + let (tls_input, tls_output) = handshake.finish().await?; + + // Display TLS connection status: + println!(tls_connection.server_name()); + println!(tls_connection.alpn_id()); + println!(tls_connection.client_identity()); + println!(tls_connection.server_identity()); + println!(tls_connection.protocol_version()); + println!(tls_connection.cipher_suite()); + + let http_request = tls_input.blocking_read(); + println!(http_request); + + // Perform post-handshake authentication based on HTTP path: + if http_request.starts_with("GET /secure") && tls_connection.client_identity() == None { + let _client_cert = tls_connection.request_client_identity().await; + } + + tls_output.blocking_write_and_flush("HTTP/1.1 200 OK\r\n\r\n"); +} +``` + ## Mapping to .NET types ### `SslStream` @@ -9,28 +201,28 @@ | `CipherStrength` | ⛔ Not supported. | | `HashAlgorithm` | ⛔ Not supported. | | `HashStrength` | ⛔ Not supported. | -| `IsAuthenticated` | ✅ `true` after the `connected`/`accepted` suspension occurred. | +| `IsAuthenticated` | ✅ `true` after the handshake finished successfully. | | `IsEncrypted` | ✅ Alias for `IsAuthenticated` | | `IsMutuallyAuthenticated` | ✅ Check that the connection `IsAuthenticated`, and that both `client-identity` and `server-identity` are not null. | | `IsServer` | ✅ To be maintained in userland | | `IsSigned` | ✅ Alias for `IsAuthenticated` | | `KeyExchangeAlgorithm` | ⛔ Not supported. | | `KeyExchangeStrength` | ⛔ Not supported. | -| `LocalCertificate` | ✅ `tls-client::client-identity` / `tls-server::server-identity` | -| `NegotiatedApplicationProtocol` | ✅ `tls-client::alpn-id` / `tls-server::alpn-id` | -| `NegotiatedCipherSuite` | ⛔ Not supported. | -| `RemoteCertificate` | ✅ `tls-client::server-identity` / `tls-server::client-identity` | -| `SslProtocol` | ✅ `tls-client::protocol-version` / `tls-server::protocol-version` | -| `TargetHostName` | ✅ `tls-client::server-name` / `tls-server::server-name` | +| `LocalCertificate` | ✅ `client-connection::client-identity` / `server-connection::server-identity` | +| `NegotiatedApplicationProtocol` | ✅ `client-connection::alpn-id` / `server-connection::alpn-id` | +| `NegotiatedCipherSuite` | ✅ `client-connection::cipher-suite` / `server-connection::cipher-suite` | +| `RemoteCertificate` | ✅ `client-connection::server-identity` / `server-connection::client-identity` | +| `SslProtocol` | ✅ `client-connection::protocol-version` / `server-connection::protocol-version` | +| `TargetHostName` | ✅ `client-connection::server-name` / `server-connection::server-name` | | `TransportContext` | ❔ Unknown | -| `AuthenticateAsClient`, `AuthenticateAsClientAsync`, `BeginAuthenticateAsClient`, `EndAuthenticateAsClient` | ✅ Construct `tls-client` with at least the `connected` suspension enabled, configure it (see `SslClientAuthenticationOptions` table below), call `resume`, wait for the `connected` suspension. | -| `AuthenticateAsServer`, `AuthenticateAsServerAsync`, `BeginAuthenticateAsServer`, `EndAuthenticateAsServer` | ✅ Construct `tls-server` with at least the `accepted` suspension enabled, configure it (see `SslServerAuthenticationOptions` table below), call `resume`, wait for the `accepted` suspension. For the `ServerOptionsSelectionCallback` overload, enable `client-hello` suspension (see `SslClientHelloInfo` table below). | -| `NegotiateClientCertificateAsync` | ⛔ Not supported. | -| `Read`, `ReadAsync`, `BeginRead`, `EndRead`, `ReadByte`, `ReadAtLeast`, `ReadAtLeastAsync`, `ReadExactly`, `ReadExactlyAsync` | ✅ `private-input::read` | -| `Write`, `WriteAsync`, `BeginWrite`, `EndWrite`, `WriteByte` | ✅ `private-output::write` | +| `AuthenticateAsClient`, `AuthenticateAsClientAsync`, `BeginAuthenticateAsClient`, `EndAuthenticateAsClient` | ✅ `client-connection::connect`. See `SslClientAuthenticationOptions` table below for more details. | +| `AuthenticateAsServer`, `AuthenticateAsServerAsync`, `BeginAuthenticateAsServer`, `EndAuthenticateAsServer` | ✅ `server-connection::accept`. See `SslServerAuthenticationOptions` table below for more details. | +| `NegotiateClientCertificateAsync` | ✅ `server-connection::request-client-identity` | +| `Read`, `ReadAsync`, `BeginRead`, `EndRead`, `ReadByte`, `ReadAtLeast`, `ReadAtLeastAsync`, `ReadExactly`, `ReadExactlyAsync` | ✅ Use the `input-stream` returned by the handshake `finish` method. | +| `Write`, `WriteAsync`, `BeginWrite`, `EndWrite`, `WriteByte` | ✅ Use the `output-stream` returned by the handshake `finish` method. | | `CopyTo`, `CopyToAsync` | ✅ Currently implemented in user space. Could be specialized as `output-stream::splice` in case both sides are WASI streams. | -| `Flush`, `FlushAsync` | ✅ `private-output::flush` | -| `Dispose`, `DisposeAsync`, `Close`, `Finalize`, `ShutdownAsync` | ✅ Fully flush and drop the `private-output` | +| `Flush`, `FlushAsync` | ✅ Use the `output-stream` returned by the handshake `finish` method. | +| `Dispose`, `DisposeAsync`, `Close`, `Finalize`, `ShutdownAsync` | ⛔ TODO: graceful shutdown | | `CanRead` | ✅ Implemented in user space | | `ReadTimeout` | ✅ Implemented in user space | | `CanWrite` | ✅ Implemented in user space | @@ -52,17 +244,17 @@ |---------------------------------------|--| | `AllowRenegotiation` | ⛔ Not supported. | | `AllowTlsResume` | ⛔ Not supported. | -| `ApplicationProtocols` | ✅ `tls-client::configure-alpn-ids` | +| `ApplicationProtocols` | ✅ `client-handshake::configure-alpn-ids` | | `CertificateChainPolicy` | ❔ Unknown | | `CertificateRevocationCheckMode` | ⚠️ Not supported. Or effectively: only `NoCheck` is supported. | | `CipherSuitesPolicy` | ⚠️ Not supported. Or effectively: only `null` (== OS default) is supported. | -| `ClientCertificateContext` | ❔ Unknown | -| `ClientCertificates` | ✅ `tls-client::configure-identities` | +| `ClientCertificateContext` | ✅ Use `client-identity-request::respond` returned by `client-handshake::receive-client-identity-request` | +| `ClientCertificates` | ✅ Use `client-identity-request::respond` returned by `client-handshake::receive-client-identity-request` | | `EnabledSslProtocols` | ⚠️ Not supported. Or effectively: only `None` (== OS default) is supported. | | `EncryptionPolicy` | ⚠️ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | -| `LocalCertificateSelectionCallback` | ✅ Enable `select-client-identity` suspension, while suspended call `tls-client::configure-identities`, and then `resume` | -| `RemoteCertificateValidationCallback` | ✅ Enable `verify-server-identity` suspension, perform validation and then either `resume` or abort the connection. | -| `TargetHost` | ✅ The `server-name` parameter of the `tls-client` constructor. | +| `LocalCertificateSelectionCallback` | ✅ `client-handshake::receive-client-identity-request` | +| `RemoteCertificateValidationCallback` | ✅ `client-handshake::receive-server-identity` | +| `TargetHost` | ✅ The `server-name` parameter of `client-connection::connect` | ### `SslServerAuthenticationOptions` @@ -71,25 +263,25 @@ |---------------------------------------|--| | `AllowRenegotiation` | ⛔ Not supported. | | `AllowTlsResume` | ⛔ Not supported. | -| `ApplicationProtocols` | ✅ `tls-server::configure-alpn-ids` | +| `ApplicationProtocols` | ✅ `server-handshake::configure-alpn-ids` | | `CertificateChainPolicy` | ❔ Unknown | | `CertificateRevocationCheckMode` | ⚠️ Not supported. Or effectively: only `NoCheck` is supported. | | `CipherSuitesPolicy` | ⚠️ Not supported. Or effectively: only `null` (== OS default) is supported. | -| `ClientCertificateRequired` | ⛔ Not supported. | +| `ClientCertificateRequired` | ✅ `abort` the handshake if `server-handshake::request-client-identity` resolves with `none`. | | `EnabledSslProtocols` | ⚠️ Not supported. Or effectively: only `None` (== OS default) is supported. | | `EncryptionPolicy` | ⚠️ (Obsolete) Not supported. Or effectively: only `RequireEncryption` is supported. | -| `RemoteCertificateValidationCallback` | ✅ Enable `verify-client-identity` suspension, perform validation and then either `resume` or abort the connection. | -| `ServerCertificate` | ✅ `tls-server::configure-identities` | -| `ServerCertificateContext` | ❔ Unknown | -| `ServerCertificateSelectionCallback` | ✅ Enable `client-hello` suspension, while suspended call `tls-server::configure-identities`, and then `resume` | +| `RemoteCertificateValidationCallback` | ✅ `server-handshake::request-client-identity` | +| `ServerCertificate` | ✅ `server-handshake::configure-identities` | +| `ServerCertificateContext` | ✅ `server-handshake::configure-identities` | +| `ServerCertificateSelectionCallback` | ✅ Wait for the ClientHello using `server-handshake::receive-client-hello` and call `server-handshake::configure-identities` after that. | ### `SslClientHelloInfo` | Member | WASI equivalent | |----------------|--| -| `ServerName` | ✅ `server-suspension::requested-server-name` | -| `SslProtocols` | ✅ `server-suspension::requested-protocol-versions` | +| `ServerName` | ✅ `client-hello::server-name` | +| `SslProtocols` | ⛔ Not supported. | ## Mapping to Node.js `tls` module @@ -99,7 +291,7 @@ | API | WASI equivalent | |------------------------------------|--| | `TLSSocket.localAddress`, `TLSSocket.localPort`, `TLSSocket.remoteAddress`, `TLSSocket.remoteFamily`, `TLSSocket.remotePort`, `TLSSocket.address`, `Server.address`, `Server.listen`, `Server: 'connection' event`, `Server.close` | ✅ These APIs can be implemented using [wasi-sockets](https://github.com/WebAssembly/wasi-sockets). The WASI TLS interface does not do any I/O. | -| `connect`, `createSecureContext`, `createSecurePair`, `createServer`, `TLSSocket.constructor` | ✅ Use the `tls-client` or `tls-server` resource. See the connection options table below for a more detailed breakdown. | +| `connect`, `createSecureContext`, `createSecurePair`, `createServer`, `TLSSocket.constructor` | ✅ Use the `client-connection` or `server-connection` resource. See the connection options table below for a more detailed breakdown. | | `checkServerIdentity` | ✅ Implementable in user space. | | `getCiphers` | ⛔ Not supported. | | `rootCertificates` | ⛔ Not supported. | @@ -111,37 +303,37 @@ | `DEFAULT_CIPHERS` | ⛔ Not supported. | | `TLSSocket: 'keylog' event`, `Server: 'keylog' event` | ⛔ Not supported. | | `TLSSocket: 'OCSPResponse' event` | ⛔ Not supported. | -| `TLSSocket: 'secureConnect' event`, `Server: 'secureConnection' event` | ✅ Use the `connected` or `accepted` suspension points. | +| `TLSSocket: 'secureConnect' event`, `Server: 'secureConnection' event` | ✅ Triggered when the handshake's `finish` future resolves successfully. | | `TLSSocket: 'session' event` | ⛔ Not supported. | -| `TLSSocket.alpnProtocol` | ✅ `tls-client::alpn-id` / `tls-server::alpn-id` | +| `TLSSocket.alpnProtocol` | ✅ `client-connection::alpn-id` / `server-connection::alpn-id` | | `TLSSocket.authorizationError` | ⚠️ See `TLSSocket.authorized`. | -| `TLSSocket.authorized` | ⚠️ Peer certificate validation can not be disabled. So if the `tls-client::server-identity` or `tls-server::client-identity` is not null, the connection can be considered "authorized". | +| `TLSSocket.authorized` | ⚠️ Peer certificate validation can not be disabled. So if the `client-connection::server-identity` or `server-connection::client-identity` is not null, the connection can be considered "authorized". | | `TLSSocket.disableRenegotiation` | ⛔ Not supported. | -| `TLSSocket.enableTrace` | ⚠️ Technically, the raw TLS data can be captured from the `public-input/output` streams and re-parsed into whatever format Node.JS/OpenSSL wants. However, it is unlikely this is worth the effort. | +| `TLSSocket.enableTrace` | ⛔ Not supported. | | `TLSSocket.encrypted` | ✅ Always `true`. | | `TLSSocket.exportKeyingMaterial` | ⛔ Not supported. | -| `TLSSocket.getCertificate` | ✅ `tls-client::client-identity` / `tls-server::server-identity` | -| `TLSSocket.getCipher` | ⛔ Not supported. | +| `TLSSocket.getCertificate` | ✅ `client-connection::client-identity` / `server-connection::server-identity` | +| `TLSSocket.getCipher` | ✅ `client-connection::cipher-suite` / `server-connection::cipher-suite` | | `TLSSocket.getEphemeralKeyInfo` | ⛔ Not supported. | | `TLSSocket.getFinished` | ⛔ Not supported. | -| `TLSSocket.getPeerCertificate` | ✅ `tls-client::server-identity` / `tls-server::client-identity` | +| `TLSSocket.getPeerCertificate` | ✅ `client-connection::server-identity` / `server-connection::client-identity` | | `TLSSocket.getPeerFinished` | ⛔ Not supported. | -| `TLSSocket.getPeerX509Certificate` | ✅ `tls-client::server-identity` / `tls-server::client-identity` | -| `TLSSocket.getProtocol` | ✅ `tls-client::protocol-version` / `tls-server::protocol-version` | +| `TLSSocket.getPeerX509Certificate` | ✅ `client-connection::server-identity` / `server-connection::client-identity` | +| `TLSSocket.getProtocol` | ✅ `client-connection::protocol-version` / `server-connection::protocol-version` | | `TLSSocket.getSession` | ⛔ Not supported. Can be faked to return `undefined`. | | `TLSSocket.getSharedSigalgs` | ⛔ Not supported. | | `TLSSocket.getTLSTicket` | ⛔ Not supported. Can be faked to return `undefined`. | -| `TLSSocket.getX509Certificate` | ✅ `tls-client::client-identity` / `tls-server::server-identity` | +| `TLSSocket.getX509Certificate` | ✅ `client-connection::client-identity` / `server-connection::server-identity` | | `TLSSocket.isSessionReused` | ⛔ Not supported. Can be faked to return `false`. | -| `TLSSocket.renegotiate` | ⛔ Not supported. | -| `TLSSocket.servername` | ✅ `tls-client::server-name` / `tls-server::server-name` | -| `TLSSocket.setKeyCert` | ✅ `tls-server::configure-identities` | +| `TLSSocket.renegotiate` | ⚠️ Partially supported. Only client certificate requests are supported post-handshake. See `server-connection::request-client-identity` | +| `TLSSocket.servername` | ✅ `client-connection::server-name` / `server-connection::server-name` | +| `TLSSocket.setKeyCert` | ✅ `server-handshake::configure-identities` | | `TLSSocket.setMaxSendFragment` | ⛔ Not supported. | | `Server: 'newSession' event` | ⛔ Not supported. | | `Server: 'OCSPRequest' event` | ⛔ Not supported. | | `Server: 'resumeSession' event` | ⛔ Not supported. | -| `Server: 'tlsClientError' event` | ✅ If the `tls-server` is closed prematurely (i.e.: before receiving the `accepted` event), then that's a 'tlsClientError'. | -| `Server.addContext` | ✅ To be implemented in user space. Activate the `client-hello` hook on the `tls-server`, and upon arrival of a client hello configure the `tls-server` based on the registered contexts. | +| `Server: 'tlsClientError' event` | ✅ To be implemented in user space. `'tlsClientError'` == any reason the `server-handshake` could not be `finish`ed successfully. | +| `Server.addContext` | ✅ To be implemented in user space. Use `server-handshake::receive-client-hello` to wait for the ClientHello and then configure the `server-handshake` based on the registered contexts. | | `Server.getTicketKeys` | ⛔ Not supported. | | `Server.setSecureContext` | ✅ See `Server.addContext`. | | `Server.setTicketKeys` | ⛔ Not supported. | @@ -151,23 +343,23 @@ | Option | WASI equivalent | |------------------------|--| -| `ALPNProtocols` | ✅ `tls-client::configure-alpn-ids` / `tls-server::configure-alpn-ids` | -| `ALPNCallback` | ✅ Activate `client-hello` hook. Use `server-suspension::requested-server-name` & `server-suspension::requested-alpn-ids` as parameters to the callback. The return value of the callback can be passed as single item list into `tls-server::configure-alpn-ids`. | -| `SNICallback` | ✅ Activate `client-hello` hook. Use `server-suspension::requested-server-name` as parameter to the callback. | +| `ALPNProtocols` | ✅ `client-handshake::configure-alpn-ids` / `server-handshake::configure-alpn-ids` | +| `ALPNCallback` | ✅ Use `server-handshake::receive-client-hello` to receive the ClientHello. The return value of the callback can be passed as single item list into `server-handshake::configure-alpn-ids`. | +| `SNICallback` | ✅ `server-handshake::receive-client-hello` | | `ca` | ⛔ Not supported. | -| `cert`, `key`, `pfx` | ✅ `tls-client::configure-identities` / `tls-server::configure-identities` | +| `cert`, `key`, `pfx` | ✅ `client-identity-request::respond` / `server-handshake::configure-identities` | | `passphrase` | ⚠️ Not applicable. Only raw private key data is accepted by `private-identity::parse`. | -| `checkServerIdentity` | ✅ Enable `verify-server-identity` suspension, perform validation and then either `resume` or abort the connection. | +| `checkServerIdentity` | ✅ `client-handshake::verify-server-identity` | | `ciphers` | ⛔ Not supported. | | `clientCertEngine` | ⛔ (Deprecated) Not supported. | | `crl` | ⛔ Not supported. | | `dhparam` | ⛔ Not supported. | | `ecdhCurve` | ⛔ Not supported. | -| `enableTrace` | ⚠️ See `TLSSocket.enableTrace` | +| `enableTrace` | ⛔ Not supported. | | `handshakeTimeout` | ✅ Implementable in user space | | `honorCipherOrder` | ⛔ Not supported. | -| `isServer` | ✅ Indicates whether a `tls-client` or `tls-server` should be constructed. | -| `key` | ✅ `tls-client::configure-identities` / `tls-server::configure-identities` | +| `isServer` | ✅ Indicates whether a `client-connection` or `server-connection` should be constructed. | +| `key` | ✅ `client-identity-request::respond` / `server-handshake::configure-identities` | | `minDHSize` | ⛔ Not supported. | | `minVersion`, `maxVersion`, `secureProtocol` | ⛔ Not supported. | | `privateKeyEngine` | ⛔ (Deprecated) Not supported. | @@ -175,10 +367,10 @@ | `pskCallback` | ⛔ Not supported. | | `pskIdentityHint` | ⛔ Not supported. | | `rejectUnauthorized` | ⚠️ Not supported. Certificates are always validated. Or effectively: only `true` is supported. | -| `requestCert` | ⛔ Not supported. | +| `requestCert` | ✅ When `true`, use `server-handshake::receive-client-hello` | | `requestOCSP` | ⛔ Not supported. | | `secureOptions` | ⛔ Not supported. | -| `servername` | ✅ The `server-name` parameter of the `tls-client` constructor. | +| `servername` | ✅ The `server-name` parameter of `client-connection::connect` | | `session` | ⛔ Not supported. | | `sessionIdContext` | ⛔ Not supported. | | `sessionTimeout` | ⛔ Not supported. | diff --git a/wit/tls.wit b/wit/tls.wit index 9a53f7c..3fa6e97 100644 --- a/wit/tls.wit +++ b/wit/tls.wit @@ -15,6 +15,18 @@ interface tls { /// TODO: Want to use regular WIT `enum` type, but then adding a new protocol is backwards incompatible. type protocol-version = u16; + /// TLS Cipher suite. + /// + /// These are maintained by IANA. Examples: + /// - 0x1301: TLS_AES_128_GCM_SHA256 + /// - 0x1302: TLS_AES_256_GCM_SHA384 + /// - 0xC030: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + /// - 0xC02B: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + /// - etc. + /// + /// See: + type cipher-suite = u16; + /// Application-Layer Protocol Negotiation (ALPN) protocol ID. /// /// ALPN IDs are between 1 and 255 bytes long. Typically, they represent the @@ -32,278 +44,441 @@ interface tls { /// The private key data can not be exported. resource private-identity { /// TODO: find a way to "preopen" these private-identity resources, so that the sensitive private key data never has to flow through the guest. + /// TODO: specify which exact binary format parse: static func(private-key: list, x509-chain: list>) -> result; public-identity: func() -> public-identity; } - /// Resource to configure, control & observe a TLS client stream. - /// - /// # Transform stream - /// A TLS client resource does not perform any I/O on its own. It is a pure - /// stream transformer that takes cleartext data on one side and emits - /// secured TLS data on the other side and vice-versa. - /// The user of this resource is responsible for continually - /// "pumping" the data from the underlying socket into the TLS client's - /// `public-output` stream and the `public-input` back into the socket. + + + /// A TLS client connection. /// - /// # Usage - /// The general usage pattern looks something like this: - /// - Instantiate new `client`. - /// - (Optional) Further refine the settings using the various `configure-*` methods. - /// - Forward the "public" streams acquired in the previous step into the underlying socket. - /// - Call `resume`. - /// - Read & write application data into the "private" streams. + /// The various properties on this type become available as the handshake + /// progresses. /// - /// # Suspend / resume - /// A `client` always starts out suspended. During this initial suspension - /// the various `configure-*` methods may be called. After configuration, - /// the TLS handshake must be manually initiated using the `resume` method. + /// # Design + /// TLS connections can be divided up into roughly two phases: the handshake + /// phase and the bulk data transfer phase. (TLS renegotiation is not exposed + /// in this interface.) + /// By far, most of the API complexity is in the handshake phase, which has + /// been split off into a separate `client-handshake` type. The + /// `client-handshake` is used to configure and control advanced (but optional) + /// operations such as custom server certificate validation + /// and client certificate selection. Once the `client-handshake` has been + /// set up appropriately, the handshake can be completed using the `finish` + /// method. That consumes the `client-handshake` and gives a pair of + /// cleartext streams back in return. /// - /// Many TLS libraries let users provide custom behavior through the - /// registration of callbacks. The WebAssembly Component Model does not - /// support callbacks. Instead, these customization points are modeled as - /// additional suspensions. + /// The `client-connection` and its handshake resource don't not perform + /// any I/O on their own. It is a pure stream "wrapper" that consumes raw + /// TLS data streams and exposes cleartext streams. + /// The `input` and `output` parameters of constructor map to the + /// raw TLS data streams and are typically obtained from a WASI TCP socket. + /// The `client-handshake::finish` method returns the cleartext streams. /// - /// The `suspend-at` parameter of the `connect` constructor controls at which - /// moments during the lifetime of the TLS stream the client should - /// automatically suspend itself. + /// ## Consumer drives the handshake + /// Many TLS libraries let users inject custom behavior at various points + /// of interest through the registration of callbacks. + /// The WebAssembly Component Model does not support callbacks. Instead, the + /// WASI TLS interface exposes direct-style APIs for the consumer to actively + /// drive the handshake forwards. /// - /// The consumer drives this transition using the `suspend` method. If the - /// client is not ready to be suspended, the pollable returned by `subscribe` - /// can be used to wait for its readiness. When `suspend` succeeds, the client - /// is suspended and some settings are open for configuration again. Once - /// ready to continue the connection, the consumer should call `resume`. + /// The `client-handshake` starts out "suspended". The consumer then drives + /// the handshake to the next point of interest. For example using the + /// `verify-server-identity` method. Calling it: + /// - resumes the handshake, + /// - asynchronously waits for the server to send its certificate + /// - upon receipt suspends the handshake again + /// - resolves the returned future. + /// It is then back up to the consumer to drive the handshake along to the + /// next point of interest (e.g. `receive-client-identity-request`) + /// or finish the handshake entirely (`finish`). /// - /// In general: - /// - While a client is suspended, no data flows through the I/O streams. - /// - A client may only be configured while it is suspended. + /// No data flows through the I/O streams while a client-handshake is suspended. + /// + /// # Usage + /// The general usage pattern looks like this: + /// - Construct a new connection using the `client-connection` constructor. + /// - Obtain the handshake resource using `client-connection::connect`. The + /// handshake starts out suspended. + /// - (Optional) Further refine the settings using the various + /// `configure-*` methods. These may only be called while the + /// handshake is suspended. + /// - (Optional) Resume the handshake and wait for the server to send its + /// certificate using: `verify-server-identity`. + /// - (Optional) Resume the handshake and wait for the server to send a + /// client certificate request using: `receive-client-identity-request`. + /// - Resume and run the handshake to completion using: `finish`. + /// - Read & write application data into the streams returned by `finish`. + /// + /// The optional steps must happen in the order as defined above. + /// + /// ## Example + /// A minimal, but production-ready, example in pseudocode: + /// ```text + /// let conn = new client-connection(tcp-input, tcp-output); + /// let (tls-input, tls-output) = conn.connect("example.com").finish().await?; + /// ``` + /// + /// This assumes `tcp-input` and `tcp-output` are input/output streams + /// obtained from e.g. `tcp-socket::connect`. /// /// # Secure by default - /// Implementations should pick reasonably safe defaults for all security related - /// settings. Users of this interface should be able to confidently instantiate - /// a new `client` and then, without further configuration, immediately - /// initiate the handshake. - resource client { - /// Create a new suspended TLS client. - constructor(server-name: string, suspend-at: client-suspension-points); - - /// Obtain the I/O streams associated with this client. - /// These must be obtained exactly once, before the first call to `resume`. - /// - /// Returns an error if they were already obtained before. + /// Implementations should pick reasonably safe defaults for all security + /// related settings. Users of this interface should be able to confidently + /// instantiate a new client connection and then, without further configuration, + /// immediately initiate the handshake. As demonstrated in the example above. + resource client-connection { + // TODO: handle post-handshake client certificate requests. + // TODO: graceful shutdown + + /// Create a new connection instance that wraps the provided + /// raw TLS data streams. + constructor(input: input-stream, output: output-stream); + + /// Create a new client handshake. This method may be called at most once. + /// This method itself does not perform any I/O. Use the returned resource + /// to actually drive the handshake forwards. /// - /// The I/O streams are child resources of the client. They must be - /// dropped before the client is dropped. - streams: func() -> result; + /// The returned `client-handshake` is a child resources of the `client-connection`. + connect: func(server-name: string) -> result; - - - - /// Configure the ALPN IDs for this client to adertise to the server, - /// in descending order of preference. - /// - /// This may only be configured while suspended in the `constructed` phase. - configure-alpn-ids: func(value: list) -> result; - - /// Configure the client certificates, in descending order of preference. - /// - /// If the server requests a certificate from the client, - /// the TLS implementation will consult this list of configured identities - /// in the provided order and pick the first one that satisfies the - /// constraints sent by the server. If there was no match, then by default - /// it is up to the server to decide whether to continue or abort the connection. - /// - /// This may only be configured while suspended in the `constructed`, - /// `verify-server-identity` or `select-client-identity` phases. - configure-identities: func(value: list>) -> result; - - - - - - /// The server name that was provided during construction of this client. - server-name: func() -> string; + /// The server name that was provided to `connect`. + server-name: func() -> option; /// The negotiated ALPN ID, if any. /// /// Returns `none` when: - /// - the handshake did not take place yet, - /// - the client did not advertise any ALPN IDs, - /// - a successful handshake has occurred, but there was no intersection - /// between the IDs advertised by the client and the IDs supported by - /// the server. + /// - the client did not advertise any ALPN IDs, or: + /// - there was no intersection between the IDs advertised by the client + /// and the IDs supported by the server. alpn-id: func() -> option; - - /// The negotiated TLS protocol version. - /// - /// Returns `none` if the handshake did not take place yet. - protocol-version: func() -> option; /// The client's identity advertised to the server, if any. - /// This will be one of the identities configured using `configure-identities`. + /// This will be one of the identities passed to + /// `client-identity-request::respond`. /// - /// This becomes available _after_ the `select-client-identity` phase. - /// /// Returns `none` when: - /// - the handshake did not take place yet, /// - the server did not request a client certificate, /// - the server did request a client certificate, but there was no match /// with the configured identities. client-identity: func() -> option; - /// The validated certificate of the server. - /// - /// This becomes available _after_ the `verify-server-identity` phase. + /// The verified certificate of the server. server-identity: func() -> option; + /// The negotiated TLS protocol version. + protocol-version: func() -> option; + + /// The negotiated cipher suite. + cipher-suite: func() -> option; + } + /// Resource to control a TLS client handshake. + /// + /// See `client-connection` for more information. + resource client-handshake { + /// Configure the ALPN IDs for the client to adertise to the server, + /// in descending order of preference. + /// + /// By default, no ALPN IDs will be advertised. + configure-alpn-ids: func(value: list) -> result; + // TODO: configure-protocol-versions: func(value: list) -> result; + // TODO: configure-cipher-suites: func(value: list) -> result; - /// Attempt to suspend the client at one of the places specified by - /// `suspend-at` during construction of the client. + /// (Optional) Partially continue the handshake and verify the + /// certificate sent by the server. + /// + /// Can be called at most once per handshake. Also, this method fails if + /// the handshake has already progressed too far for this to be possible. /// - /// Returns `error(not-ready)` if the client is not ready to be suspended. - /// Use the pollable returned by `subscribe` to wait for its readiness. - /// - /// Returns `error(already-suspended)` is already suspended. + /// The returned future is a child resources of the `client-handshake`. /// - /// Returns `error(closed)` if the connection has shut down either - /// successfully or abornmally. + /// TODO: See `server-handshake::request-client-identity` for TODOs. + verify-server-identity: func() -> result; + + /// (Optional) Partially continue the handshake and wait for the server + /// to send a client certificate request. The future resolves with: + /// - `some(client-identity-request)` if the server indeed requested a + /// client certificate, or: + /// - `none` if the server progressed the handshake past the point where + /// client authentication is possible. /// - /// The suspension resource is a child resource of the client. Dropping - /// the `client` while it still has an active suspension resource may trap. - suspend: func() -> result; - - /// Resume the suspended client. Returns an error if the client is not - /// suspended. - /// - /// If the client was suspended though `suspend` (i.e. it is not the - /// initial resumption), this method will trap if the suspension - /// resource hasn't been dropped yet. + /// Can be called at most once per handshake. Also, this method fails if + /// the handshake has already progressed too far for this to be possible. /// - /// Returns an error if the client is not suspended. - resume: func() -> result; + /// The returned future and `client-identity-request` are child resources + /// of the `client-handshake`. + receive-client-identity-request: func() -> result; - /// Create a `pollable` which can be used to poll for - /// the client to be suspendable or closed. + /// Perform (the remainder of) the handshake. Future resolves when the + /// handshake has completed. /// - /// `subscribe` only has to be called once per client and can be (re)used - /// for the remainder of the client's lifetime. - subscribe: func() -> pollable; + /// The returned future is a child resource of the `client-connection`. + finish: static func(this: client-handshake) -> future-streams; + + /// Cancel the handshake because of an error. The returned pollable + /// resolves when the error alert has been written out. + /// + /// The pollable is a child resource of the `client-connection`. + /// + /// TODO: add reason/errorcode parameter? + abort: static func(this: client-handshake) -> pollable; } - /// The I/O streams that represent both sides of the transform. + + /// A TLS server connection. /// - /// The application side interacts with the cleartext "private" streams. - /// The network side interacts with the encrypted "public" streams. + /// The various properties on this type become available as the handshake + /// progresses. /// - /// A typical setup looks something like this: - /// + /// # Design + /// TLS connections can be divided up into roughly two phases: the handshake + /// phase and the bulk data transfer phase. (TLS renegotiation is not exposed + /// in this interface.) + /// By far, most of the API complexity is in the handshake phase, which has + /// been split off into a separate `server-handshake` type. The + /// `server-handshake` is used to configure and control advanced (but optional) + /// operations such as deriving server settings from the client hello or + /// requesting and validating a client certificate. + /// Once the `server-handshake` has been set up appropriately, the handshake + /// can be completed using the `finish` method. That consumes the + /// `server-handshake` and gives a pair of cleartext streams back in return. + /// + /// The `server-connection` and its handshake resource don't not perform + /// any I/O on their own. It is a pure stream "wrapper" that consumes raw + /// TLS data streams and exposes cleartext streams. + /// The `input` and `output` parameters of constructor map to the + /// raw TLS data streams and are typically obtained from a WASI TCP socket. + /// The `server-handshake::finish` method returns the cleartext streams. + /// + /// ## Consumer drives the handshake + /// Many TLS libraries let users inject custom behavior at various points + /// of interest through the registration of callbacks. + /// The WebAssembly Component Model does not support callbacks. Instead, the + /// WASI TLS interface exposes direct-style APIs for the consumer to actively + /// drive the handshake forwards. + /// + /// The `server-handshake` starts out "suspended". The consumer then drives + /// the handshake to the next point of interest. For example using the + /// `receive-client-hello` method. Calling it: + /// - resumes the handshake, + /// - asynchronously waits for the client to send its hello message, + /// - upon receipt suspends the handshake again + /// - resolves the returned future. + /// It is then back up to the consumer to drive the handshake along to the + /// next point of interest (e.g. `request-client-identity`) + /// or finish the handshake entirely (`finish`). + /// + /// No data flows through the I/O streams while a server-handshake is suspended. + /// + /// # Usage + /// The general usage pattern looks like this: + /// - Construct a new connection using the `server-connection` constructor. + /// - Obtain the handshake resource using `server-connection::accept`. The + /// handshake starts out suspended. + /// - (Optional) Configure the settings using the various + /// `configure-*` methods. These may only be called while the + /// handshake is suspended. + /// - (Optional) Resume the handshake and wait for the client to send its + /// handshake using: `receive-client-hello`. + /// - (Optional) Further refine the settings based on the received + /// client hello using the `configure-*` methods. + /// - (Optional) Resume the handshake by requesting a client certificate and + /// waiting for it using: `request-client-identity`. + /// - Resume and run the handshake to completion using: `finish`. + /// - Read & write application data into the streams returned by `finish`. + /// + /// The optional steps must happen in the order as defined above. + /// + /// ## Example + /// A minimal, but production-ready, example in pseudocode: /// ```text - /// : TCP Socket TLS Client/Server - /// +-----------------+ +---------------------------------------------+ - /// | | splice | decryption | read - /// | `input-stream` | ========>> | `public-output` =========>> `private-input` | ======>> your - /// | | | | app - /// | | | | lives - /// | `output-stream` | <<======== | `public-input` <<========= `private-output` | <<====== here - /// | | splice | encryption | write - /// +-----------------+ +---------------------------------------------+ + /// let my-identity = ...; + /// + /// let conn = new server-connection(tcp-input, tcp-output); + /// let handshake = conn.accept(); + /// handshake.configure-server-identities([my-identity]); + /// let (tls-input, tls-output) = handshake.finish().await?; /// ``` /// - /// The user of this interface is responsible for continually forwarding - /// data from the socket into the `public-output` stream and - /// data from the `public-input` into the socket. + /// This assumes `tcp-input` and `tcp-output` are input/output streams + /// obtained from e.g. `tcp-socket::accept`. /// - /// # Caution - /// Because the guest acts as both the producer and the consumer for these - /// streams, do not use the `blocking_*` methods as that will deadlock yourself. - record io-streams { - public-input: input-stream, - public-output: output-stream, - private-output: output-stream, - private-input: input-stream, - } + /// # Secure by default + /// Implementations should pick reasonably safe defaults for all security + /// related settings. Users of this interface should be able to confidently + /// instantiate a new server connection and then, after configuring only the + /// server identity, immediately initiate the handshake. As demonstrated in + /// the example above. + resource server-connection { + // TODO: graceful shutdown + + /// Create a new connection instance that wraps the provided + /// raw TLS data streams. + constructor(input: input-stream, output: output-stream); + + /// Create a new server handshake. This method may be called at most once. + /// This method itself does not perform any I/O. Use the returned resource + /// to actually drive the handshake forwards. + /// + /// The returned `server-handshake` is a child resources of the `server-connection`. + accept: func() -> result; - flags client-suspension-points { - /// When the client received the server's certificate. - verify-server-identity, + /// Request post-handshake authentication. See `server-handshake::request-client-identity`. + request-client-identity: func() -> result; - /// When the client received a certificate request from the server. - select-client-identity, - /// When the initial handshake was successful. - connected, - } + /// The server name sent by the client or `none` if the client doesn't + /// support SNI. + server-name: func() -> option; - resource client-suspension { - /// At which point the TLS stream is suspended. - /// Exactly one flag is set in the return value. - at: func() -> client-suspension-points; + /// The negotiated ALPN ID, if any. + /// + /// Returns `none` when: + /// - the client did not advertise any ALPN IDs, or: + /// - there was no intersection between the IDs advertised by the client + /// and the IDs supported by the server. + alpn-id: func() -> option; - /// Only for select-client-identity: - // TODO: acceptable-authorities: func() -> result>; - - /// Only for verify-server-identity: - // TODO: unverified-identity: func() -> result; - } + /// The client's identity accepted by the server, if any. + /// + /// Returns `none` when: + /// - the server did not request a client certificate, + /// - the server did request a client certificate, but the client didn't + /// respond with a valid certificate. + client-identity: func() -> option; - enum suspend-error { - not-ready, - already-suspended, - closed, - } + /// The certificate of the server. + /// This is one of the identities passed to `server-handshake::configure-server-identities`. + server-identity: func() -> option; + /// The negotiated TLS protocol version. + protocol-version: func() -> option; + /// The negotiated cipher suite. + cipher-suite: func() -> option; + } + + /// Resource to control a TLS server handshake. + /// + /// See `server-connection` for more information. + resource server-handshake { + /// Configure which ALPN IDs the server is willing to accept from + /// the client, in descending order of preference. + configure-alpn-ids: func(value: list) -> result; + /// Configure the server certificates, in descending order of preference. + /// + /// The TLS implementation will select the best match from this list + /// based on parameters submitted in the client hello. Not all of those + /// parameters may be exposed in the `client-hello` resource. + configure-server-identities: func(value: list>) -> result; + // TODO: configure-protocol-versions: func(value: list) -> result; + // TODO: configure-cipher-suites: func(value: list) -> result; + /// (Optional) Partially continue the handshake and wait for the + /// ClientHello message to be received. + /// + /// Can be called at most once per handshake. Also, this method fails if + /// the handshake has already progressed too far for this to be possible. + /// + /// The returned future and `client-hello` are child resources of + /// the `server-handshake`. + receive-client-hello: func() -> result; + /// (Optional) Partially continue the handshake and request the client + /// to provide a certificate. Future resolves when the client's response + /// has been received and validated. + /// + /// Can be called at most once per handshake. Also, this method fails if + /// the handshake has already progressed too far for this to be possible. + /// + /// The returned future is a child resources of the `server-handshake`. + /// + /// TODO: custom set of root certificates + /// TODO: identity required true/false + /// TODO: Add ability to specify requirements such as: authorities, supported-signature-algorithms, oid-filters, .. + /// TODO: disable built-in validations (SSL_VERIFY_NONE). Should this still send the authorities? + request-client-identity: func() -> result; + + /// Perform the remainder of the handshake. Future resolves when the + /// handshake has completed. + /// + /// The returned future is a child resource of the `server-connection`. + finish: static func(this: server-handshake) -> future-streams; - // TODO - resource server { - constructor(suspend-at: server-suspension-points); - streams: func() -> result; + /// Cancel the handshake because of an error. The returned pollable + /// resolves when the error alert has been written out. + /// + /// The pollable is a child resource of the `server-connection`. + /// + /// TODO: add reason/errorcode parameter? + abort: static func(this: server-handshake) -> pollable; + } - configure-alpn-ids: func(value: list) -> result; - configure-identities: func(value: list>) -> result; + resource client-hello { + // TODO: expose the requested TLS version(s)? TLS1.2 and lower only communicates the highest version. TLS1.3 and higher communicates an exact list of versions. + /// The server name sent by the client or `none` if the client doesn't + /// support SNI. server-name: func() -> option; - alpn-id: func() -> option; - protocol-version: func() -> option; - client-identity: func() -> option; - server-identity: func() -> option; - suspend: func() -> result; - resume: func() -> result; - subscribe: func() -> pollable; + /// The ALPN IDs advertised by the client. Returns an empty list if the + /// client didn't provide any IDs or doesn't support ALPN. + alpn-ids: func() -> list; + + /// The supported cipher suites of the client. + cipher-suites: func() -> list; + } + + /// Dropping the request is equivalent to offering no certificate. + resource client-identity-request { + // TODO: Add ability to get the requirements sent by the server. Such as: authorities, supported-signature-algorithms, oid-filters, .. + + /// TODO + respond: static func(this: client-identity-request, identities: list>); } - flags server-suspension-points { - /// When the server received the initial message from the client. - client-hello, - /// When the server received the client's certificate. - verify-client-identity, - /// When the initial handshake was successful. - accepted, + + + + + + + // Boilerplate: + + + /// `future>` + resource future-streams { + subscribe: func() -> pollable; + get: func() -> option>>>; } - resource server-suspension { - at: func() -> server-suspension-points; + /// `future` + resource future-client-hello { + subscribe: func() -> pollable; + get: func() -> option>>; + } - /// Only for client-hello: - // TODO: requested-protocol-versions: func() -> result>; - // TODO: requested-server-name: func() -> result>; - // TODO: requested-alpn-ids: func() -> result>; + /// `future>` + resource future-public-identity { + subscribe: func() -> pollable; + get: func() -> option>>>; + } - /// Only for verify-client-identity: - // TODO: unverified-identity: func() -> result; + /// `future>` + resource future-client-identity-request { + subscribe: func() -> pollable; + get: func() -> option>>>; } }