summaryrefslogtreecommitdiff
path: root/src/interfaces/libpq/fe-secure-common.c
blob: de115b376498337c62955af84c7629bcf083f23d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/*-------------------------------------------------------------------------
 *
 * fe-secure-common.c
 *
 * common implementation-independent SSL support code
 *
 * While fe-secure.c contains the interfaces that the rest of libpq call, this
 * file contains support routines that are used by the library-specific
 * implementations such as fe-secure-openssl.c.
 *
 * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/interfaces/libpq/fe-secure-common.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres_fe.h"

#include <arpa/inet.h>

#include "fe-secure-common.h"

#include "libpq-int.h"
#include "pqexpbuffer.h"

/*
 * Check if a wildcard certificate matches the server hostname.
 *
 * The rule for this is:
 *	1. We only match the '*' character as wildcard
 *	2. We match only wildcards at the start of the string
 *	3. The '*' character does *not* match '.', meaning that we match only
 *	   a single pathname component.
 *	4. We don't support more than one '*' in a single pattern.
 *
 * This is roughly in line with RFC2818, but contrary to what most browsers
 * appear to be implementing (point 3 being the difference)
 *
 * Matching is always case-insensitive, since DNS is case insensitive.
 */
static bool
wildcard_certificate_match(const char *pattern, const char *string)
{
	int			lenpat = strlen(pattern);
	int			lenstr = strlen(string);

	/* If we don't start with a wildcard, it's not a match (rule 1 & 2) */
	if (lenpat < 3 ||
		pattern[0] != '*' ||
		pattern[1] != '.')
		return false;

	/* If pattern is longer than the string, we can never match */
	if (lenpat > lenstr)
		return false;

	/*
	 * If string does not end in pattern (minus the wildcard), we don't match
	 */
	if (pg_strcasecmp(pattern + 1, string + lenstr - lenpat + 1) != 0)
		return false;

	/*
	 * If there is a dot left of where the pattern started to match, we don't
	 * match (rule 3)
	 */
	if (strchr(string, '.') < string + lenstr - lenpat)
		return false;

	/* String ended with pattern, and didn't have a dot before, so we match */
	return true;
}

/*
 * Check if a name from a server's certificate matches the peer's hostname.
 *
 * Returns 1 if the name matches, and 0 if it does not. On error, returns
 * -1, and sets the libpq error message.
 *
 * The name extracted from the certificate is returned in *store_name. The
 * caller is responsible for freeing it.
 */
int
pq_verify_peer_name_matches_certificate_name(PGconn *conn,
											 const char *namedata, size_t namelen,
											 char **store_name)
{
	char	   *name;
	int			result;
	char	   *host = conn->connhost[conn->whichhost].host;

	*store_name = NULL;

	if (!(host && host[0] != '\0'))
	{
		libpq_append_conn_error(conn, "host name must be specified");
		return -1;
	}

	/*
	 * There is no guarantee the string returned from the certificate is
	 * NULL-terminated, so make a copy that is.
	 */
	name = malloc(namelen + 1);
	if (name == NULL)
	{
		libpq_append_conn_error(conn, "out of memory");
		return -1;
	}
	memcpy(name, namedata, namelen);
	name[namelen] = '\0';

	/*
	 * Reject embedded NULLs in certificate common or alternative name to
	 * prevent attacks like CVE-2009-4034.
	 */
	if (namelen != strlen(name))
	{
		free(name);
		libpq_append_conn_error(conn, "SSL certificate's name contains embedded null");
		return -1;
	}

	if (pg_strcasecmp(name, host) == 0)
	{
		/* Exact name match */
		result = 1;
	}
	else if (wildcard_certificate_match(name, host))
	{
		/* Matched wildcard name */
		result = 1;
	}
	else
	{
		result = 0;
	}

	*store_name = name;
	return result;
}

/*
 * Check if an IP address from a server's certificate matches the peer's
 * hostname (which must itself be an IPv4/6 address).
 *
 * Returns 1 if the address matches, and 0 if it does not. On error, returns
 * -1, and sets the libpq error message.
 *
 * A string representation of the certificate's IP address is returned in
 * *store_name. The caller is responsible for freeing it.
 */
int
pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
										   const unsigned char *ipdata,
										   size_t iplen,
										   char **store_name)
{
	char	   *addrstr;
	int			match = 0;
	char	   *host = conn->connhost[conn->whichhost].host;
	int			family;
	char		tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"];
	char		sebuf[PG_STRERROR_R_BUFLEN];

	*store_name = NULL;

	if (!(host && host[0] != '\0'))
	{
		libpq_append_conn_error(conn, "host name must be specified");
		return -1;
	}

	/*
	 * The data from the certificate is in network byte order. Convert our
	 * host string to network-ordered bytes as well, for comparison. (The host
	 * string isn't guaranteed to actually be an IP address, so if this
	 * conversion fails we need to consider it a mismatch rather than an
	 * error.)
	 */
	if (iplen == 4)
	{
		/* IPv4 */
		struct in_addr addr;

		family = AF_INET;

		/*
		 * The use of inet_aton() is deliberate; we accept alternative IPv4
		 * address notations that are accepted by inet_aton() but not
		 * inet_pton() as server addresses.
		 */
		if (inet_aton(host, &addr))
		{
			if (memcmp(ipdata, &addr.s_addr, iplen) == 0)
				match = 1;
		}
	}

	/*
	 * If they don't have inet_pton(), skip this.  Then, an IPv6 address in a
	 * certificate will cause an error.
	 */
#ifdef HAVE_INET_PTON
	else if (iplen == 16)
	{
		/* IPv6 */
		struct in6_addr addr;

		family = AF_INET6;

		if (inet_pton(AF_INET6, host, &addr) == 1)
		{
			if (memcmp(ipdata, &addr.s6_addr, iplen) == 0)
				match = 1;
		}
	}
#endif
	else
	{
		/*
		 * Not IPv4 or IPv6. We could ignore the field, but leniency seems
		 * wrong given the subject matter.
		 */
		libpq_append_conn_error(conn, "certificate contains IP address with invalid length %zu",
						  iplen);
		return -1;
	}

	/* Generate a human-readable representation of the certificate's IP. */
	addrstr = pg_inet_net_ntop(family, ipdata, 8 * iplen, tmp, sizeof(tmp));
	if (!addrstr)
	{
		libpq_append_conn_error(conn, "could not convert certificate's IP address to string: %s",
						  strerror_r(errno, sebuf, sizeof(sebuf)));
		return -1;
	}

	*store_name = strdup(addrstr);
	return match;
}

/*
 * Verify that the server certificate matches the hostname we connected to.
 *
 * The certificate's Common Name and Subject Alternative Names are considered.
 */
bool
pq_verify_peer_name_matches_certificate(PGconn *conn)
{
	char	   *host = conn->connhost[conn->whichhost].host;
	int			rc;
	int			names_examined = 0;
	char	   *first_name = NULL;

	/*
	 * If told not to verify the peer name, don't do it. Return true
	 * indicating that the verification was successful.
	 */
	if (strcmp(conn->sslmode, "verify-full") != 0)
		return true;

	/* Check that we have a hostname to compare with. */
	if (!(host && host[0] != '\0'))
	{
		libpq_append_conn_error(conn, "host name must be specified for a verified SSL connection");
		return false;
	}

	rc = pgtls_verify_peer_name_matches_certificate_guts(conn, &names_examined, &first_name);

	if (rc == 0)
	{
		/*
		 * No match. Include the name from the server certificate in the error
		 * message, to aid debugging broken configurations. If there are
		 * multiple names, only print the first one to avoid an overly long
		 * error message.
		 */
		if (names_examined > 1)
		{
			appendPQExpBuffer(&conn->errorMessage,
							  libpq_ngettext("server certificate for \"%s\" (and %d other name) does not match host name \"%s\"",
											 "server certificate for \"%s\" (and %d other names) does not match host name \"%s\"",
											 names_examined - 1),
							  first_name, names_examined - 1, host);
			appendPQExpBufferChar(&conn->errorMessage, '\n');
		}
		else if (names_examined == 1)
		{
			libpq_append_conn_error(conn, "server certificate for \"%s\" does not match host name \"%s\"",
							  first_name, host);
		}
		else
		{
			libpq_append_conn_error(conn, "could not get server's host name from server certificate");
		}
	}

	/* clean up */
	free(first_name);

	return (rc == 1);
}