Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS (Transport Layer Security) #100

Open
badeend opened this issue Mar 21, 2024 · 20 comments
Open

TLS (Transport Layer Security) #100

badeend opened this issue Mar 21, 2024 · 20 comments

Comments

@badeend
Copy link
Collaborator

badeend commented Mar 21, 2024

TODO

https://github.com/Mbed-TLS/mbedtls
https://github.com/enarx-archive/tlssock

@dicej
Copy link

dicej commented Mar 21, 2024

I made an informal proposal at today's WASI meeting today: https://docs.google.com/presentation/d/1C55ph_fSTRhb4A4Nlpwvp9JGy8HBL6A1MvgY2jrapyQ/edit?usp=sharing

In a nutshell: I'd propose we translate the API of Rust's native-tls library to WIT as a starting point.

As @badeend pointed out, we could start even simpler than that and add an e.g. wasi:sockets/easy-client-tls interface with a single function, e.g. wrap-tls: func(input: input-stream, output: output-stream, server-name: string) -> tuple<input-stream, output-stream>, which would likely cover 80% of use cases.

@stevedoyle
Copy link

In a nutshell: I'd propose we translate the API of Rust's native-tls library to WIT as a starting point.

If taking the native-tls API, the initial WIT version should aim to fill some of its current gaps:

PQC support in TLS is a hot topic at the moment and ensuring that the wasi-tls design allows the selection of TLSv1.3 and specific ciphersuites is important to ensuring that the design is flexible enough to support PQC related features that are coming.

@badeend
Copy link
Collaborator Author

badeend commented Aug 18, 2024

I drafted up an interface at: #104

I don't think a distinct easy-client-tls interface as mentioned above is needed anymore. Even "fully fledged" interface is pretty straight forward to set up.
Below is an example of a TCP/TLS client in pseudo code. If you ignore the setup boilerplate, you'll see that the actual TLS client surface area is only two lines long: /* A */ & /* B */

let ip = resolve_addresses("example.com").await?[0];

let tcp_client = TcpSocket::new();
let (tcp_input, tcp_output) = tcp_client.connect(ip, 443).await;

let tls_client = TlsClient::new("example.com", SuspensionPoints::none()); /* A */
let tls_streams = tls_client.streams()?;

forward(tcp_input, tls_streams.public_output);
forward(tls_streams.public_input, tcp_output);

tls_client.resume(); /* B */

tls_streams.private_output.blocking_write_and_flush("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
let response = tls_streams.private_input.blocking_read();

println!(response);




/// Pump all data from `src` to `dest` on a background task
fn forward(src: InputStream, dest: OutputStream) -> JoinHandle {
    spawn_blocking(|| {
        loop {
            dest.blocking_splice(src).await?;
        }
    })
}

If you're wondering what the SuspensionPoints & resume stuff is about, I invite you to read the docs on the tls-client resource in the PR. TLDR; it is the least bad solution I could come up with to handle callbacks, which TLS libraries love to use but the CM doesn't support.
Below is a more detailed showcase of these suspensions, this time from the POV of a server:

let id1 = PrivateIdentity::parse(
    fs::read("private1.key"),
    fs::read("public1.crt"),
)?;
let id2 = PrivateIdentity::parse(
    fs::read("private2.key"),
    fs::read("public2.crt"),
)?;

let tcp_server = TcpSocket::new();
tcp_server.bind(443);
tcp_server.listen();

loop {
    let (tcp_client, tcp_input, tcp_output) = tcp_server.accept().await;

    let tls_server = TlsServer::new(SuspensionPoints::ClientHello | SuspensionPoints::Accepted);
    let tls_streams = tls_server.streams()?;

    {
        tls_client.configure_alpn_ids(["h2"]);

        forward(tcp_input, tls_streams.public_output);
        forward(tls_streams.public_input, tcp_output);

        tls_server.resume(); // Accept initiate handshake
    }
    {
        let suspension = wait_suspend(tls_server).await; // Wait for client hello
        assert!(suspension.at() == SuspensionPoints::ClientHello);

        // Select certificate based on SNI:
        match suspension.requested_server_name()? {
            Some("example.com") => {
                tls_server.configure_identities([id1]);
            }
            _ => tls_server.configure_identities([id2]);
        }

        tls_server.resume(); // Continue handshake
    }
    {
        let suspension = wait_suspend(tls_server).await; // Wait for handshake to complete
        assert!(suspension.at() == SuspensionPoints::Accepted);

        println!("Fully connected!");

        tls_server.resume();
    }
    {
        let request = tls_streams.private_input.blocking_read();

        println!(request);
    }
}



async fn wait_suspend(tls: TlsServer) {
    loop {
        let result = tls.suspend();
        if result == Err(NotReady) {
            tls.subscribe().block();
        } else {
            return result;
        }
    }
}

@pavelsavara
Copy link

Here is SslStream Platform Abstraction Layer (PAL) for dotnet running on Android (consuming Java API).
https://github.com/dotnet/runtime/blob/main/src/native/libs/System.Security.Cryptography.Native.Android/pal_sslstream.c
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Android.cs

I guess this is closest to what we need to do for WASI TLS on dotnet. TLSv1.3 only I think.
It seems managing and validating certificates is complex feature.

@dicej
Copy link

dicej commented Aug 19, 2024

The other important thing to notice about .NET's SSLStream is that it can wrap an arbitrary Stream implementation, e.g. a MemoryStream, a FileStream, a SocketStream, etc. So we need to make sure that this interface can support such an abstraction.

If I'm reading @badeend's draft interface correctly, it looks like we could do that by piping the public-input and public-output streams to the wrapped .NET Stream, possibly optimizing the the FileStream and SocketStream cases by using splice, and falling back to using read and write for e.g. MemoryStream and others.

@pavelsavara
Copy link

pavelsavara commented Aug 19, 2024

I guess WASI stream will be Pollable (or Promise in the future).
While we implement it in single-thread we can only support it for async APIs of C# Stream
We can't be blocked by Pollable and so synchronous APIs of Stream can't be implemented.
That's fine, we already have the same for HTTP response stream when running in a browser.

@dicej
Copy link

dicej commented Aug 19, 2024

Right, I was assuming we'd only support e.g. ReadAsync and WriteAsync.

@dicej
Copy link

dicej commented Aug 19, 2024

For reference, here are the thin wrappers I built around wasi:io/streams/input-stream and wasi:io/streams/output-stream, respectively:

https://github.com/dicej/spin-dotnet-sdk/blob/main/src/InputStream.cs
https://github.com/dicej/spin-dotnet-sdk/blob/main/src/OutputStream.cs

@badeend
Copy link
Collaborator Author

badeend commented Aug 19, 2024

The other important thing to notice about .NET's SSLStream is that it can wrap an arbitrary Stream implementation, e.g. a MemoryStream, a FileStream, a SocketStream, etc. So we need to make sure that this interface can support such an abstraction.

If I'm reading @badeend's draft interface correctly, it looks like we could do that by piping the public-input and public-output streams to the wrapped .NET Stream, possibly optimizing the the FileStream and SocketStream cases by using splice, and falling back to using read and write for e.g. MemoryStream and others.

Pretty much, yes. 👍 The tls-client/server doesn't care where its TLS data comes from and goes to.


Here is SslStream Platform Abstraction Layer (PAL) for dotnet running on Android (consuming Java API). [...] I guess this is closest to what we need to do for WASI TLS on dotnet.

Thanks for the links. Good to know what the dotnet team has already encountered and how they solved the various differences.

Looking at the source for the Android backend and similarly for the OpenSSL backend; there's a lot of "intricate" code. Don't know if this is what you were suggesting, but; I don't we should attempt to emulate one of these existing backend interfaces with all its warts. Rather, I think we should start at the user-facing interface and work our way back from there.


We can't be blocked by Pollable and so synchronous APIs of Stream can't be implemented.

Interesting, why's that? Can't you use blocking_read etc?

@dicej
Copy link

dicej commented Aug 19, 2024

We can't be blocked by Pollable and so synchronous APIs of Stream can't be implemented.

Interesting, why's that? Can't you use blocking_read etc?

Perhaps he meant "we can implement them, but using them means no concurrency" (which might be fine for simple use cases).

@badeend
Copy link
Collaborator Author

badeend commented Aug 19, 2024

I did some exploratory investigation on how much of the .NET interface is theoretically supported by the draft. The results are now included in the PR. Overall; not bad.

What sticks out is that about half of the "Not supported"s are to do with lower-level primitives (cipher suites, hashing algorithms, their parameters, etc) i.e. the potential footguns. So we'll need to carefully consider if/how to expose them.

@pavelsavara
Copy link

pavelsavara commented Aug 20, 2024

We can't be blocked by Pollable and so synchronous APIs of Stream can't be implemented.

Interesting, why's that? Can't you use blocking_read etc?

Perhaps he meant "we can implement them, but using them means no concurrency" (which might be fine for simple use cases).

Yes, we can implement it, but we would not do that for ST build, because

  • it would stop progress on all other pending async tasks (for unknown duration up to timeout on network level)
  • which would be very surprising and unexpected behavior to C# audience

I think we are not "simple use case" with dotnet, but it may make sense for others.

Looking at the source for the Android backend and similarly for the OpenSSL backend; there's a lot of "intricate" code. Don't know if this is what you were suggesting, but; I don't we should attempt to emulate one of these existing backend interfaces with all its warts.

Agreed, we don't want/need to implement full set of those features. I only shared it to broaden our design perspective.
The actual scope should be limited initially.

After we enable the initial experience, we will learn more. About the use cases which are not possible without those missing features. We can add more incrementally, hopefully without too many breaking changes.

@pavelsavara
Copy link

I did some exploratory investigation on how much of the .NET interface is theoretically supported by the draft. The results are now included in the PR. Overall; not bad.

This is great, thanks!

@pavelsavara
Copy link

cc @ManickaP @karelz @wfurt could you guys please provide feedback about the proposed design of

WASI interface definition for TLS stream
https://github.com/badeend/wasi-sockets/blob/tls/wit/tls.wit

And proposed scope for C# SslStream ?
https://github.com/badeend/wasi-sockets/blob/tls/TLS.md

What would be major existing use-cases which would not work ?
Is that enough for Microsoft.Data.SqlClient and similar use cases ?

@karelz
Copy link

karelz commented Aug 20, 2024

Adding @rzikm

@badeend
Copy link
Collaborator Author

badeend commented Aug 21, 2024

I did some exploratory investigation on how much of the .NET interface is theoretically supported by the draft. The results are now included in the PR. Overall; not bad.

And it now also includes the same thing for Node.JS.

@rzikm
Copy link

rzikm commented Aug 21, 2024

Sorry for longer text, the topics/ideas kept accumulating as I read the two linked files.

Re SslStream members

most of the "Not supported" members are informative only and *Strength and *Algorithm members will be obsoleted in .NET 10, what remains is

  • NegotiatedCipherSuite - for audit purposes, I suggest exposing this information as it should be straightforward
  • NegotiateClientCertificateAsync - used by servers to perform delayed client authentication (e.g. some http servers may want to do this if they detect access to specific urls). This generally requires only propagating the request to the underlying platform implementation so it should not be too difficult.

Ssl(Client/Server)AuthenticationOptions

I assume "not supported" stands for lack of configuration support, not for lack of feature in general. lack of customizability of some features should not be a problem as long as the behavior is sane.

  • AllowTlsResume - TLS Resume is an optimization, it does not block any scenarios if missing or can't be configured. (.NET defaults to true)
  • AllowRenegotiation - on TLS 1.2, renegotiation is used for post-handshake client authentication (see NegotiateClientCertificateAsync above), so lack of renegotiation support could break client-authenticated scenarios (note that some servers like IIS always uses the post-handshake auth method even if client auth is required for all requests). .NET defaults to true on clients and false on servers. TLS 1.3 does not have a concept of renegotiation.
    - ClientCertificateRequired - This one is slightly concerning, lack of customization here likely means that mutual TLS scenarios are going to be blocked (clients can't send certs by themselves, server must request them explicitly)
  • CertificateRevocationCheckMode,CertificateChainPolicy - these are related to remote certificate validation, these are not something that would be passed to the TLS stack (.NET performs its own validation via X509Chain.Build, and whatever that uses on given platform).
  • EnabledSslProtocol - None (OS default) is the recommended value, there is no practical reason to set anything else, so this not being configurable in the first version is not a problem.

Re (Client/Server)CertificateContext and private-identity

I see that the WIT file has

    /// 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<u8>, x509-chain: list<list<u8>>) -> result<private-identity>;

        public-identity: func() -> public-identity;
    }

The 'private-identity' is more or less equivalent to the SslStreamCertificateContext, it is a certificate + intermediates + some extra info, so you can consider it supported.

While on the topic of certificates, there is an important point to consider when designing APIs. Windows is very specific in a way how it works with certificates, basically, the sensitive operations with private keys are performed by a separate process (lsass) and application communicates with lsass over IPC. Applications refer to a certificate using a handle.

This becomes somewhat problematic when attempting to send certificate chains when intermediates are not stored in the OS certificate store. The relevant SChannel API takes only a single certificate handle parameter, and attempts to build the certificate chain internally. Since the certificate chain is bulit by separate process and the intermediates are not in a store, lsass/Schannel can't find the intermediates, which can result in only the leaf certificate being sent over the wire, which in the end means connection errors because the remote peer may not be able to verify the certificate. In .NET, we workaround this fairly common case by inserting the intermediates to the cert stores when building the SslStreamCertificateContext.

https://github.com/dotnet/runtime/blob/4bbde33ac01496375ee8902c886c9f0c3c7c709c/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs#L43-L101

There is some discussion about the topic at dotnet/runtime#26323

The other reason I am bringing this up is that on Windows, Having an access to certificate handle does not mean you have access to a certificate private key. You can lookup certs in a store by Thumbprint and get handle back, but by configuration the application may not be able to export private key to be able to pass it to your API, so consider adding more options of creating private-identity

Also, note that there are multiple binary formats for X509 certificates (DER, PKCS12, ...) so you probably want to reflect that in the parse function.

suggested additions

I suggest adding following

    type tls-cipher-suite = u16; // wire identifier of the negotiated cipher suite. e.g. 0x1301 for TLS_AES_128_GCM_SHA256 
    
    resource client {
        negotiated-cipher-suite: func() -> option<tls-cipher-suite>
    }

    resource server {
        negotiated-cipher-suite: func() -> option<tls-cipher-suite>

        /// enables mutual auth, should be set during client-hello suspension point at the latest
        configure-require-client-cert: func(bool) -> result;
    }

otherwise the proposed API seems reasonable to be able to cover most of the use cases.

Questions/other

  • how do you expect to surface errors from the underlying stack? These can range from protocol-level errors (TLS alert message received from the other side, corrupted data/decryption failure) network errors, or implementation-specific failures (say, "Local security authority could not be contacted on Windows")

cc @wfurt in case I forgot something.

@dicej
Copy link

dicej commented Sep 18, 2024

I've taken a subset of @badeend's draft WIT file and created .NET guest implementation providing a corresponding subset of the System.Net.Security.SslStream API, along with a Wasmtime-based host implementation here. It works well enough to send HTTPS GET requests to arbitrary webservers and print the response.

Next, @jsturtevant and I will lightly modify Microsoft.Data.SqlClient to use the above and try to get it to open an encrypted connection to an SQL Server, adding any features as needed.

@dicej
Copy link

dicej commented Sep 23, 2024

Quick update on the above: @jsturtevant and I were able to get a modified Microsoft.Data.SqlClient to connect to a SQL server and query it using TDS 8.0 (i.e. the latest Tabular Data Stream format, and the first to be entirely encapsulated in TLS).

We didn't need anything fancy -- just this minimal subset of @badeend's proposed draft. I'm sure additional knobs would be needed for more advanced cases (e.g. client certificates), but we've demonstrated that the minimal subset is sufficient for a real-world scenario like this.

@pavelsavara
Copy link

Related dotnet/runtime#109569

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants