From: Avi Kivity <
a...@scylladb.com>
Committer: Avi Kivity <
a...@scylladb.com>
Branch: master
Merge 'TLS: support for extracting certificate subject alt names from client certs' from Calle Wilund
Fixes #1628
Subject alt name info can contain more extensive and detailed info on a connecting client. Add interface to allow querying this from a connected socket.
Initially we don't include this in accept-sequence auth verification, though we maybe should include it as an option.
Closes #1629
* github.com:scylladb/seastar:
tls: Add alt name ostream operators
tls_test: Add test for alt names
tls: Add query interface for client certificate subject alt name info
test cmake: Generate test certs with subject alt names included
---
diff --git a/include/seastar/net/tls.hh b/include/seastar/net/tls.hh
--- a/include/seastar/net/tls.hh
+++ b/include/seastar/net/tls.hh
@@ -31,6 +31,7 @@
#include <seastar/core/sstring.hh>
#include <seastar/core/shared_ptr.hh>
#include <seastar/net/socket_defs.hh>
+#include <seastar/net/inet_address.hh>
#include <seastar/util/std-compat.hh>
#include <seastar/net/api.hh>
@@ -354,6 +355,57 @@ namespace tls {
* system_error exception will be thrown.
*/
future<std::optional<session_dn>> get_dn_information(connected_socket& socket);
+
+ /**
+ * Subject alt name types.
+ */
+ enum class subject_alt_name_type {
+ dnsname = 1, // string value representing a 'DNS' entry
+ rfc822name, // string value representing an 'email' entry
+ uri, // string value representing an 'uri' entry
+ ipaddress, // inet_address value representing an 'IP' entry
+ othername, // string value
+ dn, // string value
+ };
+
+ // Subject alt name entry
+ struct subject_alt_name {
+ using value_type = std::variant<
+ sstring,
+ net::inet_address
+ >;
+ subject_alt_name_type type;
+ value_type value;
+ };
+
+ /**
+ * Returns the alt name entries of matching types, or all entries if 'types' is empty
+ * The values are extracted from the client authentication certificate, if available.
+ * If no certificate authentication is used in the connection, en empty list is returned.
+ *
+ * If the socket is not connected a system_error exception will be thrown.
+ * If the socket is not a TLS socket an exception will be thrown.
+ */
+ future<std::vector<subject_alt_name>> get_alt_name_information(connected_socket& socket, std::unordered_set<subject_alt_name_type> types = {});
+
+ std::ostream& operator<<(std::ostream&, const subject_alt_name::value_type&);
+ std::ostream& operator<<(std::ostream&, const subject_alt_name&);
+
+ /**
+ * Alt name to string.
+ * Note: because naming of alternative names is inconsistent between tools,
+ * and because openssl is probably more popular when creating certs anyway,
+ * this routine will be inconsistent with both gnutls and openssl (though more
+ * in line with the latter) and name the constants as follows:
+ *
+ * dnsname: "DNS"
+ * rfc822name: "EMAIL"
+ * uri: "URI"
+ * ipaddress "IP"
+ * othername: "OTHERNAME"
+ * dn: "DIRNAME"
+ */
+ std::ostream& operator<<(std::ostream&, subject_alt_name_type);
}
}
diff --git a/src/net/tls.cc b/src/net/tls.cc
--- a/src/net/tls.cc
+++ b/src/net/tls.cc
@@ -1533,25 +1533,125 @@ class session : public enable_lw_shared_from_this<session> {
result_t dn = extract_dn_information();
return make_ready_future<result_t>(std::move(dn));
}
+ future<std::vector<subject_alt_name>> get_alt_name_information(std::unordered_set<subject_alt_name_type> types) {
+ using result_t = std::vector<subject_alt_name>;
+
+ if (_error) {
+ return make_exception_future<result_t>(_error);
+ }
+ if (_shutdown) {
+ return make_exception_future<result_t>(std::system_error(ENOTCONN, std::system_category()));
+ }
+ if (!_connected) {
+ return handshake().then([this, types = std::move(types)]() mutable {
+ return get_alt_name_information(std::move(types));
+ });
+ }
+
+ auto peer = get_peer_certificate();
+ if (!peer) {
+ return make_ready_future<result_t>();
+ }
+
+ return futurize_invoke([&] {
+ result_t res;
+ for (auto i = 0u; ; i++) {
+ size_t size = 0;
+
+ auto err = gnutls_x509_crt_get_subject_alt_name(peer.get(), i, nullptr, &size, nullptr);
+
+ if (err == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
+ break;
+ }
+ if (err != GNUTLS_E_SHORT_MEMORY_BUFFER) {
+ gtls_chk(err); // will throw
+ }
+ sstring buf;
+ buf.resize(size);
+
+ err = gnutls_x509_crt_get_subject_alt_name(peer.get(), i, buf.data(), &size, nullptr);
+ if (err < 0) {
+ gtls_chk(err); // will throw
+ }
+
+ static_assert(int(subject_alt_name_type::dnsname) == GNUTLS_SAN_DNSNAME);
+ static_assert(int(subject_alt_name_type::rfc822name) == GNUTLS_SAN_RFC822NAME);
+ static_assert(int(subject_alt_name_type::uri) == GNUTLS_SAN_URI);
+ static_assert(int(subject_alt_name_type::ipaddress) == GNUTLS_SAN_IPADDRESS);
+ static_assert(int(subject_alt_name_type::othername) == GNUTLS_SAN_OTHERNAME);
+ static_assert(int(subject_alt_name_type::dn) == GNUTLS_SAN_DN);
+
+ subject_alt_name v;
+
+ v.type = subject_alt_name_type(err);
+
+ if (!types.empty() && !types.count(v.type)) {
+ continue;
+ }
+
+ switch (v.type) {
+ case subject_alt_name_type::ipaddress:
+ {
+ union {
+ char c;
+ ::in_addr in;
+ ::in6_addr in6;
+ } tmp;
+
+ memcpy(&tmp.c, buf.data(), size);
+ if (size == sizeof(::in_addr)) {
+ v.value = net::inet_address(
tmp.in);
+ } else if (size == sizeof(::in6_addr)) {
+ v.value = net::inet_address(tmp.in6);
+ } else {
+ throw std::runtime_error(fmt::format("Unexpected size {} for ipaddress alt name value", size));
+ }
+ break;
+ }
+ default:
+ // data we get back is null-terminated.
+ while (buf.back() == 0) {
+ buf.resize(buf.size() - 1);
+ }
+ v.value = std::move(buf);
+ break;
+ }
+
+ res.emplace_back(std::move(v));
+ }
+ return res;
+ });
+ }
struct session_ref;
private:
- std::optional<session_dn> extract_dn_information() const {
- unsigned int list_size;
+ using x509_ctr_ptr = std::unique_ptr<gnutls_x509_crt_int, void (*)(gnutls_x509_crt_t)>;
+
+ x509_ctr_ptr get_peer_certificate() const {
+ unsigned int list_size = 0;
const gnutls_datum_t* client_cert_list = gnutls_certificate_get_peers(*this, &list_size);
- if (list_size == 0) {
+ if (client_cert_list && list_size > 0) {
+ gnutls_x509_crt_t peer_leaf_cert = nullptr;
+ gtls_chk(gnutls_x509_crt_init(&peer_leaf_cert));
+
+ x509_ctr_ptr res(peer_leaf_cert, &gnutls_x509_crt_deinit);
+ gtls_chk(gnutls_x509_crt_import(peer_leaf_cert, &(client_cert_list[0]), GNUTLS_X509_FMT_DER));
+ return res;
+ }
+ return x509_ctr_ptr(nullptr, &gnutls_x509_crt_deinit);
+ }
+
+ std::optional<session_dn> extract_dn_information() const {
+ auto peer_leaf_cert = get_peer_certificate();
+ if (!peer_leaf_cert) {
return std::nullopt;
}
- gnutls_x509_crt_t peer_leaf_cert;
- gtls_chk(gnutls_x509_crt_init(&peer_leaf_cert));
- gtls_chk(gnutls_x509_crt_import(peer_leaf_cert, &(client_cert_list[0]), GNUTLS_X509_FMT_DER));
- auto [ec, subject] = get_gtls_string(gnutls_x509_crt_get_dn, peer_leaf_cert);
- auto [ec2, issuer] = get_gtls_string(gnutls_x509_crt_get_issuer_dn, peer_leaf_cert);
+ auto [ec, subject] = get_gtls_string(gnutls_x509_crt_get_dn, peer_leaf_cert.get());
+ auto [ec2, issuer] = get_gtls_string(gnutls_x509_crt_get_issuer_dn, peer_leaf_cert.get());
if (ec || ec2) {
throw std::runtime_error("error while extracting certificate DN strings");
}
- gnutls_x509_crt_deinit(peer_leaf_cert);
return session_dn{.subject=subject, .issuer=issuer};
}
@@ -1648,6 +1748,9 @@ class tls_connected_socket_impl : public net::connected_socket_impl, public sess
future<std::optional<session_dn>> get_distinguished_name() {
return _session->get_distinguished_name();
}
+ future<std::vector<subject_alt_name>> get_alt_name_information(std::unordered_set<subject_alt_name_type> types) {
+ return _session->get_alt_name_information(std::move(types));
+ }
future<> wait_input_shutdown() override {
return _session->socket().wait_input_shutdown();
}
@@ -1787,18 +1890,49 @@ server_socket tls::listen(shared_ptr<server_credentials> creds, server_socket ss
return server_socket(std::move(ssls));
}
-future<std::optional<session_dn>> tls::get_dn_information(connected_socket& socket) {
+static tls::tls_connected_socket_impl* get_tls_socket(connected_socket& socket) {
auto impl = net::get_impl::maybe_get_ptr(socket);
if (impl == nullptr) {
// the socket is not yet created or moved from
throw std::system_error(ENOTCONN, std::system_category());
}
- auto tls_impl = dynamic_cast<tls_connected_socket_impl*>(impl);
+ auto tls_impl = dynamic_cast<tls::tls_connected_socket_impl*>(impl);
if (!tls_impl) {
// bad cast here means that we're dealing with wrong socket type
throw std::invalid_argument("Not a TLS socket");
}
- return tls_impl->get_distinguished_name();
+ return tls_impl;
+}
+
+future<std::optional<session_dn>> tls::get_dn_information(connected_socket& socket) {
+ return get_tls_socket(socket)->get_distinguished_name();
}
+future<std::vector<tls::subject_alt_name>> tls::get_alt_name_information(connected_socket& socket, std::unordered_set<subject_alt_name_type> types) {
+ return get_tls_socket(socket)->get_alt_name_information(std::move(types));
+}
+
+std::ostream& tls::operator<<(std::ostream& os, subject_alt_name_type type) {
+ switch (type) {
+ case subject_alt_name_type::dnsname: os << "DNS"; break;
+ case subject_alt_name_type::rfc822name: os << "EMAIL"; break;
+ case subject_alt_name_type::uri: os << "URI"; break;
+ case subject_alt_name_type::ipaddress: os << "IP"; break;
+ case subject_alt_name_type::othername: os << "OTHERNAME"; break;
+ case subject_alt_name_type::dn: os << "DIRNAME"; break;
+ default: break;
+ }
+ return os;
+}
+
+std::ostream& tls::operator<<(std::ostream& os, const subject_alt_name::value_type& v) {
+ std::visit([&](auto& vv) { os << vv; }, v);
+ return os;
+}
+
+std::ostream& tls::operator<<(std::ostream& os, const subject_alt_name& a) {
+ return os << a.type << "=" << a.value;
+}
+
+
}
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -488,6 +488,18 @@ function(seastar_add_certgen name)
if (NOT CERT_EMAIL)
set(CERT_EMAIL postmaster@${CERT_DOMAIN})
endif()
+ if (NOT CERT_ALT_EMAIL_1)
+ set(CERT_ALT_EMAIL_1 alt1@${CERT_DOMAIN})
+ endif()
+ if (NOT CERT_ALT_EMAIL_2)
+ set(CERT_ALT_EMAIL_2 alt2@${CERT_DOMAIN})
+ endif()
+ if (NOT CERT_ALT_IP_1)
+ set(CERT_ALT_IP_1 127.0.0.1)
+ endif()
+ if (NOT CERT_ALT_DNS)
+ set(CERT_ALT_DNS ${CERT_COMMON})
+ endif()
if (NOT CERT_WIDTH)
set(CERT_WIDTH 4096)
endif()
@@ -520,7 +532,7 @@ function(seastar_add_certgen name)
)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CERT_REQ}"
COMMAND ${OPENSSL} req -new -key ${CERT_PRIVKEY} -out ${CERT_REQ} -config ${CERT_NAME}.cfg
- DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_PRIVKEY}"
+ DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_PRIVKEY}" "${CMAKE_CURRENT_BINARY_DIR}/${CERT_NAME}.cfg"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
@@ -530,13 +542,14 @@ function(seastar_add_certgen name)
)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CAROOT}"
COMMAND ${OPENSSL} req -x509 -new -nodes -key ${CERT_CAPRIVKEY} -days ${CERT_DAYS} -config ${CERT_NAME}.cfg -out ${CERT_CAROOT}
- DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CAPRIVKEY}"
+ DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CAPRIVKEY}" "${CMAKE_CURRENT_BINARY_DIR}/${CERT_NAME}.cfg"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
+
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CERT}"
- COMMAND ${OPENSSL} x509 -req -in ${CERT_REQ} -CA ${CERT_CAROOT} -CAkey ${CERT_CAPRIVKEY} -CAcreateserial -out ${CERT_CERT} -days ${CERT_DAYS}
- DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_REQ}" "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CAROOT}"
+ COMMAND ${OPENSSL} x509 -req -in ${CERT_REQ} -CA ${CERT_CAROOT} -CAkey ${CERT_CAPRIVKEY} -CAcreateserial -out ${CERT_CERT} -days ${CERT_DAYS} -extensions req_ext -extfile ${CERT_NAME}.cfg
+ DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_REQ}" "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CAROOT}" "${CMAKE_CURRENT_BINARY_DIR}/${CERT_NAME}.cfg"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
diff --git a/tests/unit/
cert.cfg.in b/tests/unit/
cert.cfg.in
--- a/tests/unit/
cert.cfg.in
+++ b/tests/unit/
cert.cfg.in
@@ -21,3 +21,6 @@ basicConstraints = CA:true
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+[req_ext]
+subjectAltName=email:@CERT_ALT_EMAIL_1@,email:@CERT_ALT_EMAIL_2@,IP:@CERT_ALT_IP_1@,DNS:@CERT_ALT_DNS@
diff --git a/tests/unit/tls_test.cc b/tests/unit/tls_test.cc
--- a/tests/unit/tls_test.cc
+++ b/tests/unit/tls_test.cc
@@ -1174,6 +1174,7 @@ SEASTAR_THREAD_TEST_CASE(test_closed_write) {
b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
b.set_dh_level();
b.set_system_trust().get();
+ b.set_client_auth(tls::client_auth::REQUIRE);
auto creds = b.build_certificate_credentials();
auto serv = b.build_server_credentials();
@@ -1365,3 +1366,71 @@ SEASTAR_THREAD_TEST_CASE(test_dn_name_handling) {
fetch_dn("
client1.org", client1_creds);
fetch_dn("
client2.org", client2_creds);
}
+
+SEASTAR_THREAD_TEST_CASE(test_alt_names) {
+ tls::credentials_builder b;
+
+ b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();
+ b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
+ b.set_client_auth(tls::client_auth::REQUIRE);
+
+ auto creds = b.build_certificate_credentials();
+ auto serv = b.build_server_credentials();
+
+ ::listen_options opts;
+ opts.reuse_address = true;
+ opts.set_fixed_cpu(this_shard_id());
+
+ auto addr = ::make_ipv4_address( {0x7f000001, 4712});
+ auto server = tls::listen(serv, addr, opts);
+
+ {
+ auto sa = server.accept();
+ auto c = tls::connect(creds, addr).get0();
+ auto s = sa.get0();
+
+ auto in = s.connection.input();
+ output_stream<char> out(c.output().detach(), 1024);
+ out.write("nils").get();
+
+ auto falt_names = tls::get_alt_name_information(s.connection);
+
+ auto fout = out.flush();
+ auto fin = in.read();
+
+ fout.get();
+
+ auto alt_names = falt_names.get();
+ fin.get();
+
+ in.close().get();
+ out.close().get();
+
+ s.connection.shutdown_input();
+ s.connection.shutdown_output();
+
+ c.shutdown_input();
+ c.shutdown_output();
+
+ auto ensure_alt_name = [&](tls::subject_alt_name_type type, size_t min_count) {
+ for (auto& v : alt_names) {
+ if (type != v.type) {
+ continue;
+ }
+ std::visit([&](auto& val) {
+ BOOST_TEST_MESSAGE(fmt::format("Alt name type: {}: {}", int(type), val).c_str());
+ }, v.value);
+ if (--min_count == 0) {
+ return;
+ }
+ }
+ BOOST_FAIL("Missing " + std::to_string(min_count) + " alt name attributes of type " + std::to_string(int(type)));
+ };
+
+ ensure_alt_name(tls::subject_alt_name_type::ipaddress, 1);
+ ensure_alt_name(tls::subject_alt_name_type::rfc822name, 2);
+ ensure_alt_name(tls::subject_alt_name_type::dnsname, 1);
+ }
+
+}
+