This is an automated email from the ASF dual-hosted git repository. cmcfarlen pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit fdf194ef809148c12b8a3509fba3cc0ebb3f89b1 Author: Leif Hedstrom <[email protected]> AuthorDate: Wed Jul 9 15:23:17 2025 -0500 Cripts: Adds some certificate introspection (#12320) * Cripts: Adds some certificate introspection * Use the Cripts mixin string_view type * Fixes autest for Cripts, and adds some more tests (cherry picked from commit 671f791ab513b9245430765520b22fffa826ac99) (cherry picked from commit 2d13cff2028a4e6bbf075662b218b925069a8e48) --- doc/developer-guide/cripts/cripts-certs.en.rst | 186 ++++++ .../cripts/cripts-connections.en.rst | 25 + doc/developer-guide/cripts/index.en.rst | 3 +- include/cripts/Certs.hpp | 633 +++++++++++++++++++++ include/cripts/ConfigsBase.hpp | 1 + include/cripts/Connections.hpp | 102 +++- include/cripts/Matcher.hpp | 6 +- include/cripts/Preamble.hpp | 1 + src/cripts/CMakeLists.txt | 1 + src/cripts/Certs.cc | 269 +++++++++ src/cripts/Connections.cc | 14 + tests/gold_tests/cripts/cripts.test.py | 30 +- tests/gold_tests/cripts/files/basic.cript | 8 + tests/gold_tests/cripts/gold/certs_cript.gold | 17 + tools/cripts/compiler.sh | 6 +- 15 files changed, 1281 insertions(+), 21 deletions(-) diff --git a/doc/developer-guide/cripts/cripts-certs.en.rst b/doc/developer-guide/cripts/cripts-certs.en.rst new file mode 100644 index 0000000000..7c9522e842 --- /dev/null +++ b/doc/developer-guide/cripts/cripts-certs.en.rst @@ -0,0 +1,186 @@ +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. include:: ../../common.defs + +.. highlight:: cpp +.. default-domain:: cpp + +.. _cripts-certs: + +Certificates +************ + +Cripts provides a set of convenient classes for introspection into the various +TLS certificates that are used. These include both the server certificates used +to establish a TLS connections, as well as any client certificates used for +mutual TLS. + +In the current implementation, these objects only work on X509 certificates as +associated with the ``client`` and ``server`` connections. Let's start off with +a simple example of how to use these objects: + +.. code-block:: cpp + + do_send_response() + { + if (client.connection.IsTLS()) { + const auto tls = cripts::Certs::Server(client.connection); + + client.response["X-Subject"] = tls.subject; + client.response["X-NotBefore"] = tls.notBefore; + client.response["X-NotAfter"] = tls.notAfter; + } + } + +.. _cripts-certs-objects: + +Objects +======= + +There are two types of objects for the certificates: + +================================= =============================================================== +Object Description +================================= =============================================================== +``cripts::Certs::Server`` The certificate used on the connection for TLS handshakes. +``cripts::Certs::Client`` The mutual TLS (mTLS) certificate used on the connection. +================================= =============================================================== + +This combined with the two kinds of connections, ``cripts::Client::Connection`` and +``cripts::Server::Connection`` yields a total of four possible certificate objects. For example, to +access the client mTLS provided certificate on a client connection, you would use: + +.. code-block:: cpp + + const auto tls = cripts::Certs::Client(cripts::Client::Connection::Get()); + +Or if you are using the convenience wrappers: + +.. code-block:: cpp + + const auto tls = cripts::Certs::Client(client.connection); + +.. _cripts-certs-x509: + +X509 Values +=========== + +As part of the certificate objects, there are a number of values that can be +accessed. These values are all based on the X509 standard and can be used to +introspect the certificate. The following values are available: + +================================= =============================================================== +Value Description +================================= =============================================================== +``certificate`` The raw X509 certificate in PEM format. +``signature`` The raw signature of the certificate. +``subject`` The subject of the certificate. +``issuer`` The issuer of the certificate. +``serialNumber`` The serial number of the certificate. +``notBefore`` The date and time when the certificate is valid from. +``notAfter`` The date and time when the certificate is valid until. +``version`` The version of the certificate. +================================= =============================================================== + +.. _cripts-certs-san: + +SAN Values +========== + +We've made special provisions to access the Subject Alternative Name (SAN) values +of the certificate. These values are often used to identify the hostnames or IP +addresses that the certificate is valid for. Once you have the certificate object, +you can access the SAN values as follows: + +==================== =============== =============================================================== +Field X509 field Description +==================== =============== =============================================================== +``.san`` na An array of tuples with type and ``string_view`` of all SANs. +``.san.email`` ``GEN_EMAIL`` An array of ``string_view`` of email addresses. +``.san.dns`` ``GEN_DNS`` An array of ``string_view`` of DNS names. +``.san.uri`` ``GEN_URI`` An array of ``string_view`` of URIs. +``.san.ipadd`` ``GEN_IPADD`` An array of ``string_view`` of IP addresses. +==================== =============== =============================================================== + +.. note:: + + These arrays are empty if no SAN values are present in the certificate. We also populate these + arrays lazily, but they are kept for the lifetime of the certificate object. This means that + you can access these values multiple times without incurring additional overhead. Remember + that you can use the ``cripts::Net::IP`` class to convert the IP addresses into proper + IP address objects if needed. + + +Odds are that you will want to use one of the specific array values, such as ``.san.uri``, which is +easily done in a simple loop: + +.. code-block:: cpp + + do_remap() + { + if (client.connection.IsTLS()) { + const auto tls = cripts::Certs::Server(client.connection); + + for (auto uri : tls.san.uri) { + // Check the URI string_view + } + } + } + + +You can of course loop over all SAN values, which is where the type of the value would come in handy, +and why this is an array of tuples. In this scenario, you would iterate over the tuples like this: + +.. code-block:: cpp + + do_remap() + { + if (client.connection.IsTLS()) { + const auto tls = cripts::Certs::Server(client.connection); + + for (const [type, san] : tls.san) { + if (type == cripts::Certs::SAN::URI) { + // Check the URI string here + } else if (type == cripts::Certs::SAN::DNS) { + // Check the DNS string here + } + } + } + } + +In addition to traditional C++ iterators, you can also access SAN values by index. Make sure +you check the size of the array first, as accessing an out-of-bounds index will give you an +empty tuple. Prefer the iterator above, unless you know you want to access a specific element. + +Example of an alternative way to loop over all SAN values: + +.. code-block:: cpp + + do_remap() + { + if (client.connection.IsTLS()) { + const auto tls = cripts::Certs::Server(client.connection); + + size_t san_count = tls.san.size(); + + for (size_t i = 0; i < san_count; ++i) { + const auto [type, san] = tls.san[i]; + // Process the type and san as needed + } + } + } diff --git a/doc/developer-guide/cripts/cripts-connections.en.rst b/doc/developer-guide/cripts/cripts-connections.en.rst index 196335c8af..9137afe76f 100644 --- a/doc/developer-guide/cripts/cripts-connections.en.rst +++ b/doc/developer-guide/cripts/cripts-connections.en.rst @@ -71,6 +71,9 @@ Method Description ``LocalIP()`` The server (ATS) IP address of the connection. ``IsInternal()`` Returns ``true`` or ``false`` if the connection is internal to ATS. ``Socket()`` Returns the raw socket structure for the connection (use with care). +``IsTLS()`` Returns ``true`` if the connection is a TLS connection. +``ClientCert()`` Returns the client certificiate (mTLS) for the connection (if any). +``ServerCert()`` Returns the server certificate for the connection, if it's a TLS connection. ======================= ========================================================================= The ``IP()`` and ``LocalIP()`` methods return the IP address as an object. In addition to the @@ -102,6 +105,7 @@ Variable Description ``pacing`` Configure socket pacing for the connection. ``dscp`` Manage the DSCP value for the connection socket. ``mark`` Manage the Mark value for the connection socket. +``tls`` Access to the TLS object for the connection. ======================= ========================================================================= For other advanced features, a Cript has access to the socket file descriptor, via the ``FD()`` @@ -165,3 +169,24 @@ Method Description .. note:: All methods return string values. These are methods and not fields, so they must be called as functions. + +.. _cripts-connections-tls: + +TLS +=== + +The ``tls`` variable provides access to the TLS object for the session. This object +provides access to the TLS certificate and other TLS related information. The following methods +are available: + +======================= ========================================================================= +Method Description +======================= ========================================================================= +``Connection`` Returns the connection object for the TLS connection. +``GetX509()`` Returns the X509 certificate for the connection, an OpenSSL object. +======================= ========================================================================= + +Both of these can return a null pointer, if the connection is not a TLS connection or +if the certificate is not available. The ``GetX509()`` method can take an optional +boolean argument, which indicates if the certificate should be for a mutual TLS connection. The +default is ``false``, which means that the server certificate for the connection will be returned. diff --git a/doc/developer-guide/cripts/index.en.rst b/doc/developer-guide/cripts/index.en.rst index 03fa6da08f..2e1dbe8ee4 100644 --- a/doc/developer-guide/cripts/index.en.rst +++ b/doc/developer-guide/cripts/index.en.rst @@ -31,8 +31,9 @@ Cripts cripts-urls.en cripts-headers.en cripts-connections.en - cripts-matcher.en + cripts-certs.en cripts-crypto.en + cripts-matcher.en cripts-misc.en cripts-bundles.en cripts-convenience.en diff --git a/include/cripts/Certs.hpp b/include/cripts/Certs.hpp new file mode 100644 index 0000000000..f19fb8f41b --- /dev/null +++ b/include/cripts/Certs.hpp @@ -0,0 +1,633 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include <openssl/ssl.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> +#include <openssl/bio.h> +#include <vector> +#include <algorithm> + +#include "cripts/Lulu.hpp" +#include "cripts/Connections.hpp" + +namespace cripts::Certs +{ +// These *must* match the values in x509v3.h. +enum class SAN : std::uint8_t { + OTHER = GEN_OTHERNAME, + EMAIL = GEN_EMAIL, + DNS = GEN_DNS, + URI = GEN_URI, + IPADD = GEN_IPADD, +}; + +class String : public cripts::StringViewMixin<String> +{ + using super_type = cripts::StringViewMixin<String>; + using self_type = String; + +public: + virtual ~String() = default; + + operator cripts::string_view() const { return GetSV(); } + + self_type & + operator=(const cripts::string_view str) override + { + _setSV(str); + + return *this; + } + + using super_type::StringViewMixin; + +}; // Class cripts::Certs::String + +} // namespace cripts::Certs + +namespace detail +{ +class CertBase +{ + using self_type = CertBase; + + using X509 = struct x509_st; + using BIO = struct bio_st; + +public: + class X509Value + { + using self_type = X509Value; + + public: + explicit X509Value(CertBase *owner) : _owner(owner) {} + + virtual ~X509Value() = default; + + X509Value() = delete; + X509Value(const self_type &) = delete; + + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + cripts::string_view + GetSV() const + { + _load(); + return _value; + } + + operator cripts::string_view() const { return GetSV(); } + + protected: + void _update_value() const; + + virtual void + _load() const + { + if (!_bio) { + _bio.reset(BIO_new(BIO_s_mem())); + } + } + + void _load_name(X509_NAME *(*getter)(const X509 *)) const; + void _load_integer(ASN1_INTEGER *(*getter)(X509 *)) const; + void _load_long(long (*getter)(const X509 *)) const; + void _load_time(ASN1_TIME *(*getter)(const X509 *)) const; + + CertBase *_owner = nullptr; + mutable std::unique_ptr<BIO, decltype(&BIO_free)> _bio{nullptr, BIO_free}; + mutable cripts::Certs::String _value; + mutable bool _ready = false; + }; // End class CertBase::X509Value + + // Here comes all the various X509 value fields that we support. + class Certificate : public X509Value + { + using self_type = Certificate; + using super_type = X509Value; + + public: + explicit Certificate(CertBase *owner) : super_type(owner) {} + + protected: + void _load() const override; + + }; // End class CertBase::Certificate + + class Signature : public X509Value + { + using self_type = Signature; + using super_type = X509Value; + + public: + explicit Signature(CertBase *owner) : super_type(owner) {} + + protected: + void _load() const override; + + }; // End class CertBase::Signature + + class Subject : public X509Value + { + using self_type = Subject; + using super_type = X509Value; + + public: + explicit Subject(CertBase *owner) : super_type(owner) {} + + protected: + void + _load() const override + { + _load_name(X509_get_subject_name); + } + + }; // End class CertBase::Subject + + class Issuer : public X509Value + { + using self_type = Issuer; + using super_type = X509Value; + + public: + explicit Issuer(CertBase *owner) : super_type(owner) {} + + protected: + void + _load() const override + { + _load_name(X509_get_issuer_name); + } + + }; // End class CertBase::Issuer + + class SerialNumber : public X509Value + { + using self_type = SerialNumber; + using super_type = X509Value; + + public: + explicit SerialNumber(CertBase *owner) : super_type(owner) {} + + protected: + void + _load() const override + { + _load_integer(X509_get_serialNumber); + } + + }; // End class CertBase::SerialNumer + + class NotBefore : public X509Value + { + using self_type = NotBefore; + using super_type = X509Value; + + public: + explicit NotBefore(CertBase *owner) : super_type(owner) {} + + protected: + void + _load() const override + { + _load_time(X509_get_notBefore); + } + + }; // End class CertBase::NotBefore + + class NotAfter : public X509Value + { + using self_type = NotAfter; + using super_type = X509Value; + + public: + explicit NotAfter(CertBase *owner) : super_type(owner) {} + + protected: + void + _load() const override + { + _load_time(X509_get_notAfter); + } + + }; // End class CertBase::NotAfter + + class Version : public X509Value + { + using self_type = Version; + using super_type = X509Value; + + public: + explicit Version(CertBase *owner) : super_type(owner) {} + + protected: + void + _load() const override + { + _load_long(X509_get_version); + } + + }; // End class CertBase::SerialNumer + + class SAN + { + using self_type = SAN; + + class SANBase + { + using self_type = SANBase; + + public: + using Container = std::vector<std::string>; + + explicit SANBase(SAN *owner, cripts::Certs::SAN san_id) : _san_id(san_id), _owner(owner) {} + + virtual ~SANBase() = default; + + SANBase() = delete; + SANBase(const SANBase &) = delete; + SANBase(SANBase &&) = delete; + + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + [[nodiscard]] cripts::Certs::SAN + sanType() const + { + return _san_id; + } + + class Iterator + { + using self_type = Iterator; + + public: + using iterator_category = std::forward_iterator_tag; + using base_iterator = Container::const_iterator; + using value_type = cripts::Certs::String; + using reference = cripts::Certs::String; + + explicit Iterator(base_iterator iter) : _iter(iter) {} + + ~Iterator() = default; + + Iterator(const Iterator &) = default; + Iterator &operator=(const Iterator &) = default; + Iterator(Iterator &&) = default; + Iterator &operator=(Iterator &&) = default; + + [[nodiscard]] reference + operator*() const + { + return {*_iter}; + } + + self_type & + operator++() + { + ++_iter; + return *this; + } + + [[nodiscard]] bool + operator!=(const self_type &other) const + { + return _iter != other._iter; + } + + [[nodiscard]] base_iterator + base_iter() const + { + return _iter; + } + + private: + base_iterator _iter; + }; // End class SAN::SANBase::Iterator + + void + ensureLoaded() const + { + if (!_ready) { + _load(); + _ready = true; + } + } + + [[nodiscard]] Iterator + begin() const + { + ensureLoaded(); + return Iterator(_data.begin()); + } + + [[nodiscard]] Iterator + end() const + { + return Iterator(_data.end()); + } + + [[nodiscard]] Container & + Data() const + { + ensureLoaded(); + return _data; + } + + [[nodiscard]] size_t + size() const + { + ensureLoaded(); + return _data.size(); + } + + [[nodiscard]] size_t + Size() const + { + return size(); + } + [[nodiscard]] cripts::Certs::String + operator[](size_t index) const + { + ensureLoaded(); + if (index >= _data.size()) { + return {""}; + } + return {_data[index]}; + } + + [[nodiscard]] cripts::string Join(const char *delim = ",") const; + + protected: + void _load() const; + + mutable Container _data; + mutable bool _ready = false; + cripts::Certs::SAN _san_id = cripts::Certs::SAN::OTHER; + SAN *_owner = nullptr; + + }; // End class SAN::SANBase + + public: + template <cripts::Certs::SAN ID> class SANType : public SANBase + { + using super_type = SANBase; + using self_type = SANType; + + public: + explicit SANType(SAN *owner) : SANBase(owner, ID) {} + + ~SANType() override = default; + + SANType() = delete; + SANType(const SANType &) = delete; + SANType(SANType &&) = delete; + + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + }; // End class SAN::SANType + + explicit SAN(CertBase *owner) : email(this), dns(this), uri(this), ipadd(this), _owner(owner) {} + + ~SAN() = default; + + SAN() = delete; + SAN(const SAN &) = delete; + SAN(SAN &&) = delete; + + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + SAN::SANType<cripts::Certs::SAN::EMAIL> email; + SAN::SANType<cripts::Certs::SAN::DNS> dns; + SAN::SANType<cripts::Certs::SAN::URI> uri; + SAN::SANType<cripts::Certs::SAN::IPADD> ipadd; + + class Iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = std::tuple<cripts::Certs::SAN, cripts::Certs::String>; + using reference = value_type; // Return by value instead of const reference + using base_iterator = SANBase::Container::const_iterator; + + explicit Iterator(std::nullptr_t) : _ended(true) {} + explicit Iterator(const SAN *san) : _san(san) { _advance(); } + + Iterator() = default; + Iterator(const Iterator &) = default; + Iterator &operator=(const Iterator &) = default; + Iterator(Iterator &&) = default; + Iterator &operator=(Iterator &&) = default; + + ~Iterator() = default; + + [[nodiscard]] reference + operator*() const + { + return {_current}; + } + + Iterator & + operator++() + { + if (_ended) { + return *this; + } + + ++_iter; + if (_iter == _san->_sans[_index - 1]->Data().end()) { + _advance(); + } else { + _update_current(); + } + + return *this; + } + + [[nodiscard]] bool + operator!=(const Iterator &other) const + { + if (_ended && other._ended) { + return false; + } + + return _san != other._san || _index != other._index || _iter != other._iter; + } + + [[nodiscard]] bool + operator==(const Iterator &other) const + { + if (_ended && other._ended) { + return true; + } + return _san == other._san && _index == other._index && _iter == other._iter; + } + + private: + void + _update_current() const + { + if (_san && !_ended) { + _current = std::make_tuple(_san->_sans[_index - 1]->sanType(), cripts::Certs::String(*_iter)); + } + } + + void _advance(); + + mutable value_type _current; // The current value of the iterator + base_iterator _iter; // The current iterator within the SAN types + bool _ended = false; + const SAN *_san = nullptr; + size_t _index = 0; + + }; // End class CertBase::SAN::Iterator + + [[nodiscard]] Iterator + begin() const + { + auto it = Iterator(this); + + return it; + } + + [[nodiscard]] Iterator + end() const + { + Iterator it{nullptr}; + + return it; + } + + [[nodiscard]] size_t + size() const + { + size_t total = 0; + + for (const auto *san : _sans) { + total += san->Size(); + } + return total; + } + + [[nodiscard]] size_t + Size() const + { + return size(); + } + + [[nodiscard]] Iterator::value_type operator[](size_t index) const; + + private: + const std::array<const SANBase *, 4> _sans = std::to_array<const SANBase *>({&email, &dns, &uri, &ipadd}); + CertBase *_owner; + + }; // End class CertBase::SAN + +public: + CertBase(detail::ConnBase &conn) + : certificate(this), + signature(this), + subject(this), + issuer(this), + serialNumber(this), + notBefore(this), + notAfter(this), + version(this), + san(this), + _conn(&conn) + { + } + + CertBase() = delete; + CertBase(const self_type &) = delete; + + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + Certificate certificate; + Signature signature; + Subject subject; + Issuer issuer; + SerialNumber serialNumber; + NotBefore notBefore; + NotAfter notAfter; + Version version; + SAN san; + +protected: + X509 *_x509 = nullptr; + detail::ConnBase *_conn = nullptr; + +}; // End class detail::CertBase + +template <bool IsMutualTLS> class Cert : public detail::CertBase +{ + using self_type = Cert<IsMutualTLS>; + using super_type = detail::CertBase; + +public: + explicit Cert(detail::ConnBase &conn) : super_type(conn) { _x509 = conn.tls.GetX509(IsMutualTLS); } +}; // End class Cert + +} // namespace detail + +namespace cripts::Certs +{ +using Client = detail::Cert<true>; +using Server = detail::Cert<false>; + +} // namespace cripts::Certs + +namespace fmt +{ + +template <> struct formatter<cripts::Certs::SAN> { + constexpr auto + parse(format_parse_context &ctx) -> decltype(ctx.begin()) + { + return ctx.begin(); + } + + template <typename FormatContext> + auto + format(cripts::Certs::SAN san, FormatContext &ctx) const -> decltype(ctx.out()) + { + return fmt::format_to(ctx.out(), "{}", static_cast<int>(san)); + } +}; + +template <> struct formatter<cripts::Certs::String> { + constexpr auto + parse(format_parse_context &ctx) -> decltype(ctx.begin()) + { + return ctx.begin(); + } + + template <typename FormatContext> + auto + format(const cripts::Certs::String &str, FormatContext &ctx) const -> decltype(ctx.out()) + { + return fmt::format_to(ctx.out(), "{}", str.GetSV()); + } +}; + +} // namespace fmt diff --git a/include/cripts/ConfigsBase.hpp b/include/cripts/ConfigsBase.hpp index 74174ef428..6eba3628dc 100644 --- a/include/cripts/ConfigsBase.hpp +++ b/include/cripts/ConfigsBase.hpp @@ -22,6 +22,7 @@ #include <unordered_map> #include "ts/ts.h" + #include "cripts/Lulu.hpp" #include "cripts/Context.hpp" diff --git a/include/cripts/Connections.hpp b/include/cripts/Connections.hpp index 1671b5db7e..3e035105a6 100644 --- a/include/cripts/Connections.hpp +++ b/include/cripts/Connections.hpp @@ -17,10 +17,7 @@ */ #pragma once -namespace cripts -{ -class Context; -} +#include <openssl/ssl.h> #include "ts/apidefs.h" #include "ts/ts.h" @@ -28,6 +25,18 @@ class Context; #include "cripts/Lulu.hpp" #include "cripts/Matcher.hpp" +namespace detail +{ +class ConnBase; +template <bool IsMutualTLS> class Cert; +} // namespace detail + +namespace cripts::Certs +{ +using Client = detail::Cert<true>; +using Server = detail::Cert<false>; +} // namespace cripts::Certs + // This is figured out in this way because // this header has to be available to include // from cripts scripts that won't have access @@ -305,8 +314,67 @@ class ConnBase }; // End class ConnBase::TcpInfo + class TLS + { + using self_type = TLS; + + public: + friend class ConnBase; + + TLS() = default; + void operator=(const self_type &) = delete; + + operator bool() + { + auto conn = Connection(); + return conn != nullptr; + } + + [[nodiscard]] TSSslConnection + Connection() + { + if (_not_tls) [[unlikely]] { + return nullptr; // Avoid repeated attempts + } + + _ensure_initialized(_owner); + if (!_tls) { + _tls = TSVConnSslConnectionGet(_owner->_vc); + if (!_tls) [[unlikely]] { + _not_tls = true; + } + } + + return _tls; + } + + [[nodiscard]] X509 * + GetX509(bool mTLS = false) + { + auto conn = Connection(); + + if (mTLS) { +#ifdef OPENSSL_IS_OPENSSL3 + return SSL_get1_peer_certificate(reinterpret_cast<::SSL *>(conn)); +#else + return SSL_get_peer_certificate(reinterpret_cast<::SSL *>(conn)); +#endif + } else { + return SSL_get_certificate(reinterpret_cast<::SSL *>(conn)); + } + } + + private: + ConnBase *_owner = nullptr; + TSSslConnection _tls = nullptr; + bool _not_tls = false; + + }; // End class ConnBase::SSL + public: - ConnBase() { dscp._owner = congestion._owner = tcpinfo._owner = geo._owner = pacing._owner = mark._owner = this; } + ConnBase() { dscp._owner = congestion._owner = tcpinfo._owner = geo._owner = pacing._owner = mark._owner = tls._owner = this; } + + virtual ~ConnBase() = default; ConnBase(const self_type &) = delete; void operator=(const self_type &) = delete; @@ -339,6 +407,13 @@ public: return TSHttpTxnIsInternal(_state->txnp); } + [[nodiscard]] bool + IsTLS() + { + _ensure_initialized(this); + return TSVConnIsSsl(_vc); + } + [[nodiscard]] virtual cripts::IP LocalIP() const = 0; [[nodiscard]] virtual int Count() const = 0; virtual void SetDscp(int val) const = 0; @@ -351,15 +426,19 @@ public: _state = state; } - Dscp dscp; - Congestion congestion; - TcpInfo tcpinfo; - Geo geo; - Pacing pacing; - Mark mark; + Dscp dscp; + Congestion congestion; + TcpInfo tcpinfo; + Geo geo; + Pacing pacing; + Mark mark; + mutable TLS tls; cripts::string_view string(unsigned ipv4_cidr = 32, unsigned ipv6_cidr = 128); + cripts::Certs::Client ClientCert(); + cripts::Certs::Server ServerCert(); + protected: static void _ensure_initialized(self_type *ptr) @@ -418,7 +497,6 @@ namespace Client { return cripts::IP{TSHttpTxnIncomingAddrGet(_state->txnp)}; } - }; // End class Client::Connection } // namespace Client diff --git a/include/cripts/Matcher.hpp b/include/cripts/Matcher.hpp index 4cb46ee8f0..30574b67ac 100644 --- a/include/cripts/Matcher.hpp +++ b/include/cripts/Matcher.hpp @@ -17,17 +17,15 @@ */ #pragma once -#include "cripts/Headers.hpp" -#include "cripts/Lulu.hpp" -#include "swoc/IPRange.h" // Setup for PCRE2 #define PCRE2_CODE_UNIT_WIDTH 8 #include <pcre2.h> #include <vector> #include <tuple> -#include "ts/ts.h" +#include "cripts/Headers.hpp" #include "cripts/Lulu.hpp" +#include "swoc/IPRange.h" namespace cripts::Matcher { diff --git a/include/cripts/Preamble.hpp b/include/cripts/Preamble.hpp index 5c61d469d9..b484826f18 100644 --- a/include/cripts/Preamble.hpp +++ b/include/cripts/Preamble.hpp @@ -92,6 +92,7 @@ #include "cripts/Matcher.hpp" #include "cripts/Time.hpp" #include "cripts/Crypto.hpp" +#include "cripts/Certs.hpp" #include "cripts/Files.hpp" #include "cripts/Metrics.hpp" #include "cripts/Plugins.hpp" diff --git a/src/cripts/CMakeLists.txt b/src/cripts/CMakeLists.txt index 2b32b89d5c..6e151f46f3 100644 --- a/src/cripts/CMakeLists.txt +++ b/src/cripts/CMakeLists.txt @@ -28,6 +28,7 @@ list(REMOVE_ITEM CPP_FILES ${TEST_CPP_FILES}) set(CRIPTS_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/cripts/Bundle.hpp + ${PROJECT_SOURCE_DIR}/include/cripts/Certs.hpp ${PROJECT_SOURCE_DIR}/include/cripts/Configs.hpp ${PROJECT_SOURCE_DIR}/include/cripts/ConfigsBase.hpp ${PROJECT_SOURCE_DIR}/include/cripts/Connections.hpp diff --git a/src/cripts/Certs.cc b/src/cripts/Certs.cc new file mode 100644 index 0000000000..abb85c9a5d --- /dev/null +++ b/src/cripts/Certs.cc @@ -0,0 +1,269 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#include <arpa/inet.h> + +#include "ts/ts.h" + +#include "cripts/Certs.hpp" + +namespace +{ +} // namespace + +namespace detail +{ +// The first two are essentially taken from our sslheaders plugin, for compatiblity. +void +CertBase::Certificate::_load() const +{ + if (!_ready && _owner->_x509) { + long remain; + char *ptr; + + super_type::_load(); + PEM_write_bio_X509(_bio.get(), _owner->_x509); + + // The PEM format has newlines in it. mod_ssl replaces those with spaces. + remain = BIO_get_mem_data(_bio.get(), &ptr); + for (char *nl; (nl = static_cast<char *>(memchr(ptr, '\n', remain))); ptr = nl) { + *nl = ' '; + remain -= nl - ptr; + } + _update_value(); + } +} + +void +CertBase::Signature::_load() const +{ + if (!_ready && _owner->_x509) { + const ASN1_BIT_STRING *sig; + X509_get0_signature(&sig, nullptr, _owner->_x509); + const char *ptr = reinterpret_cast<const char *>(sig->data); + const char *end = ptr + sig->length; + + super_type::_load(); + for (; ptr < end; ++ptr) { + BIO_printf(_bio.get(), "%02X", static_cast<unsigned char>(*ptr)); + } + _update_value(); + } +} + +void +CertBase::X509Value::_update_value() const +{ + if (BIO_pending(_bio.get())) { + char *data = nullptr; + long len = BIO_get_mem_data(_bio.get(), &data); + + _value = cripts::Certs::String(data, static_cast<size_t>(len)); + _ready = true; + } +} + +void +CertBase::X509Value::_load_name(X509_NAME *(*getter)(const X509 *)) const +{ + if (!_ready && _owner->_x509) { + auto *name = getter(_owner->_x509); + + if (name) { + X509Value::_load(); + X509_NAME_print_ex(_bio.get(), name, 0, XN_FLAG_ONELINE); + _update_value(); + } else [[unlikely]] { + _value = cripts::Certs::String(); + _ready = true; + } + } +} + +void +CertBase::X509Value::_load_integer(ASN1_INTEGER *(*getter)(X509 *)) const +{ + if (!_ready && _owner->_x509) { + auto *value = getter(_owner->_x509); + + X509Value::_load(); + i2a_ASN1_INTEGER(_bio.get(), value); + X509Value::_update_value(); + } +} + +void +CertBase::X509Value::_load_long(long (*getter)(const X509 *)) const +{ + if (!_ready && _owner->_x509) { + auto value = getter(_owner->_x509); + + X509Value::_load(); + BIO_printf(_bio.get(), "%ld", value); + X509Value::_update_value(); + } +} + +void +CertBase::X509Value::_load_time(ASN1_TIME *(*getter)(const X509 *)) const +{ + if (!_ready && _owner->_x509) { + auto *time = getter(_owner->_x509); + + if (time) { + X509Value::_load(); + ASN1_TIME_print(_bio.get(), time); + X509Value::_update_value(); + } else [[unlikely]] { + _value = cripts::Certs::String(); + _ready = true; + } + } +} + +namespace +{ + + using GenWriterFunc = void (*)(const GENERAL_NAME *, BIO *); + + void + _write_asn1_string(const ASN1_IA5STRING *str, BIO *_bio) + { + const char *data = reinterpret_cast<const char *>(ASN1_STRING_get0_data(str)); + int len = ASN1_STRING_length(str); + + BIO_write(_bio, data, len); + } + + void + _write_ip_address(const ASN1_OCTET_STRING *ip, BIO *_bio) + { + char buffer[INET6_ADDRSTRLEN]; + const unsigned char *raw = ip->data; + int len = ip->length; + + if (inet_ntop(len == 4 ? AF_INET : AF_INET6, raw, buffer, sizeof(buffer))) { + BIO_printf(_bio, "%s", buffer); + } + } + + static constexpr GenWriterFunc _gen_writers[] = { + /* 0 */ nullptr, + /* 1 GEN_EMAIL */ [](const GENERAL_NAME *n, BIO *b) { _write_asn1_string(n->d.rfc822Name, b); }, + /* 2 GEN_DNS */ [](const GENERAL_NAME *n, BIO *b) { _write_asn1_string(n->d.dNSName, b); }, + /* 3 GEN_X400 */ nullptr, + /* 4 GEN_DIRNAME */ nullptr, + /* 5 GEN_EDIPARTY */ nullptr, + /* 6 GEN_URI */ [](const GENERAL_NAME *n, BIO *b) { _write_asn1_string(n->d.uniformResourceIdentifier, b); }, + /* 7 GEN_IPADD */ [](const GENERAL_NAME *n, BIO *b) { _write_ip_address(n->d.iPAddress, b); }, + /* 8 GEN_RID */ nullptr}; +} // end anonymous namespace + +void +CertBase::SAN::SANBase::_load() const +{ + if (_ready || !_owner->_owner->_x509) { + return; + } + + auto *san_names = + static_cast<STACK_OF(GENERAL_NAME) *>(X509_get_ext_d2i(_owner->_owner->_x509, NID_subject_alt_name, nullptr, nullptr)); + + if (!san_names) { + return; + } + + auto bio = BIO_new(BIO_s_mem()); + + if (bio) { + for (int i = 0; i < sk_GENERAL_NAME_num(san_names); ++i) { + const GENERAL_NAME *name = sk_GENERAL_NAME_value(san_names, i); + + if (static_cast<cripts::Certs::SAN>(name->type) == _san_id) { + GenWriterFunc fn = (name->type < static_cast<int>(std::size(_gen_writers))) ? _gen_writers[name->type] : nullptr; + CAssert(fn != nullptr); + + BIO_reset(bio); + fn(name, bio); + + char *ptr = nullptr; + long len = BIO_get_mem_data(bio, &ptr); + + if (ptr && len > 0) { + _data.emplace_back(ptr, static_cast<size_t>(len)); + } + } + } + BIO_free(bio); + } + sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free); +} + +cripts::string +CertBase::SAN::SANBase::Join(const char *delim) const +{ + cripts::string str; + + ensureLoaded(); + for (const auto &s : _data) { + if (!str.empty()) { + str += delim; + } + str += s; + } + + return str; // RVO +} + +void +CertBase::SAN::Iterator::_advance() +{ + if (!_san || _ended) { + return; + } + + auto it = std::find_if(_san->_sans.begin() + _index, _san->_sans.end(), [](const SANBase *entry) { return entry->Size() > 0; }); + + if (it != _san->_sans.end()) { + _index = std::distance(_san->_sans.begin(), it) + 1; + _iter = (*it)->Data().begin(); + _update_current(); + } else { + _ended = true; + } +} + +CertBase::SAN::Iterator::value_type +CertBase::SAN::operator[](size_t index) const +{ + if (index < Size()) { + size_t cur = 0; + + for (auto *san : _sans) { + size_t type_size = san->Size(); + + if (index < cur + type_size) { + // Found the SAN type that contains this index + return std::make_tuple(san->sanType(), cripts::Certs::String((*san)[index - cur])); + } + cur += type_size; + } + } + + return std::make_tuple(cripts::Certs::SAN::OTHER, cripts::Certs::String()); +} +} // namespace detail diff --git a/src/cripts/Connections.cc b/src/cripts/Connections.cc index eac1c223f0..d456a961df 100644 --- a/src/cripts/Connections.cc +++ b/src/cripts/Connections.cc @@ -89,6 +89,20 @@ detail::ConnBase::TcpInfo::Log() return _logging; } +cripts::Certs::Client +detail::ConnBase::ClientCert() +{ + _ensure_initialized(this); + return cripts::Certs::Client{*this}; +} + +cripts::Certs::Server +detail::ConnBase::ServerCert() +{ + _ensure_initialized(this); + return cripts::Certs::Server{*this}; +} + namespace cripts { diff --git a/tests/gold_tests/cripts/cripts.test.py b/tests/gold_tests/cripts/cripts.test.py index 32d9cc8702..c72f8c2ccc 100644 --- a/tests/gold_tests/cripts/cripts.test.py +++ b/tests/gold_tests/cripts/cripts.test.py @@ -19,6 +19,9 @@ Test basic cripts functionality import os +# Needed if we want to use sed -i '' on macOS, but autest doesn't like that ... +# import platform + Test.testName = "cripts: basic functions" Test.Summary = ''' Simple cripts test that sets a response header back to the client @@ -45,19 +48,26 @@ class CriptsBasicTest: self.server.addResponse("sessionfile.log", request_header, response_header) def setUpTS(self): - self.ts = Test.MakeATSProcess("ts") + self.ts = Test.MakeATSProcess("ts_in", enable_tls=True, enable_cache=False) + + self.ts.addDefaultSSLFiles() + self.ts.Disk.ssl_multicert_config.AddLine("dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key") self.ts.Setup.Copy('files/basic.cript', self.ts.Variables.CONFIGDIR) self.ts.Disk.records_config.update( { - 'proxy.config.diags.debug.enabled': 1, 'proxy.config.plugin.dynamic_reload_mode': 1, 'proxy.config.plugin.compiler_path': self._compiler_location, + "proxy.config.ssl.server.cert.path": f"{self.ts.Variables.SSLDir}", + "proxy.config.ssl.server.private_key.path": f"{self.ts.Variables.SSLDir}", }) self.ts.Disk.remap_config.AddLine( f'map http://www.example.com http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript') + self.ts.Disk.remap_config.AddLine( + f'map https://www.example.com:{self.ts.Variables.ssl_port} http://127.0.0.1:{self.server.Variables.Port} @plugin=basic.cript' + ) def updateCompilerForTest(self): '''Update the compiler script for the install location of the ATS process.''' @@ -66,7 +76,11 @@ class CriptsBasicTest: compiler_source = os.path.join(p.Variables.RepoDir, 'tools', 'cripts', 'compiler.sh') p.Setup.Copy(compiler_source, self._compiler_location) install_dir = os.path.split(p.Variables.BINDIR)[0] - p.Command = f'sed -i "s|\"/tmp/ats\"|{install_dir}|g" {self._compiler_location}' + # autest doesn't like the -i '' that's necessary on Darwin/macOS + # sed_in_place = "-i ''" if platform.system() == 'Darwin' else "-i" + # p.Command = f"sed -i '' 's|\"/tmp/ats\"|\"{install_dir}\"|' {self._compiler_location}" + p.Command = (f'perl -pi -e \'s|\\"/tmp/ats\\"|\\"{install_dir}\\"|g\' {self._compiler_location}') + p.ReturnCode = 0 def runHeaderTest(self): @@ -78,9 +92,19 @@ class CriptsBasicTest: tr.Processes.Default.Streams.stderr = "gold/basic_cript.gold" tr.StillRunningAfter = self.server + def runCertsTest(self): + tr = Test.AddTestRun('Exercise Cripts certificate introspection.') + tr.MakeCurlCommand( + f'-v --http1.1 -k -H "Host: www.example.com:{self.ts.Variables.ssl_port}" https://127.0.0.1:{self.ts.Variables.ssl_port}' + ) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stderr = "gold/certs_cript.gold" + tr.StillRunningAfter = self.server + def run(self): self.updateCompilerForTest() self.runHeaderTest() + self.runCertsTest() CriptsBasicTest().run() diff --git a/tests/gold_tests/cripts/files/basic.cript b/tests/gold_tests/cripts/files/basic.cript index b8c136668a..9c9343c092 100644 --- a/tests/gold_tests/cripts/files/basic.cript +++ b/tests/gold_tests/cripts/files/basic.cript @@ -27,7 +27,15 @@ do_read_response() do_send_response() { borrow resp = cripts::Client::Response::Get(); + borrow conn =cripts::Client::Connection::Get(); resp["criptsResponseHeader"] = "response"; + + if (conn.IsTLS()) { + const auto tls = cripts::Certs::Server(conn); + resp["X-Subject"] = tls.subject; + resp["X-NotBefore"] = tls.notBefore; + resp["X-NotAfter"] = tls.notAfter; + } } #include <cripts/Epilogue.hpp> diff --git a/tests/gold_tests/cripts/gold/certs_cript.gold b/tests/gold_tests/cripts/gold/certs_cript.gold new file mode 100644 index 0000000000..bfc31a9e71 --- /dev/null +++ b/tests/gold_tests/cripts/gold/certs_cript.gold @@ -0,0 +1,17 @@ +`` +> GET / HTTP/1.1 +> Host: www.example.com`` +> User-Agent: curl/`` +> Accept: */* +`` +< HTTP/1.1 200 OK +< responseHeader: changed +< Date: `` +< Age: `` +< Connection: keep-alive +< Server: ATS/`` +< criptsResponseHeader: response +< X-Subject: C = IE, ST = Dublin, L = Dublin, O = example.com, OU = example.com, CN = example.com +< X-NotBefore: Jan 26 14:56:08 2021 GMT +< X-NotAfter: Aug 21 14:56:08 2119 GMT +`` diff --git a/tools/cripts/compiler.sh b/tools/cripts/compiler.sh index e6a035f70a..cce575fde4 100755 --- a/tools/cripts/compiler.sh +++ b/tools/cripts/compiler.sh @@ -26,7 +26,11 @@ # Configurable parts : ${ATS_ROOT:="/tmp/ats"} : ${CXX:="clang++"} -: ${CXXFLAGS:="-x c++ -std=c++20 -I/opt/homebrew/include"} +if [[ "$(uname)" == "Darwin" ]]; then + : ${CXXFLAGS:="-x c++ -std=c++20 -I/opt/homebrew/include -undefined dynamic_lookup"} +else + : ${CXXFLAGS:="-x c++ -std=c++20"} +fi # Probably don't need to change these ? STDFLAGS="-shared -fPIC -Wall -Werror -I${ATS_ROOT}/include -L${ATS_ROOT}/lib -lcripts"
