LCOV - code coverage report
Current view: top level - src/backend/libpq - be-secure-common.c (source / functions) Coverage Total Hit
Test: PostgreSQL 20devel Lines: 72.1 % 140 101
Test Date: 2026-07-19 19:15:43 Functions: 100.0 % 4 4
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 47.6 % 124 59

             Branch data     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                 :             : 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                 :             :     {
     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