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

TlsAcceptor handling multiple domains/identity #163

Open
llacroix opened this issue Apr 28, 2020 · 4 comments
Open

TlsAcceptor handling multiple domains/identity #163

llacroix opened this issue Apr 28, 2020 · 4 comments

Comments

@llacroix
Copy link

How does one generate handshake for multiple DNS?
I'm trying to make add tls support for a proxy server. The problem I have now is that even though I can get a TlsAcceptor to work based on the examples. I'm not so sure to understand how to get it to validate a provide different certificates for different HostName.
Do I have to create an Acceptor for each domain I try to validate and then check against all of the identities until I found one that Accept the connection? Or is the Acceptor handling that itself using the PKCS12 file?

@sfackler
Copy link
Owner

This would involve adding a callback that can inspect the SNI value provided by the client and swapping the acceptor's identity out. The OpenSSL backend can do this, but I'm not sure about the others. schannel may require some feature work?

@llacroix
Copy link
Author

llacroix commented Apr 28, 2020

Ah, ok, I think I'm getting it now a bit more.
I checked the uWSGI codebase how they do it.
https://github.com/unbit/uwsgi/blob/cc409036a094637515a61533923cfd96105b327d/core/ssl.c#L169

I'd have to get the server name using SSL_get_servername then assign a proper SslContext based on what I find. The uWSGI codebase is somewhat complicated as it seems to keep a cache of existing contexts. Then when it can't load an existing context, it will try to search for an appropriate one based on sni_dir and crt/key/ca files.
Most of the code can be summed up to this I think:

SSL_set_SSL_CTX(ssl, (SSL_CTX *) usl->custom_ptr);
// the following steps are taken from nginx
SSL_set_verify(ssl, SSL_CTX_get_verify_mode((SSL_CTX *)usl->custom_ptr),
SSL_CTX_get_verify_callback((SSL_CTX *)usl->custom_ptr));
SSL_set_verify_depth(ssl, SSL_CTX_get_verify_depth((SSL_CTX *)usl->custom_ptr));	
SSL_clear_options(ssl, SSL_get_options(ssl) & ~SSL_CTX_get_options((SSL_CTX *)usl->custom_ptr));

SSL_set_options(ssl, SSL_CTX_get_options((SSL_CTX *)usl->custom_ptr));

So unless a SwapContext is already defined somewhere... It seems like the only thing required is to build a SslContext based on the keys I want. Define a callback using SSL_CTX_set_tlsext_servername_callback (named a bit differently in the openssl crate)

Some of it can be simplified... I'd say that calling SSL_CTX_get_verify_callback is probably useless here unless there is some weird internal that requires to get the callback at least once to make things works...

Here as long as result is () for the Callback it will set the proper OK status for the callback result:
https://github.com/sfackler/rust-openssl/blob/f85d631fcfb8e20db53d3e2d0d77cfc92cf2e2c8/openssl/src/ssl/callbacks.rs#L159

But apparently the method to set the callback is only available at build time...

pub fn new(builder: &TlsAcceptorBuilder) -> Result<TlsAcceptor, Error> {
let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
acceptor.set_private_key(&builder.identity.0.pkey)?;
acceptor.set_certificate(&builder.identity.0.cert)?;
for cert in builder.identity.0.chain.iter().rev() {
acceptor.add_extra_chain_cert(cert.to_owned())?;
}
supported_protocols(builder.min_protocol, builder.max_protocol, &mut acceptor)?;
Ok(TlsAcceptor(acceptor.build()))
}

Except this code prevent the builder from setting the callback for the TlsAcceptor as it dirrectly build the acceptor. In fact, the let acceptor is a bit misleading because it's not a

And I can't redefine the BuilderMethod because some fields are private like builder.identity... So it's not an easy fix until there's an API for it... Or I could probably just fallback to use OpenSSL directly.

@sfackler
Copy link
Owner

To expose this in native-tls, it will need to work on all three backends, not just OpenSSL.

If you want to use OpenSSL directly, this is possible already. You set a servername callback which looks at the SNI field sent by the client with servername and then updates to the correct context for that domain with set_ssl_context.

@llacroix
Copy link
Author

llacroix commented Apr 28, 2020

Ah yess. Finally got it to work!

I'm using tokio-openssl instead of tokio-tls in the meantime.

fn new_acceptor_builder() -> Result<SslAcceptorBuilder, ErrorStack> {
    let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;

    acceptor.set_private_key_file("./certs/domain.key", SslFiletype::PEM).unwrap();
    acceptor.set_certificate_chain_file("./certs/domain.crt").unwrap();
    acceptor.set_min_proto_version(Some(SslVersion::TLS1));

    let mut ssl_context_s = SslContextBuilder::new(SslMethod::tls())?;
    ssl_context_s.set_private_key_file("./certs/snakeoil.key", SslFiletype::PEM).unwrap();
    ssl_context_s.set_certificate_chain_file("./certs/snakeoil.pem").unwrap();
    let context_s = ssl_context_s.build();

    //  ..... more .....
    let mut hashmap = HashMap::new();

    hashmap.insert("snakeoil", context_s);
    //.... more ....

    // deref to get access to the SslContextBuilder in the SslAcceptorBuilder
    let mut context_builder = &mut *acceptor;

    context_builder.set_servername_callback(move |ssl, alert| -> Result<(), SniError> {
        ssl.set_ssl_context({
            let domain = ssl.servername(NameType::HOST_NAME);
            if let Some(domain_str) = domain {
                if let Some(ctx) = hashmap.get(domain_str) {
                    ctx
                } else {
                    hashmap.get("default").unwrap()
                }
            } else {
                hashmap.get("default").unwrap()
            }
        });                                                                                                                                                                                                                                   
        Ok(())
    });

    Ok(acceptor)
}

Then use it with

let mut socket = tokio_openssl::accept(&tls_acceptor, socket).await.expect("accept error");

The API for tokio-openssl doesn't seem to be in line with tokio-tls and other *Acceptor strucs...
It would have been nice to simply do

tls_acceptor.accept(socket).await.expect("accept error")

But the accept method doesn't return an awaitable object like in the other libraries you made.
Anyway thank you! Being able to switch the ssl contexts means I can now serve HTTPS requests rather easily and provide different SSL certificates by domains.

Currently I'm only storing it in a hashmap but a more efficient method of storing them could be used. Technically it should be possible to generate certificates on the fly and eventually use ACME to generate the certificates through challenges and add the certificates to some kind of certificate storage.

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

2 participants