mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
net/tls: certificates for IP addresses; no server name for them
A TLS connection to an IP address - https://[::1]:8443/ or https://192.0.2.1/ - was treated like one to a host name, which is wrong in three places: - net_http handed the TLS layer the host as it appears in the URL, so an IPv6 literal kept its brackets ("[::1]"), which nothing can match. The brackets belong to the URL and the Host header only; the TLS layer now gets the bare address. - The built-in client sent the address as server_name. RFC 6066 allows host names only there; for an IP address the extension is now left out. - x509_match_hostname() compared an address against dNSName entries and the commonName as text, so a name certificate whose CN or a DNS entry happened to read "10.0.0.1" vouched for that address, while a certificate carrying the address as a proper iPAddress entry never matched. An IP address now matches only iPAddress entries, byte for byte (RFC 9525 6.3); a host name matches as before. x509_parse_ip() is the new, exact parser behind that: dotted IPv4 without leading zeros, IPv6 with "::" compression and a trailing dotted IPv4, brackets and a zone id allowed. On the way: the built-in client kept the caller's pointer for the host name, and net_http passes one that dies with the transfer while the connection lives on in the pool - where a TLS 1.3 session ticket arriving later is filed under that name. It keeps its own copy now. BearSSL and mbedTLS get the bare address too; their own IP handling is theirs (BearSSL's minimal verifier has none, so such a connection fails verification, as it did). Tests: the crypto suite parses valid and malformed literals and matches a SAN with DNS, IPv4, IPv6 and a DNS-spelled address, plus an IP-reading commonName. The local openssl matrix connects to 127.0.0.1 against a leaf with IP:127.0.0.1 (refused before this change) and one with DNS:127.0.0.1 (now refused).
This commit is contained in:
1 parent
b5658e5a7f
commit
9b5f6ed768
7 files changed
+263
-6
No files matched your search
@@ -70,7 +70,9 @@ jobs:
|
||||
# net_socket_ssl_retro.c end to end: every cipher suite, both
|
||||
# curves, RSA / P-256 / P-384 certificates under a CA made on
|
||||
# the spot, verify required / optional / disabled, and the
|
||||
# refusals that must happen (wrong hostname, untrusted CA).
|
||||
# refusals that must happen (wrong hostname, untrusted CA). An
|
||||
# IP address matches only an iPAddress SAN entry, never the
|
||||
# same address written as a DNS name.
|
||||
samples/network/tls_retro/local_server_test.sh
|
||||
# The same tool cross-linked for Windows: the sample's link
|
||||
# line has to carry Winsock, which nothing on Linux checks.
|
||||
|
||||
@@ -682,11 +682,153 @@ static int x509_name_match(const uint8_t *pat, size_t pl, const char *host)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int x509_hexval(int c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Dotted IPv4 at @s up to @end into 4 bytes. */
|
||||
static int x509_parse_ipv4(const char *s, const char *end, uint8_t *out)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 4; i++)
|
||||
{
|
||||
unsigned v = 0;
|
||||
int digits = 0;
|
||||
while (s < end && *s >= '0' && *s <= '9')
|
||||
{
|
||||
if (digits && v == 0)
|
||||
return -1; /* no leading zeros: not octal, not ambiguous */
|
||||
v = v * 10 + (unsigned)(*s++ - '0');
|
||||
if (++digits > 3 || v > 255)
|
||||
return -1;
|
||||
}
|
||||
if (!digits)
|
||||
return -1;
|
||||
out[i] = (uint8_t)v;
|
||||
if (i < 3)
|
||||
{
|
||||
if (s >= end || *s != '.')
|
||||
return -1;
|
||||
s++;
|
||||
}
|
||||
}
|
||||
return s == end ? 0 : -1;
|
||||
}
|
||||
|
||||
int x509_parse_ip(const char *host, uint8_t out[16])
|
||||
{
|
||||
const char *end;
|
||||
uint8_t a[16];
|
||||
int n = 0, gap = -1;
|
||||
const char *s;
|
||||
|
||||
if (!host || !*host)
|
||||
return 0;
|
||||
if (!strchr(host, ':'))
|
||||
return x509_parse_ipv4(host, host + strlen(host), out) == 0 ? 4 : 0;
|
||||
|
||||
/* IPv6, with or without brackets; a zone ("%eth0") is not part of
|
||||
* the address. */
|
||||
s = host + (*host == '[');
|
||||
end = s + strcspn(s, "]%");
|
||||
if (*host == '[' && !strchr(s, ']'))
|
||||
return 0;
|
||||
if (end - s >= 2 && s[0] == ':' && s[1] == ':')
|
||||
{
|
||||
gap = 0;
|
||||
s += 2;
|
||||
}
|
||||
else if (*s == ':')
|
||||
return 0;
|
||||
while (s < end)
|
||||
{
|
||||
unsigned v = 0;
|
||||
int digits = 0;
|
||||
const char *start = s;
|
||||
while (s < end && x509_hexval(*s) >= 0)
|
||||
{
|
||||
v = (v << 4) | (unsigned)x509_hexval(*s++);
|
||||
if (++digits > 4)
|
||||
return 0;
|
||||
}
|
||||
if (s < end && *s == '.')
|
||||
{
|
||||
/* trailing dotted IPv4: the last 32 bits */
|
||||
if (n > 12 || x509_parse_ipv4(start, end, a + n) != 0)
|
||||
return 0;
|
||||
n += 4;
|
||||
s = end;
|
||||
break;
|
||||
}
|
||||
if (!digits || n > 14)
|
||||
return 0;
|
||||
a[n++] = (uint8_t)(v >> 8);
|
||||
a[n++] = (uint8_t)v;
|
||||
if (s == end)
|
||||
break;
|
||||
if (*s++ != ':')
|
||||
return 0;
|
||||
if (s < end && *s == ':')
|
||||
{
|
||||
if (gap >= 0)
|
||||
return 0;
|
||||
gap = n;
|
||||
s++;
|
||||
}
|
||||
else if (s == end)
|
||||
return 0; /* trailing single colon */
|
||||
}
|
||||
if (gap >= 0)
|
||||
{
|
||||
int fill = 16 - n;
|
||||
if (fill < 2)
|
||||
return 0;
|
||||
memmove(a + gap + fill, a + gap, (size_t)(n - gap));
|
||||
memset(a + gap, 0, (size_t)fill);
|
||||
n = 16;
|
||||
}
|
||||
if (n != 16)
|
||||
return 0;
|
||||
memcpy(out, a, 16);
|
||||
return 16;
|
||||
}
|
||||
|
||||
int x509_match_hostname(const struct x509_cert *c, const char *host)
|
||||
{
|
||||
uint8_t ip[16];
|
||||
int iplen;
|
||||
|
||||
if (!host || !*host)
|
||||
return -1;
|
||||
|
||||
/* An IP address matches only an iPAddress entry, byte for byte; never
|
||||
* a dNSName or the CN, which would let a name certificate whose CN
|
||||
* happens to read "10.0.0.1" vouch for that address (RFC 6125 6.2.1,
|
||||
* RFC 9525 6.3). */
|
||||
if ((iplen = x509_parse_ip(host, ip)) != 0)
|
||||
{
|
||||
struct der san;
|
||||
if (!c->san)
|
||||
return -1;
|
||||
san.p = c->san;
|
||||
san.end = c->san + c->san_len;
|
||||
while (san.p < san.end)
|
||||
{
|
||||
const uint8_t *v;
|
||||
size_t vl;
|
||||
uint8_t tag = *san.p;
|
||||
if (der_read(&san, tag, &v, &vl) != 0)
|
||||
return -1;
|
||||
if (tag == 0x87 && vl == (size_t)iplen && !memcmp(v, ip, vl))
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (c->san)
|
||||
{
|
||||
struct der san;
|
||||
|
||||
@@ -124,12 +124,23 @@ int x509_verify_ecdsa_digest(const struct x509_cert *key,
|
||||
*
|
||||
* dNSName entries of the subjectAltName, with a wildcard allowed as
|
||||
* the whole leftmost label; the subject commonName only when there is
|
||||
* no subjectAltName at all.
|
||||
* no subjectAltName at all. An IP address (see x509_parse_ip()) matches
|
||||
* only an iPAddress entry, never a dNSName or the commonName.
|
||||
*
|
||||
* Returns: 0 on a match, -1 otherwise.
|
||||
**/
|
||||
int x509_match_hostname(const struct x509_cert *c, const char *host);
|
||||
|
||||
/**
|
||||
* x509_parse_ip:
|
||||
*
|
||||
* @host as an IP address: dotted IPv4 (4 bytes, no leading zeros), or
|
||||
* IPv6 with "::" compression and an optional trailing dotted IPv4,
|
||||
* brackets and a "%zone" allowed (16 bytes). Returns 4 or 16 with the
|
||||
* address in @out, 0 when @host is not an IP literal - a host name.
|
||||
**/
|
||||
int x509_parse_ip(const char *host, uint8_t out[16]);
|
||||
|
||||
/**
|
||||
* x509_trust_load_pem:
|
||||
* @pem, @len : Concatenated PEM certificates (cacert.h).
|
||||
|
||||
@@ -1680,6 +1680,21 @@ static bool net_http_connect(struct http_t *state)
|
||||
#else
|
||||
if (state->ssl)
|
||||
{
|
||||
/* The TLS layer gets the host as a server name, so an IPv6
|
||||
* literal goes without the brackets the URL and the Host header
|
||||
* need; the backend then checks the certificate against the
|
||||
* address rather than as a name. */
|
||||
char tls_host[256];
|
||||
const char *tls_host_src = state->request.domain;
|
||||
size_t dl = strlen(tls_host_src);
|
||||
if ( tls_host_src[0] == '[' && dl > 2 && tls_host_src[dl - 1] == ']'
|
||||
&& dl - 2 < sizeof(tls_host))
|
||||
{
|
||||
memcpy(tls_host, tls_host_src + 1, dl - 2);
|
||||
tls_host[dl - 2] = '\0';
|
||||
}
|
||||
else
|
||||
strlcpy(tls_host, tls_host_src, sizeof(tls_host));
|
||||
if (!conn)
|
||||
{
|
||||
net_http_log_transport_state(state, "connect_missing_dns_or_conn", -1);
|
||||
@@ -1688,7 +1703,7 @@ static bool net_http_connect(struct http_t *state)
|
||||
for (next_addr = addr; next_addr && conn->fd >= 0;
|
||||
conn->fd = net_http_socket_next(&next_addr))
|
||||
{
|
||||
if (!(conn->ssl_ctx = ssl_socket_init(conn->fd, state->request.domain)))
|
||||
if (!(conn->ssl_ctx = ssl_socket_init(conn->fd, tls_host)))
|
||||
{
|
||||
net_http_log_transport_state(state, "ssl_init_failed", -1);
|
||||
socket_close(conn->fd);
|
||||
|
||||
@@ -885,7 +885,11 @@ static int tls_send_client_hello(struct ssl_state *s)
|
||||
const uint8_t *suites = crypto_aes_hw() ? suites_aes : suites_chacha;
|
||||
uint8_t *h;
|
||||
uint8_t *p;
|
||||
size_t dlen = s->domain ? strlen(s->domain) : 0;
|
||||
uint8_t iplit[16];
|
||||
/* No server_name for an IP address: RFC 6066 3 allows host names
|
||||
* only, and the certificate is checked against the address anyway. */
|
||||
size_t dlen = (s->domain && !x509_parse_ip(s->domain, iplit))
|
||||
? strlen(s->domain) : 0;
|
||||
size_t ext_len;
|
||||
int ret;
|
||||
const struct tls_session *cached;
|
||||
@@ -2160,15 +2164,19 @@ void* ssl_socket_init(int fd, const char *domain)
|
||||
}
|
||||
#endif
|
||||
s->fd = fd;
|
||||
s->domain = domain;
|
||||
/* A copy: the connection can outlive the caller's string - net_http
|
||||
* pools it after the transfer that opened it is gone, and a TLS 1.3
|
||||
* ticket arriving later is filed under this name. */
|
||||
s->domain = domain ? strdup(domain) : NULL;
|
||||
s->rx = (uint8_t*)malloc(TLS_RX_SIZE);
|
||||
s->hs = (uint8_t*)malloc(TLS_HS_MAX);
|
||||
s->cork = (uint8_t*)malloc(TLS_CORK_MAX);
|
||||
if (!s->rx || !s->hs || !s->cork)
|
||||
if (!s->rx || !s->hs || !s->cork || (domain && !s->domain))
|
||||
{
|
||||
free(s->cork);
|
||||
free(s->rx);
|
||||
free(s->hs);
|
||||
free((char*)s->domain);
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2454,6 +2462,7 @@ void ssl_socket_free(void *state_data)
|
||||
free(s->hs);
|
||||
free(s->cork);
|
||||
free(s->tx);
|
||||
free((char*)s->domain);
|
||||
free(s);
|
||||
}
|
||||
|
||||
|
||||
@@ -1044,6 +1044,71 @@ START_TEST(test_bulk_paths)
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* IP literals: parsed exactly, and matched only against iPAddress SAN
|
||||
* entries - never a dNSName or the commonName that happens to spell the
|
||||
* address. */
|
||||
START_TEST (test_x509_ip_literals)
|
||||
{
|
||||
static const uint8_t v6_loop[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 };
|
||||
static const uint8_t v6_mapped[16] = { 0,0,0,0,0,0,0,0,0,0,0xff,0xff,192,0,2,1 };
|
||||
/* GeneralNames contents: dNSName example.com, iPAddress 127.0.0.1,
|
||||
* iPAddress ::1, and a dNSName that merely reads "10.0.0.1" */
|
||||
static const uint8_t san[] = {
|
||||
0x82, 11, 'e','x','a','m','p','l','e','.','c','o','m',
|
||||
0x87, 4, 127, 0, 0, 1,
|
||||
0x87, 16, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
|
||||
0x82, 8, '1','0','.','0','.','0','.','1'
|
||||
};
|
||||
static const uint8_t cn_ip[] = { '1','0','.','0','.','0','.','1' };
|
||||
struct x509_cert c;
|
||||
uint8_t ip[16];
|
||||
|
||||
ck_assert_int_eq(x509_parse_ip("127.0.0.1", ip), 4);
|
||||
ck_assert(ip[0] == 127 && ip[1] == 0 && ip[2] == 0 && ip[3] == 1);
|
||||
ck_assert_int_eq(x509_parse_ip("::1", ip), 16);
|
||||
ck_assert(!memcmp(ip, v6_loop, 16));
|
||||
ck_assert_int_eq(x509_parse_ip("[::1]", ip), 16);
|
||||
ck_assert(!memcmp(ip, v6_loop, 16));
|
||||
ck_assert_int_eq(x509_parse_ip("0:0:0:0:0:0:0:1", ip), 16);
|
||||
ck_assert(!memcmp(ip, v6_loop, 16));
|
||||
ck_assert_int_eq(x509_parse_ip("fe80::1%25eth0", ip), 16);
|
||||
ck_assert(ip[0] == 0xfe && ip[1] == 0x80 && ip[15] == 1);
|
||||
ck_assert_int_eq(x509_parse_ip("::ffff:192.0.2.1", ip), 16);
|
||||
ck_assert(!memcmp(ip, v6_mapped, 16));
|
||||
ck_assert_int_eq(x509_parse_ip("::", ip), 16);
|
||||
/* not IP literals: host names, and malformed addresses */
|
||||
ck_assert_int_eq(x509_parse_ip("example.com", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("1.2.3", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("1.2.3.4.5", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("256.0.0.1", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("01.2.3.4", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("1::2::3", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("1:2:3:4:5:6:7:8:9", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("1:2:3:4:5:6:7:8::", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip(":1", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("1:", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("12345::1", ip), 0);
|
||||
ck_assert_int_eq(x509_parse_ip("[::1", ip), 0);
|
||||
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.san = san;
|
||||
c.san_len = sizeof(san);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "example.com"), 0);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "127.0.0.1"), 0);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "::1"), 0);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "0::0:1"), 0);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "127.0.0.2"), -1);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "::2"), -1);
|
||||
/* an address the certificate lists only as a dNSName string */
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "10.0.0.1"), -1);
|
||||
/* nor through the commonName, with no subjectAltName at all */
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.cn = cn_ip;
|
||||
c.cn_len = sizeof(cn_ip);
|
||||
ck_assert_int_eq(x509_match_hostname(&c, "10.0.0.1"), -1);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
Suite *create_suite(void)
|
||||
{
|
||||
Suite *s = suite_create(SUITE_NAME);
|
||||
@@ -1068,6 +1133,7 @@ Suite *create_suite(void)
|
||||
tcase_add_test(tc_core, test_p384);
|
||||
tcase_add_test(tc_core, test_smb_prereqs);
|
||||
tcase_add_test(tc_core, test_x509);
|
||||
tcase_add_test(tc_core, test_x509_ip_literals);
|
||||
tcase_add_test(tc_core, test_aes_decrypt_hmac_sha1);
|
||||
tcase_add_test(tc_core, test_krb5_enctypes);
|
||||
tcase_add_test(tc_core, test_bulk_paths);
|
||||
|
||||
@@ -37,6 +37,16 @@ mkleafx clientonly 'extendedKeyUsage=clientAuth\n'
|
||||
mkleafx codesign 'extendedKeyUsage=codeSigning\n'
|
||||
mkleafx serverauth 'extendedKeyUsage=serverAuth,clientAuth\nkeyUsage=digitalSignature,keyEncipherment\n'
|
||||
mkleafx nosign 'keyUsage=keyEncipherment,dataEncipherment\n'
|
||||
# IP addresses: matched against iPAddress SAN entries only. One leaf
|
||||
# carries IP:127.0.0.1, the other the same address as a DNS name, which
|
||||
# must not vouch for the address.
|
||||
mkleafip() { # name san
|
||||
openssl req -newkey rsa:2048 -nodes -keyout $D/$1.key -out $D/$1.csr -subj "/CN=127.0.0.1" 2>/dev/null
|
||||
printf 'subjectAltName=%s\nbasicConstraints=CA:FALSE\n' "$2" > $D/$1.ext
|
||||
openssl x509 -req -in $D/$1.csr -CA $D/ca.pem -CAkey $D/ca.key -CAcreateserial -out $D/$1.pem -days 2 -extfile $D/$1.ext 2>/dev/null
|
||||
}
|
||||
mkleafip ipsan "IP:127.0.0.1"
|
||||
mkleafip ipdns "DNS:127.0.0.1"
|
||||
# a real-world shape: root -> intermediate CA -> leaf, the server sending
|
||||
# leaf + intermediate and the client trusting only the root; one with an
|
||||
# RSA intermediate, one with a P-384 intermediate under the RSA root
|
||||
@@ -83,6 +93,8 @@ run "policy: clientAuth-only EKU is refused" clientonly ECDHE-RSA-A
|
||||
run "policy: codeSigning-only EKU is refused" codesign ECDHE-RSA-AES128-GCM-SHA256 prime256v1 1 localhost 0 $D/ca.pem
|
||||
run "policy: serverAuth EKU with digitalSignature is fine" serverauth ECDHE-RSA-AES128-GCM-SHA256 prime256v1 0 localhost 0 $D/ca.pem
|
||||
run "policy: keyUsage without digitalSignature is refused" nosign ECDHE-RSA-AES128-GCM-SHA256 prime256v1 1 localhost 0 $D/ca.pem
|
||||
run "IP address matches an iPAddress entry" ipsan ECDHE-RSA-AES128-GCM-SHA256 prime256v1 0 127.0.0.1 0 $D/ca.pem
|
||||
run "IP address written as a DNS name is refused" ipdns ECDHE-RSA-AES128-GCM-SHA256 prime256v1 1 127.0.0.1 0 $D/ca.pem
|
||||
run "chain: root -> RSA intermediate -> leaf" viarsa ECDHE-RSA-AES128-GCM-SHA256 prime256v1 0 localhost 0 $D/ca.pem
|
||||
run "chain: root -> P-384 intermediate -> leaf" viap384 ECDHE-RSA-AES128-GCM-SHA256 prime256v1 0 localhost 0 $D/ca.pem
|
||||
run "chain: trusting the intermediate directly also works" viarsa ECDHE-RSA-AES128-GCM-SHA256 prime256v1 0 localhost 0 $D/irsa.pem
|
||||
|
||||
Reference in new issue
Block a user