Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * be-secure-common.c
4 : : *
5 : : * common implementation-independent SSL support code
6 : : *
7 : : * While be-secure.c contains the interfaces that the rest of the
8 : : * communications code calls, this file contains support routines that are
9 : : * used by the library-specific implementations such as be-secure-openssl.c.
10 : : *
11 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
12 : : * Portions Copyright (c) 1994, Regents of the University of California
13 : : *
14 : : * IDENTIFICATION
15 : : * src/backend/libpq/be-secure-common.c
16 : : *
17 : : *-------------------------------------------------------------------------
18 : : */
19 : :
20 : : #include "postgres.h"
21 : :
22 : : #include <sys/stat.h>
23 : : #include <unistd.h>
24 : :
25 : : #include "common/percentrepl.h"
26 : : #include "common/string.h"
27 : : #include "libpq/libpq.h"
28 : : #include "storage/fd.h"
29 : : #include "utils/builtins.h"
30 : : #include "utils/guc.h"
31 : :
32 : : static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
33 : :
34 : : /*
35 : : * Run ssl_passphrase_command
36 : : *
37 : : * prompt will be substituted for %p. is_server_start determines the loglevel
38 : : * of error messages from executing the command, the loglevel for failures in
39 : : * param substitution will be ERROR regardless of is_server_start. The actual
40 : : * command used depends on the configuration for the current host.
41 : : *
42 : : * The result will be put in buffer buf, which is of size size. The return
43 : : * value is the length of the actual result.
44 : : */
45 : : int
48 dgustafsson@postgres 46 :GNC 17 : run_ssl_passphrase_command(const char *cmd, const char *prompt,
47 : : bool is_server_start, char *buf, int size)
48 : : {
2990 peter_e@gmx.net 49 [ + + ]:CBC 17 : int loglevel = is_server_start ? ERROR : LOG;
50 : : char *command;
51 : : FILE *fh;
52 : : int pclose_rc;
53 : 17 : size_t len = 0;
54 : :
55 [ - + ]: 17 : Assert(prompt);
56 [ - + ]: 17 : Assert(size > 0);
57 : 17 : buf[0] = '\0';
58 : :
48 dgustafsson@postgres 59 :GNC 17 : command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
60 : :
1210 peter@eisentraut.org 61 :CBC 17 : fh = OpenPipeStream(command, "r");
2990 peter_e@gmx.net 62 [ - + ]: 17 : if (fh == NULL)
63 : : {
2990 peter_e@gmx.net 64 [ # # ]:UBC 0 : ereport(loglevel,
65 : : (errcode_for_file_access(),
66 : : errmsg("could not execute command \"%s\": %m",
67 : : command)));
68 : 0 : goto error;
69 : : }
70 : :
2990 peter_e@gmx.net 71 [ - + ]:CBC 17 : if (!fgets(buf, size, fh))
72 : : {
2990 peter_e@gmx.net 73 [ # # ]:UBC 0 : if (ferror(fh))
74 : : {
2434 peter@eisentraut.org 75 : 0 : explicit_bzero(buf, size);
2990 peter_e@gmx.net 76 [ # # ]: 0 : ereport(loglevel,
77 : : (errcode_for_file_access(),
78 : : errmsg("could not read from command \"%s\": %m",
79 : : command)));
80 : 0 : goto error;
81 : : }
82 : : }
83 : :
2990 peter_e@gmx.net 84 :CBC 17 : pclose_rc = ClosePipeStream(fh);
85 [ - + ]: 17 : if (pclose_rc == -1)
86 : : {
2434 peter@eisentraut.org 87 :UBC 0 : explicit_bzero(buf, size);
2990 peter_e@gmx.net 88 [ # # ]: 0 : ereport(loglevel,
89 : : (errcode_for_file_access(),
90 : : errmsg("could not close pipe to external command: %m")));
91 : 0 : goto error;
92 : : }
2990 peter_e@gmx.net 93 [ - + ]:CBC 17 : else if (pclose_rc != 0)
94 : : {
95 : : char *reason;
96 : :
2434 peter@eisentraut.org 97 :UBC 0 : explicit_bzero(buf, size);
721 dgustafsson@postgres 98 : 0 : reason = wait_result_to_str(pclose_rc);
2990 peter_e@gmx.net 99 [ # # ]: 0 : ereport(loglevel,
100 : : (errcode_for_file_access(),
101 : : errmsg("command \"%s\" failed",
102 : : command),
103 : : errdetail_internal("%s", reason)));
721 dgustafsson@postgres 104 : 0 : pfree(reason);
2990 peter_e@gmx.net 105 : 0 : goto error;
106 : : }
107 : :
108 : : /* strip trailing newline and carriage return */
2461 michael@paquier.xyz 109 :CBC 17 : len = pg_strip_crlf(buf);
110 : :
2990 peter_e@gmx.net 111 : 17 : error:
1210 peter@eisentraut.org 112 : 17 : pfree(command);
2990 peter_e@gmx.net 113 : 17 : return len;
114 : : }
115 : :
116 : :
117 : : /*
118 : : * Check permissions for SSL key files.
119 : : */
120 : : bool
2955 121 : 76 : check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
122 : : {
123 [ + + ]: 76 : int loglevel = isServerStart ? FATAL : LOG;
124 : : struct stat buf;
125 : :
126 [ - + ]: 76 : if (stat(ssl_key_file, &buf) != 0)
127 : : {
2955 peter_e@gmx.net 128 [ # # ]:UBC 0 : ereport(loglevel,
129 : : (errcode_for_file_access(),
130 : : errmsg("could not access private key file \"%s\": %m",
131 : : ssl_key_file)));
132 : 0 : return false;
133 : : }
134 : :
135 : : /* Key file must be a regular file */
2955 peter_e@gmx.net 136 [ - + ]:CBC 76 : if (!S_ISREG(buf.st_mode))
137 : : {
2955 peter_e@gmx.net 138 [ # # ]:UBC 0 : ereport(loglevel,
139 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
140 : : errmsg("private key file \"%s\" is not a regular file",
141 : : ssl_key_file)));
142 : 0 : return false;
143 : : }
144 : :
145 : : /*
146 : : * Refuse to load key files owned by users other than us or root, and
147 : : * require no public access to the key file. If the file is owned by us,
148 : : * require mode 0600 or less. If owned by root, require 0640 or less to
149 : : * allow read access through either our gid or a supplementary gid that
150 : : * allows us to read system-wide certificates.
151 : : *
152 : : * Note that roughly similar checks are performed in
153 : : * src/interfaces/libpq/fe-secure-openssl.c so any changes here may need
154 : : * to be made there as well. The environment is different though; this
155 : : * code can assume that we're not running as root.
156 : : *
157 : : * Ideally we would do similar permissions checks on Windows, but it is
158 : : * not clear how that would work since Unix-style permissions may not be
159 : : * available.
160 : : */
161 : : #if !defined(WIN32) && !defined(__CYGWIN__)
2955 peter_e@gmx.net 162 [ - + - - ]:CBC 76 : if (buf.st_uid != geteuid() && buf.st_uid != 0)
163 : : {
2955 peter_e@gmx.net 164 [ # # ]:UBC 0 : ereport(loglevel,
165 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
166 : : errmsg("private key file \"%s\" must be owned by the database user or root",
167 : : ssl_key_file)));
168 : 0 : return false;
169 : : }
170 : :
2955 peter_e@gmx.net 171 [ + - + - ]:CBC 76 : if ((buf.st_uid == geteuid() && buf.st_mode & (S_IRWXG | S_IRWXO)) ||
172 [ - + - - ]: 76 : (buf.st_uid == 0 && buf.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)))
173 : : {
2955 peter_e@gmx.net 174 [ # # ]:UBC 0 : ereport(loglevel,
175 : : (errcode(ERRCODE_CONFIG_FILE_ERROR),
176 : : errmsg("private key file \"%s\" has group or world access",
177 : : ssl_key_file),
178 : : errdetail("File must have permissions u=rw (0600) or less if owned by the database user, or permissions u=rw,g=r (0640) or less if owned by root.")));
179 : 0 : return false;
180 : : }
181 : : #endif
182 : :
2955 peter_e@gmx.net 183 :CBC 76 : return true;
184 : : }
185 : :
186 : : /*
187 : : * parse_hosts_line
188 : : *
189 : : * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
190 : : * hostname, certificate, key and CA parts in order to build an SNI config in
191 : : * the TLS backend. Validation of the parsed values is left for the TLS backend
192 : : * to implement.
193 : : */
194 : : static HostsLine *
48 dgustafsson@postgres 195 :GNC 37 : parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
196 : : {
197 : : HostsLine *parsedline;
198 : : List *tokens;
199 : : ListCell *field;
200 : : AuthToken *token;
201 : :
202 : 37 : parsedline = palloc0(sizeof(HostsLine));
203 : 37 : parsedline->sourcefile = pstrdup(tok_line->file_name);
204 : 37 : parsedline->linenumber = tok_line->line_num;
205 : 37 : parsedline->rawline = pstrdup(tok_line->raw_line);
206 : 37 : parsedline->hostnames = NIL;
207 : :
208 : : /* Initialize optional fields */
209 : 37 : parsedline->ssl_passphrase_cmd = NULL;
210 : 37 : parsedline->ssl_passphrase_reload = false;
211 : :
212 : : /* Hostname */
213 : 37 : field = list_head(tok_line->fields);
214 : 37 : tokens = lfirst(field);
215 [ + - + + : 116 : foreach_ptr(AuthToken, hostname, tokens)
+ + ]
216 : : {
217 [ + + ]: 44 : if ((tokens->length > 1) &&
218 [ + + - + ]: 11 : (strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0))
219 : : {
220 [ + - ]: 1 : ereport(elevel,
221 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
222 : : errmsg("default and non-SNI entries cannot be mixed with other entries"),
223 : : errcontext("line %d of configuration file \"%s\"",
224 : : tok_line->line_num, tok_line->file_name));
225 : 1 : return NULL;
226 : : }
227 : :
228 : 43 : parsedline->hostnames = lappend(parsedline->hostnames, pstrdup(hostname->string));
229 : : }
230 : :
231 : : /* SSL Certificate (Required) */
232 : 36 : field = lnext(tok_line->fields, field);
233 [ - + ]: 36 : if (!field)
234 : : {
48 dgustafsson@postgres 235 [ # # ]:UNC 0 : ereport(elevel,
236 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
237 : : errmsg("missing entry at end of line"),
238 : : errcontext("line %d of configuration file \"%s\"",
239 : : tok_line->line_num, tok_line->file_name));
240 : 0 : return NULL;
241 : : }
48 dgustafsson@postgres 242 :GNC 36 : tokens = lfirst(field);
243 [ - + ]: 36 : if (tokens->length > 1)
244 : : {
48 dgustafsson@postgres 245 [ # # ]:UNC 0 : ereport(elevel,
246 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
247 : : errmsg("multiple values specified for SSL certificate"),
248 : : errcontext("line %d of configuration file \"%s\"",
249 : : tok_line->line_num, tok_line->file_name));
250 : 0 : return NULL;
251 : : }
48 dgustafsson@postgres 252 :GNC 36 : token = linitial(tokens);
253 : 36 : parsedline->ssl_cert = pstrdup(token->string);
254 : :
255 : : /* SSL key (Required) */
256 : 36 : field = lnext(tok_line->fields, field);
257 [ - + ]: 36 : if (!field)
258 : : {
48 dgustafsson@postgres 259 [ # # ]:UNC 0 : ereport(elevel,
260 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
261 : : errmsg("missing entry at end of line"),
262 : : errcontext("line %d of configuration file \"%s\"",
263 : : tok_line->line_num, tok_line->file_name));
264 : 0 : return NULL;
265 : : }
48 dgustafsson@postgres 266 :GNC 36 : tokens = lfirst(field);
267 [ - + ]: 36 : if (tokens->length > 1)
268 : : {
48 dgustafsson@postgres 269 [ # # ]:UNC 0 : ereport(elevel,
270 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
271 : : errmsg("multiple values specified for SSL key"),
272 : : errcontext("line %d of configuration file \"%s\"",
273 : : tok_line->line_num, tok_line->file_name));
274 : 0 : return NULL;
275 : : }
48 dgustafsson@postgres 276 :GNC 36 : token = linitial(tokens);
277 : 36 : parsedline->ssl_key = pstrdup(token->string);
278 : :
279 : : /* SSL CA (optional) */
280 : 36 : field = lnext(tok_line->fields, field);
281 [ + + ]: 36 : if (!field)
282 : 14 : return parsedline;
283 : 22 : tokens = lfirst(field);
284 [ - + ]: 22 : if (tokens->length > 1)
285 : : {
48 dgustafsson@postgres 286 [ # # ]:UNC 0 : ereport(elevel,
287 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
288 : : errmsg("multiple values specified for SSL CA"),
289 : : errcontext("line %d of configuration file \"%s\"",
290 : : tok_line->line_num, tok_line->file_name));
291 : 0 : return NULL;
292 : : }
48 dgustafsson@postgres 293 :GNC 22 : token = linitial(tokens);
294 : 22 : parsedline->ssl_ca = pstrdup(token->string);
295 : :
296 : : /* SSL Passphrase Command (optional) */
297 : 22 : field = lnext(tok_line->fields, field);
298 [ + + ]: 22 : if (field)
299 : : {
300 : 12 : tokens = lfirst(field);
301 [ - + ]: 12 : if (tokens->length > 1)
302 : : {
48 dgustafsson@postgres 303 [ # # ]:UNC 0 : ereport(elevel,
304 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
305 : : errmsg("multiple values specified for SSL passphrase command"),
306 : : errcontext("line %d of configuration file \"%s\"",
307 : : tok_line->line_num, tok_line->file_name));
308 : 0 : return NULL;
309 : : }
48 dgustafsson@postgres 310 :GNC 12 : token = linitial(tokens);
311 : 12 : parsedline->ssl_passphrase_cmd = pstrdup(token->string);
312 : :
313 : : /*
314 : : * SSL Passphrase Command support reload (optional). This field is
315 : : * only supported if there was a passphrase command parsed first, so
316 : : * nest it under the previous token.
317 : : */
318 : 12 : field = lnext(tok_line->fields, field);
319 [ + - ]: 12 : if (field)
320 : : {
321 : 12 : tokens = lfirst(field);
322 : 12 : token = linitial(tokens);
323 : :
324 : : /*
325 : : * There should be no more tokens after this, if there are break
326 : : * parsing and report error to avoid silently accepting incorrect
327 : : * config.
328 : : */
329 [ + + ]: 12 : if (lnext(tok_line->fields, field))
330 : : {
331 [ + - ]: 1 : ereport(elevel,
332 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
333 : : errmsg("extra fields at end of line"),
334 : : errcontext("line %d of configuration file \"%s\"",
335 : : tok_line->line_num, tok_line->file_name));
336 : 1 : return NULL;
337 : : }
338 : :
339 [ + - + + ]: 11 : if (tokens->length > 1 || !parse_bool(token->string, &parsedline->ssl_passphrase_reload))
340 : : {
341 [ + - ]: 1 : ereport(elevel,
342 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
343 : : errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
344 : : errcontext("line %d of configuration file \"%s\"",
345 : : tok_line->line_num, tok_line->file_name));
346 : 1 : return NULL;
347 : : }
348 : : }
349 : : }
350 : :
351 : 20 : return parsedline;
352 : : }
353 : :
354 : : /*
355 : : * load_hosts
356 : : *
357 : : * Reads and parses the pg_hosts.conf configuration file and passes back a List
358 : : * of HostsLine elements containing the parsed lines, or NIL in case of an empty
359 : : * file. The list is returned in the hosts parameter. The function will return
360 : : * a HostsFileLoadResult value detailing the result of the operation. When
361 : : * the hosts configuration failed to load, the err_msg variable may have more
362 : : * information in case it was passed as non-NULL.
363 : : */
364 : : HostsFileLoadResult
365 : 25 : load_hosts(List **hosts, char **err_msg)
366 : : {
367 : : FILE *file;
368 : : ListCell *line;
369 : 25 : List *hosts_lines = NIL;
370 : 25 : List *parsed_lines = NIL;
371 : : HostsLine *newline;
372 : 25 : bool ok = true;
373 : :
374 : : /*
375 : : * If we cannot return results then error out immediately. This implies
376 : : * API misuse or a similar kind of programmer error.
377 : : */
378 [ - + ]: 25 : if (!hosts)
379 : : {
48 dgustafsson@postgres 380 [ # # ]:UNC 0 : if (err_msg)
381 : 0 : *err_msg = psprintf("cannot load config from \"%s\", return variable missing",
382 : : HostsFileName);
383 : 0 : return HOSTSFILE_LOAD_FAILED;
384 : : }
48 dgustafsson@postgres 385 :GNC 25 : *hosts = NIL;
386 : :
387 : : /*
388 : : * This is not an auth file per se, but it is using the same file format
389 : : * as the pg_hba and pg_ident files and thus the same code infrastructure.
390 : : * A future TODO might be to rename the supporting code with a more
391 : : * generic name?
392 : : */
393 : 25 : file = open_auth_file(HostsFileName, LOG, 0, err_msg);
394 [ + + ]: 25 : if (file == NULL)
395 : : {
396 [ + - ]: 1 : if (errno == ENOENT)
397 : 1 : return HOSTSFILE_MISSING;
398 : :
48 dgustafsson@postgres 399 :UNC 0 : return HOSTSFILE_LOAD_FAILED;
400 : : }
401 : :
48 dgustafsson@postgres 402 :GNC 24 : tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
403 : :
404 [ + - + + : 61 : foreach(line, hosts_lines)
+ + ]
405 : : {
406 : 37 : TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
407 : :
408 : : /*
409 : : * Mark processing as not-ok in case lines are found with errors in
410 : : * tokenization (.err_msg is set) or during parsing.
411 : : */
412 [ + - + + ]: 74 : if ((tok_line->err_msg != NULL) ||
413 : 37 : ((newline = parse_hosts_line(tok_line, LOG)) == NULL))
414 : : {
415 : 3 : ok = false;
416 : 3 : continue;
417 : : }
418 : :
419 : 34 : parsed_lines = lappend(parsed_lines, newline);
420 : : }
421 : :
422 : : /* Free memory from tokenizer */
423 : 24 : free_auth_file(file, 0);
424 : 24 : *hosts = parsed_lines;
425 : :
426 [ + + ]: 24 : if (!ok)
427 : : {
428 [ + - ]: 3 : if (err_msg)
429 : 3 : *err_msg = psprintf("loading config from \"%s\" failed due to parsing error",
430 : : HostsFileName);
431 : 3 : return HOSTSFILE_LOAD_FAILED;
432 : : }
433 : :
434 [ - + ]: 21 : if (parsed_lines == NIL)
48 dgustafsson@postgres 435 :UNC 0 : return HOSTSFILE_EMPTY;
436 : :
48 dgustafsson@postgres 437 :GNC 21 : return HOSTSFILE_LOAD_OK;
438 : : }
|