Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * fe-secure-common.c
4 : : *
5 : : * common implementation-independent SSL support code
6 : : *
7 : : * While fe-secure.c contains the interfaces that the rest of libpq call, this
8 : : * file contains support routines that are used by the library-specific
9 : : * implementations such as fe-secure-openssl.c.
10 : : *
11 : : * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
12 : : * Portions Copyright (c) 1994, Regents of the University of California
13 : : *
14 : : * IDENTIFICATION
15 : : * src/interfaces/libpq/fe-secure-common.c
16 : : *
17 : : *-------------------------------------------------------------------------
18 : : */
19 : :
20 : : #include "postgres_fe.h"
21 : :
22 : : #include <arpa/inet.h>
23 : :
24 : : #include "fe-secure-common.h"
25 : :
26 : : #include "libpq-int.h"
27 : : #include "pqexpbuffer.h"
28 : :
29 : : /*
30 : : * Check if a wildcard certificate matches the server hostname.
31 : : *
32 : : * The rule for this is:
33 : : * 1. We only match the '*' character as wildcard
34 : : * 2. We match only wildcards at the start of the string
35 : : * 3. The '*' character does *not* match '.', meaning that we match only
36 : : * a single pathname component.
37 : : * 4. We don't support more than one '*' in a single pattern.
38 : : *
39 : : * This is roughly in line with RFC2818, but contrary to what most browsers
40 : : * appear to be implementing (point 3 being the difference)
41 : : *
42 : : * Matching is always case-insensitive, since DNS is case insensitive.
43 : : */
44 : : static bool
2269 peter_e@gmx.net 45 :CBC 21 : wildcard_certificate_match(const char *pattern, const char *string)
46 : : {
47 : 21 : int lenpat = strlen(pattern);
48 : 21 : int lenstr = strlen(string);
49 : :
50 : : /* If we don't start with a wildcard, it's not a match (rule 1 & 2) */
51 [ + - ]: 21 : if (lenpat < 3 ||
52 [ + + ]: 21 : pattern[0] != '*' ||
53 [ - + ]: 3 : pattern[1] != '.')
54 : 18 : return false;
55 : :
56 : : /* If pattern is longer than the string, we can never match */
57 [ - + ]: 3 : if (lenpat > lenstr)
2269 peter_e@gmx.net 58 :UBC 0 : return false;
59 : :
60 : : /*
61 : : * If string does not end in pattern (minus the wildcard), we don't match
62 : : */
2269 peter_e@gmx.net 63 [ + + ]:CBC 3 : if (pg_strcasecmp(pattern + 1, string + lenstr - lenpat + 1) != 0)
64 : 1 : return false;
65 : :
66 : : /*
67 : : * If there is a dot left of where the pattern started to match, we don't
68 : : * match (rule 3)
69 : : */
70 [ + + ]: 2 : if (strchr(string, '.') < string + lenstr - lenpat)
71 : 1 : return false;
72 : :
73 : : /* String ended with pattern, and didn't have a dot before, so we match */
74 : 1 : return true;
75 : : }
76 : :
77 : : /*
78 : : * Check if a name from a server's certificate matches the peer's hostname.
79 : : *
80 : : * Returns 1 if the name matches, and 0 if it does not. On error, returns
81 : : * -1, and sets the libpq error message.
82 : : *
83 : : * The name extracted from the certificate is returned in *store_name. The
84 : : * caller is responsible for freeing it.
85 : : */
86 : : int
87 : 34 : pq_verify_peer_name_matches_certificate_name(PGconn *conn,
88 : : const char *namedata, size_t namelen,
89 : : char **store_name)
90 : : {
91 : : char *name;
92 : : int result;
2081 tgl@sss.pgh.pa.us 93 : 34 : char *host = conn->connhost[conn->whichhost].host;
94 : :
2269 peter_e@gmx.net 95 : 34 : *store_name = NULL;
96 : :
2081 tgl@sss.pgh.pa.us 97 [ + - - + ]: 34 : if (!(host && host[0] != '\0'))
98 : : {
516 peter@eisentraut.org 99 :UBC 0 : libpq_append_conn_error(conn, "host name must be specified");
2081 tgl@sss.pgh.pa.us 100 : 0 : return -1;
101 : : }
102 : :
103 : : /*
104 : : * There is no guarantee the string returned from the certificate is
105 : : * NULL-terminated, so make a copy that is.
106 : : */
2269 peter_e@gmx.net 107 :CBC 34 : name = malloc(namelen + 1);
108 [ - + ]: 34 : if (name == NULL)
109 : : {
516 peter@eisentraut.org 110 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
2269 peter_e@gmx.net 111 : 0 : return -1;
112 : : }
2269 peter_e@gmx.net 113 :CBC 34 : memcpy(name, namedata, namelen);
114 : 34 : name[namelen] = '\0';
115 : :
116 : : /*
117 : : * Reject embedded NULLs in certificate common or alternative name to
118 : : * prevent attacks like CVE-2009-4034.
119 : : */
120 [ - + ]: 34 : if (namelen != strlen(name))
121 : : {
2269 peter_e@gmx.net 122 :UBC 0 : free(name);
516 peter@eisentraut.org 123 : 0 : libpq_append_conn_error(conn, "SSL certificate's name contains embedded null");
2269 peter_e@gmx.net 124 : 0 : return -1;
125 : : }
126 : :
2269 peter_e@gmx.net 127 [ + + ]:CBC 34 : if (pg_strcasecmp(name, host) == 0)
128 : : {
129 : : /* Exact name match */
130 : 13 : result = 1;
131 : : }
132 [ + + ]: 21 : else if (wildcard_certificate_match(name, host))
133 : : {
134 : : /* Matched wildcard name */
135 : 1 : result = 1;
136 : : }
137 : : else
138 : : {
139 : 20 : result = 0;
140 : : }
141 : :
142 : 34 : *store_name = name;
143 : 34 : return result;
144 : : }
145 : :
146 : : /*
147 : : * Check if an IP address from a server's certificate matches the peer's
148 : : * hostname (which must itself be an IPv4/6 address).
149 : : *
150 : : * Returns 1 if the address matches, and 0 if it does not. On error, returns
151 : : * -1, and sets the libpq error message.
152 : : *
153 : : * A string representation of the certificate's IP address is returned in
154 : : * *store_name. The caller is responsible for freeing it.
155 : : */
156 : : int
744 peter@eisentraut.org 157 : 24 : pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
158 : : const unsigned char *ipdata,
159 : : size_t iplen,
160 : : char **store_name)
161 : : {
162 : : char *addrstr;
163 : 24 : int match = 0;
164 : 24 : char *host = conn->connhost[conn->whichhost].host;
165 : : int family;
166 : : char tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"];
167 : : char sebuf[PG_STRERROR_R_BUFLEN];
168 : :
169 : 24 : *store_name = NULL;
170 : :
171 [ + - - + ]: 24 : if (!(host && host[0] != '\0'))
172 : : {
516 peter@eisentraut.org 173 :UBC 0 : libpq_append_conn_error(conn, "host name must be specified");
744 174 : 0 : return -1;
175 : : }
176 : :
177 : : /*
178 : : * The data from the certificate is in network byte order. Convert our
179 : : * host string to network-ordered bytes as well, for comparison. (The host
180 : : * string isn't guaranteed to actually be an IP address, so if this
181 : : * conversion fails we need to consider it a mismatch rather than an
182 : : * error.)
183 : : */
744 peter@eisentraut.org 184 [ + + ]:CBC 24 : if (iplen == 4)
185 : : {
186 : : /* IPv4 */
187 : : struct in_addr addr;
188 : :
189 : 14 : family = AF_INET;
190 : :
191 : : /*
192 : : * The use of inet_aton() is deliberate; we accept alternative IPv4
193 : : * address notations that are accepted by inet_aton() but not
194 : : * inet_pton() as server addresses.
195 : : */
196 [ + + ]: 14 : if (inet_aton(host, &addr))
197 : : {
198 [ + + ]: 6 : if (memcmp(ipdata, &addr.s_addr, iplen) == 0)
199 : 4 : match = 1;
200 : : }
201 : : }
202 : :
203 : : /*
204 : : * If they don't have inet_pton(), skip this. Then, an IPv6 address in a
205 : : * certificate will cause an error.
206 : : */
207 : : #ifdef HAVE_INET_PTON
208 [ + - ]: 10 : else if (iplen == 16)
209 : : {
210 : : /* IPv6 */
211 : : struct in6_addr addr;
212 : :
213 : 10 : family = AF_INET6;
214 : :
215 [ + + ]: 10 : if (inet_pton(AF_INET6, host, &addr) == 1)
216 : : {
217 [ + + ]: 6 : if (memcmp(ipdata, &addr.s6_addr, iplen) == 0)
218 : 5 : match = 1;
219 : : }
220 : : }
221 : : #endif
222 : : else
223 : : {
224 : : /*
225 : : * Not IPv4 or IPv6. We could ignore the field, but leniency seems
226 : : * wrong given the subject matter.
227 : : */
516 peter@eisentraut.org 228 :UBC 0 : libpq_append_conn_error(conn, "certificate contains IP address with invalid length %zu",
229 : : iplen);
744 230 : 0 : return -1;
231 : : }
232 : :
233 : : /* Generate a human-readable representation of the certificate's IP. */
744 peter@eisentraut.org 234 :CBC 24 : addrstr = pg_inet_net_ntop(family, ipdata, 8 * iplen, tmp, sizeof(tmp));
235 [ - + ]: 24 : if (!addrstr)
236 : : {
516 peter@eisentraut.org 237 :UBC 0 : libpq_append_conn_error(conn, "could not convert certificate's IP address to string: %s",
331 tgl@sss.pgh.pa.us 238 : 0 : strerror_r(errno, sebuf, sizeof(sebuf)));
744 peter@eisentraut.org 239 : 0 : return -1;
240 : : }
241 : :
744 peter@eisentraut.org 242 :CBC 24 : *store_name = strdup(addrstr);
243 : 24 : return match;
244 : : }
245 : :
246 : : /*
247 : : * Verify that the server certificate matches the hostname we connected to.
248 : : *
249 : : * The certificate's Common Name and Subject Alternative Names are considered.
250 : : */
251 : : bool
2269 peter_e@gmx.net 252 : 175 : pq_verify_peer_name_matches_certificate(PGconn *conn)
253 : : {
2081 tgl@sss.pgh.pa.us 254 : 175 : char *host = conn->connhost[conn->whichhost].host;
255 : : int rc;
2269 peter_e@gmx.net 256 : 175 : int names_examined = 0;
257 : 175 : char *first_name = NULL;
258 : :
259 : : /*
260 : : * If told not to verify the peer name, don't do it. Return true
261 : : * indicating that the verification was successful.
262 : : */
263 [ + + ]: 175 : if (strcmp(conn->sslmode, "verify-full") != 0)
264 : 139 : return true;
265 : :
266 : : /* Check that we have a hostname to compare with. */
267 [ + - - + ]: 36 : if (!(host && host[0] != '\0'))
268 : : {
516 peter@eisentraut.org 269 :UBC 0 : libpq_append_conn_error(conn, "host name must be specified for a verified SSL connection");
2269 peter_e@gmx.net 270 : 0 : return false;
271 : : }
272 : :
2269 peter_e@gmx.net 273 :CBC 36 : rc = pgtls_verify_peer_name_matches_certificate_guts(conn, &names_examined, &first_name);
274 : :
275 [ + + ]: 36 : if (rc == 0)
276 : : {
277 : : /*
278 : : * No match. Include the name from the server certificate in the error
279 : : * message, to aid debugging broken configurations. If there are
280 : : * multiple names, only print the first one to avoid an overly long
281 : : * error message.
282 : : */
283 [ + + ]: 13 : if (names_examined > 1)
284 : : {
1189 tgl@sss.pgh.pa.us 285 : 7 : appendPQExpBuffer(&conn->errorMessage,
516 peter@eisentraut.org 286 : 7 : libpq_ngettext("server certificate for \"%s\" (and %d other name) does not match host name \"%s\"",
287 : : "server certificate for \"%s\" (and %d other names) does not match host name \"%s\"",
2269 peter_e@gmx.net 288 : 7 : names_examined - 1),
289 : : first_name, names_examined - 1, host);
516 peter@eisentraut.org 290 : 7 : appendPQExpBufferChar(&conn->errorMessage, '\n');
291 : : }
2269 peter_e@gmx.net 292 [ + + ]: 6 : else if (names_examined == 1)
293 : : {
516 peter@eisentraut.org 294 : 5 : libpq_append_conn_error(conn, "server certificate for \"%s\" does not match host name \"%s\"",
295 : : first_name, host);
296 : : }
297 : : else
298 : : {
299 : 1 : libpq_append_conn_error(conn, "could not get server's host name from server certificate");
300 : : }
301 : : }
302 : :
303 : : /* clean up */
668 304 : 36 : free(first_name);
305 : :
2269 peter_e@gmx.net 306 : 36 : return (rc == 1);
307 : : }
|