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
46 17 : run_ssl_passphrase_command(const char *cmd, const char *prompt,
47 : bool is_server_start, char *buf, int size)
48 : {
49 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 : Assert(prompt);
56 : Assert(size > 0);
57 17 : buf[0] = '\0';
58 :
59 17 : command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
60 :
61 17 : fh = OpenPipeStream(command, "r");
62 17 : if (fh == NULL)
63 : {
64 0 : ereport(loglevel,
65 : (errcode_for_file_access(),
66 : errmsg("could not execute command \"%s\": %m",
67 : command)));
68 0 : goto error;
69 : }
70 :
71 17 : if (!fgets(buf, size, fh))
72 : {
73 0 : if (ferror(fh))
74 : {
75 0 : explicit_bzero(buf, size);
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 :
84 17 : pclose_rc = ClosePipeStream(fh);
85 17 : if (pclose_rc == -1)
86 : {
87 0 : explicit_bzero(buf, size);
88 0 : ereport(loglevel,
89 : (errcode_for_file_access(),
90 : errmsg("could not close pipe to external command: %m")));
91 0 : goto error;
92 : }
93 17 : else if (pclose_rc != 0)
94 : {
95 : char *reason;
96 :
97 0 : explicit_bzero(buf, size);
98 0 : reason = wait_result_to_str(pclose_rc);
99 0 : ereport(loglevel,
100 : (errcode_for_file_access(),
101 : errmsg("command \"%s\" failed",
102 : command),
103 : errdetail_internal("%s", reason)));
104 0 : pfree(reason);
105 0 : goto error;
106 : }
107 :
108 : /* strip trailing newline and carriage return */
109 17 : len = pg_strip_crlf(buf);
110 :
111 17 : error:
112 17 : pfree(command);
113 17 : return len;
114 : }
115 :
116 :
117 : /*
118 : * Check permissions for SSL key files.
119 : */
120 : bool
121 75 : check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
122 : {
123 75 : int loglevel = isServerStart ? FATAL : LOG;
124 : struct stat buf;
125 :
126 75 : if (stat(ssl_key_file, &buf) != 0)
127 : {
128 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 */
136 75 : if (!S_ISREG(buf.st_mode))
137 : {
138 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__)
162 75 : if (buf.st_uid != geteuid() && buf.st_uid != 0)
163 : {
164 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 :
171 75 : if ((buf.st_uid == geteuid() && buf.st_mode & (S_IRWXG | S_IRWXO)) ||
172 75 : (buf.st_uid == 0 && buf.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)))
173 : {
174 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 :
183 75 : 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 *
195 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 : {
235 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 : }
242 36 : tokens = lfirst(field);
243 36 : if (tokens->length > 1)
244 : {
245 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 : }
252 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 : {
259 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 : }
266 36 : tokens = lfirst(field);
267 36 : if (tokens->length > 1)
268 : {
269 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 : }
276 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 : {
286 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 : }
293 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 : {
303 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 : }
310 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 : int
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 : {
380 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 : }
385 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 :
399 0 : return HOSTSFILE_LOAD_FAILED;
400 : }
401 :
402 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)
435 0 : return HOSTSFILE_EMPTY;
436 :
437 21 : return HOSTSFILE_LOAD_OK;
438 : }
|