Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Bug 5363: Handle IP-based X.509 SANs better #1793

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9724436
Ensure bumped SSL certs get IP SANs if available
Feb 29, 2024
e24d680
Enable matchX509CommonNames to match on IP
Feb 29, 2024
7b2541e
Declare compare_ip_addresses as static
Apr 28, 2024
d387570
Switch to Ip::Address for IP verification
walkert May 9, 2024
e1d60a9
Add VerifyAddress class for matching IP/Domainname
walkert May 9, 2024
fd1b782
Pass explicit address types to VerifyAddress
May 15, 2024
5d7b0c0
fixup: Undo unwanted out-of-scope formatting change
rousskov Jul 26, 2024
d3377ec
Migrate from (ASN1_STRING, often-ignored type) pairs to GeneralName
rousskov Jul 26, 2024
f97b58e
Simplified GeneralName by dropping support for UnsupportedVariant
rousskov Jul 29, 2024
5960067
fixup: Addressed duplication flagged in previous branch commits
rousskov Jul 29, 2024
ae203d2
fixup: Documented matchX509CommonNames() short-circuit effect
rousskov Jul 29, 2024
aadf227
fixup: Fixed matchX509CommonNames() name to match the new scope
rousskov Jul 29, 2024
79766df
fixup: Addressed recently added branch XXX
rousskov Jul 29, 2024
45861c0
fixup: Formatted branch-modified sources
rousskov Jul 30, 2024
6a04acc
fixup: Fix #include problems detected by source-maintenance.sh
rousskov Jul 30, 2024
bb979a2
fixup: Detailed raw input reporting problems
rousskov Jul 30, 2024
c0cc468
fixup: Addressed critical documentation TODO
rousskov Jul 30, 2024
d051cff
fixup: Documented another IPv6 handling bug
rousskov Jul 30, 2024
d77c89f
fixup: matchDomainName() is case-insensitive
rousskov Jul 30, 2024
f5f8717
fixup: Polished branch-added comments
rousskov Jul 30, 2024
1a5e766
Added Ip::Address::Parse() to reduce IP parsing problems
rousskov Jul 30, 2024
d47612e
Add my work address to CONTRIBUTORS
Jul 31, 2024
ff58c41
Add AnyP::Host to encapsulate domain-vs-IP URI authority info
rousskov Aug 6, 2024
21d5539
fixup: Disassociated Anyp::Host from URI
rousskov Aug 6, 2024
d6cfbef
Reuse AnyP::Host for Ssl::GeneralName, addressing earlier TODO
rousskov Aug 6, 2024
1cd2bcb
fixup: Polished comments and marked problems
rousskov Aug 7, 2024
9fb154f
fixup: Addressed XXX re "treats CN as a domain name"
rousskov Aug 7, 2024
1d58a5b
fixup: Fix "CONNECT <IP>:443 HTTP/1.1" handling
rousskov Aug 7, 2024
a1af897
fixup: Addressed (invalid) branch-added XXX
rousskov Aug 7, 2024
432eba0
fixup: Addressed branch-added ParseAsWildDomainName() duplication XXX
rousskov Aug 8, 2024
7e4f16a
fixup: Clarified source code comment
rousskov Aug 8, 2024
7098ec4
fixup: Polished parsing method names, API
rousskov Aug 8, 2024
bc9c6b9
Do not bracket IPv6 addresses when matching server_name parameters
rousskov Aug 8, 2024
cdc1ca3
fixup: Fixed DomainName namespace
rousskov Aug 9, 2024
0d5e877
fixup: formatted modified sources
rousskov Aug 9, 2024
1223451
fixup: Fix build on some platforms (missing header)
rousskov Aug 9, 2024
2ea18f7
fixup: Fix "make distcheck" in CodeQL-tests (missing header)
rousskov Aug 9, 2024
a8e992a
fixup: Better names for new functions
rousskov Nov 16, 2024
eb650b0
Merged master to get the new set of CI tests
rousskov Nov 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ Thank you!
Tomas Hozza <thozza@redhat.com>
tomofumi-yoshida <51390036+tomofumi-yoshida@users.noreply.github.com>
Tony Lorimer <tlorimer@au.mdis.com>
Tony Walker <tony.walker@twosigma.com>
trapexit <trapexit@spawn.link>
Trever Adams <trever@middleearth.sapphiresunday.org>
Tsantilas Christos <chtsanti@users.sourceforge.net>
Expand Down
109 changes: 67 additions & 42 deletions src/acl/ServerName.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
#include "squid.h"
#include "acl/FilledChecklist.h"
#include "acl/ServerName.h"
#include "anyp/Host.h"
#include "client_side.h"
#include "http/Stream.h"
#include "HttpRequest.h"
#include "sbuf/Stream.h"
#include "ssl/bio.h"
#include "ssl/ServerBump.h"
#include "ssl/support.h"
Expand Down Expand Up @@ -45,31 +47,50 @@ ACLServerNameData::match(const char *host)

}

/// A helper function to be used with Ssl::matchX509CommonNames().
/// \retval 0 when the name (cn or an alternate name) matches acl data
/// \retval 1 when the name does not match
template<class MatchType>
int
check_cert_domain( void *check_data, ASN1_STRING *cn_data)
namespace Acl {

/// GeneralNameMatcher for matching configured ACL parameters
class ServerNameMatcher: public Ssl::GeneralNameMatcher
{
char cn[1024];
ACLData<MatchType> * data = (ACLData<MatchType> *)check_data;

if (cn_data->length > (int)sizeof(cn) - 1)
return 1; // ignore data that does not fit our buffer

char *s = reinterpret_cast<char *>(cn_data->data);
char *d = cn;
for (int i = 0; i < cn_data->length; ++i, ++d, ++s) {
if (*s == '\0')
return 1; // always a domain mismatch. contains 0x00
*d = *s;
}
cn[cn_data->length] = '\0';
debugs(28, 4, "Verifying certificate name/subjectAltName " << cn);
if (data->match(cn))
return 0;
return 1;
public:
explicit ServerNameMatcher(ServerNameCheck::Parameters &p): parameters(p) {}

protected:
/* GeneralNameMatcher API */
bool matchDomainName(const Dns::DomainName &) const override;
bool matchIp(const Ip::Address &) const override;

private:
// TODO: Make ServerNameCheck::Parameters::match() and this reference constant.
ServerNameCheck::Parameters &parameters; ///< configured ACL parameters
};

} // namespace Acl

bool
Acl::ServerNameMatcher::matchDomainName(const Dns::DomainName &domain) const
{
return parameters.match(SBuf(domain).c_str()); // TODO: Upgrade string-matching ACLs to SBuf
}

bool
Acl::ServerNameMatcher::matchIp(const Ip::Address &ip) const
{
// We are given an Ip::Address, but our ACL parameters use case-insensitive
// string equality (::matchDomainName) or regex string matches. There are
// many ways to convert an IPv6 address to a string, but only one format can
// correctly match certain configured parameters. Our ssl::server_name docs
// request the following ACL parameter formatting (that this to-string
// conversion code produces): IPv6 addresses use "::" notation (where
// applicable) and are not bracketed.
//
// Similar problems affect dstdomain ACLs. TODO: Instead of relying on users
// reading docs and following their inet_ntop(3) implementation to match
// IPv6 addresses handled by matchDomainName(), enhance matchDomainName()
// code and ACL parameter storage to support Ip::Address objects.
char hostStr[MAX_IPSTRLEN];
(void)ip.toStr(hostStr, sizeof(hostStr)); // no brackets
return parameters.match(hostStr);
}

int
Expand All @@ -79,37 +100,41 @@ Acl::ServerNameCheck::match(ACLChecklist * const ch)

assert(checklist != nullptr && checklist->request != nullptr);

const char *serverName = nullptr;
SBuf clientSniKeeper; // because c_str() is not constant
std::optional<AnyP::Host> serverNameFromConn;
if (ConnStateData *conn = checklist->conn()) {
const char *clientRequestedServerName = nullptr;
clientSniKeeper = conn->tlsClientSni();
if (clientSniKeeper.isEmpty()) {
const char *host = checklist->request->url.host();
if (host && *host) // paranoid first condition: host() is never nil
clientRequestedServerName = host;
} else
clientRequestedServerName = clientSniKeeper.c_str();
std::optional<AnyP::Host> clientRequestedServerName;
const auto &clientSni = conn->tlsClientSni();
if (clientSni.isEmpty()) {
clientRequestedServerName = checklist->request->url.parsedHost();
} else {
// RFC 6066: "The hostname is represented as a byte string using
// ASCII encoding"; "Literal IPv4 and IPv6 addresses are not
// permitted". TODO: Store TlsDetails::serverName and similar
// domains using a new domain-only type instead of SBuf.
clientRequestedServerName = AnyP::Host::ParseSimpleDomainName(clientSni);
}

if (useConsensus) {
X509 *peer_cert = conn->serverBump() ? conn->serverBump()->serverCert.get() : nullptr;
// use the client requested name if it matches the server
// certificate or if the certificate is not available
if (!peer_cert || Ssl::checkX509ServerValidity(peer_cert, clientRequestedServerName))
serverName = clientRequestedServerName;
if (!peer_cert || !clientRequestedServerName ||
Ssl::HasSubjectName(*peer_cert, *clientRequestedServerName))
serverNameFromConn = clientRequestedServerName;
} else if (useClientRequested)
serverName = clientRequestedServerName;
serverNameFromConn = clientRequestedServerName;
else { // either no options or useServerProvided
if (X509 *peer_cert = (conn->serverBump() ? conn->serverBump()->serverCert.get() : nullptr))
return Ssl::matchX509CommonNames(peer_cert, data.get(), check_cert_domain<const char*>);
return Ssl::HasMatchingSubjectName(*peer_cert, ServerNameMatcher(*data));
if (!useServerProvided)
serverName = clientRequestedServerName;
serverNameFromConn = clientRequestedServerName;
}
}

if (!serverName)
serverName = "none";

std::optional<SBuf> printedServerName;
if (serverNameFromConn)
printedServerName = ToSBuf(*serverNameFromConn); // no brackets
const auto serverName = printedServerName ? printedServerName->c_str() : "none";
return data->match(serverName);
}

Expand Down
113 changes: 113 additions & 0 deletions src/anyp/Host.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (C) 1996-2023 The Squid Software Foundation and contributors
*
* Squid software is distributed under GPLv2+ license and includes
* contributions from numerous individuals and organizations.
* Please see the COPYING and CONTRIBUTORS files for details.
*/

#include "squid.h"
#include "anyp/Host.h"

#include <iostream>

std::optional<AnyP::Host>
AnyP::Host::ParseIp(const Ip::Address &ip)
{
// any preparsed IP value is acceptable
debugs(23, 7, ip);
return Host(ip);
}

/// common parts of FromSimpleDomain() and FromWildDomain()
std::optional<AnyP::Host>
AnyP::Host::ParseDomainName(const SBuf &rawName)
{
if (rawName.isEmpty()) {
debugs(23, 3, "rejecting empty name");
return std::nullopt;
}

// Reject bytes incompatible with rfc1035NamePack() and ::matchDomainName()
// implementations (at least). Such bytes can come from percent-encoded HTTP
// URIs or length-based X.509 fields, for example. Higher-level parsers must
// reject or convert domain name encodings like UTF-16, but this low-level
// check works as an additional (albeit unreliable) layer of defense against
// those (unsupported by Squid DNS code) encodings.
if (rawName.find('\0') != SBuf::npos) {
debugs(83, 3, "rejecting ASCII NUL character in " << rawName);
return std::nullopt;
}

// TODO: Consider rejecting names with isspace(3) bytes.

debugs(23, 7, rawName);
return Host(rawName);
}

std::optional<AnyP::Host>
AnyP::Host::ParseSimpleDomainName(const SBuf &rawName)
{
if (rawName.find('*') != SBuf::npos) {
debugs(23, 3, "rejecting wildcard in " << rawName);
return std::nullopt;
}
return ParseDomainName(rawName);
}

std::optional<AnyP::Host>
AnyP::Host::ParseWildDomainName(const SBuf &rawName)
{
const static SBuf wildcardLabel("*.");
if (rawName.startsWith(wildcardLabel)) {
if (rawName.find('*', 2) != SBuf::npos) {
debugs(23, 3, "rejecting excessive wildcards in " << rawName);
return std::nullopt;
}
// else: fall through to final checks
} else {
if (rawName.find('*', 0) != SBuf::npos) {
// this case includes "*" and "example.*" input
debugs(23, 3, "rejecting unsupported wildcard in " << rawName);
return std::nullopt;
}
// else: fall through to final checks
}
return ParseDomainName(rawName);
}

std::ostream &
AnyP::operator <<(std::ostream &os, const Host &host)
{
if (const auto ip = host.ip()) {
char buf[MAX_IPSTRLEN];
(void)ip->toStr(buf, sizeof(buf)); // no brackets
os << buf;
} else {
// If Host object creators start applying Uri::Decode() to reg-names,
// then we must start applying Uri::Encode() here, but only to names
// that require it. See "The reg-name syntax allows percent-encoded
// octets" paragraph in RFC 3986.
const auto domainName = host.domainName();
Assure(domainName);
os << *domainName;
}
return os;
}

std::ostream &
AnyP::operator <<(std::ostream &os, const Bracketed &hostWrapper)
{
bool addBrackets = false;
if (const auto ip = hostWrapper.host.ip())
addBrackets = ip->isIPv6();

if (addBrackets)
os << '[';
os << hostWrapper.host;
if (addBrackets)
os << ']';

return os;
}

81 changes: 81 additions & 0 deletions src/anyp/Host.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 1996-2023 The Squid Software Foundation and contributors
*
* Squid software is distributed under GPLv2+ license and includes
* contributions from numerous individuals and organizations.
* Please see the COPYING and CONTRIBUTORS files for details.
*/

#ifndef SQUID_SRC_ANYP_HOST_H
#define SQUID_SRC_ANYP_HOST_H

#include "dns/forward.h"
#include "ip/Address.h"
#include "sbuf/SBuf.h"

#include <iosfwd>
#include <optional>
#include <variant>

namespace AnyP
{

/// either a domain name (as defined in DNS RFC 1034) or an IP address
class Host
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is at the core of this PR changes. In official code, a single address that may be either an IP address or a DNS domain name is either isolated into two or more co-existing objects (e.g., host information inside AnyP::Uri) or morphed into one opaque SBuf object (e.g., server in ssl_verify_cb()). Neither approach works well, for obvious reasons. This PR uses the new Host class to accurately and as-safely-as-possible represent this naming duality in several X.509 SAN-related contexts. Eventually, more code should be converted to use this class. I have left a few related TODOs.

The proposed "host" name works well enough IMO, but I would not be surprised if there are better alternatives our there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @rousskov - do you know if there's anything else that needs to be done for the review to continue? Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@walkert, unfortunately, the answer to your question is "no": I do not know how to convince @yadij to review PRs awaiting his review. IMO, this persistent problem is one of the existential threats faced by the Squid Project, but all my attempts to find a solution have failed. I can only hope that we will resume making progress with this PR soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rousskov understood. Thanks for taking the time to respond. I'll keep watching and hoping.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @rousskov, is there scope to hand this off to another reviewer if @yadij is unavailable? Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@walkert, there is no formal procedure to overwrite, move, or clear a "this PR needs to be reviewed by me" flag set by a core developer. Project core developers hold a lot of power, and multiple attempts to establish timeouts (or otherwise restructure the governing rules) have failed so far. FWIW, I plan to bring this PR up again during the next meeting which should happen on October 22, 2024.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rousskov understood. Thanks for the info. 🙏

{
public:
/// converts an already parsed IP address to a Host object
static std::optional<Host> ParseIp(const Ip::Address &);

/// Parses input as a literal ASCII domain name (A-labels OK; see RFC 5890).
/// Does not allow wildcards; \sa ParseWildDomainName().
static std::optional<Host> ParseSimpleDomainName(const SBuf &);

/// Same as ParseSimpleDomainName() but allows the first label to be a
/// wildcard (RFC 9525 Section 6.3).
static std::optional<Host> ParseWildDomainName(const SBuf &);

// Accessor methods below are mutually exclusive: Exactly one method is
// guaranteed to return a result other than std::nullopt.

/// stored IPv or IPv6 address (if any)
///
/// Ip::Address::isNoAddr() may be true for the returned address.
/// Ip::Address::isAnyAddr() may be true for the returned address.
auto ip() const { return std::get_if<Ip::Address>(&raw_); }

/// stored domain name (if any)
auto domainName() const { return std::get_if<SBuf>(&raw_); }

private:
using Storage = std::variant<Ip::Address, Dns::DomainName>;

static std::optional<Host> ParseDomainName(const SBuf &);

// use a Parse*() function to create Host objects
Host(const Storage &raw): raw_(raw) {}

Storage raw_; ///< the host we are providing access to
};

/// helps print Host value in RFC 3986 Section 3.2.2 format, with square
/// brackets around an IPv6 address (if the Host value is an IPv6 address)
class Bracketed
{
public:
explicit Bracketed(const Host &aHost): host(aHost) {}
const Host &host;
};

/// prints Host value _without_ square brackets around an IPv6 address (even
/// when the Host value is an IPv6 address); \sa Bracketed
std::ostream &operator <<(std::ostream &, const Host &);

/// prints Host value _without_ square brackets around an IPv6 address (even
/// when the Host value is an IPv6 address); \sa Bracketed
std::ostream &operator <<(std::ostream &, const Bracketed &);

} // namespace Anyp

#endif /* SQUID_SRC_ANYP_HOST_H */

2 changes: 2 additions & 0 deletions src/anyp/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ include $(top_srcdir)/src/Common.am
noinst_LTLIBRARIES = libanyp.la

libanyp_la_SOURCES = \
Host.cc \
Host.h \
PortCfg.cc \
PortCfg.h \
ProtocolType.cc \
Expand Down
Loading