LCOV - code coverage report
Current view: top level - contrib/sslinfo - sslinfo.c (source / functions) Hit Total Coverage
Test: PostgreSQL 18devel Lines: 109 138 79.0 %
Date: 2025-04-01 15:15:16 Functions: 22 23 95.7 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*
       2             :  * module for PostgreSQL to access client SSL certificate information
       3             :  *
       4             :  * Written by Victor B. Wagner <vitus@cryptocom.ru>, Cryptocom LTD
       5             :  * This file is distributed under BSD-style license.
       6             :  *
       7             :  * contrib/sslinfo/sslinfo.c
       8             :  */
       9             : 
      10             : #include "postgres.h"
      11             : 
      12             : #include <openssl/x509.h>
      13             : #include <openssl/x509v3.h>
      14             : #include <openssl/asn1.h>
      15             : 
      16             : #include "access/htup_details.h"
      17             : #include "funcapi.h"
      18             : #include "libpq/libpq-be.h"
      19             : #include "miscadmin.h"
      20             : #include "utils/builtins.h"
      21             : 
      22          44 : PG_MODULE_MAGIC_EXT(
      23             :                     .name = "sslinfo",
      24             :                     .version = PG_VERSION
      25             : );
      26             : 
      27             : static Datum X509_NAME_field_to_text(X509_NAME *name, text *fieldName);
      28             : static Datum ASN1_STRING_to_text(ASN1_STRING *str);
      29             : 
      30             : /*
      31             :  * Function context for data persisting over repeated calls.
      32             :  */
      33             : typedef struct
      34             : {
      35             :     TupleDesc   tupdesc;
      36             : } SSLExtensionInfoContext;
      37             : 
      38             : /*
      39             :  * Indicates whether current session uses SSL
      40             :  *
      41             :  * Function has no arguments.  Returns bool.  True if current session
      42             :  * is SSL session and false if it is local or non-ssl session.
      43             :  */
      44          14 : PG_FUNCTION_INFO_V1(ssl_is_used);
      45             : Datum
      46           2 : ssl_is_used(PG_FUNCTION_ARGS)
      47             : {
      48           2 :     PG_RETURN_BOOL(MyProcPort->ssl_in_use);
      49             : }
      50             : 
      51             : 
      52             : /*
      53             :  * Returns SSL version currently in use.
      54             :  */
      55          14 : PG_FUNCTION_INFO_V1(ssl_version);
      56             : Datum
      57           2 : ssl_version(PG_FUNCTION_ARGS)
      58             : {
      59             :     const char *version;
      60             : 
      61           2 :     if (!MyProcPort->ssl_in_use)
      62           0 :         PG_RETURN_NULL();
      63             : 
      64           2 :     version = be_tls_get_version(MyProcPort);
      65           2 :     if (version == NULL)
      66           0 :         PG_RETURN_NULL();
      67             : 
      68           2 :     PG_RETURN_TEXT_P(cstring_to_text(version));
      69             : }
      70             : 
      71             : 
      72             : /*
      73             :  * Returns SSL cipher currently in use.
      74             :  */
      75          14 : PG_FUNCTION_INFO_V1(ssl_cipher);
      76             : Datum
      77           2 : ssl_cipher(PG_FUNCTION_ARGS)
      78             : {
      79             :     const char *cipher;
      80             : 
      81           2 :     if (!MyProcPort->ssl_in_use)
      82           0 :         PG_RETURN_NULL();
      83             : 
      84           2 :     cipher = be_tls_get_cipher(MyProcPort);
      85           2 :     if (cipher == NULL)
      86           0 :         PG_RETURN_NULL();
      87             : 
      88           2 :     PG_RETURN_TEXT_P(cstring_to_text(cipher));
      89             : }
      90             : 
      91             : 
      92             : /*
      93             :  * Indicates whether current client provided a certificate
      94             :  *
      95             :  * Function has no arguments.  Returns bool.  True if current session
      96             :  * is SSL session and client certificate is verified, otherwise false.
      97             :  */
      98          24 : PG_FUNCTION_INFO_V1(ssl_client_cert_present);
      99             : Datum
     100          12 : ssl_client_cert_present(PG_FUNCTION_ARGS)
     101             : {
     102          12 :     PG_RETURN_BOOL(MyProcPort->peer_cert_valid);
     103             : }
     104             : 
     105             : 
     106             : /*
     107             :  * Returns serial number of certificate used to establish current
     108             :  * session
     109             :  *
     110             :  * Function has no arguments.  It returns the certificate serial
     111             :  * number as numeric or null if current session doesn't use SSL or if
     112             :  * SSL connection is established without sending client certificate.
     113             :  */
     114          14 : PG_FUNCTION_INFO_V1(ssl_client_serial);
     115             : Datum
     116           2 : ssl_client_serial(PG_FUNCTION_ARGS)
     117             : {
     118             :     char decimal[NAMEDATALEN];
     119             :     Datum       result;
     120             : 
     121           2 :     if (!MyProcPort->ssl_in_use || !MyProcPort->peer_cert_valid)
     122           0 :         PG_RETURN_NULL();
     123             : 
     124           2 :     be_tls_get_peer_serial(MyProcPort, decimal, NAMEDATALEN);
     125             : 
     126           2 :     if (!*decimal)
     127           0 :         PG_RETURN_NULL();
     128             : 
     129           2 :     result = DirectFunctionCall3(numeric_in,
     130             :                                  CStringGetDatum(decimal),
     131             :                                  ObjectIdGetDatum(0),
     132             :                                  Int32GetDatum(-1));
     133           2 :     return result;
     134             : }
     135             : 
     136             : 
     137             : /*
     138             :  * Converts OpenSSL ASN1_STRING structure into text
     139             :  *
     140             :  * Converts ASN1_STRING into text, converting all the characters into
     141             :  * current database encoding if possible.  Any invalid characters are
     142             :  * replaced by question marks.
     143             :  *
     144             :  * Parameter: str - OpenSSL ASN1_STRING structure.  Memory management
     145             :  * of this structure is responsibility of caller.
     146             :  *
     147             :  * Returns Datum, which can be directly returned from a C language SQL
     148             :  * function.
     149             :  */
     150             : static Datum
     151           4 : ASN1_STRING_to_text(ASN1_STRING *str)
     152             : {
     153             :     BIO        *membuf;
     154             :     size_t      size;
     155             :     char        nullterm;
     156             :     char       *sp;
     157             :     char       *dp;
     158             :     text       *result;
     159             : 
     160           4 :     membuf = BIO_new(BIO_s_mem());
     161           4 :     if (membuf == NULL)
     162           0 :         ereport(ERROR,
     163             :                 (errcode(ERRCODE_OUT_OF_MEMORY),
     164             :                  errmsg("could not create OpenSSL BIO structure")));
     165           4 :     (void) BIO_set_close(membuf, BIO_CLOSE);
     166           4 :     ASN1_STRING_print_ex(membuf, str,
     167             :                          ((ASN1_STRFLGS_RFC2253 & ~ASN1_STRFLGS_ESC_MSB)
     168             :                           | ASN1_STRFLGS_UTF8_CONVERT));
     169             :     /* ensure null termination of the BIO's content */
     170           4 :     nullterm = '\0';
     171           4 :     BIO_write(membuf, &nullterm, 1);
     172           4 :     size = BIO_get_mem_data(membuf, &sp);
     173           4 :     dp = pg_any_to_server(sp, size - 1, PG_UTF8);
     174           4 :     result = cstring_to_text(dp);
     175           4 :     if (dp != sp)
     176           0 :         pfree(dp);
     177           4 :     if (BIO_free(membuf) != 1)
     178           0 :         elog(ERROR, "could not free OpenSSL BIO structure");
     179             : 
     180           4 :     PG_RETURN_TEXT_P(result);
     181             : }
     182             : 
     183             : 
     184             : /*
     185             :  * Returns specified field of specified X509_NAME structure
     186             :  *
     187             :  * Common part of ssl_client_dn and ssl_issuer_dn functions.
     188             :  *
     189             :  * Parameter: X509_NAME *name - either subject or issuer of certificate
     190             :  * Parameter: text fieldName  - field name string like 'CN' or commonName
     191             :  *            to be looked up in the OpenSSL ASN1 OID database
     192             :  *
     193             :  * Returns result of ASN1_STRING_to_text applied to appropriate
     194             :  * part of name
     195             :  */
     196             : static Datum
     197           6 : X509_NAME_field_to_text(X509_NAME *name, text *fieldName)
     198             : {
     199             :     char       *string_fieldname;
     200             :     int         nid,
     201             :                 index;
     202             :     ASN1_STRING *data;
     203             : 
     204           6 :     string_fieldname = text_to_cstring(fieldName);
     205           6 :     nid = OBJ_txt2nid(string_fieldname);
     206           6 :     if (nid == NID_undef)
     207           2 :         ereport(ERROR,
     208             :                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     209             :                  errmsg("invalid X.509 field name: \"%s\"",
     210             :                         string_fieldname)));
     211           4 :     pfree(string_fieldname);
     212           4 :     index = X509_NAME_get_index_by_NID(name, nid, -1);
     213           4 :     if (index < 0)
     214           0 :         return (Datum) 0;
     215           4 :     data = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(name, index));
     216           4 :     return ASN1_STRING_to_text(data);
     217             : }
     218             : 
     219             : 
     220             : /*
     221             :  * Returns specified field of client certificate distinguished name
     222             :  *
     223             :  * Receives field name (like 'commonName' and 'emailAddress') and
     224             :  * returns appropriate part of certificate subject converted into
     225             :  * database encoding.
     226             :  *
     227             :  * Parameter: fieldname text - will be looked up in OpenSSL object
     228             :  * identifier database
     229             :  *
     230             :  * Returns text string with appropriate value.
     231             :  *
     232             :  * Throws an error if argument cannot be converted into ASN1 OID by
     233             :  * OpenSSL.  Returns null if no client certificate is present, or if
     234             :  * there is no field with such name in the certificate.
     235             :  */
     236          18 : PG_FUNCTION_INFO_V1(ssl_client_dn_field);
     237             : Datum
     238           6 : ssl_client_dn_field(PG_FUNCTION_ARGS)
     239             : {
     240           6 :     text       *fieldname = PG_GETARG_TEXT_PP(0);
     241             :     Datum       result;
     242             : 
     243           6 :     if (!MyProcPort->ssl_in_use || !MyProcPort->peer_cert_valid)
     244           2 :         PG_RETURN_NULL();
     245             : 
     246           4 :     result = X509_NAME_field_to_text(X509_get_subject_name(MyProcPort->peer), fieldname);
     247             : 
     248           2 :     if (!result)
     249           0 :         PG_RETURN_NULL();
     250             :     else
     251           2 :         return result;
     252             : }
     253             : 
     254             : 
     255             : /*
     256             :  * Returns specified field of client certificate issuer name
     257             :  *
     258             :  * Receives field name (like 'commonName' and 'emailAddress') and
     259             :  * returns appropriate part of certificate subject converted into
     260             :  * database encoding.
     261             :  *
     262             :  * Parameter: fieldname text - would be looked up in OpenSSL object
     263             :  * identifier database
     264             :  *
     265             :  * Returns text string with appropriate value.
     266             :  *
     267             :  * Throws an error if argument cannot be converted into ASN1 OID by
     268             :  * OpenSSL.  Returns null if no client certificate is present, or if
     269             :  * there is no field with such name in the certificate.
     270             :  */
     271          14 : PG_FUNCTION_INFO_V1(ssl_issuer_field);
     272             : Datum
     273           2 : ssl_issuer_field(PG_FUNCTION_ARGS)
     274             : {
     275           2 :     text       *fieldname = PG_GETARG_TEXT_PP(0);
     276             :     Datum       result;
     277             : 
     278           2 :     if (!(MyProcPort->peer))
     279           0 :         PG_RETURN_NULL();
     280             : 
     281           2 :     result = X509_NAME_field_to_text(X509_get_issuer_name(MyProcPort->peer), fieldname);
     282             : 
     283           2 :     if (!result)
     284           0 :         PG_RETURN_NULL();
     285             :     else
     286           2 :         return result;
     287             : }
     288             : 
     289             : 
     290             : /*
     291             :  * Returns current client certificate subject as one string
     292             :  *
     293             :  * This function returns distinguished name (subject) of the client
     294             :  * certificate used in the current SSL connection, converting it into
     295             :  * the current database encoding.
     296             :  *
     297             :  * Returns text datum.
     298             :  */
     299          12 : PG_FUNCTION_INFO_V1(ssl_client_dn);
     300             : Datum
     301           0 : ssl_client_dn(PG_FUNCTION_ARGS)
     302             : {
     303             :     char        subject[NAMEDATALEN];
     304             : 
     305           0 :     if (!MyProcPort->ssl_in_use || !MyProcPort->peer_cert_valid)
     306           0 :         PG_RETURN_NULL();
     307             : 
     308           0 :     be_tls_get_peer_subject_name(MyProcPort, subject, NAMEDATALEN);
     309             : 
     310           0 :     if (!*subject)
     311           0 :         PG_RETURN_NULL();
     312             : 
     313           0 :     PG_RETURN_TEXT_P(cstring_to_text(subject));
     314             : }
     315             : 
     316             : 
     317             : /*
     318             :  * Returns current client certificate issuer as one string
     319             :  *
     320             :  * This function returns issuer's distinguished name of the client
     321             :  * certificate used in the current SSL connection, converting it into
     322             :  * the current database encoding.
     323             :  *
     324             :  * Returns text datum.
     325             :  */
     326          14 : PG_FUNCTION_INFO_V1(ssl_issuer_dn);
     327             : Datum
     328           2 : ssl_issuer_dn(PG_FUNCTION_ARGS)
     329             : {
     330             :     char        issuer[NAMEDATALEN];
     331             : 
     332           2 :     if (!MyProcPort->ssl_in_use || !MyProcPort->peer_cert_valid)
     333           0 :         PG_RETURN_NULL();
     334             : 
     335           2 :     be_tls_get_peer_issuer_name(MyProcPort, issuer, NAMEDATALEN);
     336             : 
     337           2 :     if (!*issuer)
     338           0 :         PG_RETURN_NULL();
     339             : 
     340           2 :     PG_RETURN_TEXT_P(cstring_to_text(issuer));
     341             : }
     342             : 
     343             : 
     344             : /*
     345             :  * Returns information about available SSL extensions.
     346             :  *
     347             :  * Returns setof record made of the following values:
     348             :  * - name of the extension.
     349             :  * - value of the extension.
     350             :  * - critical status of the extension.
     351             :  */
     352          14 : PG_FUNCTION_INFO_V1(ssl_extension_info);
     353             : Datum
     354          10 : ssl_extension_info(PG_FUNCTION_ARGS)
     355             : {
     356          10 :     X509       *cert = MyProcPort->peer;
     357             :     FuncCallContext *funcctx;
     358             :     int         call_cntr;
     359             :     int         max_calls;
     360             :     MemoryContext oldcontext;
     361             :     SSLExtensionInfoContext *fctx;
     362             : 
     363          10 :     if (SRF_IS_FIRSTCALL())
     364             :     {
     365             : 
     366             :         TupleDesc   tupdesc;
     367             : 
     368             :         /* create a function context for cross-call persistence */
     369           2 :         funcctx = SRF_FIRSTCALL_INIT();
     370             : 
     371             :         /*
     372             :          * Switch to memory context appropriate for multiple function calls
     373             :          */
     374           2 :         oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
     375             : 
     376             :         /* Create a user function context for cross-call persistence */
     377           2 :         fctx = (SSLExtensionInfoContext *) palloc(sizeof(SSLExtensionInfoContext));
     378             : 
     379             :         /* Construct tuple descriptor */
     380           2 :         if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
     381           0 :             ereport(ERROR,
     382             :                     (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
     383             :                      errmsg("function returning record called in context that cannot accept type record")));
     384           2 :         fctx->tupdesc = BlessTupleDesc(tupdesc);
     385             : 
     386             :         /* Set max_calls as a count of extensions in certificate */
     387           2 :         max_calls = cert != NULL ? X509_get_ext_count(cert) : 0;
     388             : 
     389           2 :         if (max_calls > 0)
     390             :         {
     391             :             /* got results, keep track of them */
     392           2 :             funcctx->max_calls = max_calls;
     393           2 :             funcctx->user_fctx = fctx;
     394             :         }
     395             :         else
     396             :         {
     397             :             /* fast track when no results */
     398           0 :             MemoryContextSwitchTo(oldcontext);
     399           0 :             SRF_RETURN_DONE(funcctx);
     400             :         }
     401             : 
     402           2 :         MemoryContextSwitchTo(oldcontext);
     403             :     }
     404             : 
     405             :     /* stuff done on every call of the function */
     406          10 :     funcctx = SRF_PERCALL_SETUP();
     407             : 
     408             :     /*
     409             :      * Initialize per-call variables.
     410             :      */
     411          10 :     call_cntr = funcctx->call_cntr;
     412          10 :     max_calls = funcctx->max_calls;
     413          10 :     fctx = funcctx->user_fctx;
     414             : 
     415             :     /* do while there are more left to send */
     416          10 :     if (call_cntr < max_calls)
     417             :     {
     418             :         Datum       values[3];
     419             :         bool        nulls[3];
     420             :         char       *buf;
     421             :         HeapTuple   tuple;
     422             :         Datum       result;
     423             :         BIO        *membuf;
     424             :         X509_EXTENSION *ext;
     425             :         ASN1_OBJECT *obj;
     426             :         int         nid;
     427             :         int         len;
     428             : 
     429             :         /* need a BIO for this */
     430           8 :         membuf = BIO_new(BIO_s_mem());
     431           8 :         if (membuf == NULL)
     432           0 :             ereport(ERROR,
     433             :                     (errcode(ERRCODE_OUT_OF_MEMORY),
     434             :                      errmsg("could not create OpenSSL BIO structure")));
     435             : 
     436             :         /* Get the extension from the certificate */
     437           8 :         ext = X509_get_ext(cert, call_cntr);
     438           8 :         obj = X509_EXTENSION_get_object(ext);
     439             : 
     440             :         /* Get the extension name */
     441           8 :         nid = OBJ_obj2nid(obj);
     442           8 :         if (nid == NID_undef)
     443           0 :             ereport(ERROR,
     444             :                     (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
     445             :                      errmsg("unknown OpenSSL extension in certificate at position %d",
     446             :                             call_cntr)));
     447           8 :         values[0] = CStringGetTextDatum(OBJ_nid2sn(nid));
     448           8 :         nulls[0] = false;
     449             : 
     450             :         /* Get the extension value */
     451           8 :         if (X509V3_EXT_print(membuf, ext, 0, 0) <= 0)
     452           0 :             ereport(ERROR,
     453             :                     (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
     454             :                      errmsg("could not print extension value in certificate at position %d",
     455             :                             call_cntr)));
     456           8 :         len = BIO_get_mem_data(membuf, &buf);
     457           8 :         values[1] = PointerGetDatum(cstring_to_text_with_len(buf, len));
     458           8 :         nulls[1] = false;
     459             : 
     460             :         /* Get critical status */
     461           8 :         values[2] = BoolGetDatum(X509_EXTENSION_get_critical(ext));
     462           8 :         nulls[2] = false;
     463             : 
     464             :         /* Build tuple */
     465           8 :         tuple = heap_form_tuple(fctx->tupdesc, values, nulls);
     466           8 :         result = HeapTupleGetDatum(tuple);
     467             : 
     468           8 :         if (BIO_free(membuf) != 1)
     469           0 :             elog(ERROR, "could not free OpenSSL BIO structure");
     470             : 
     471           8 :         SRF_RETURN_NEXT(funcctx, result);
     472             :     }
     473             : 
     474             :     /* All done */
     475           2 :     SRF_RETURN_DONE(funcctx);
     476             : }

Generated by: LCOV version 1.14