Coverage Report

Created: 2026-10-16 06:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}