/src/postgres/src/backend/libpq/be-secure-common.c
Line | Count | Source |
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 | | run_ssl_passphrase_command(const char *cmd, const char *prompt, |
47 | | bool is_server_start, char *buf, int size) |
48 | 0 | { |
49 | 0 | int loglevel = is_server_start ? ERROR : LOG; |
50 | 0 | char *command; |
51 | 0 | FILE *fh; |
52 | 0 | int pclose_rc; |
53 | 0 | size_t len = 0; |
54 | |
|
55 | 0 | Assert(prompt); |
56 | 0 | Assert(size > 0); |
57 | 0 | buf[0] = '\0'; |
58 | |
|
59 | 0 | command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt); |
60 | |
|
61 | 0 | fh = OpenPipeStream(command, "r"); |
62 | 0 | if (fh == NULL) |
63 | 0 | { |
64 | 0 | ereport(loglevel, |
65 | 0 | (errcode_for_file_access(), |
66 | 0 | errmsg("could not execute command \"%s\": %m", |
67 | 0 | command))); |
68 | 0 | goto error; |
69 | 0 | } |
70 | | |
71 | 0 | if (!fgets(buf, size, fh)) |
72 | 0 | { |
73 | 0 | if (ferror(fh)) |
74 | 0 | { |
75 | 0 | explicit_bzero(buf, size); |
76 | 0 | ereport(loglevel, |
77 | 0 | (errcode_for_file_access(), |
78 | 0 | errmsg("could not read from command \"%s\": %m", |
79 | 0 | command))); |
80 | 0 | goto error; |
81 | 0 | } |
82 | 0 | } |
83 | | |
84 | 0 | pclose_rc = ClosePipeStream(fh); |
85 | 0 | if (pclose_rc == -1) |
86 | 0 | { |
87 | 0 | explicit_bzero(buf, size); |
88 | 0 | ereport(loglevel, |
89 | 0 | (errcode_for_file_access(), |
90 | 0 | errmsg("could not close pipe to external command: %m"))); |
91 | 0 | goto error; |
92 | 0 | } |
93 | 0 | else if (pclose_rc != 0) |
94 | 0 | { |
95 | 0 | char *reason; |
96 | |
|
97 | 0 | explicit_bzero(buf, size); |
98 | 0 | reason = wait_result_to_str(pclose_rc); |
99 | 0 | ereport(loglevel, |
100 | 0 | (errcode_for_file_access(), |
101 | 0 | errmsg("command \"%s\" failed", |
102 | 0 | command), |
103 | 0 | errdetail_internal("%s", reason))); |
104 | 0 | pfree(reason); |
105 | 0 | goto error; |
106 | 0 | } |
107 | | |
108 | | /* strip trailing newline and carriage return */ |
109 | 0 | len = pg_strip_crlf(buf); |
110 | |
|
111 | 0 | error: |
112 | 0 | pfree(command); |
113 | 0 | return len; |
114 | 0 | } |
115 | | |
116 | | |
117 | | /* |
118 | | * Check permissions for SSL key files. |
119 | | */ |
120 | | bool |
121 | | check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart) |
122 | 0 | { |
123 | 0 | int loglevel = isServerStart ? FATAL : LOG; |
124 | 0 | struct stat buf; |
125 | |
|
126 | 0 | if (stat(ssl_key_file, &buf) != 0) |
127 | 0 | { |
128 | 0 | ereport(loglevel, |
129 | 0 | (errcode_for_file_access(), |
130 | 0 | errmsg("could not access private key file \"%s\": %m", |
131 | 0 | ssl_key_file))); |
132 | 0 | return false; |
133 | 0 | } |
134 | | |
135 | | /* Key file must be a regular file */ |
136 | 0 | if (!S_ISREG(buf.st_mode)) |
137 | 0 | { |
138 | 0 | ereport(loglevel, |
139 | 0 | (errcode(ERRCODE_CONFIG_FILE_ERROR), |
140 | 0 | errmsg("private key file \"%s\" is not a regular file", |
141 | 0 | ssl_key_file))); |
142 | 0 | return false; |
143 | 0 | } |
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 | 0 | #if !defined(WIN32) && !defined(__CYGWIN__) |
162 | 0 | if (buf.st_uid != geteuid() && buf.st_uid != 0) |
163 | 0 | { |
164 | 0 | ereport(loglevel, |
165 | 0 | (errcode(ERRCODE_CONFIG_FILE_ERROR), |
166 | 0 | errmsg("private key file \"%s\" must be owned by the database user or root", |
167 | 0 | ssl_key_file))); |
168 | 0 | return false; |
169 | 0 | } |
170 | | |
171 | 0 | if ((buf.st_uid == geteuid() && buf.st_mode & (S_IRWXG | S_IRWXO)) || |
172 | 0 | (buf.st_uid == 0 && buf.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO))) |
173 | 0 | { |
174 | 0 | ereport(loglevel, |
175 | 0 | (errcode(ERRCODE_CONFIG_FILE_ERROR), |
176 | 0 | errmsg("private key file \"%s\" has group or world access", |
177 | 0 | ssl_key_file), |
178 | 0 | 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 | 0 | } |
181 | 0 | #endif |
182 | | |
183 | 0 | return true; |
184 | 0 | } |
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 | | parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) |
196 | 0 | { |
197 | 0 | HostsLine *parsedline; |
198 | 0 | List *tokens; |
199 | 0 | ListCell *field; |
200 | 0 | AuthToken *token; |
201 | |
|
202 | 0 | parsedline = palloc0_object(HostsLine); |
203 | 0 | parsedline->sourcefile = pstrdup(tok_line->file_name); |
204 | 0 | parsedline->linenumber = tok_line->line_num; |
205 | 0 | parsedline->rawline = pstrdup(tok_line->raw_line); |
206 | 0 | parsedline->hostnames = NIL; |
207 | | |
208 | | /* Initialize optional fields */ |
209 | 0 | parsedline->ssl_passphrase_cmd = NULL; |
210 | 0 | parsedline->ssl_passphrase_reload = false; |
211 | | |
212 | | /* Hostname */ |
213 | 0 | field = list_head(tok_line->fields); |
214 | 0 | tokens = lfirst(field); |
215 | 0 | foreach_ptr(AuthToken, hostname, tokens) |
216 | 0 | { |
217 | 0 | if ((tokens->length > 1) && |
218 | 0 | (strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0)) |
219 | 0 | { |
220 | 0 | ereport(elevel, |
221 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
222 | 0 | errmsg("default and non-SNI entries cannot be mixed with other entries"), |
223 | 0 | errcontext("line %d of configuration file \"%s\"", |
224 | 0 | tok_line->line_num, tok_line->file_name)); |
225 | 0 | return NULL; |
226 | 0 | } |
227 | | |
228 | 0 | parsedline->hostnames = lappend(parsedline->hostnames, pstrdup(hostname->string)); |
229 | 0 | } |
230 | | |
231 | | /* SSL Certificate (Required) */ |
232 | 0 | field = lnext(tok_line->fields, field); |
233 | 0 | if (!field) |
234 | 0 | { |
235 | 0 | ereport(elevel, |
236 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
237 | 0 | errmsg("missing entry at end of line"), |
238 | 0 | errcontext("line %d of configuration file \"%s\"", |
239 | 0 | tok_line->line_num, tok_line->file_name)); |
240 | 0 | return NULL; |
241 | 0 | } |
242 | 0 | tokens = lfirst(field); |
243 | 0 | if (tokens->length > 1) |
244 | 0 | { |
245 | 0 | ereport(elevel, |
246 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
247 | 0 | errmsg("multiple values specified for SSL certificate"), |
248 | 0 | errcontext("line %d of configuration file \"%s\"", |
249 | 0 | tok_line->line_num, tok_line->file_name)); |
250 | 0 | return NULL; |
251 | 0 | } |
252 | 0 | token = linitial(tokens); |
253 | 0 | parsedline->ssl_cert = pstrdup(token->string); |
254 | | |
255 | | /* SSL key (Required) */ |
256 | 0 | field = lnext(tok_line->fields, field); |
257 | 0 | if (!field) |
258 | 0 | { |
259 | 0 | ereport(elevel, |
260 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
261 | 0 | errmsg("missing entry at end of line"), |
262 | 0 | errcontext("line %d of configuration file \"%s\"", |
263 | 0 | tok_line->line_num, tok_line->file_name)); |
264 | 0 | return NULL; |
265 | 0 | } |
266 | 0 | tokens = lfirst(field); |
267 | 0 | if (tokens->length > 1) |
268 | 0 | { |
269 | 0 | ereport(elevel, |
270 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
271 | 0 | errmsg("multiple values specified for SSL key"), |
272 | 0 | errcontext("line %d of configuration file \"%s\"", |
273 | 0 | tok_line->line_num, tok_line->file_name)); |
274 | 0 | return NULL; |
275 | 0 | } |
276 | 0 | token = linitial(tokens); |
277 | 0 | parsedline->ssl_key = pstrdup(token->string); |
278 | | |
279 | | /* SSL CA (optional) */ |
280 | 0 | field = lnext(tok_line->fields, field); |
281 | 0 | if (!field) |
282 | 0 | return parsedline; |
283 | 0 | tokens = lfirst(field); |
284 | 0 | if (tokens->length > 1) |
285 | 0 | { |
286 | 0 | ereport(elevel, |
287 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
288 | 0 | errmsg("multiple values specified for SSL CA"), |
289 | 0 | errcontext("line %d of configuration file \"%s\"", |
290 | 0 | tok_line->line_num, tok_line->file_name)); |
291 | 0 | return NULL; |
292 | 0 | } |
293 | 0 | token = linitial(tokens); |
294 | 0 | parsedline->ssl_ca = pstrdup(token->string); |
295 | | |
296 | | /* SSL Passphrase Command (optional) */ |
297 | 0 | field = lnext(tok_line->fields, field); |
298 | 0 | if (field) |
299 | 0 | { |
300 | 0 | tokens = lfirst(field); |
301 | 0 | if (tokens->length > 1) |
302 | 0 | { |
303 | 0 | ereport(elevel, |
304 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
305 | 0 | errmsg("multiple values specified for SSL passphrase command"), |
306 | 0 | errcontext("line %d of configuration file \"%s\"", |
307 | 0 | tok_line->line_num, tok_line->file_name)); |
308 | 0 | return NULL; |
309 | 0 | } |
310 | 0 | token = linitial(tokens); |
311 | 0 | 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 | 0 | field = lnext(tok_line->fields, field); |
319 | 0 | if (field) |
320 | 0 | { |
321 | 0 | tokens = lfirst(field); |
322 | 0 | 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 | 0 | if (lnext(tok_line->fields, field)) |
330 | 0 | { |
331 | 0 | ereport(elevel, |
332 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
333 | 0 | errmsg("extra fields at end of line"), |
334 | 0 | errcontext("line %d of configuration file \"%s\"", |
335 | 0 | tok_line->line_num, tok_line->file_name)); |
336 | 0 | return NULL; |
337 | 0 | } |
338 | | |
339 | 0 | if (tokens->length > 1 || !parse_bool(token->string, &parsedline->ssl_passphrase_reload)) |
340 | 0 | { |
341 | 0 | ereport(elevel, |
342 | 0 | errcode(ERRCODE_CONFIG_FILE_ERROR), |
343 | 0 | errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"), |
344 | 0 | errcontext("line %d of configuration file \"%s\"", |
345 | 0 | tok_line->line_num, tok_line->file_name)); |
346 | 0 | return NULL; |
347 | 0 | } |
348 | 0 | } |
349 | 0 | } |
350 | | |
351 | 0 | return parsedline; |
352 | 0 | } |
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 | | load_hosts(List **hosts, char **err_msg) |
366 | 0 | { |
367 | 0 | FILE *file; |
368 | 0 | ListCell *line; |
369 | 0 | List *hosts_lines = NIL; |
370 | 0 | List *parsed_lines = NIL; |
371 | 0 | HostsLine *newline; |
372 | 0 | 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 | 0 | if (!hosts) |
379 | 0 | { |
380 | 0 | if (err_msg) |
381 | 0 | *err_msg = psprintf("cannot load config from \"%s\", return variable missing", |
382 | 0 | HostsFileName); |
383 | 0 | return HOSTSFILE_LOAD_FAILED; |
384 | 0 | } |
385 | 0 | *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 | 0 | file = open_auth_file(HostsFileName, LOG, 0, err_msg); |
394 | 0 | if (file == NULL) |
395 | 0 | { |
396 | 0 | if (errno == ENOENT) |
397 | 0 | return HOSTSFILE_MISSING; |
398 | | |
399 | 0 | return HOSTSFILE_LOAD_FAILED; |
400 | 0 | } |
401 | | |
402 | 0 | tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0); |
403 | |
|
404 | 0 | foreach(line, hosts_lines) |
405 | 0 | { |
406 | 0 | 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 | 0 | if ((tok_line->err_msg != NULL) || |
413 | 0 | ((newline = parse_hosts_line(tok_line, LOG)) == NULL)) |
414 | 0 | { |
415 | 0 | ok = false; |
416 | 0 | continue; |
417 | 0 | } |
418 | | |
419 | 0 | parsed_lines = lappend(parsed_lines, newline); |
420 | 0 | } |
421 | | |
422 | | /* Free memory from tokenizer */ |
423 | 0 | free_auth_file(file, 0); |
424 | 0 | *hosts = parsed_lines; |
425 | |
|
426 | 0 | if (!ok) |
427 | 0 | { |
428 | 0 | if (err_msg) |
429 | 0 | *err_msg = psprintf("loading config from \"%s\" failed due to parsing error", |
430 | 0 | HostsFileName); |
431 | 0 | return HOSTSFILE_LOAD_FAILED; |
432 | 0 | } |
433 | | |
434 | 0 | if (parsed_lines == NIL) |
435 | 0 | return HOSTSFILE_EMPTY; |
436 | | |
437 | 0 | return HOSTSFILE_LOAD_OK; |
438 | 0 | } |