Skip to content

Commit fd09728

Browse files
lucasnetaubukka
authored andcommitted
Fix bug GH-9356: Incomplete SAN validation of IPv6 address
IPv6 addresses are valid entries in subjectAltNames. Certificate Authorities may issue certificates including IPv6 addresses except if they fall within addresses in the RFC 4193 range. Google and CloudFlare provide IPv6 addresses in their DNS over HTTPS services. Internal CAs do not have those restrictions and can issue Unique local addresses in certificates. Closes GH-11145
1 parent 06d6873 commit fd09728

File tree

3 files changed

+116
-4
lines changed

3 files changed

+116
-4
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ PHP NEWS
3434
. Fixed bug GH-11336 (php still tries to unlock the shared memory ZendSem
3535
with opcache.file_cache_only=1 but it was never locked). (nielsdos)
3636

37+
- OpenSSL:
38+
. Fixed bug GH-9356 Incomplete validation of IPv6 Address fields in
39+
subjectAltNames (James Lucas, Jakub Zelenka).
40+
3741
- SPL:
3842
. Fixed bug GH-11338 (SplFileInfo empty getBasename with more than one
3943
slash). (nielsdos)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
--TEST--
2+
IPv6 Peer verification matches SAN names
3+
--EXTENSIONS--
4+
openssl
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
if (getenv("CI_NO_IPV6") || !defined("AF_INET6")) {
9+
die('skip no IPv6 support');
10+
}
11+
if (@stream_socket_client('udp://[::1]:8888') === false)
12+
die('skip no IPv6 support');
13+
?>
14+
--FILE--
15+
<?php
16+
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'san_ipv6_peer_matching.pem.tmp';
17+
$san = 'IP:2001:db8:85a3:8d3:1319:8a2e:370:7348';
18+
19+
$serverCode = <<<'CODE'
20+
$serverUri = "ssl://[::1]:64324";
21+
$serverFlags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN;
22+
$serverCtx = stream_context_create(['ssl' => [
23+
'local_cert' => '%s',
24+
]]);
25+
26+
$server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx);
27+
phpt_notify();
28+
29+
@stream_socket_accept($server, 1);
30+
@stream_socket_accept($server, 1);
31+
CODE;
32+
$serverCode = sprintf($serverCode, $certFile);
33+
34+
$clientCode = <<<'CODE'
35+
$serverUri = "ssl://[::1]:64324";
36+
$clientFlags = STREAM_CLIENT_CONNECT;
37+
$clientCtx = stream_context_create(['ssl' => [
38+
'verify_peer' => false,
39+
]]);
40+
41+
phpt_wait();
42+
43+
stream_context_set_option($clientCtx, 'ssl', 'peer_name', '2001:db8:85a3:8d3:1319:8a2e:370:7348');
44+
var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx));
45+
46+
stream_context_set_option($clientCtx, 'ssl', 'peer_name', '2001:db8:85a3:8d3:1319:8a2e:370:7349');
47+
var_dump(stream_socket_client($serverUri, $errno, $errstr, 1, $clientFlags, $clientCtx));
48+
CODE;
49+
50+
include 'CertificateGenerator.inc';
51+
$certificateGenerator = new CertificateGenerator();
52+
$certificateGenerator->saveNewCertAsFileWithKey(null, $certFile, null, $san);
53+
54+
include 'ServerClientTestCase.inc';
55+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
56+
?>
57+
--CLEAN--
58+
<?php
59+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'san_ipv6_peer_matching.pem.tmp');
60+
?>
61+
--EXPECTF--
62+
resource(%d) of type (stream)
63+
64+
Warning: stream_socket_client(): Unable to locate peer certificate CN in %s on line %d
65+
66+
Warning: stream_socket_client(): Failed to enable crypto in %s on line %d
67+
68+
Warning: stream_socket_client(): Unable to connect to ssl://[::1]:64324 (Unknown error) in %s on line %d
69+
bool(false)

ext/openssl/xp_ssl.c

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,18 @@
3939
#ifdef PHP_WIN32
4040
#include "win32/winutil.h"
4141
#include "win32/time.h"
42+
#include <Ws2tcpip.h>
4243
#include <Wincrypt.h>
4344
/* These are from Wincrypt.h, they conflict with OpenSSL */
4445
#undef X509_NAME
4546
#undef X509_CERT_PAIR
4647
#undef X509_EXTENSIONS
4748
#endif
4849

50+
#ifdef HAVE_ARPA_INET_H
51+
#include <arpa/inet.h>
52+
#endif
53+
4954
/* Flags for determining allowed stream crypto methods */
5055
#define STREAM_CRYPTO_IS_CLIENT (1<<0)
5156
#define STREAM_CRYPTO_METHOD_SSLv2 (1<<1)
@@ -110,6 +115,21 @@
110115
#define PHP_X509_NAME_ENTRY_TO_UTF8(ne, i, out) \
111116
ASN1_STRING_to_UTF8(&out, X509_NAME_ENTRY_get_data(X509_NAME_get_entry(ne, i)))
112117

118+
/* Used for IPv6 Address peer verification */
119+
#define EXPAND_IPV6_ADDRESS(_str, _bytes) \
120+
do { \
121+
snprintf(_str, 40, "%X:%X:%X:%X:%X:%X:%X:%X", \
122+
_bytes[0] << 8 | _bytes[1], \
123+
_bytes[2] << 8 | _bytes[3], \
124+
_bytes[4] << 8 | _bytes[5], \
125+
_bytes[6] << 8 | _bytes[7], \
126+
_bytes[8] << 8 | _bytes[9], \
127+
_bytes[10] << 8 | _bytes[11], \
128+
_bytes[12] << 8 | _bytes[13], \
129+
_bytes[14] << 8 | _bytes[15] \
130+
); \
131+
} while(0)
132+
113133
#if PHP_OPENSSL_API_VERSION < 0x10100
114134
static RSA *php_openssl_tmp_rsa_cb(SSL *s, int is_export, int keylength);
115135
#endif
@@ -421,6 +441,18 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) /
421441
GENERAL_NAMES *alt_names = X509_get_ext_d2i(peer, NID_subject_alt_name, 0, 0);
422442
int alt_name_count = sk_GENERAL_NAME_num(alt_names);
423443

444+
#if defined(HAVE_IPV6) && defined(HAVE_INET_PTON)
445+
/* detect if subject name is an IPv6 address and expand once if required */
446+
char subject_name_ipv6_expanded[40];
447+
unsigned char ipv6[16];
448+
bool subject_name_is_ipv6 = false;
449+
subject_name_ipv6_expanded[0] = 0;
450+
if (inet_pton(AF_INET6, subject_name, &ipv6)) {
451+
EXPAND_IPV6_ADDRESS(subject_name_ipv6_expanded, ipv6);
452+
subject_name_is_ipv6 = true;
453+
}
454+
#endif
455+
424456
for (i = 0; i < alt_name_count; i++) {
425457
GENERAL_NAME *san = sk_GENERAL_NAME_value(alt_names, i);
426458

@@ -459,10 +491,17 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) /
459491
return 1;
460492
}
461493
}
462-
/* No, we aren't bothering to check IPv6 addresses. Why?
463-
* Because IP SAN names are officially deprecated and are
464-
* not allowed by CAs starting in 2015. Deal with it.
465-
*/
494+
#if defined(HAVE_IPV6) && defined(HAVE_INET_PTON)
495+
else if (san->d.ip->length == 16 && subject_name_is_ipv6) {
496+
ipbuffer[0] = 0;
497+
EXPAND_IPV6_ADDRESS(ipbuffer, san->d.iPAddress->data);
498+
if (strcasecmp((const char*)subject_name_ipv6_expanded, (const char*)ipbuffer) == 0) {
499+
sk_GENERAL_NAME_pop_free(alt_names, GENERAL_NAME_free);
500+
501+
return 1;
502+
}
503+
}
504+
#endif
466505
}
467506
}
468507

0 commit comments

Comments
 (0)