diff --git a/cassandane/Cassandane/Cyrus/ALPN.pm b/cassandane/Cassandane/Cyrus/ALPN.pm new file mode 100644 index 0000000000..5640a74146 --- /dev/null +++ b/cassandane/Cassandane/Cyrus/ALPN.pm @@ -0,0 +1,425 @@ +#!/usr/bin/perl +# +# Copyright (c) 2011-2024 FastMail Pty Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# 3. The name "Fastmail Pty Ltd" must not be used to +# endorse or promote products derived from this software without +# prior written permission. For permission or any legal +# details, please contact +# FastMail Pty Ltd +# PO Box 234 +# Collins St West 8007 +# Victoria +# Australia +# +# 4. Redistributions of any form whatsoever must retain the following +# acknowledgment: +# "This product includes software developed by Fastmail Pty. Ltd." +# +# FASTMAIL PTY LTD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +# EVENT SHALL OPERA SOFTWARE AUSTRALIA BE LIABLE FOR ANY SPECIAL, INDIRECT +# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +package Cassandane::Cyrus::ALPN; +use strict; +use warnings; +use Cwd qw(abs_path); +use Data::Dumper; +use HTTP::Tiny; +use XML::Spice; + +use lib '.'; +use base qw(Cassandane::Cyrus::TestCase); +use Cassandane::Util::Log; + +# When ALPN negotiation fails, depending on the openssl version, we might get +# a sensible error message, or an opaque reference to "1120". +# https://github.com/openssl/openssl/issues/24300 +my $alpn_fail_pattern = qr{(?: tlsv1\salert\sno\sapplication\sprotocol + | ssl3_read_bytes:reason\(1120\) + )}x; + +sub new +{ + my $class = shift; + + my $config = Cassandane::Config->default()->clone(); + $config->set(tls_server_cert => '@basedir@/conf/certs/cert.pem', + tls_server_key => '@basedir@/conf/certs/key.pem', + httpmodules => 'caldav'); + + my $self = $class->SUPER::new({ + config => $config, + install_certificates => 1, + services => [ 'imap', 'imaps' ], + }, @_); + + $self->needs('dependency', 'openssl'); + $self->needs('dependency', 'openssl_alpn'); + + return $self; +} + +sub set_up +{ + my ($self) = @_; + $self->SUPER::set_up(); +} + +sub tear_down +{ + my ($self) = @_; + $self->SUPER::tear_down(); +} + +sub do_imap_starttls +{ + my ($self, $talk, $alpn_map) = @_; + my $ca_file = abs_path("data/certs/cacert.pem"); + + my $starttls_tag = $talk->{CmdId}; + + $talk->_imap_cmd('starttls', 0, 'starttls'); + + die $talk->get_last_error() + if $talk->get_last_completion_response() ne 'ok'; + + IO::Socket::SSL->start_SSL($talk->{Socket}, + SSL_ca_file => $ca_file, + SSL_verifycn_scheme => 'none', + SSL_alpn_protocols => $alpn_map, + ); + + if (ref $talk->{Socket} ne 'IO::Socket::SSL') { + # STARTTLS failed! Rummage inside Mail::IMAPTalk to put it back into + # a sane state, because the tag having two responses confuses it + + # make it read the tagged "NO Starttls negotiation failed" response + $talk->{CmdId} = $starttls_tag; + my ($no, $msg) = $talk->_parse_response({}, {}); + die "uh oh, response '$no' wasn't 'no'" if $no ne 'no'; + + # make get_last_completion_response() work + $talk->{LastRespCode} = $no; + + # throw an exception here by default, eval the call if you want + # to handle the failure and keep using the plaintext session + die $msg; + } +} + +# check if we selected the expected ALPN protocol. +# if the underlying IO::Socket::SSL object isn't accessible, pass undef +# and this check will examine logs instead +sub assert_alpn_protocol +{ + my ($self, $socket, $protocol) = @_; + + if ($socket) { + if ($protocol) { + $self->assert_str_equals($protocol, $socket->alpn_selected()); + } + else { + $self->assert_null($socket->alpn_selected()); + } + } + else { + return if !$self->{instance}->{have_syslog_replacement}; + + my @lines = $self->{instance}->getsyslog(qr{starttls: \S+ with cipher}); + $self->assert_num_equals(1, scalar @lines); + + if ($protocol) { + $self->assert_matches(qr{; application protocol = $protocol}, + $lines[0]); + } + else { + $self->assert_does_not_match(qr{; application protocol =}, + $lines[0]); + } + } +} + +sub test_imap_none +{ + my ($self) = @_; + + # get a pristine connection + $self->{store}->disconnect(); + my $talk = $self->{store}->get_client(NoLogin => 1); + + $self->do_imap_starttls($talk, undef); + + $talk->login('cassandane', 'secret'); + $self->assert_str_equals('ok', $talk->get_last_completion_response()); + + $talk->select("INBOX"); + $self->assert_str_equals('ok', $talk->get_last_completion_response()); + + $self->assert_alpn_protocol($talk->{Socket}, undef); +} + +sub test_imap_good +{ + my ($self) = @_; + + # get a pristine connection + $self->{store}->disconnect(); + my $talk = $self->{store}->get_client(NoLogin => 1); + + $self->do_imap_starttls($talk, [ 'imap' ]); + + $talk->login('cassandane', 'secret'); + $self->assert_str_equals('ok', $talk->get_last_completion_response()); + + $talk->select("INBOX"); + $self->assert_str_equals('ok', $talk->get_last_completion_response()); + + $self->assert_alpn_protocol($talk->{Socket}, 'imap'); +} + +sub test_imap_bad +{ + my ($self) = @_; + + # get a pristine connection + $self->{store}->disconnect(); + my $talk = $self->{store}->get_client(NoLogin => 1); + + eval { + $self->do_imap_starttls($talk, [ 'bogus' ]); + }; + + my $e = $@; + $self->assert_not_null($e); + $self->assert_matches(qr{Starttls negotiation failed}, $e); + $self->assert_str_equals('no', $talk->get_last_completion_response()); +} + +sub test_imaps_none +{ + my ($self) = @_; + + my $imaps = $self->{instance}->get_service('imaps'); + my $store = $imaps->create_store(username => 'cassandane'); + my $talk = $store->get_client(OverrideALPN => undef); + + $talk->select("INBOX"); + $self->assert_str_equals('ok', $talk->get_last_completion_response()); + + $self->assert_alpn_protocol($talk->{Socket}, undef); +} + +sub test_imaps_good +{ + my ($self) = @_; + + my $imaps = $self->{instance}->get_service('imaps'); + my $store = $imaps->create_store(username => 'cassandane'); + my $talk = $store->get_client(); # correct ALPN map is the default + + $talk->select("INBOX"); + $self->assert_str_equals('ok', $talk->get_last_completion_response()); + + $self->assert_alpn_protocol($talk->{Socket}, 'imap'); +} + +sub test_imaps_bad +{ + my ($self) = @_; + + my $imaps = $self->{instance}->get_service('imaps'); + my $store = $imaps->create_store(username => 'cassandane'); + my $talk; + + eval { + $talk = $store->get_client(OverrideALPN => [ 'bogus' ]); + }; + my $e = $@; + + $self->assert_not_null($e); + $self->assert_matches($alpn_fail_pattern, $e); +} + +sub do_https_request +{ + my ($self, $service, $alpn_map) = @_; + + my $ca_file = abs_path("data/certs/cacert.pem"); + + my $client = HTTP::Tiny->new(verify_SSL => 1, + SSL_options => { + SSL_ca_file => $ca_file, + SSL_verifycn_scheme => 'none', + SSL_alpn_protocols => $alpn_map, + }); + + my $url = sprintf("https://%s:%s@%s:%s/dav/calendars", + 'cassandane', 'secret', + $service->host(), $service->port()); + + my $content = x('D:propfind', { 'xmlns:D' => 'DAV:' }, + x('D:prop', + x('D:current-user-principal'))); + + my $response = $client->request('PROPFIND', $url, { + headers => { + 'content-type' => 'application/xml; charset=utf8', + }, + content => "$content", # stringify the XML::Spice::Chunk + }); + + return $response; +} + +sub test_https_none + :want_service_https :needs_component_httpd +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = undef; + + my $response = $self->do_https_request($https, $alpn_map); + + $self->assert_num_equals(1, $response->{success}); + $self->assert_alpn_protocol(undef, undef); +} + +sub test_https_http10 + :want_service_https :needs_component_httpd +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = [ 'http/1.0' ]; + + my $response = $self->do_https_request($https, $alpn_map); + + $self->assert_num_equals(1, $response->{success}); + # XXX if we negotiated HTTP/1.0 via ALPN, the server still talks HTTP/1.1 + # XXX anyway... + $self->assert_str_equals('HTTP/1.1', $response->{protocol}); + $self->assert_alpn_protocol(undef, $alpn_map->[0]); +} + +sub test_https_http11 + :want_service_https :needs_component_httpd +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = [ 'http/1.1' ]; + + my $response = $self->do_https_request($https, $alpn_map); + + $self->assert_num_equals(1, $response->{success}); + $self->assert_str_equals('HTTP/1.1', $response->{protocol}); + $self->assert_alpn_protocol(undef, $alpn_map->[0]); +} + +sub test_https_multi + :want_service_https :needs_component_httpd +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = [ 'http/1.1', 'http/1.0' ]; + + my $response = $self->do_https_request($https, $alpn_map); + + $self->assert_num_equals(1, $response->{success}); + $self->assert_str_equals('HTTP/1.1', $response->{protocol}); + $self->assert_alpn_protocol(undef, $alpn_map->[0]); +} + +sub test_https_h2 + :want_service_https :needs_component_httpd :needs_dependency_nghttp2 +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = [ 'h2' ]; + + my $response = $self->do_https_request($https, $alpn_map); + + # HTTP::Tiny can't speak h2, so the request itself will fail + $self->assert_num_equals(0, $response->{success}); + $self->assert_str_equals('Internal Exception', $response->{reason}); + + # but we can still examine the log to check the ALPN result + $self->assert_alpn_protocol(undef, $alpn_map->[0]); +} + +sub test_https_multi2 + :want_service_https :needs_component_httpd :needs_dependency_nghttp2 +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = [ 'h2', 'http/1.1', 'http/1.0' ]; + + my $response = $self->do_https_request($https, $alpn_map); + + # HTTP::Tiny can't speak h2, so the request itself will fail + $self->assert_num_equals(0, $response->{success}); + $self->assert_str_equals('Internal Exception', $response->{reason}); + + # but we can still examine the log to check the ALPN result + $self->assert_alpn_protocol(undef, $alpn_map->[0]); +} + +sub test_https_bad + :want_service_https :needs_component_httpd +{ + my ($self) = @_; + + # skip past setup logs + $self->{instance}->getsyslog(); + + my $https = $self->{instance}->get_service('https'); + my $alpn_map = [ 'bogus' ]; + + my $response = $self->do_https_request($https, $alpn_map); + + $self->assert_num_equals(0, $response->{success}); + $self->assert_str_equals('Internal Exception', $response->{reason}); + $self->assert_matches($alpn_fail_pattern, $response->{content}); +} + +1; diff --git a/cassandane/Cassandane/Cyrus/TestCase.pm b/cassandane/Cassandane/Cyrus/TestCase.pm index cb9a32aa96..6a4da85838 100644 --- a/cassandane/Cassandane/Cyrus/TestCase.pm +++ b/cassandane/Cassandane/Cyrus/TestCase.pm @@ -795,22 +795,23 @@ sub _setup_http_service_objects { my ($self) = @_; - # nothing to do if no http service - require Mail::JMAPTalk; - require Net::CalDAVTalk; - require Net::CardDAVTalk; - + # nothing to do if no http or https service my $service = $self->{instance}->get_service("http"); + $service ||= $self->{instance}->get_service("https"); return if !$service; + my %common_args = ( + user => 'cassandane', + password => 'pass', + host => $service->host(), + port => $service->port(), + scheme => ($service->is_ssl() ? 'https' : 'http'), + ); + if ($self->{instance}->{config}->get_bit('httpmodules', 'carddav')) { require Net::CardDAVTalk; $self->{carddav} = Net::CardDAVTalk->new( - user => 'cassandane', - password => 'pass', - host => $service->host(), - port => $service->port(), - scheme => 'http', + %common_args, url => '/', expandurl => 1, ); @@ -818,26 +819,18 @@ sub _setup_http_service_objects if ($self->{instance}->{config}->get_bit('httpmodules', 'caldav')) { require Net::CalDAVTalk; $self->{caldav} = Net::CalDAVTalk->new( - user => 'cassandane', - password => 'pass', - host => $service->host(), - port => $service->port(), - scheme => 'http', + %common_args, url => '/', expandurl => 1, ); $self->{caldav}->UpdateAddressSet("Test User", - "cassandane\@example.com"); + "cassandane\@example.com"); } if ($self->{instance}->{config}->get_bit('httpmodules', 'jmap')) { require Mail::JMAPTalk; $ENV{DEBUGJMAP} = 1; $self->{jmap} = Mail::JMAPTalk->new( - user => 'cassandane', - password => 'pass', - host => $service->host(), - port => $service->port(), - scheme => 'http', + %common_args, url => '/jmap/', ); } diff --git a/cassandane/Cassandane/IMAPMessageStore.pm b/cassandane/Cassandane/IMAPMessageStore.pm index fd75bcde60..6c2b337f31 100644 --- a/cassandane/Cassandane/IMAPMessageStore.pm +++ b/cassandane/Cassandane/IMAPMessageStore.pm @@ -97,6 +97,10 @@ sub connect if ($self->{ssl}) { my $ca_file = abs_path("data/certs/cacert.pem"); + my $alpn_map = exists $params{OverrideALPN} + ? delete $params{OverrideALPN} + : [ 'imap' ]; + # XXX https://github.com/noxxi/p5-io-socket-ssl/issues/121 # XXX With newer IO::Socket::SSL, hostname verification fails # XXX because our hostname is an IP address and the certificate @@ -111,6 +115,7 @@ sub connect UseSSL => $self->{ssl}, SSL_ca_file => $ca_file, SSL_verifycn_scheme => 'none', + SSL_alpn_protocols => $alpn_map, UseBlocking => 1, # must be blocking for SSL Pedantic => 1, PreserveINBOX => 1, diff --git a/cassandane/Cassandane/Service.pm b/cassandane/Cassandane/Service.pm index 010ec123a9..d62aa6ee85 100644 --- a/cassandane/Cassandane/Service.pm +++ b/cassandane/Cassandane/Service.pm @@ -108,6 +108,15 @@ sub set_port return $self->{_listener}->set_port($port); } +sub is_ssl +{ + my ($self) = @_; + + # assume '-s' service argument indicates SSL and its absense + # indicates plaintext + return scalar grep { $_ eq '-s' } @{$self->{argv}}; +} + # Return a hash of parameters suitable for passing # to MessageStoreFactory::create. sub store_params diff --git a/cassandane/Cassandane/ServiceFactory.pm b/cassandane/Cassandane/ServiceFactory.pm index 5507431ee6..fea981e5f6 100644 --- a/cassandane/Cassandane/ServiceFactory.pm +++ b/cassandane/Cassandane/ServiceFactory.pm @@ -60,17 +60,11 @@ sub create } # try and guess some service-specific defaults - if ($name =~ m/imaps/) + if ($name =~ m/imap(s?)/) { - return Cassandane::IMAPService->new( - argv => ['imapd', '-s'], - %params); - } - elsif ($name =~ m/imap/) - { - return Cassandane::IMAPService->new( - argv => ['imapd'], - %params); + my @argv = 'imapd'; + push @argv, '-s' if $1; + return Cassandane::IMAPService->new(argv => \@argv, %params); } elsif ($name =~ m/sync/) { @@ -78,11 +72,11 @@ sub create argv => ['imapd'], %params); } - elsif ($name =~ m/http/) + elsif ($name =~ m/http(s?)/) { - return Cassandane::Service->new( - argv => ['httpd'], - %params); + my @argv = 'httpd'; + push @argv, '-s' if $1; + return Cassandane::Service->new(argv => \@argv, %params); } elsif ($name =~ m/lmtp/) { diff --git a/cunit/backend.testc b/cunit/backend.testc index c14074f559..d3abad696f 100644 --- a/cunit/backend.testc +++ b/cunit/backend.testc @@ -20,6 +20,7 @@ #include "lib/xstrlcpy.h" #define DBDIR "test-dbdir" +#define MAX_ALPN_MAP (4) struct server_config { int sasl_plain; @@ -27,6 +28,7 @@ struct server_config { int starttls; int deflate; int caps_one_per_line; + struct tls_alpn_t alpn_map[MAX_ALPN_MAP + 1]; }; /* @@ -54,6 +56,7 @@ struct server_state { sasl_conn_t *saslconn; /* the sasl connection context */ #ifdef HAVE_SSL SSL *tls_conn; + char tls_alpn_proto[64]; #endif struct protstream *in; struct protstream *out; @@ -72,7 +75,8 @@ static const struct server_config default_server_config = { .sasl_login = 0, .starttls = 0, .deflate = 0, - .caps_one_per_line = 1 + .caps_one_per_line = 1, + .alpn_map = {{{0}, NULL, NULL }}, }; static const struct capa_t default_capa[] = { { "CCSASL", CAPA_AUTH }, @@ -89,6 +93,7 @@ static struct protocol_t test_prot = { /* .service is setup in default_conditions() */ .sasl_service = SASLSERVICE, + .alpn_map = NULL, .type = TYPE_STD, .u.std = { .banner = { @@ -690,12 +695,12 @@ static void test_oneline_caps(void) caps_common(); } -#ifdef HAVE_SSL /* * Test STARTTLS */ static void test_starttls(void) { +#ifdef HAVE_SSL struct backend *be; const char *auth_status = NULL; char *mechs; @@ -723,14 +728,290 @@ static void test_starttls(void) backend_disconnect(be); free(be); +#endif } -#else + /* - * cunit.pl doesn't process C macros, so it expects this to exist - * regardless of the state of HAVE_SSL + * ALPN tests */ -static void test_starttls(void) { } +enum { + ALPN_COMMON_EXPECT_FAIL = 0, + ALPN_COMMON_EXPECT_SUCCESS = 1, +}; + +static void alpn_common(const struct tls_alpn_t *client_alpn_map, + unsigned n_client_protocols, + const struct tls_alpn_t *server_alpn_map, + unsigned n_server_protocols, + int expect_success, + const char *expect_protocol) +{ + struct backend *be; + const char *auth_status = NULL; + int r; + + CU_ASSERT_FATAL(n_client_protocols <= MAX_ALPN_MAP); + CU_ASSERT_FATAL(n_server_protocols <= MAX_ALPN_MAP); + + default_conditions(); + server_state->config.sasl_plain = 1; + server_state->config.starttls = 1; + if (n_server_protocols) { + memcpy(server_state->config.alpn_map, + server_alpn_map, + n_server_protocols * sizeof(struct tls_alpn_t)); + } + + test_prot.alpn_map = client_alpn_map; + + be = backend_connect(NULL, HOST, &test_prot, + USERID, callbacks, &auth_status, /*fd*/-1); + if (expect_success) { + CU_ASSERT_PTR_NOT_NULL_FATAL(be); + CU_ASSERT_PTR_NOT_NULL_FATAL(be->tlsconn); + + CU_ASSERT_EQUAL(server_state->is_connected, 1); + CU_ASSERT_EQUAL(server_state->is_authenticated, 1); + CU_ASSERT_EQUAL(server_state->is_tls, 1); + + CU_ASSERT(CAPA(be, CAPA_STARTTLS)) + + if (expect_protocol) { + char *client_proto = NULL; + + client_proto = tls_get_alpn_protocol(be->tlsconn); + CU_ASSERT_PTR_NOT_NULL(client_proto); + CU_ASSERT_STRING_EQUAL(client_proto, expect_protocol); + free(client_proto); + + CU_ASSERT_STRING_EQUAL(server_state->tls_alpn_proto, + expect_protocol); + } + else { + CU_ASSERT_PTR_NULL(tls_get_alpn_protocol(be->tlsconn)); + CU_ASSERT_STRING_EQUAL(server_state->tls_alpn_proto, ""); + } + + r = backend_ping(be, NULL); + CU_ASSERT_EQUAL(r, 0); + + backend_disconnect(be); + free(be); + } + else { + CU_ASSERT_PTR_NULL(be); + } +} + +static void test_alpn_match_single(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_SUCCESS, + "foo"); +#endif +} + +static void test_alpn_match_client_only(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + NULL, + 0, + ALPN_COMMON_EXPECT_SUCCESS, + NULL); +#endif +} + +static void test_alpn_match_server_only(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t server_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(NULL, + 0, + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_SUCCESS, + NULL); +#endif +} + +static void test_alpn_match_client_multi(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_SUCCESS, + "foo"); +#endif +} + +static void test_alpn_match_server_multi(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "foo", NULL, NULL }, + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_SUCCESS, + "foo"); +#endif +} + +static void test_alpn_match_multi(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "bar", NULL, NULL }, + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_SUCCESS, + "bar"); +#endif +} + +static void test_alpn_nomatch_single(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_FAIL, + NULL); +#endif +} + +static void test_alpn_nomatch_client_multi(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "foo", NULL, NULL }, + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "qux", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_FAIL, + NULL); #endif +} + +static void test_alpn_nomatch_server_multi(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "qux", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "foo", NULL, NULL }, + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_FAIL, + NULL); +#endif +} + +static void test_alpn_nomatch_multi(void) +{ +#if defined(HAVE_SSL) && defined(HAVE_TLS_ALPN) + struct tls_alpn_t client_map[] = { + { "baz", NULL, NULL }, + { "qux", NULL, NULL }, + { "", NULL, NULL }, + }; + struct tls_alpn_t server_map[] = { + { "foo", NULL, NULL }, + { "bar", NULL, NULL }, + { "", NULL, NULL }, + }; + + alpn_common(client_map, + sizeof(client_map) / sizeof(client_map[0]), + server_map, + sizeof(server_map) / sizeof(server_map[0]), + ALPN_COMMON_EXPECT_FAIL, + NULL); +#endif +} /* TODO: test UNIX socket comms too */ /* TODO: test IPv6 socket comms too */ @@ -917,6 +1198,7 @@ static void server_unaccept(struct server_state *state) tls_reset_servertls(&state->tls_conn); state->tls_conn = NULL; } + memset(state->tls_alpn_proto, 0, sizeof(state->tls_alpn_proto)); #endif } @@ -1208,6 +1490,7 @@ static void cmd_starttls(struct server_state *state) #ifdef HAVE_SSL int r; SSL *tls_conn = NULL; + char *alpn_proto = NULL; static struct saslprops_t saslprops = SASLPROPS_INITIALIZER; r = tls_init_serverengine("backend_test", /*verifydepth*/5, @@ -1226,6 +1509,7 @@ static void cmd_starttls(struct server_state *state) /*writefd*/state->out->fd, /*timeout_sec*/3000, &saslprops, + state->config.alpn_map, &tls_conn); if (r < 0) { server_printf(state, "BAD STARTTLS negotiation failed\r\n"); @@ -1247,6 +1531,15 @@ static void cmd_starttls(struct server_state *state) prot_settls(state->out, tls_conn); state->tls_conn = tls_conn; state->is_tls = 1; + + if ((alpn_proto = tls_get_alpn_protocol(tls_conn))) { + snprintf(state->tls_alpn_proto, sizeof(state->tls_alpn_proto), + "%s", alpn_proto); + free(alpn_proto); + } + else { + memset(state->tls_alpn_proto, 0, sizeof(state->tls_alpn_proto)); + } #else server_printf(state, "BAD this server is not built with SSL\r\n"); server_flush(state); diff --git a/imap/attachextract.c b/imap/attachextract.c index ea1a69dd14..7ce08fd3b8 100644 --- a/imap/attachextract.c +++ b/imap/attachextract.c @@ -129,9 +129,15 @@ static int logout(struct backend *s __attribute__((unused))) return 0; } +static const struct tls_alpn_t http_alpn_map[] = { + { "http/1.1", NULL, NULL }, + { "", NULL, NULL }, +}; static struct protocol_t http = -{ "http", "HTTP", TYPE_SPEC, { .spec = { &login, &ping, &logout } } }; +{ "http", "HTTP", http_alpn_map, TYPE_SPEC, + { .spec = { &login, &ping, &logout } } +}; static int extractor_connect(struct extractor_ctx *ext) { diff --git a/imap/backend.c b/imap/backend.c index 0bbb65ed08..6d64a158c5 100644 --- a/imap/backend.c +++ b/imap/backend.c @@ -536,6 +536,7 @@ EXPORTED int backend_starttls( struct backend *s, /* SASL and openssl have different ideas about whether ssf is signed */ layerp = (int *) &s->ext_ssf; r = tls_start_clienttls(s->in->fd, s->out->fd, layerp, &auth_id, + s->prot ? s->prot->alpn_map : NULL, &s->tlsconn, &s->tlssess); if (r == -1) return -1; diff --git a/imap/cyr_buildinfo.c b/imap/cyr_buildinfo.c index cff2c95bae..e613a0802f 100644 --- a/imap/cyr_buildinfo.c +++ b/imap/cyr_buildinfo.c @@ -179,8 +179,14 @@ static json_t *buildinfo() #endif #ifdef HAVE_SSL json_object_set_new(dependency, "openssl", json_true()); +# ifdef HAVE_TLS_ALPN + json_object_set_new(dependency, "openssl_alpn", json_true()); +# else + json_object_set_new(dependency, "openssl_alpn", json_false()); +# endif #else json_object_set_new(dependency, "openssl", json_false()); + json_object_set_new(dependency, "openssl_alpn", json_false()); #endif #ifdef HAVE_ZLIB json_object_set_new(dependency, "zlib", json_true()); diff --git a/imap/deliver.c b/imap/deliver.c index d3b32cec50..065b1a7e06 100644 --- a/imap/deliver.c +++ b/imap/deliver.c @@ -83,7 +83,7 @@ static struct protstream *deliver_out, *deliver_in; static const char *sockaddr; static struct protocol_t lmtp_protocol = -{ "lmtp", "lmtp", TYPE_STD, +{ "lmtp", "lmtp", NULL, TYPE_STD, { { { 0, "220 " }, { "LHLO", "deliver", "250 ", NULL, CAPAF_ONE_PER_LINE|CAPAF_SKIP_FIRST_WORD|CAPAF_DASH_STUFFING, diff --git a/imap/http_proxy.c b/imap/http_proxy.c index 41e54b6737..c06be46113 100644 --- a/imap/http_proxy.c +++ b/imap/http_proxy.c @@ -82,8 +82,13 @@ static int ping(struct backend *s, const char *userid); static int logout(struct backend *s __attribute__((unused))); +static const struct tls_alpn_t http_alpn_map[] = { + { "http/1.1", NULL, NULL }, + { "", NULL, NULL }, +}; + HIDDEN struct protocol_t http_protocol = -{ "http", "HTTP", TYPE_SPEC, +{ "http", "HTTP", http_alpn_map, TYPE_SPEC, { .spec = { &login, &ping, &logout } } }; diff --git a/imap/httpd.c b/imap/httpd.c index 1f7fe74780..8483711e2e 100644 --- a/imap/httpd.c +++ b/imap/httpd.c @@ -1045,15 +1045,23 @@ int service_main(int argc __attribute__((unused)), /* we were connected on https port so we should do TLS negotiation immediately */ + int do_h2 = 0; if (https == 1) { starttls(&http_conn, 180 /* timeout */); + + /* Check negotiated protocol */ + char *alpn = tls_get_alpn_protocol(http_conn.tls_ctx); + do_h2 = !strcmpsafe(alpn, "h2"); + free(alpn); } - else if (http2_preface(&http_conn)) { + else { /* HTTP/2 client connection preface */ - if (http2_start_session(NULL, &http_conn) != 0) - fatal("Failed initializing HTTP/2 session", EX_TEMPFAIL); + do_h2 = http2_preface(&http_conn); } + if (do_h2 && http2_start_session(NULL, &http_conn) != 0) + fatal("Failed initializing HTTP/2 session", EX_TEMPFAIL); + /* Setup the signal handler for keepalive heartbeat */ httpd_keepalive = config_getduration(IMAPOPT_HTTPKEEPALIVE, 's'); if (httpd_keepalive < 0) httpd_keepalive = 0; @@ -1246,16 +1254,16 @@ EXPORTED void fatal(const char* s, int code) #ifdef HAVE_SSL -static unsigned h2_is_available(void *http_conn) +static unsigned h2_is_available(void *rock __attribute__((unused))) { - return (http2_enabled && http2_start_session(NULL, http_conn) == 0); + return http2_enabled; } static const struct tls_alpn_t http_alpn_map[] = { - { "h2", &h2_is_available, &http_conn }, + { "h2", &h2_is_available, NULL }, { "http/1.1", NULL, NULL }, { "http/1.0", NULL, NULL }, - { NULL, NULL, NULL } + { "", NULL, NULL }, }; static void _reset_tls(struct http_connection *conn) @@ -1294,11 +1302,6 @@ static int tls_init(int client_auth, struct buf *serverinfo) return HTTP_SERVER_ERROR; } -#ifdef HAVE_TLS_ALPN - /* enable TLS ALPN extension */ - SSL_CTX_set_alpn_select_cb(ctx, tls_alpn_select, (void *) http_alpn_map); -#endif - httpd_tls_enabled = 1; return 0; @@ -1307,7 +1310,9 @@ static int tls_init(int client_auth, struct buf *serverinfo) static void starttls(struct http_connection *conn, int timeout) { int result = tls_start_servertls(conn->pin->fd, conn->pout->fd, - timeout, &saslprops, (SSL **) &conn->tls_ctx); + timeout, &saslprops, + http_alpn_map, + (SSL **) &conn->tls_ctx); /* if error */ if (result == -1) { diff --git a/imap/imap_proxy.c b/imap/imap_proxy.c index 461807580b..68af669b40 100644 --- a/imap/imap_proxy.c +++ b/imap/imap_proxy.c @@ -86,8 +86,13 @@ static void imap_postcapability(struct backend *s) } } +static const struct tls_alpn_t imap_alpn_map[] = { + { "imap", NULL, NULL }, + { "", NULL, NULL }, +}; + struct protocol_t imap_protocol = -{ "imap", "imap", TYPE_STD, +{ "imap", "imap", imap_alpn_map, TYPE_STD, { { { 1, NULL }, { "C01 CAPABILITY", NULL, "C01 ", imap_postcapability, CAPAF_MANY_PER_LINE, diff --git a/imap/imapd.c b/imap/imapd.c index 58c255ca32..9ab135e838 100644 --- a/imap/imapd.c +++ b/imap/imapd.c @@ -9277,7 +9277,7 @@ void cmd_setquota(const char *tag, const char *quotaroot) #ifdef HAVE_SSL static const struct tls_alpn_t imap_alpn_map[] = { { "imap", NULL, NULL }, - { NULL, NULL, NULL } + { "", NULL, NULL } }; /* @@ -9318,11 +9318,6 @@ static void cmd_starttls(char *tag, int imaps) return; } -#ifdef HAVE_TLS_ALPN - /* enable TLS ALPN extension */ - SSL_CTX_set_alpn_select_cb(ctx, tls_alpn_select, (void *) imap_alpn_map); -#endif - if (imaps == 0) { prot_printf(imapd_out, "%s OK Begin TLS negotiation now\r\n", tag); @@ -9337,11 +9332,12 @@ static void cmd_starttls(char *tag, int imaps) localip = buf_release(&saslprops.iplocalport); remoteip = buf_release(&saslprops.ipremoteport); - result=tls_start_servertls(0, /* read */ - 1, /* write */ - imaps ? 180 : imapd_timeout, - &saslprops, - &tls_conn); + result = tls_start_servertls(0, /* read */ + 1, /* write */ + imaps ? 180 : imapd_timeout, + &saslprops, + imap_alpn_map, + &tls_conn); /* put the iplocalport and ipremoteport back */ if (localip) buf_initm(&saslprops.iplocalport, localip, strlen(localip)); diff --git a/imap/lmtpd.c b/imap/lmtpd.c index 7220841a39..a2d601147a 100644 --- a/imap/lmtpd.c +++ b/imap/lmtpd.c @@ -167,7 +167,7 @@ int deliver_logfd = -1; /* used in lmtpengine.c */ static ptrarray_t backend_cached = PTRARRAY_INITIALIZER; static struct protocol_t lmtp_protocol = -{ "lmtp", "lmtp", TYPE_STD, +{ "lmtp", "lmtp", NULL, TYPE_STD, { { { 0, "220 " }, { "LHLO", "lmtpproxyd", "250 ", NULL, CAPAF_ONE_PER_LINE|CAPAF_SKIP_FIRST_WORD|CAPAF_DASH_STUFFING, diff --git a/imap/lmtpengine.c b/imap/lmtpengine.c index 3cafa86f47..7c61226fb0 100644 --- a/imap/lmtpengine.c +++ b/imap/lmtpengine.c @@ -1459,6 +1459,7 @@ void lmtpmode(struct lmtp_func *func, 1, /* write */ 360, /* 6 minutes */ &saslprops, + NULL, /* no ALPN id for lmtp */ &(cd.tls_conn)); /* if error */ diff --git a/imap/mupdate-client.c b/imap/mupdate-client.c index ced0db2fe6..0446feccb4 100644 --- a/imap/mupdate-client.c +++ b/imap/mupdate-client.c @@ -73,7 +73,7 @@ #include "xstrlcat.h" static struct protocol_t mupdate_protocol = -{ "mupdate", "mupdate", TYPE_STD, +{ "mupdate", "mupdate", NULL, TYPE_STD, { { { 1, "* OK" }, { NULL, NULL, "* OK", NULL, CAPAF_ONE_PER_LINE|CAPAF_SKIP_FIRST_WORD, diff --git a/imap/mupdate.c b/imap/mupdate.c index 725a2923b3..2eeb0547e1 100644 --- a/imap/mupdate.c +++ b/imap/mupdate.c @@ -1993,6 +1993,7 @@ static void cmd_starttls(struct conn *C, const char *tag) C->pout->fd, /* write */ 180, /* 3 minutes */ &C->saslprops, + NULL, /* no ALPN id for mupdate */ &C->tlsconn); /* if error */ diff --git a/imap/nntpd.c b/imap/nntpd.c index 05ce9989db..15427cfa27 100644 --- a/imap/nntpd.c +++ b/imap/nntpd.c @@ -261,8 +261,13 @@ static char *nntp_parsesuccess(char *str, const char **status) return success; } +static const struct tls_alpn_t nntp_alpn_map[] = { + { "nntp", NULL, NULL }, + { "", NULL, NULL } +}; + static struct protocol_t nntp_protocol = -{ "nntp", "nntp", TYPE_STD, +{ "nntp", "nntp", nntp_alpn_map, TYPE_STD, { { { 0, "20" }, { "CAPABILITIES", NULL, ".", NULL, CAPAF_ONE_PER_LINE, @@ -3999,11 +4004,6 @@ static void cmd_post(char *msgid, int mode) } #ifdef HAVE_SSL -static const struct tls_alpn_t nntp_alpn_map[] = { - { "nntp", NULL, NULL }, - { NULL, NULL, NULL } -}; - static void cmd_starttls(int nntps) { int result; @@ -4037,11 +4037,6 @@ static void cmd_starttls(int nntps) return; } -#ifdef HAVE_TLS_ALPN - /* enable TLS ALPN extension */ - SSL_CTX_set_alpn_select_cb(ctx, tls_alpn_select, (void *) nntp_alpn_map); -#endif - if (nntps == 0) { prot_printf(nntp_out, "382 %s\r\n", "Begin TLS negotiation now"); @@ -4053,6 +4048,7 @@ static void cmd_starttls(int nntps) 1, /* write */ nntps ? 180 : nntp_timeout, &saslprops, + nntp_alpn_map, &tls_conn); /* if error */ diff --git a/imap/pop3d.c b/imap/pop3d.c index 6020154df3..ae0d1f02c0 100644 --- a/imap/pop3d.c +++ b/imap/pop3d.c @@ -164,8 +164,13 @@ static struct namespace popd_namespace; /* PROXY stuff */ static struct backend *backend = NULL; +static const struct tls_alpn_t pop3_alpn_map[] = { + { "pop3", NULL, NULL }, + { "", NULL, NULL } +}; + static struct protocol_t pop3_protocol = -{ "pop3", "pop", TYPE_STD, +{ "pop3", "pop", pop3_alpn_map, TYPE_STD, { { { 0, "+OK " }, { "CAPA", NULL, ".", NULL, CAPAF_ONE_PER_LINE, @@ -1150,11 +1155,6 @@ void uidl_msg(uint32_t msgno) } #ifdef HAVE_SSL -static const struct tls_alpn_t pop3_alpn_map[] = { - { "pop3", NULL, NULL }, - { NULL, NULL, NULL } -}; - static void cmd_starttls(int pop3s) { int result; @@ -1185,11 +1185,6 @@ static void cmd_starttls(int pop3s) return; } -#ifdef HAVE_TLS_ALPN - /* enable TLS ALPN extension */ - SSL_CTX_set_alpn_select_cb(ctx, tls_alpn_select, (void *) pop3_alpn_map); -#endif - if (pop3s == 0) { prot_printf(popd_out, "+OK %s\r\n", "Begin TLS negotiation now"); @@ -1208,6 +1203,7 @@ static void cmd_starttls(int pop3s) 1, /* write */ pop3s ? 180 : popd_timeout, &saslprops, + pop3_alpn_map, &tls_conn); /* put the iplocalport and ipremoteport back */ diff --git a/imap/protocol.h b/imap/protocol.h index fafd4b633d..1a7b4d1243 100644 --- a/imap/protocol.h +++ b/imap/protocol.h @@ -44,6 +44,7 @@ #define _INCLUDED_PROTOCOL_H #include "saslclient.h" +#include "tls.h" enum { /* protocol types */ @@ -122,8 +123,9 @@ struct stdprot_t { }; struct protocol_t { - const char *service; /* INET service name */ - const char *sasl_service; /* SASL service name */ + const char *service; /* INET service name */ + const char *sasl_service; /* SASL service name */ + const struct tls_alpn_t *alpn_map; /* ALPN protocols */ unsigned type; union { struct stdprot_t std; @@ -138,6 +140,4 @@ struct protocol_t { } u; }; - - #endif /* _INCLUDED_PROTOCOL_H */ diff --git a/imap/smtpclient.c b/imap/smtpclient.c index 6cd3ed0e4d..ed29766521 100644 --- a/imap/smtpclient.c +++ b/imap/smtpclient.c @@ -105,7 +105,7 @@ enum { static const char *smtpclient_ehlo_hostname = NULL; static struct protocol_t smtp_protocol = -{ "smtp", "smtp", TYPE_STD, +{ "smtp", "smtp", NULL, TYPE_STD, { { { 0, "220 " }, // EHLO hostname will be set in smtpclient_open(). { "EHLO", NULL, "250 ", NULL, diff --git a/imap/sync_server.c b/imap/sync_server.c index d19b70b1ca..7951917f52 100644 --- a/imap/sync_server.c +++ b/imap/sync_server.c @@ -853,6 +853,7 @@ static void cmd_starttls(void) 1, /* write */ 180, /* 3 minutes */ &saslprops, + NULL, /* no ALPN id for csync */ &tls_conn); /* if error */ diff --git a/imap/sync_support.c b/imap/sync_support.c index 75bd47ee1f..7d06ea69a9 100644 --- a/imap/sync_support.c +++ b/imap/sync_support.c @@ -107,8 +107,13 @@ static struct sync_client_state rightnow_sync_cs; static char *imap_sasl_parsesuccess(char *str, const char **status); static void imap_postcapability(struct backend *s); +static const struct tls_alpn_t imap_alpn_map[] = { + { "imap", NULL, NULL }, + { "", NULL, NULL }, +}; + struct protocol_t imap_csync_protocol = -{ "imap", "imap", TYPE_STD, +{ "imap", "imap", imap_alpn_map, TYPE_STD, { { { 1, NULL }, { "C01 CAPABILITY", NULL, "C01 ", imap_postcapability, CAPAF_MANY_PER_LINE, @@ -136,7 +141,7 @@ struct protocol_t imap_csync_protocol = }; struct protocol_t csync_protocol = -{ "csync", "csync", TYPE_STD, +{ "csync", "csync", NULL, TYPE_STD, { { { 1, "* OK" }, { NULL, NULL, "* OK", NULL, CAPAF_ONE_PER_LINE|CAPAF_SKIP_FIRST_WORD, diff --git a/imap/tls.c b/imap/tls.c index a2d3a0d1a7..f831427af8 100644 --- a/imap/tls.c +++ b/imap/tls.c @@ -119,6 +119,7 @@ /* Application-specific. */ #include "assert.h" +#include "hash.h" #include "nonblock.h" #include "util.h" #include "xmalloc.h" @@ -716,6 +717,78 @@ static int tls_rand_init(void) #endif } +static char *alpn_to_string(const struct tls_alpn_t *alpn_map) +{ + const struct tls_alpn_t *alpn; + struct buf buf = BUF_INITIALIZER; + const char *sep = ""; + + for (alpn = alpn_map; alpn && alpn->id[0]; alpn++) { + if (!alpn->check_availability || alpn->check_availability(alpn->rock)) { + buf_printf(&buf, "%s%s", sep, alpn->id); + sep = ", "; + } + } + + return buf_releasenull(&buf); +} + +/* Select an application protocol from the client list in order of preference */ +static int alpn_select_cb(SSL *ssl __attribute__((unused)), + const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + void *server_list) +{ + hash_table client_protos = HASH_TABLE_INITIALIZER; + struct tls_alpn_t *alpn; + int r = SSL_TLSEXT_ERR_ALERT_FATAL; + + /* keys are protocols as cstrings, values are where we saw it */ + construct_hash_table(&client_protos, 10, 1); + + for (; inlen; inlen -= (in[0] + 1), in += in[0] + 1) { + char proto[MAX_TLS_ALPN_ID + 1] = ""; + unsigned len = in[0]; + + if (len <= MAX_TLS_ALPN_ID) { + memcpy(proto, &in[1], len); + hash_insert(proto, (void *) &in[0], &client_protos); + } + } + + for (alpn = (struct tls_alpn_t *) server_list; alpn->id[0]; alpn++) { + const unsigned char *found; + + if ((found = hash_lookup(alpn->id, &client_protos)) + && (!alpn->check_availability + || alpn->check_availability(alpn->rock))) + { + *out = &found[1]; + *outlen = found[0]; + r = SSL_TLSEXT_ERR_OK; + goto done; + } + } + +done: + if (r != SSL_TLSEXT_ERR_OK) { + strarray_t *client_wanted = hash_keys(&client_protos); + char *print_client = strarray_join(client_wanted, ", "); + char *print_server = alpn_to_string(server_list); + + xsyslog(LOG_NOTICE, "ALPN failed", + "client_protos=<%s> server_protos=<%s>", + print_client, print_server); + + free(print_client); + free(print_server); + strarray_free(client_wanted); + } + + free_hash_table(&client_protos, NULL); + return r; +} + /* * This is the setup routine for the SSL server. As smtpd might be called * more than once, we only want to do the initialization one time. @@ -1128,7 +1201,9 @@ static long bio_dump_cb(BIO * bio, int cmd, const char *argp, * on success. */ EXPORTED int tls_start_servertls(int readfd, int writefd, int timeout, - struct saslprops_t *saslprops, SSL **ret) + struct saslprops_t *saslprops, + const struct tls_alpn_t *alpn_map, + SSL **ret) { int sts; unsigned int n; @@ -1151,6 +1226,13 @@ EXPORTED int tls_start_servertls(int readfd, int writefd, int timeout, saslprops_reset(saslprops); +#ifdef HAVE_TLS_ALPN + if (alpn_map && alpn_map->id[0]) + SSL_CTX_set_alpn_select_cb(s_ctx, alpn_select_cb, (void *) alpn_map); + else + SSL_CTX_set_alpn_select_cb(s_ctx, NULL, NULL); +#endif + tls_conn = (SSL *) SSL_new(s_ctx); if (tls_conn == NULL) { *ret = NULL; @@ -1367,6 +1449,25 @@ EXPORTED int tls_start_servertls(int readfd, int writefd, int timeout, return r; } +/* query which (if any) ALPN protocol was chosen + * caller must free the returned string + */ +EXPORTED char *tls_get_alpn_protocol(const SSL *conn) +{ + char *proto = NULL; + +#ifdef HAVE_TLS_ALPN + const unsigned char *data = NULL; + unsigned int len = 0; + + SSL_get0_alpn_selected(conn, &data, &len); + if (data && len) + proto = xstrndup((const char *) data, len); +#endif + + return proto; +} + EXPORTED int tls_reset_servertls(SSL **conn) { int r = 0; @@ -1530,6 +1631,30 @@ EXPORTED int tls_get_info(SSL *conn, char *buf, size_t len) return (strlen(buf)); } +/* takes a cyrus alpn map, mallocs and constructs an openssl one in + * protos, which caller must free when done + */ +static void alpn_get_protos(const struct tls_alpn_t *alpn_map, + unsigned char **pprotos, + unsigned int *pprotos_len) +{ + struct buf protos = BUF_INITIALIZER; + const struct tls_alpn_t *alpn; + + for (alpn = alpn_map; alpn && alpn->id[0]; alpn++) { + if (!alpn->check_availability || alpn->check_availability(alpn->rock)) { + size_t len = strlen(alpn->id); + assert(len > 0 && len <= MAX_TLS_ALPN_ID); + buf_putc(&protos, len); + buf_appendcstr(&protos, alpn->id); + } + } + + assert(buf_len(&protos) <= UINT_MAX); + *pprotos_len = buf_len(&protos); + *pprotos = (unsigned char *) buf_releasenull(&protos); +} + // I am the client HIDDEN int tls_init_clientengine(int verifydepth, const char *var_server_cert, @@ -1619,8 +1744,9 @@ HIDDEN int tls_init_clientengine(int verifydepth, } HIDDEN int tls_start_clienttls(int readfd, int writefd, - int *layerbits, char **authid, SSL **ret, - SSL_SESSION **sess) + int *layerbits, char **authid, + const struct tls_alpn_t *alpn_map, + SSL **ret, SSL_SESSION **sess) { const SSL_CIPHER *cipher; X509 *peer; @@ -1638,6 +1764,24 @@ HIDDEN int tls_start_clienttls(int readfd, int writefd, if (authid) *authid = NULL; +#ifdef HAVE_TLS_ALPN + if (alpn_map && alpn_map->id[0]) { + unsigned char *protos = NULL; + unsigned int protos_len; + + alpn_get_protos(alpn_map, &protos, &protos_len); + + if (SSL_CTX_set_alpn_protos(c_ctx, protos, protos_len)) { + syslog(LOG_ERR, "TLS client engine: failed to set ALPN protos"); + } + + free(protos); + } + else { + SSL_CTX_set_alpn_protos(c_ctx, NULL, 0); + } +#endif + tls_conn = (SSL *) SSL_new(c_ctx); if (tls_conn == NULL) { *ret = NULL; @@ -1751,41 +1895,6 @@ HIDDEN int tls_start_clienttls(int readfd, int writefd, return r; } -/* Select an application protocol from the client list in order of preference */ -EXPORTED int tls_alpn_select(SSL *ssl __attribute__((unused)), - const unsigned char **out, unsigned char *outlen, - const unsigned char *in, unsigned int inlen, - void *server_list) -{ - strarray_t ids = STRARRAY_INITIALIZER; - - for (; inlen; inlen -= (in[0] + 1), in += in[0] + 1) { - struct tls_alpn_t *alpn; - - for (alpn = (struct tls_alpn_t *) server_list; alpn->id; alpn++) { - if ((in[0] == strlen(alpn->id)) && - memcmp(alpn->id, in + 1, in[0]) == 0 && - (!alpn->check_availabilty || alpn->check_availabilty(alpn->rock))) { - - strarray_fini(&ids); - - *out = in + 1; - *outlen = in[0]; - return SSL_TLSEXT_ERR_OK; - } - } - - strarray_appendm(&ids, xstrndup((const char *) in + 1, in[0])); - } - - char *proto = strarray_join(&ids, ", "); - xsyslog(LOG_NOTICE, "ALPN failed", "proto=<%s>", proto); - free(proto); - strarray_fini(&ids); - - return SSL_TLSEXT_ERR_ALERT_FATAL; -} - #else EXPORTED int tls_enabled(void) diff --git a/imap/tls.h b/imap/tls.h index 801ea93a49..975c1b50c1 100644 --- a/imap/tls.h +++ b/imap/tls.h @@ -53,18 +53,19 @@ int tls_enabled(void); /* name of the SSL/TLS sessions database */ #define FNAME_TLSSESSIONS "/tls_sessions.db" +#define MAX_TLS_ALPN_ID (15) +struct tls_alpn_t { + char id[MAX_TLS_ALPN_ID + 1]; + unsigned (*check_availability)(void *rock); + void *rock; +}; + #ifdef HAVE_SSL #include #include "global.h" /* for saslprops_t */ -struct tls_alpn_t { - const char *id; - unsigned (*check_availabilty)(void *rock); - void *rock; -}; - /* init tls */ int tls_init_serverengine(const char *ident, int verifydepth, /* depth to verify */ @@ -77,11 +78,19 @@ int tls_init_clientengine(int verifydepth, /* start tls negotiation */ int tls_start_servertls(int readfd, int writefd, int timeout, - struct saslprops_t *saslprops, SSL **ret); + struct saslprops_t *saslprops, + const struct tls_alpn_t *alpn_map, + SSL **ret); int tls_start_clienttls(int readfd, int writefd, - int *layerbits, char **authid, SSL **ret, - SSL_SESSION **sess); + int *layerbits, char **authid, + const struct tls_alpn_t *alpn_map, + SSL **ret, SSL_SESSION **sess); + +/* query which (if any) ALPN protocol was chosen + * caller must free the returned string + */ +char *tls_get_alpn_protocol(const SSL *conn); /* reset tls */ int tls_reset_servertls(SSL **conn); @@ -95,12 +104,6 @@ int tls_prune_sessions(void); /* fill string buffer with info about tls connection */ int tls_get_info(SSL *conn, char *buf, size_t len); -/* Select an application protocol from the client list in order of preference */ -int tls_alpn_select(SSL *ssl, - const unsigned char **out, unsigned char *outlen, - const unsigned char *in, unsigned int inlen, - void *server_list); - #endif /* HAVE_SSL */ #endif /* INCLUDED_TLS_H */ diff --git a/ptclient/http.c b/ptclient/http.c index 1b915c83d8..8b5d757ef0 100644 --- a/ptclient/http.c +++ b/ptclient/http.c @@ -123,8 +123,15 @@ static int logout(struct backend *be __attribute__((unused))) return 0; } +static const struct tls_alpn_t http_alpn_map[] = { + { "http/1.1", NULL, NULL }, + { "", NULL, NULL }, +}; + static struct protocol_t protocol = -{ "http", "HTTP", TYPE_SPEC, { .spec = { &login, &ping, &logout } } }; +{ "http", "HTTP", http_alpn_map, TYPE_SPEC, + { .spec = { &login, &ping, &logout } } +}; /* API */ diff --git a/timsieved/parser.c b/timsieved/parser.c index 4c7b6b260f..6beb7d3365 100644 --- a/timsieved/parser.c +++ b/timsieved/parser.c @@ -123,8 +123,13 @@ static char *sieve_parsesuccess(char *str, const char **status) return success; } +static const struct tls_alpn_t sieve_alpn_map[] = { + { "managesieve", NULL, NULL }, + { "", NULL, NULL }, +}; + static struct protocol_t sieve_protocol = -{ "sieve", SIEVE_SERVICE_NAME, TYPE_STD, +{ "sieve", SIEVE_SERVICE_NAME, sieve_alpn_map, TYPE_STD, { { { 1, "OK" }, { "CAPABILITY", NULL, "OK", NULL, CAPAF_ONE_PER_LINE|CAPAF_QUOTE_WORDS, @@ -916,11 +921,6 @@ static int cmd_authenticate(struct protstream *sieved_out, } #ifdef HAVE_SSL -static const struct tls_alpn_t sieve_alpn_map[] = { - { "managesieve", NULL, NULL }, - { NULL, NULL, NULL } -}; - static int cmd_starttls(struct protstream *sieved_out, struct protstream *sieved_in, struct saslprops_t *saslprops) @@ -948,11 +948,6 @@ static int cmd_starttls(struct protstream *sieved_out, return TIMSIEVE_FAIL; } -#ifdef HAVE_TLS_ALPN - /* enable TLS ALPN extension */ - SSL_CTX_set_alpn_select_cb(ctx, tls_alpn_select, (void *) sieve_alpn_map); -#endif - prot_printf(sieved_out, "OK \"Begin TLS negotiation now\"\r\n"); /* must flush our buffers before starting tls */ prot_flush(sieved_out); @@ -961,6 +956,7 @@ static int cmd_starttls(struct protstream *sieved_out, 1, /* write */ sieved_timeout, saslprops, + sieve_alpn_map, &tls_conn); /* if error */