LCOV - code coverage report
Current view: top level - src/backend/libpq - be-secure-common.c (source / functions) Coverage Total Hit
Test: PostgreSQL 19devel Lines: 72.1 % 140 101
Test Date: 2026-03-21 17:16:16 Functions: 100.0 % 4 4
Legend: Lines:     hit not hit

            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              : }
        

Generated by: LCOV version 2.0-1