diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:15:05 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:15:05 +0000 |
commit | 46651ce6fe013220ed397add242004d764fc0153 (patch) | |
tree | 6e5299f990f88e60174a1d3ae6e48eedd2688b2b /src/backend/libpq/auth.c | |
parent | Initial commit. (diff) | |
download | postgresql-14-upstream.tar.xz postgresql-14-upstream.zip |
Adding upstream version 14.5.upstream/14.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/backend/libpq/auth.c')
-rw-r--r-- | src/backend/libpq/auth.c | 3492 |
1 files changed, 3492 insertions, 0 deletions
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c new file mode 100644 index 0000000..be14f2f --- /dev/null +++ b/src/backend/libpq/auth.c @@ -0,0 +1,3492 @@ +/*------------------------------------------------------------------------- + * + * auth.c + * Routines to handle network authentication + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/libpq/auth.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include <sys/param.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <unistd.h> +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif + +#include "commands/user.h" +#include "common/ip.h" +#include "common/md5.h" +#include "common/scram-common.h" +#include "libpq/auth.h" +#include "libpq/crypt.h" +#include "libpq/libpq.h" +#include "libpq/pqformat.h" +#include "libpq/scram.h" +#include "miscadmin.h" +#include "port/pg_bswap.h" +#include "postmaster/postmaster.h" +#include "replication/walsender.h" +#include "storage/ipc.h" +#include "utils/guc.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" + +/*---------------------------------------------------------------- + * Global authentication functions + *---------------------------------------------------------------- + */ +static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, + int extralen); +static void auth_failed(Port *port, int status, char *logdetail); +static char *recv_password_packet(Port *port); +static void set_authn_id(Port *port, const char *id); + + +/*---------------------------------------------------------------- + * Password-based authentication methods (password, md5, and scram-sha-256) + *---------------------------------------------------------------- + */ +static int CheckPasswordAuth(Port *port, char **logdetail); +static int CheckPWChallengeAuth(Port *port, char **logdetail); + +static int CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail); +static int CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail); + + +/*---------------------------------------------------------------- + * Ident authentication + *---------------------------------------------------------------- + */ +/* Max size of username ident server can return (per RFC 1413) */ +#define IDENT_USERNAME_MAX 512 + +/* Standard TCP port number for Ident service. Assigned by IANA */ +#define IDENT_PORT 113 + +static int ident_inet(hbaPort *port); + + +/*---------------------------------------------------------------- + * Peer authentication + *---------------------------------------------------------------- + */ +static int auth_peer(hbaPort *port); + + +/*---------------------------------------------------------------- + * PAM authentication + *---------------------------------------------------------------- + */ +#ifdef USE_PAM +#ifdef HAVE_PAM_PAM_APPL_H +#include <pam/pam_appl.h> +#endif +#ifdef HAVE_SECURITY_PAM_APPL_H +#include <security/pam_appl.h> +#endif + +#define PGSQL_PAM_SERVICE "postgresql" /* Service name passed to PAM */ + +static int CheckPAMAuth(Port *port, const char *user, const char *password); +static int pam_passwd_conv_proc(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr); + +static struct pam_conv pam_passw_conv = { + &pam_passwd_conv_proc, + NULL +}; + +static const char *pam_passwd = NULL; /* Workaround for Solaris 2.6 + * brokenness */ +static Port *pam_port_cludge; /* Workaround for passing "Port *port" into + * pam_passwd_conv_proc */ +static bool pam_no_password; /* For detecting no-password-given */ +#endif /* USE_PAM */ + + +/*---------------------------------------------------------------- + * BSD authentication + *---------------------------------------------------------------- + */ +#ifdef USE_BSD_AUTH +#include <bsd_auth.h> + +static int CheckBSDAuth(Port *port, char *user); +#endif /* USE_BSD_AUTH */ + + +/*---------------------------------------------------------------- + * LDAP authentication + *---------------------------------------------------------------- + */ +#ifdef USE_LDAP +#ifndef WIN32 +/* We use a deprecated function to keep the codepath the same as win32. */ +#define LDAP_DEPRECATED 1 +#include <ldap.h> +#else +#include <winldap.h> + +/* Correct header from the Platform SDK */ +typedef +ULONG (*__ldap_start_tls_sA) (IN PLDAP ExternalHandle, + OUT PULONG ServerReturnValue, + OUT LDAPMessage **result, + IN PLDAPControlA * ServerControls, + IN PLDAPControlA * ClientControls +); +#endif + +static int CheckLDAPAuth(Port *port); + +/* LDAP_OPT_DIAGNOSTIC_MESSAGE is the newer spelling */ +#ifndef LDAP_OPT_DIAGNOSTIC_MESSAGE +#define LDAP_OPT_DIAGNOSTIC_MESSAGE LDAP_OPT_ERROR_STRING +#endif + +#endif /* USE_LDAP */ + +/*---------------------------------------------------------------- + * Cert authentication + *---------------------------------------------------------------- + */ +#ifdef USE_SSL +static int CheckCertAuth(Port *port); +#endif + + +/*---------------------------------------------------------------- + * Kerberos and GSSAPI GUCs + *---------------------------------------------------------------- + */ +char *pg_krb_server_keyfile; +bool pg_krb_caseins_users; + + +/*---------------------------------------------------------------- + * GSSAPI Authentication + *---------------------------------------------------------------- + */ +#ifdef ENABLE_GSS +#include "libpq/be-gssapi-common.h" + +static int pg_GSS_checkauth(Port *port); +static int pg_GSS_recvauth(Port *port); +#endif /* ENABLE_GSS */ + + +/*---------------------------------------------------------------- + * SSPI Authentication + *---------------------------------------------------------------- + */ +#ifdef ENABLE_SSPI +typedef SECURITY_STATUS + (WINAPI * QUERY_SECURITY_CONTEXT_TOKEN_FN) (PCtxtHandle, void **); +static int pg_SSPI_recvauth(Port *port); +static int pg_SSPI_make_upn(char *accountname, + size_t accountnamesize, + char *domainname, + size_t domainnamesize, + bool update_accountname); +#endif + +/*---------------------------------------------------------------- + * RADIUS Authentication + *---------------------------------------------------------------- + */ +static int CheckRADIUSAuth(Port *port); +static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd); + + +/* + * Maximum accepted size of GSS and SSPI authentication tokens. + * We also use this as a limit on ordinary password packet lengths. + * + * Kerberos tickets are usually quite small, but the TGTs issued by Windows + * domain controllers include an authorization field known as the Privilege + * Attribute Certificate (PAC), which contains the user's Windows permissions + * (group memberships etc.). The PAC is copied into all tickets obtained on + * the basis of this TGT (even those issued by Unix realms which the Windows + * realm trusts), and can be several kB in size. The maximum token size + * accepted by Windows systems is determined by the MaxAuthToken Windows + * registry setting. Microsoft recommends that it is not set higher than + * 65535 bytes, so that seems like a reasonable limit for us as well. + */ +#define PG_MAX_AUTH_TOKEN_LENGTH 65535 + +/* + * Maximum accepted size of SASL messages. + * + * The messages that the server or libpq generate are much smaller than this, + * but have some headroom. + */ +#define PG_MAX_SASL_MESSAGE_LENGTH 1024 + +/*---------------------------------------------------------------- + * Global authentication functions + *---------------------------------------------------------------- + */ + +/* + * This hook allows plugins to get control following client authentication, + * but before the user has been informed about the results. It could be used + * to record login events, insert a delay after failed authentication, etc. + */ +ClientAuthentication_hook_type ClientAuthentication_hook = NULL; + +/* + * Tell the user the authentication failed, but not (much about) why. + * + * There is a tradeoff here between security concerns and making life + * unnecessarily difficult for legitimate users. We would not, for example, + * want to report the password we were expecting to receive... + * But it seems useful to report the username and authorization method + * in use, and these are items that must be presumed known to an attacker + * anyway. + * Note that many sorts of failure report additional information in the + * postmaster log, which we hope is only readable by good guys. In + * particular, if logdetail isn't NULL, we send that string to the log. + */ +static void +auth_failed(Port *port, int status, char *logdetail) +{ + const char *errstr; + char *cdetail; + int errcode_return = ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION; + + /* + * If we failed due to EOF from client, just quit; there's no point in + * trying to send a message to the client, and not much point in logging + * the failure in the postmaster log. (Logging the failure might be + * desirable, were it not for the fact that libpq closes the connection + * unceremoniously if challenged for a password when it hasn't got one to + * send. We'll get a useless log entry for every psql connection under + * password auth, even if it's perfectly successful, if we log STATUS_EOF + * events.) + */ + if (status == STATUS_EOF) + proc_exit(0); + + switch (port->hba->auth_method) + { + case uaReject: + case uaImplicitReject: + errstr = gettext_noop("authentication failed for user \"%s\": host rejected"); + break; + case uaTrust: + errstr = gettext_noop("\"trust\" authentication failed for user \"%s\""); + break; + case uaIdent: + errstr = gettext_noop("Ident authentication failed for user \"%s\""); + break; + case uaPeer: + errstr = gettext_noop("Peer authentication failed for user \"%s\""); + break; + case uaPassword: + case uaMD5: + case uaSCRAM: + errstr = gettext_noop("password authentication failed for user \"%s\""); + /* We use it to indicate if a .pgpass password failed. */ + errcode_return = ERRCODE_INVALID_PASSWORD; + break; + case uaGSS: + errstr = gettext_noop("GSSAPI authentication failed for user \"%s\""); + break; + case uaSSPI: + errstr = gettext_noop("SSPI authentication failed for user \"%s\""); + break; + case uaPAM: + errstr = gettext_noop("PAM authentication failed for user \"%s\""); + break; + case uaBSD: + errstr = gettext_noop("BSD authentication failed for user \"%s\""); + break; + case uaLDAP: + errstr = gettext_noop("LDAP authentication failed for user \"%s\""); + break; + case uaCert: + errstr = gettext_noop("certificate authentication failed for user \"%s\""); + break; + case uaRADIUS: + errstr = gettext_noop("RADIUS authentication failed for user \"%s\""); + break; + default: + errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method"); + break; + } + + cdetail = psprintf(_("Connection matched pg_hba.conf line %d: \"%s\""), + port->hba->linenumber, port->hba->rawline); + if (logdetail) + logdetail = psprintf("%s\n%s", logdetail, cdetail); + else + logdetail = cdetail; + + ereport(FATAL, + (errcode(errcode_return), + errmsg(errstr, port->user_name), + logdetail ? errdetail_log("%s", logdetail) : 0)); + + /* doesn't return */ +} + + +/* + * Sets the authenticated identity for the current user. The provided string + * will be copied into the TopMemoryContext. The ID will be logged if + * log_connections is enabled. + * + * Auth methods should call this routine exactly once, as soon as the user is + * successfully authenticated, even if they have reasons to know that + * authorization will fail later. + * + * The provided string will be copied into TopMemoryContext, to match the + * lifetime of the Port, so it is safe to pass a string that is managed by an + * external library. + */ +static void +set_authn_id(Port *port, const char *id) +{ + Assert(id); + + if (port->authn_id) + { + /* + * An existing authn_id should never be overwritten; that means two + * authentication providers are fighting (or one is fighting itself). + * Don't leak any authn details to the client, but don't let the + * connection continue, either. + */ + ereport(FATAL, + (errmsg("authentication identifier set more than once"), + errdetail_log("previous identifier: \"%s\"; new identifier: \"%s\"", + port->authn_id, id))); + } + + port->authn_id = MemoryContextStrdup(TopMemoryContext, id); + + if (Log_connections) + { + ereport(LOG, + errmsg("connection authenticated: identity=\"%s\" method=%s " + "(%s:%d)", + port->authn_id, hba_authname(port->hba->auth_method), HbaFileName, + port->hba->linenumber)); + } +} + + +/* + * Client authentication starts here. If there is an error, this + * function does not return and the backend process is terminated. + */ +void +ClientAuthentication(Port *port) +{ + int status = STATUS_ERROR; + char *logdetail = NULL; + + /* + * Get the authentication method to use for this frontend/database + * combination. Note: we do not parse the file at this point; this has + * already been done elsewhere. hba.c dropped an error message into the + * server logfile if parsing the hba config file failed. + */ + hba_getauthmethod(port); + + CHECK_FOR_INTERRUPTS(); + + /* + * This is the first point where we have access to the hba record for the + * current connection, so perform any verifications based on the hba + * options field that should be done *before* the authentication here. + */ + if (port->hba->clientcert != clientCertOff) + { + /* If we haven't loaded a root certificate store, fail */ + if (!secure_loaded_verify_locations()) + ereport(FATAL, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("client certificates can only be checked if a root certificate store is available"))); + + /* + * If we loaded a root certificate store, and if a certificate is + * present on the client, then it has been verified against our root + * certificate store, and the connection would have been aborted + * already if it didn't verify ok. + */ + if (!port->peer_cert_valid) + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("connection requires a valid client certificate"))); + } + + /* + * Now proceed to do the actual authentication check + */ + switch (port->hba->auth_method) + { + case uaReject: + + /* + * An explicit "reject" entry in pg_hba.conf. This report exposes + * the fact that there's an explicit reject entry, which is + * perhaps not so desirable from a security standpoint; but the + * message for an implicit reject could confuse the DBA a lot when + * the true situation is a match to an explicit reject. And we + * don't want to change the message for an implicit reject. As + * noted below, the additional information shown here doesn't + * expose anything not known to an attacker. + */ + { + char hostinfo[NI_MAXHOST]; + const char *encryption_state; + + pg_getnameinfo_all(&port->raddr.addr, port->raddr.salen, + hostinfo, sizeof(hostinfo), + NULL, 0, + NI_NUMERICHOST); + + encryption_state = +#ifdef ENABLE_GSS + (port->gss && port->gss->enc) ? _("GSS encryption") : +#endif +#ifdef USE_SSL + port->ssl_in_use ? _("SSL encryption") : +#endif + _("no encryption"); + + if (am_walsender && !am_db_walsender) + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + /* translator: last %s describes encryption state */ + errmsg("pg_hba.conf rejects replication connection for host \"%s\", user \"%s\", %s", + hostinfo, port->user_name, + encryption_state))); + else + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + /* translator: last %s describes encryption state */ + errmsg("pg_hba.conf rejects connection for host \"%s\", user \"%s\", database \"%s\", %s", + hostinfo, port->user_name, + port->database_name, + encryption_state))); + break; + } + + case uaImplicitReject: + + /* + * No matching entry, so tell the user we fell through. + * + * NOTE: the extra info reported here is not a security breach, + * because all that info is known at the frontend and must be + * assumed known to bad guys. We're merely helping out the less + * clueful good guys. + */ + { + char hostinfo[NI_MAXHOST]; + const char *encryption_state; + + pg_getnameinfo_all(&port->raddr.addr, port->raddr.salen, + hostinfo, sizeof(hostinfo), + NULL, 0, + NI_NUMERICHOST); + + encryption_state = +#ifdef ENABLE_GSS + (port->gss && port->gss->enc) ? _("GSS encryption") : +#endif +#ifdef USE_SSL + port->ssl_in_use ? _("SSL encryption") : +#endif + _("no encryption"); + +#define HOSTNAME_LOOKUP_DETAIL(port) \ + (port->remote_hostname ? \ + (port->remote_hostname_resolv == +1 ? \ + errdetail_log("Client IP address resolved to \"%s\", forward lookup matches.", \ + port->remote_hostname) : \ + port->remote_hostname_resolv == 0 ? \ + errdetail_log("Client IP address resolved to \"%s\", forward lookup not checked.", \ + port->remote_hostname) : \ + port->remote_hostname_resolv == -1 ? \ + errdetail_log("Client IP address resolved to \"%s\", forward lookup does not match.", \ + port->remote_hostname) : \ + port->remote_hostname_resolv == -2 ? \ + errdetail_log("Could not translate client host name \"%s\" to IP address: %s.", \ + port->remote_hostname, \ + gai_strerror(port->remote_hostname_errcode)) : \ + 0) \ + : (port->remote_hostname_resolv == -2 ? \ + errdetail_log("Could not resolve client IP address to a host name: %s.", \ + gai_strerror(port->remote_hostname_errcode)) : \ + 0)) + + if (am_walsender && !am_db_walsender) + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + /* translator: last %s describes encryption state */ + errmsg("no pg_hba.conf entry for replication connection from host \"%s\", user \"%s\", %s", + hostinfo, port->user_name, + encryption_state), + HOSTNAME_LOOKUP_DETAIL(port))); + else + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + /* translator: last %s describes encryption state */ + errmsg("no pg_hba.conf entry for host \"%s\", user \"%s\", database \"%s\", %s", + hostinfo, port->user_name, + port->database_name, + encryption_state), + HOSTNAME_LOOKUP_DETAIL(port))); + break; + } + + case uaGSS: +#ifdef ENABLE_GSS + /* We might or might not have the gss workspace already */ + if (port->gss == NULL) + port->gss = (pg_gssinfo *) + MemoryContextAllocZero(TopMemoryContext, + sizeof(pg_gssinfo)); + port->gss->auth = true; + + /* + * If GSS state was set up while enabling encryption, we can just + * check the client's principal. Otherwise, ask for it. + */ + if (port->gss->enc) + status = pg_GSS_checkauth(port); + else + { + sendAuthRequest(port, AUTH_REQ_GSS, NULL, 0); + status = pg_GSS_recvauth(port); + } +#else + Assert(false); +#endif + break; + + case uaSSPI: +#ifdef ENABLE_SSPI + if (port->gss == NULL) + port->gss = (pg_gssinfo *) + MemoryContextAllocZero(TopMemoryContext, + sizeof(pg_gssinfo)); + sendAuthRequest(port, AUTH_REQ_SSPI, NULL, 0); + status = pg_SSPI_recvauth(port); +#else + Assert(false); +#endif + break; + + case uaPeer: + status = auth_peer(port); + break; + + case uaIdent: + status = ident_inet(port); + break; + + case uaMD5: + case uaSCRAM: + status = CheckPWChallengeAuth(port, &logdetail); + break; + + case uaPassword: + status = CheckPasswordAuth(port, &logdetail); + break; + + case uaPAM: +#ifdef USE_PAM + status = CheckPAMAuth(port, port->user_name, ""); +#else + Assert(false); +#endif /* USE_PAM */ + break; + + case uaBSD: +#ifdef USE_BSD_AUTH + status = CheckBSDAuth(port, port->user_name); +#else + Assert(false); +#endif /* USE_BSD_AUTH */ + break; + + case uaLDAP: +#ifdef USE_LDAP + status = CheckLDAPAuth(port); +#else + Assert(false); +#endif + break; + case uaRADIUS: + status = CheckRADIUSAuth(port); + break; + case uaCert: + /* uaCert will be treated as if clientcert=verify-full (uaTrust) */ + case uaTrust: + status = STATUS_OK; + break; + } + + if ((status == STATUS_OK && port->hba->clientcert == clientCertFull) + || port->hba->auth_method == uaCert) + { + /* + * Make sure we only check the certificate if we use the cert method + * or verify-full option. + */ +#ifdef USE_SSL + status = CheckCertAuth(port); +#else + Assert(false); +#endif + } + + if (ClientAuthentication_hook) + (*ClientAuthentication_hook) (port, status); + + if (status == STATUS_OK) + sendAuthRequest(port, AUTH_REQ_OK, NULL, 0); + else + auth_failed(port, status, logdetail); +} + + +/* + * Send an authentication request packet to the frontend. + */ +static void +sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, int extralen) +{ + StringInfoData buf; + + CHECK_FOR_INTERRUPTS(); + + pq_beginmessage(&buf, 'R'); + pq_sendint32(&buf, (int32) areq); + if (extralen > 0) + pq_sendbytes(&buf, extradata, extralen); + + pq_endmessage(&buf); + + /* + * Flush message so client will see it, except for AUTH_REQ_OK and + * AUTH_REQ_SASL_FIN, which need not be sent until we are ready for + * queries. + */ + if (areq != AUTH_REQ_OK && areq != AUTH_REQ_SASL_FIN) + pq_flush(); + + CHECK_FOR_INTERRUPTS(); +} + +/* + * Collect password response packet from frontend. + * + * Returns NULL if couldn't get password, else palloc'd string. + */ +static char * +recv_password_packet(Port *port) +{ + StringInfoData buf; + int mtype; + + pq_startmsgread(); + + /* Expect 'p' message type */ + mtype = pq_getbyte(); + if (mtype != 'p') + { + /* + * If the client just disconnects without offering a password, don't + * make a log entry. This is legal per protocol spec and in fact + * commonly done by psql, so complaining just clutters the log. + */ + if (mtype != EOF) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("expected password response, got message type %d", + mtype))); + return NULL; /* EOF or bad message type */ + } + + initStringInfo(&buf); + if (pq_getmessage(&buf, PG_MAX_AUTH_TOKEN_LENGTH)) /* receive password */ + { + /* EOF - pq_getmessage already logged a suitable message */ + pfree(buf.data); + return NULL; + } + + /* + * Apply sanity check: password packet length should agree with length of + * contained string. Note it is safe to use strlen here because + * StringInfo is guaranteed to have an appended '\0'. + */ + if (strlen(buf.data) + 1 != buf.len) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("invalid password packet size"))); + + /* + * Don't allow an empty password. Libpq treats an empty password the same + * as no password at all, and won't even try to authenticate. But other + * clients might, so allowing it would be confusing. + * + * Note that this only catches an empty password sent by the client in + * plaintext. There's also a check in CREATE/ALTER USER that prevents an + * empty string from being stored as a user's password in the first place. + * We rely on that for MD5 and SCRAM authentication, but we still need + * this check here, to prevent an empty password from being used with + * authentication methods that check the password against an external + * system, like PAM, LDAP and RADIUS. + */ + if (buf.len == 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PASSWORD), + errmsg("empty password returned by client"))); + + /* Do not echo password to logs, for security. */ + elog(DEBUG5, "received password packet"); + + /* + * Return the received string. Note we do not attempt to do any + * character-set conversion on it; since we don't yet know the client's + * encoding, there wouldn't be much point. + */ + return buf.data; +} + + +/*---------------------------------------------------------------- + * Password-based authentication mechanisms + *---------------------------------------------------------------- + */ + +/* + * Plaintext password authentication. + */ +static int +CheckPasswordAuth(Port *port, char **logdetail) +{ + char *passwd; + int result; + char *shadow_pass; + + sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); + + passwd = recv_password_packet(port); + if (passwd == NULL) + return STATUS_EOF; /* client wouldn't send password */ + + shadow_pass = get_role_password(port->user_name, logdetail); + if (shadow_pass) + { + result = plain_crypt_verify(port->user_name, shadow_pass, passwd, + logdetail); + } + else + result = STATUS_ERROR; + + if (shadow_pass) + pfree(shadow_pass); + pfree(passwd); + + if (result == STATUS_OK) + set_authn_id(port, port->user_name); + + return result; +} + +/* + * MD5 and SCRAM authentication. + */ +static int +CheckPWChallengeAuth(Port *port, char **logdetail) +{ + int auth_result; + char *shadow_pass; + PasswordType pwtype; + + Assert(port->hba->auth_method == uaSCRAM || + port->hba->auth_method == uaMD5); + + /* First look up the user's password. */ + shadow_pass = get_role_password(port->user_name, logdetail); + + /* + * If the user does not exist, or has no password or it's expired, we + * still go through the motions of authentication, to avoid revealing to + * the client that the user didn't exist. If 'md5' is allowed, we choose + * whether to use 'md5' or 'scram-sha-256' authentication based on current + * password_encryption setting. The idea is that most genuine users + * probably have a password of that type, and if we pretend that this user + * had a password of that type, too, it "blends in" best. + */ + if (!shadow_pass) + pwtype = Password_encryption; + else + pwtype = get_password_type(shadow_pass); + + /* + * If 'md5' authentication is allowed, decide whether to perform 'md5' or + * 'scram-sha-256' authentication based on the type of password the user + * has. If it's an MD5 hash, we must do MD5 authentication, and if it's a + * SCRAM secret, we must do SCRAM authentication. + * + * If MD5 authentication is not allowed, always use SCRAM. If the user + * had an MD5 password, CheckSCRAMAuth() will fail. + */ + if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5) + auth_result = CheckMD5Auth(port, shadow_pass, logdetail); + else + auth_result = CheckSCRAMAuth(port, shadow_pass, logdetail); + + if (shadow_pass) + pfree(shadow_pass); + + /* + * If get_role_password() returned error, return error, even if the + * authentication succeeded. + */ + if (!shadow_pass) + { + Assert(auth_result != STATUS_OK); + return STATUS_ERROR; + } + + if (auth_result == STATUS_OK) + set_authn_id(port, port->user_name); + + return auth_result; +} + +static int +CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail) +{ + char md5Salt[4]; /* Password salt */ + char *passwd; + int result; + + if (Db_user_namespace) + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"))); + + /* include the salt to use for computing the response */ + if (!pg_strong_random(md5Salt, 4)) + { + ereport(LOG, + (errmsg("could not generate random MD5 salt"))); + return STATUS_ERROR; + } + + sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4); + + passwd = recv_password_packet(port); + if (passwd == NULL) + return STATUS_EOF; /* client wouldn't send password */ + + if (shadow_pass) + result = md5_crypt_verify(port->user_name, shadow_pass, passwd, + md5Salt, 4, logdetail); + else + result = STATUS_ERROR; + + pfree(passwd); + + return result; +} + +static int +CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) +{ + StringInfoData sasl_mechs; + int mtype; + StringInfoData buf; + void *scram_opaq = NULL; + char *output = NULL; + int outputlen = 0; + const char *input; + int inputlen; + int result; + bool initial; + + /* + * Send the SASL authentication request to user. It includes the list of + * authentication mechanisms that are supported. + */ + initStringInfo(&sasl_mechs); + + pg_be_scram_get_mechanisms(port, &sasl_mechs); + /* Put another '\0' to mark that list is finished. */ + appendStringInfoChar(&sasl_mechs, '\0'); + + sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs.data, sasl_mechs.len); + pfree(sasl_mechs.data); + + /* + * Loop through SASL message exchange. This exchange can consist of + * multiple messages sent in both directions. First message is always + * from the client. All messages from client to server are password + * packets (type 'p'). + */ + initial = true; + do + { + pq_startmsgread(); + mtype = pq_getbyte(); + if (mtype != 'p') + { + /* Only log error if client didn't disconnect. */ + if (mtype != EOF) + { + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("expected SASL response, got message type %d", + mtype))); + } + else + return STATUS_EOF; + } + + /* Get the actual SASL message */ + initStringInfo(&buf); + if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH)) + { + /* EOF - pq_getmessage already logged error */ + pfree(buf.data); + return STATUS_ERROR; + } + + elog(DEBUG4, "processing received SASL response of length %d", buf.len); + + /* + * The first SASLInitialResponse message is different from the others. + * It indicates which SASL mechanism the client selected, and contains + * an optional Initial Client Response payload. The subsequent + * SASLResponse messages contain just the SASL payload. + */ + if (initial) + { + const char *selected_mech; + + selected_mech = pq_getmsgrawstring(&buf); + + /* + * Initialize the status tracker for message exchanges. + * + * If the user doesn't exist, or doesn't have a valid password, or + * it's expired, we still go through the motions of SASL + * authentication, but tell the authentication method that the + * authentication is "doomed". That is, it's going to fail, no + * matter what. + * + * This is because we don't want to reveal to an attacker what + * usernames are valid, nor which users have a valid password. + */ + scram_opaq = pg_be_scram_init(port, selected_mech, shadow_pass); + + inputlen = pq_getmsgint(&buf, 4); + if (inputlen == -1) + input = NULL; + else + input = pq_getmsgbytes(&buf, inputlen); + + initial = false; + } + else + { + inputlen = buf.len; + input = pq_getmsgbytes(&buf, buf.len); + } + pq_getmsgend(&buf); + + /* + * The StringInfo guarantees that there's a \0 byte after the + * response. + */ + Assert(input == NULL || input[inputlen] == '\0'); + + /* + * we pass 'logdetail' as NULL when doing a mock authentication, + * because we should already have a better error message in that case + */ + result = pg_be_scram_exchange(scram_opaq, input, inputlen, + &output, &outputlen, + logdetail); + + /* input buffer no longer used */ + pfree(buf.data); + + if (output) + { + /* + * Negotiation generated data to be sent to the client. + */ + elog(DEBUG4, "sending SASL challenge of length %u", outputlen); + + if (result == SASL_EXCHANGE_SUCCESS) + sendAuthRequest(port, AUTH_REQ_SASL_FIN, output, outputlen); + else + sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen); + + pfree(output); + } + } while (result == SASL_EXCHANGE_CONTINUE); + + /* Oops, Something bad happened */ + if (result != SASL_EXCHANGE_SUCCESS) + { + return STATUS_ERROR; + } + + return STATUS_OK; +} + + +/*---------------------------------------------------------------- + * GSSAPI authentication system + *---------------------------------------------------------------- + */ +#ifdef ENABLE_GSS +static int +pg_GSS_recvauth(Port *port) +{ + OM_uint32 maj_stat, + min_stat, + lmin_s, + gflags; + int mtype; + StringInfoData buf; + gss_buffer_desc gbuf; + + /* + * Use the configured keytab, if there is one. Unfortunately, Heimdal + * doesn't support the cred store extensions, so use the env var. + */ + if (pg_krb_server_keyfile != NULL && pg_krb_server_keyfile[0] != '\0') + { + if (setenv("KRB5_KTNAME", pg_krb_server_keyfile, 1) != 0) + { + /* The only likely failure cause is OOM, so use that errcode */ + ereport(FATAL, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("could not set environment: %m"))); + } + } + + /* + * We accept any service principal that's present in our keytab. This + * increases interoperability between kerberos implementations that see + * for example case sensitivity differently, while not really opening up + * any vector of attack. + */ + port->gss->cred = GSS_C_NO_CREDENTIAL; + + /* + * Initialize sequence with an empty context + */ + port->gss->ctx = GSS_C_NO_CONTEXT; + + /* + * Loop through GSSAPI message exchange. This exchange can consist of + * multiple messages sent in both directions. First message is always from + * the client. All messages from client to server are password packets + * (type 'p'). + */ + do + { + pq_startmsgread(); + + CHECK_FOR_INTERRUPTS(); + + mtype = pq_getbyte(); + if (mtype != 'p') + { + /* Only log error if client didn't disconnect. */ + if (mtype != EOF) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("expected GSS response, got message type %d", + mtype))); + return STATUS_ERROR; + } + + /* Get the actual GSS token */ + initStringInfo(&buf); + if (pq_getmessage(&buf, PG_MAX_AUTH_TOKEN_LENGTH)) + { + /* EOF - pq_getmessage already logged error */ + pfree(buf.data); + return STATUS_ERROR; + } + + /* Map to GSSAPI style buffer */ + gbuf.length = buf.len; + gbuf.value = buf.data; + + elog(DEBUG4, "processing received GSS token of length %u", + (unsigned int) gbuf.length); + + maj_stat = gss_accept_sec_context(&min_stat, + &port->gss->ctx, + port->gss->cred, + &gbuf, + GSS_C_NO_CHANNEL_BINDINGS, + &port->gss->name, + NULL, + &port->gss->outbuf, + &gflags, + NULL, + NULL); + + /* gbuf no longer used */ + pfree(buf.data); + + elog(DEBUG5, "gss_accept_sec_context major: %d, " + "minor: %d, outlen: %u, outflags: %x", + maj_stat, min_stat, + (unsigned int) port->gss->outbuf.length, gflags); + + CHECK_FOR_INTERRUPTS(); + + if (port->gss->outbuf.length != 0) + { + /* + * Negotiation generated data to be sent to the client. + */ + elog(DEBUG4, "sending GSS response token of length %u", + (unsigned int) port->gss->outbuf.length); + + sendAuthRequest(port, AUTH_REQ_GSS_CONT, + port->gss->outbuf.value, port->gss->outbuf.length); + + gss_release_buffer(&lmin_s, &port->gss->outbuf); + } + + if (maj_stat != GSS_S_COMPLETE && maj_stat != GSS_S_CONTINUE_NEEDED) + { + gss_delete_sec_context(&lmin_s, &port->gss->ctx, GSS_C_NO_BUFFER); + pg_GSS_error(_("accepting GSS security context failed"), + maj_stat, min_stat); + return STATUS_ERROR; + } + + if (maj_stat == GSS_S_CONTINUE_NEEDED) + elog(DEBUG4, "GSS continue needed"); + + } while (maj_stat == GSS_S_CONTINUE_NEEDED); + + if (port->gss->cred != GSS_C_NO_CREDENTIAL) + { + /* + * Release service principal credentials + */ + gss_release_cred(&min_stat, &port->gss->cred); + } + return pg_GSS_checkauth(port); +} + +/* + * Check whether the GSSAPI-authenticated user is allowed to connect as the + * claimed username. + */ +static int +pg_GSS_checkauth(Port *port) +{ + int ret; + OM_uint32 maj_stat, + min_stat, + lmin_s; + gss_buffer_desc gbuf; + char *princ; + + /* + * Get the name of the user that authenticated, and compare it to the pg + * username that was specified for the connection. + */ + maj_stat = gss_display_name(&min_stat, port->gss->name, &gbuf, NULL); + if (maj_stat != GSS_S_COMPLETE) + { + pg_GSS_error(_("retrieving GSS user name failed"), + maj_stat, min_stat); + return STATUS_ERROR; + } + + /* + * gbuf.value might not be null-terminated, so turn it into a regular + * null-terminated string. + */ + princ = palloc(gbuf.length + 1); + memcpy(princ, gbuf.value, gbuf.length); + princ[gbuf.length] = '\0'; + gss_release_buffer(&lmin_s, &gbuf); + + /* + * Copy the original name of the authenticated principal into our backend + * memory for display later. + * + * This is also our authenticated identity. Set it now, rather than + * waiting for the usermap check below, because authentication has already + * succeeded and we want the log file to reflect that. + */ + port->gss->princ = MemoryContextStrdup(TopMemoryContext, princ); + set_authn_id(port, princ); + + /* + * Split the username at the realm separator + */ + if (strchr(princ, '@')) + { + char *cp = strchr(princ, '@'); + + /* + * If we are not going to include the realm in the username that is + * passed to the ident map, destructively modify it here to remove the + * realm. Then advance past the separator to check the realm. + */ + if (!port->hba->include_realm) + *cp = '\0'; + cp++; + + if (port->hba->krb_realm != NULL && strlen(port->hba->krb_realm)) + { + /* + * Match the realm part of the name first + */ + if (pg_krb_caseins_users) + ret = pg_strcasecmp(port->hba->krb_realm, cp); + else + ret = strcmp(port->hba->krb_realm, cp); + + if (ret) + { + /* GSS realm does not match */ + elog(DEBUG2, + "GSSAPI realm (%s) and configured realm (%s) don't match", + cp, port->hba->krb_realm); + pfree(princ); + return STATUS_ERROR; + } + } + } + else if (port->hba->krb_realm && strlen(port->hba->krb_realm)) + { + elog(DEBUG2, + "GSSAPI did not return realm but realm matching was requested"); + pfree(princ); + return STATUS_ERROR; + } + + ret = check_usermap(port->hba->usermap, port->user_name, princ, + pg_krb_caseins_users); + + pfree(princ); + + return ret; +} +#endif /* ENABLE_GSS */ + + +/*---------------------------------------------------------------- + * SSPI authentication system + *---------------------------------------------------------------- + */ +#ifdef ENABLE_SSPI + +/* + * Generate an error for SSPI authentication. The caller should apply + * _() to errmsg to make it translatable. + */ +static void +pg_SSPI_error(int severity, const char *errmsg, SECURITY_STATUS r) +{ + char sysmsg[256]; + + if (FormatMessage(FORMAT_MESSAGE_IGNORE_INSERTS | + FORMAT_MESSAGE_FROM_SYSTEM, + NULL, r, 0, + sysmsg, sizeof(sysmsg), NULL) == 0) + ereport(severity, + (errmsg_internal("%s", errmsg), + errdetail_internal("SSPI error %x", (unsigned int) r))); + else + ereport(severity, + (errmsg_internal("%s", errmsg), + errdetail_internal("%s (%x)", sysmsg, (unsigned int) r))); +} + +static int +pg_SSPI_recvauth(Port *port) +{ + int mtype; + StringInfoData buf; + SECURITY_STATUS r; + CredHandle sspicred; + CtxtHandle *sspictx = NULL, + newctx; + TimeStamp expiry; + ULONG contextattr; + SecBufferDesc inbuf; + SecBufferDesc outbuf; + SecBuffer OutBuffers[1]; + SecBuffer InBuffers[1]; + HANDLE token; + TOKEN_USER *tokenuser; + DWORD retlen; + char accountname[MAXPGPATH]; + char domainname[MAXPGPATH]; + DWORD accountnamesize = sizeof(accountname); + DWORD domainnamesize = sizeof(domainname); + SID_NAME_USE accountnameuse; + HMODULE secur32; + char *authn_id; + + QUERY_SECURITY_CONTEXT_TOKEN_FN _QuerySecurityContextToken; + + /* + * Acquire a handle to the server credentials. + */ + r = AcquireCredentialsHandle(NULL, + "negotiate", + SECPKG_CRED_INBOUND, + NULL, + NULL, + NULL, + NULL, + &sspicred, + &expiry); + if (r != SEC_E_OK) + pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r); + + /* + * Loop through SSPI message exchange. This exchange can consist of + * multiple messages sent in both directions. First message is always from + * the client. All messages from client to server are password packets + * (type 'p'). + */ + do + { + pq_startmsgread(); + mtype = pq_getbyte(); + if (mtype != 'p') + { + if (sspictx != NULL) + { + DeleteSecurityContext(sspictx); + free(sspictx); + } + FreeCredentialsHandle(&sspicred); + + /* Only log error if client didn't disconnect. */ + if (mtype != EOF) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("expected SSPI response, got message type %d", + mtype))); + return STATUS_ERROR; + } + + /* Get the actual SSPI token */ + initStringInfo(&buf); + if (pq_getmessage(&buf, PG_MAX_AUTH_TOKEN_LENGTH)) + { + /* EOF - pq_getmessage already logged error */ + pfree(buf.data); + if (sspictx != NULL) + { + DeleteSecurityContext(sspictx); + free(sspictx); + } + FreeCredentialsHandle(&sspicred); + return STATUS_ERROR; + } + + /* Map to SSPI style buffer */ + inbuf.ulVersion = SECBUFFER_VERSION; + inbuf.cBuffers = 1; + inbuf.pBuffers = InBuffers; + InBuffers[0].pvBuffer = buf.data; + InBuffers[0].cbBuffer = buf.len; + InBuffers[0].BufferType = SECBUFFER_TOKEN; + + /* Prepare output buffer */ + OutBuffers[0].pvBuffer = NULL; + OutBuffers[0].BufferType = SECBUFFER_TOKEN; + OutBuffers[0].cbBuffer = 0; + outbuf.cBuffers = 1; + outbuf.pBuffers = OutBuffers; + outbuf.ulVersion = SECBUFFER_VERSION; + + elog(DEBUG4, "processing received SSPI token of length %u", + (unsigned int) buf.len); + + r = AcceptSecurityContext(&sspicred, + sspictx, + &inbuf, + ASC_REQ_ALLOCATE_MEMORY, + SECURITY_NETWORK_DREP, + &newctx, + &outbuf, + &contextattr, + NULL); + + /* input buffer no longer used */ + pfree(buf.data); + + if (outbuf.cBuffers > 0 && outbuf.pBuffers[0].cbBuffer > 0) + { + /* + * Negotiation generated data to be sent to the client. + */ + elog(DEBUG4, "sending SSPI response token of length %u", + (unsigned int) outbuf.pBuffers[0].cbBuffer); + + port->gss->outbuf.length = outbuf.pBuffers[0].cbBuffer; + port->gss->outbuf.value = outbuf.pBuffers[0].pvBuffer; + + sendAuthRequest(port, AUTH_REQ_GSS_CONT, + port->gss->outbuf.value, port->gss->outbuf.length); + + FreeContextBuffer(outbuf.pBuffers[0].pvBuffer); + } + + if (r != SEC_E_OK && r != SEC_I_CONTINUE_NEEDED) + { + if (sspictx != NULL) + { + DeleteSecurityContext(sspictx); + free(sspictx); + } + FreeCredentialsHandle(&sspicred); + pg_SSPI_error(ERROR, + _("could not accept SSPI security context"), r); + } + + /* + * Overwrite the current context with the one we just received. If + * sspictx is NULL it was the first loop and we need to allocate a + * buffer for it. On subsequent runs, we can just overwrite the buffer + * contents since the size does not change. + */ + if (sspictx == NULL) + { + sspictx = malloc(sizeof(CtxtHandle)); + if (sspictx == NULL) + ereport(ERROR, + (errmsg("out of memory"))); + } + + memcpy(sspictx, &newctx, sizeof(CtxtHandle)); + + if (r == SEC_I_CONTINUE_NEEDED) + elog(DEBUG4, "SSPI continue needed"); + + } while (r == SEC_I_CONTINUE_NEEDED); + + + /* + * Release service principal credentials + */ + FreeCredentialsHandle(&sspicred); + + + /* + * SEC_E_OK indicates that authentication is now complete. + * + * Get the name of the user that authenticated, and compare it to the pg + * username that was specified for the connection. + * + * MingW is missing the export for QuerySecurityContextToken in the + * secur32 library, so we have to load it dynamically. + */ + + secur32 = LoadLibrary("SECUR32.DLL"); + if (secur32 == NULL) + ereport(ERROR, + (errmsg("could not load library \"%s\": error code %lu", + "SECUR32.DLL", GetLastError()))); + + _QuerySecurityContextToken = (QUERY_SECURITY_CONTEXT_TOKEN_FN) (pg_funcptr_t) + GetProcAddress(secur32, "QuerySecurityContextToken"); + if (_QuerySecurityContextToken == NULL) + { + FreeLibrary(secur32); + ereport(ERROR, + (errmsg_internal("could not locate QuerySecurityContextToken in secur32.dll: error code %lu", + GetLastError()))); + } + + r = (_QuerySecurityContextToken) (sspictx, &token); + if (r != SEC_E_OK) + { + FreeLibrary(secur32); + pg_SSPI_error(ERROR, + _("could not get token from SSPI security context"), r); + } + + FreeLibrary(secur32); + + /* + * No longer need the security context, everything from here on uses the + * token instead. + */ + DeleteSecurityContext(sspictx); + free(sspictx); + + if (!GetTokenInformation(token, TokenUser, NULL, 0, &retlen) && GetLastError() != 122) + ereport(ERROR, + (errmsg_internal("could not get token information buffer size: error code %lu", + GetLastError()))); + + tokenuser = malloc(retlen); + if (tokenuser == NULL) + ereport(ERROR, + (errmsg("out of memory"))); + + if (!GetTokenInformation(token, TokenUser, tokenuser, retlen, &retlen)) + ereport(ERROR, + (errmsg_internal("could not get token information: error code %lu", + GetLastError()))); + + CloseHandle(token); + + if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize, + domainname, &domainnamesize, &accountnameuse)) + ereport(ERROR, + (errmsg_internal("could not look up account SID: error code %lu", + GetLastError()))); + + free(tokenuser); + + if (!port->hba->compat_realm) + { + int status = pg_SSPI_make_upn(accountname, sizeof(accountname), + domainname, sizeof(domainname), + port->hba->upn_username); + + if (status != STATUS_OK) + /* Error already reported from pg_SSPI_make_upn */ + return status; + } + + /* + * We have all of the information necessary to construct the authenticated + * identity. Set it now, rather than waiting for check_usermap below, + * because authentication has already succeeded and we want the log file + * to reflect that. + */ + if (port->hba->compat_realm) + { + /* SAM-compatible format. */ + authn_id = psprintf("%s\\%s", domainname, accountname); + } + else + { + /* Kerberos principal format. */ + authn_id = psprintf("%s@%s", accountname, domainname); + } + + set_authn_id(port, authn_id); + pfree(authn_id); + + /* + * Compare realm/domain if requested. In SSPI, always compare case + * insensitive. + */ + if (port->hba->krb_realm && strlen(port->hba->krb_realm)) + { + if (pg_strcasecmp(port->hba->krb_realm, domainname) != 0) + { + elog(DEBUG2, + "SSPI domain (%s) and configured domain (%s) don't match", + domainname, port->hba->krb_realm); + + return STATUS_ERROR; + } + } + + /* + * We have the username (without domain/realm) in accountname, compare to + * the supplied value. In SSPI, always compare case insensitive. + * + * If set to include realm, append it in <username>@<realm> format. + */ + if (port->hba->include_realm) + { + char *namebuf; + int retval; + + namebuf = psprintf("%s@%s", accountname, domainname); + retval = check_usermap(port->hba->usermap, port->user_name, namebuf, true); + pfree(namebuf); + return retval; + } + else + return check_usermap(port->hba->usermap, port->user_name, accountname, true); +} + +/* + * Replaces the domainname with the Kerberos realm name, + * and optionally the accountname with the Kerberos user name. + */ +static int +pg_SSPI_make_upn(char *accountname, + size_t accountnamesize, + char *domainname, + size_t domainnamesize, + bool update_accountname) +{ + char *samname; + char *upname = NULL; + char *p = NULL; + ULONG upnamesize = 0; + size_t upnamerealmsize; + BOOLEAN res; + + /* + * Build SAM name (DOMAIN\user), then translate to UPN + * (user@kerberos.realm). The realm name is returned in lower case, but + * that is fine because in SSPI auth, string comparisons are always + * case-insensitive. + */ + + samname = psprintf("%s\\%s", domainname, accountname); + res = TranslateName(samname, NameSamCompatible, NameUserPrincipal, + NULL, &upnamesize); + + if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER) + || upnamesize == 0) + { + pfree(samname); + ereport(LOG, + (errcode(ERRCODE_INVALID_ROLE_SPECIFICATION), + errmsg("could not translate name"))); + return STATUS_ERROR; + } + + /* upnamesize includes the terminating NUL. */ + upname = palloc(upnamesize); + + res = TranslateName(samname, NameSamCompatible, NameUserPrincipal, + upname, &upnamesize); + + pfree(samname); + if (res) + p = strchr(upname, '@'); + + if (!res || p == NULL) + { + pfree(upname); + ereport(LOG, + (errcode(ERRCODE_INVALID_ROLE_SPECIFICATION), + errmsg("could not translate name"))); + return STATUS_ERROR; + } + + /* Length of realm name after the '@', including the NUL. */ + upnamerealmsize = upnamesize - (p - upname + 1); + + /* Replace domainname with realm name. */ + if (upnamerealmsize > domainnamesize) + { + pfree(upname); + ereport(LOG, + (errcode(ERRCODE_INVALID_ROLE_SPECIFICATION), + errmsg("realm name too long"))); + return STATUS_ERROR; + } + + /* Length is now safe. */ + strcpy(domainname, p + 1); + + /* Replace account name as well (in case UPN != SAM)? */ + if (update_accountname) + { + if ((p - upname + 1) > accountnamesize) + { + pfree(upname); + ereport(LOG, + (errcode(ERRCODE_INVALID_ROLE_SPECIFICATION), + errmsg("translated account name too long"))); + return STATUS_ERROR; + } + + *p = 0; + strcpy(accountname, upname); + } + + pfree(upname); + return STATUS_OK; +} +#endif /* ENABLE_SSPI */ + + + +/*---------------------------------------------------------------- + * Ident authentication system + *---------------------------------------------------------------- + */ + +/* + * Parse the string "*ident_response" as a response from a query to an Ident + * server. If it's a normal response indicating a user name, return true + * and store the user name at *ident_user. If it's anything else, + * return false. + */ +static bool +interpret_ident_response(const char *ident_response, + char *ident_user) +{ + const char *cursor = ident_response; /* Cursor into *ident_response */ + + /* + * Ident's response, in the telnet tradition, should end in crlf (\r\n). + */ + if (strlen(ident_response) < 2) + return false; + else if (ident_response[strlen(ident_response) - 2] != '\r') + return false; + else + { + while (*cursor != ':' && *cursor != '\r') + cursor++; /* skip port field */ + + if (*cursor != ':') + return false; + else + { + /* We're positioned to colon before response type field */ + char response_type[80]; + int i; /* Index into *response_type */ + + cursor++; /* Go over colon */ + while (pg_isblank(*cursor)) + cursor++; /* skip blanks */ + i = 0; + while (*cursor != ':' && *cursor != '\r' && !pg_isblank(*cursor) && + i < (int) (sizeof(response_type) - 1)) + response_type[i++] = *cursor++; + response_type[i] = '\0'; + while (pg_isblank(*cursor)) + cursor++; /* skip blanks */ + if (strcmp(response_type, "USERID") != 0) + return false; + else + { + /* + * It's a USERID response. Good. "cursor" should be pointing + * to the colon that precedes the operating system type. + */ + if (*cursor != ':') + return false; + else + { + cursor++; /* Go over colon */ + /* Skip over operating system field. */ + while (*cursor != ':' && *cursor != '\r') + cursor++; + if (*cursor != ':') + return false; + else + { + int i; /* Index into *ident_user */ + + cursor++; /* Go over colon */ + while (pg_isblank(*cursor)) + cursor++; /* skip blanks */ + /* Rest of line is user name. Copy it over. */ + i = 0; + while (*cursor != '\r' && i < IDENT_USERNAME_MAX) + ident_user[i++] = *cursor++; + ident_user[i] = '\0'; + return true; + } + } + } + } + } +} + + +/* + * Talk to the ident server on "remote_addr" and find out who + * owns the tcp connection to "local_addr" + * If the username is successfully retrieved, check the usermap. + * + * XXX: Using WaitLatchOrSocket() and doing a CHECK_FOR_INTERRUPTS() if the + * latch was set would improve the responsiveness to timeouts/cancellations. + */ +static int +ident_inet(hbaPort *port) +{ + const SockAddr remote_addr = port->raddr; + const SockAddr local_addr = port->laddr; + char ident_user[IDENT_USERNAME_MAX + 1]; + pgsocket sock_fd = PGINVALID_SOCKET; /* for talking to Ident server */ + int rc; /* Return code from a locally called function */ + bool ident_return; + char remote_addr_s[NI_MAXHOST]; + char remote_port[NI_MAXSERV]; + char local_addr_s[NI_MAXHOST]; + char local_port[NI_MAXSERV]; + char ident_port[NI_MAXSERV]; + char ident_query[80]; + char ident_response[80 + IDENT_USERNAME_MAX]; + struct addrinfo *ident_serv = NULL, + *la = NULL, + hints; + + /* + * Might look a little weird to first convert it to text and then back to + * sockaddr, but it's protocol independent. + */ + pg_getnameinfo_all(&remote_addr.addr, remote_addr.salen, + remote_addr_s, sizeof(remote_addr_s), + remote_port, sizeof(remote_port), + NI_NUMERICHOST | NI_NUMERICSERV); + pg_getnameinfo_all(&local_addr.addr, local_addr.salen, + local_addr_s, sizeof(local_addr_s), + local_port, sizeof(local_port), + NI_NUMERICHOST | NI_NUMERICSERV); + + snprintf(ident_port, sizeof(ident_port), "%d", IDENT_PORT); + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = remote_addr.addr.ss_family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + hints.ai_addrlen = 0; + hints.ai_canonname = NULL; + hints.ai_addr = NULL; + hints.ai_next = NULL; + rc = pg_getaddrinfo_all(remote_addr_s, ident_port, &hints, &ident_serv); + if (rc || !ident_serv) + { + /* we don't expect this to happen */ + ident_return = false; + goto ident_inet_done; + } + + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = local_addr.addr.ss_family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + hints.ai_addrlen = 0; + hints.ai_canonname = NULL; + hints.ai_addr = NULL; + hints.ai_next = NULL; + rc = pg_getaddrinfo_all(local_addr_s, NULL, &hints, &la); + if (rc || !la) + { + /* we don't expect this to happen */ + ident_return = false; + goto ident_inet_done; + } + + sock_fd = socket(ident_serv->ai_family, ident_serv->ai_socktype, + ident_serv->ai_protocol); + if (sock_fd == PGINVALID_SOCKET) + { + ereport(LOG, + (errcode_for_socket_access(), + errmsg("could not create socket for Ident connection: %m"))); + ident_return = false; + goto ident_inet_done; + } + + /* + * Bind to the address which the client originally contacted, otherwise + * the ident server won't be able to match up the right connection. This + * is necessary if the PostgreSQL server is running on an IP alias. + */ + rc = bind(sock_fd, la->ai_addr, la->ai_addrlen); + if (rc != 0) + { + ereport(LOG, + (errcode_for_socket_access(), + errmsg("could not bind to local address \"%s\": %m", + local_addr_s))); + ident_return = false; + goto ident_inet_done; + } + + rc = connect(sock_fd, ident_serv->ai_addr, + ident_serv->ai_addrlen); + if (rc != 0) + { + ereport(LOG, + (errcode_for_socket_access(), + errmsg("could not connect to Ident server at address \"%s\", port %s: %m", + remote_addr_s, ident_port))); + ident_return = false; + goto ident_inet_done; + } + + /* The query we send to the Ident server */ + snprintf(ident_query, sizeof(ident_query), "%s,%s\r\n", + remote_port, local_port); + + /* loop in case send is interrupted */ + do + { + CHECK_FOR_INTERRUPTS(); + + rc = send(sock_fd, ident_query, strlen(ident_query), 0); + } while (rc < 0 && errno == EINTR); + + if (rc < 0) + { + ereport(LOG, + (errcode_for_socket_access(), + errmsg("could not send query to Ident server at address \"%s\", port %s: %m", + remote_addr_s, ident_port))); + ident_return = false; + goto ident_inet_done; + } + + do + { + CHECK_FOR_INTERRUPTS(); + + rc = recv(sock_fd, ident_response, sizeof(ident_response) - 1, 0); + } while (rc < 0 && errno == EINTR); + + if (rc < 0) + { + ereport(LOG, + (errcode_for_socket_access(), + errmsg("could not receive response from Ident server at address \"%s\", port %s: %m", + remote_addr_s, ident_port))); + ident_return = false; + goto ident_inet_done; + } + + ident_response[rc] = '\0'; + ident_return = interpret_ident_response(ident_response, ident_user); + if (!ident_return) + ereport(LOG, + (errmsg("invalidly formatted response from Ident server: \"%s\"", + ident_response))); + +ident_inet_done: + if (sock_fd != PGINVALID_SOCKET) + closesocket(sock_fd); + if (ident_serv) + pg_freeaddrinfo_all(remote_addr.addr.ss_family, ident_serv); + if (la) + pg_freeaddrinfo_all(local_addr.addr.ss_family, la); + + if (ident_return) + { + /* + * Success! Store the identity, then check the usermap. Note that + * setting the authenticated identity is done before checking the + * usermap, because at this point authentication has succeeded. + */ + set_authn_id(port, ident_user); + return check_usermap(port->hba->usermap, port->user_name, ident_user, false); + } + return STATUS_ERROR; +} + + +/*---------------------------------------------------------------- + * Peer authentication system + *---------------------------------------------------------------- + */ + +/* + * Ask kernel about the credentials of the connecting process, + * determine the symbolic name of the corresponding user, and check + * if valid per the usermap. + * + * Iff authorized, return STATUS_OK, otherwise return STATUS_ERROR. + */ +static int +auth_peer(hbaPort *port) +{ + uid_t uid; + gid_t gid; +#ifndef WIN32 + struct passwd *pw; + int ret; +#endif + + if (getpeereid(port->sock, &uid, &gid) != 0) + { + /* Provide special error message if getpeereid is a stub */ + if (errno == ENOSYS) + ereport(LOG, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("peer authentication is not supported on this platform"))); + else + ereport(LOG, + (errcode_for_socket_access(), + errmsg("could not get peer credentials: %m"))); + return STATUS_ERROR; + } + +#ifndef WIN32 + errno = 0; /* clear errno before call */ + pw = getpwuid(uid); + if (!pw) + { + int save_errno = errno; + + ereport(LOG, + (errmsg("could not look up local user ID %ld: %s", + (long) uid, + save_errno ? strerror(save_errno) : _("user does not exist")))); + return STATUS_ERROR; + } + + /* + * Make a copy of static getpw*() result area; this is our authenticated + * identity. Set it before calling check_usermap, because authentication + * has already succeeded and we want the log file to reflect that. + */ + set_authn_id(port, pw->pw_name); + + ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id, false); + + return ret; +#else + /* should have failed with ENOSYS above */ + Assert(false); + return STATUS_ERROR; +#endif +} + + +/*---------------------------------------------------------------- + * PAM authentication system + *---------------------------------------------------------------- + */ +#ifdef USE_PAM + +/* + * PAM conversation function + */ + +static int +pam_passwd_conv_proc(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) +{ + const char *passwd; + struct pam_response *reply; + int i; + + if (appdata_ptr) + passwd = (char *) appdata_ptr; + else + { + /* + * Workaround for Solaris 2.6 where the PAM library is broken and does + * not pass appdata_ptr to the conversation routine + */ + passwd = pam_passwd; + } + + *resp = NULL; /* in case of error exit */ + + if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) + return PAM_CONV_ERR; + + /* + * Explicitly not using palloc here - PAM will free this memory in + * pam_end() + */ + if ((reply = calloc(num_msg, sizeof(struct pam_response))) == NULL) + { + ereport(LOG, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + return PAM_CONV_ERR; + } + + for (i = 0; i < num_msg; i++) + { + switch (msg[i]->msg_style) + { + case PAM_PROMPT_ECHO_OFF: + if (strlen(passwd) == 0) + { + /* + * Password wasn't passed to PAM the first time around - + * let's go ask the client to send a password, which we + * then stuff into PAM. + */ + sendAuthRequest(pam_port_cludge, AUTH_REQ_PASSWORD, NULL, 0); + passwd = recv_password_packet(pam_port_cludge); + if (passwd == NULL) + { + /* + * Client didn't want to send password. We + * intentionally do not log anything about this, + * either here or at higher levels. + */ + pam_no_password = true; + goto fail; + } + } + if ((reply[i].resp = strdup(passwd)) == NULL) + goto fail; + reply[i].resp_retcode = PAM_SUCCESS; + break; + case PAM_ERROR_MSG: + ereport(LOG, + (errmsg("error from underlying PAM layer: %s", + msg[i]->msg))); + /* FALL THROUGH */ + case PAM_TEXT_INFO: + /* we don't bother to log TEXT_INFO messages */ + if ((reply[i].resp = strdup("")) == NULL) + goto fail; + reply[i].resp_retcode = PAM_SUCCESS; + break; + default: + ereport(LOG, + (errmsg("unsupported PAM conversation %d/\"%s\"", + msg[i]->msg_style, + msg[i]->msg ? msg[i]->msg : "(none)"))); + goto fail; + } + } + + *resp = reply; + return PAM_SUCCESS; + +fail: + /* free up whatever we allocated */ + for (i = 0; i < num_msg; i++) + { + if (reply[i].resp != NULL) + free(reply[i].resp); + } + free(reply); + + return PAM_CONV_ERR; +} + + +/* + * Check authentication against PAM. + */ +static int +CheckPAMAuth(Port *port, const char *user, const char *password) +{ + int retval; + pam_handle_t *pamh = NULL; + + /* + * We can't entirely rely on PAM to pass through appdata --- it appears + * not to work on at least Solaris 2.6. So use these ugly static + * variables instead. + */ + pam_passwd = password; + pam_port_cludge = port; + pam_no_password = false; + + /* + * Set the application data portion of the conversation struct. This is + * later used inside the PAM conversation to pass the password to the + * authentication module. + */ + pam_passw_conv.appdata_ptr = unconstify(char *, password); /* from password above, + * not allocated */ + + /* Optionally, one can set the service name in pg_hba.conf */ + if (port->hba->pamservice && port->hba->pamservice[0] != '\0') + retval = pam_start(port->hba->pamservice, "pgsql@", + &pam_passw_conv, &pamh); + else + retval = pam_start(PGSQL_PAM_SERVICE, "pgsql@", + &pam_passw_conv, &pamh); + + if (retval != PAM_SUCCESS) + { + ereport(LOG, + (errmsg("could not create PAM authenticator: %s", + pam_strerror(pamh, retval)))); + pam_passwd = NULL; /* Unset pam_passwd */ + return STATUS_ERROR; + } + + retval = pam_set_item(pamh, PAM_USER, user); + + if (retval != PAM_SUCCESS) + { + ereport(LOG, + (errmsg("pam_set_item(PAM_USER) failed: %s", + pam_strerror(pamh, retval)))); + pam_passwd = NULL; /* Unset pam_passwd */ + return STATUS_ERROR; + } + + if (port->hba->conntype != ctLocal) + { + char hostinfo[NI_MAXHOST]; + int flags; + + if (port->hba->pam_use_hostname) + flags = 0; + else + flags = NI_NUMERICHOST | NI_NUMERICSERV; + + retval = pg_getnameinfo_all(&port->raddr.addr, port->raddr.salen, + hostinfo, sizeof(hostinfo), NULL, 0, + flags); + if (retval != 0) + { + ereport(WARNING, + (errmsg_internal("pg_getnameinfo_all() failed: %s", + gai_strerror(retval)))); + return STATUS_ERROR; + } + + retval = pam_set_item(pamh, PAM_RHOST, hostinfo); + + if (retval != PAM_SUCCESS) + { + ereport(LOG, + (errmsg("pam_set_item(PAM_RHOST) failed: %s", + pam_strerror(pamh, retval)))); + pam_passwd = NULL; + return STATUS_ERROR; + } + } + + retval = pam_set_item(pamh, PAM_CONV, &pam_passw_conv); + + if (retval != PAM_SUCCESS) + { + ereport(LOG, + (errmsg("pam_set_item(PAM_CONV) failed: %s", + pam_strerror(pamh, retval)))); + pam_passwd = NULL; /* Unset pam_passwd */ + return STATUS_ERROR; + } + + retval = pam_authenticate(pamh, 0); + + if (retval != PAM_SUCCESS) + { + /* If pam_passwd_conv_proc saw EOF, don't log anything */ + if (!pam_no_password) + ereport(LOG, + (errmsg("pam_authenticate failed: %s", + pam_strerror(pamh, retval)))); + pam_passwd = NULL; /* Unset pam_passwd */ + return pam_no_password ? STATUS_EOF : STATUS_ERROR; + } + + retval = pam_acct_mgmt(pamh, 0); + + if (retval != PAM_SUCCESS) + { + /* If pam_passwd_conv_proc saw EOF, don't log anything */ + if (!pam_no_password) + ereport(LOG, + (errmsg("pam_acct_mgmt failed: %s", + pam_strerror(pamh, retval)))); + pam_passwd = NULL; /* Unset pam_passwd */ + return pam_no_password ? STATUS_EOF : STATUS_ERROR; + } + + retval = pam_end(pamh, retval); + + if (retval != PAM_SUCCESS) + { + ereport(LOG, + (errmsg("could not release PAM authenticator: %s", + pam_strerror(pamh, retval)))); + } + + pam_passwd = NULL; /* Unset pam_passwd */ + + if (retval == PAM_SUCCESS) + set_authn_id(port, user); + + return (retval == PAM_SUCCESS ? STATUS_OK : STATUS_ERROR); +} +#endif /* USE_PAM */ + + +/*---------------------------------------------------------------- + * BSD authentication system + *---------------------------------------------------------------- + */ +#ifdef USE_BSD_AUTH +static int +CheckBSDAuth(Port *port, char *user) +{ + char *passwd; + int retval; + + /* Send regular password request to client, and get the response */ + sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); + + passwd = recv_password_packet(port); + if (passwd == NULL) + return STATUS_EOF; + + /* + * Ask the BSD auth system to verify password. Note that auth_userokay + * will overwrite the password string with zeroes, but it's just a + * temporary string so we don't care. + */ + retval = auth_userokay(user, NULL, "auth-postgresql", passwd); + + pfree(passwd); + + if (!retval) + return STATUS_ERROR; + + set_authn_id(port, user); + return STATUS_OK; +} +#endif /* USE_BSD_AUTH */ + + +/*---------------------------------------------------------------- + * LDAP authentication system + *---------------------------------------------------------------- + */ +#ifdef USE_LDAP + +static int errdetail_for_ldap(LDAP *ldap); + +/* + * Initialize a connection to the LDAP server, including setting up + * TLS if requested. + */ +static int +InitializeLDAPConnection(Port *port, LDAP **ldap) +{ + const char *scheme; + int ldapversion = LDAP_VERSION3; + int r; + + scheme = port->hba->ldapscheme; + if (scheme == NULL) + scheme = "ldap"; +#ifdef WIN32 + if (strcmp(scheme, "ldaps") == 0) + *ldap = ldap_sslinit(port->hba->ldapserver, port->hba->ldapport, 1); + else + *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport); + if (!*ldap) + { + ereport(LOG, + (errmsg("could not initialize LDAP: error code %d", + (int) LdapGetLastError()))); + + return STATUS_ERROR; + } +#else +#ifdef HAVE_LDAP_INITIALIZE + + /* + * OpenLDAP provides a non-standard extension ldap_initialize() that takes + * a list of URIs, allowing us to request "ldaps" instead of "ldap". It + * also provides ldap_domain2hostlist() to find LDAP servers automatically + * using DNS SRV. They were introduced in the same version, so for now we + * don't have an extra configure check for the latter. + */ + { + StringInfoData uris; + char *hostlist = NULL; + char *p; + bool append_port; + + /* We'll build a space-separated scheme://hostname:port list here */ + initStringInfo(&uris); + + /* + * If pg_hba.conf provided no hostnames, we can ask OpenLDAP to try to + * find some by extracting a domain name from the base DN and looking + * up DSN SRV records for _ldap._tcp.<domain>. + */ + if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') + { + char *domain; + + /* ou=blah,dc=foo,dc=bar -> foo.bar */ + if (ldap_dn2domain(port->hba->ldapbasedn, &domain)) + { + ereport(LOG, + (errmsg("could not extract domain name from ldapbasedn"))); + return STATUS_ERROR; + } + + /* Look up a list of LDAP server hosts and port numbers */ + if (ldap_domain2hostlist(domain, &hostlist)) + { + ereport(LOG, + (errmsg("LDAP authentication could not find DNS SRV records for \"%s\"", + domain), + (errhint("Set an LDAP server name explicitly.")))); + ldap_memfree(domain); + return STATUS_ERROR; + } + ldap_memfree(domain); + + /* We have a space-separated list of host:port entries */ + p = hostlist; + append_port = false; + } + else + { + /* We have a space-separated list of hosts from pg_hba.conf */ + p = port->hba->ldapserver; + append_port = true; + } + + /* Convert the list of host[:port] entries to full URIs */ + do + { + size_t size; + + /* Find the span of the next entry */ + size = strcspn(p, " "); + + /* Append a space separator if this isn't the first URI */ + if (uris.len > 0) + appendStringInfoChar(&uris, ' '); + + /* Append scheme://host:port */ + appendStringInfoString(&uris, scheme); + appendStringInfoString(&uris, "://"); + appendBinaryStringInfo(&uris, p, size); + if (append_port) + appendStringInfo(&uris, ":%d", port->hba->ldapport); + + /* Step over this entry and any number of trailing spaces */ + p += size; + while (*p == ' ') + ++p; + } while (*p); + + /* Free memory from OpenLDAP if we looked up SRV records */ + if (hostlist) + ldap_memfree(hostlist); + + /* Finally, try to connect using the URI list */ + r = ldap_initialize(ldap, uris.data); + pfree(uris.data); + if (r != LDAP_SUCCESS) + { + ereport(LOG, + (errmsg("could not initialize LDAP: %s", + ldap_err2string(r)))); + + return STATUS_ERROR; + } + } +#else + if (strcmp(scheme, "ldaps") == 0) + { + ereport(LOG, + (errmsg("ldaps not supported with this LDAP library"))); + + return STATUS_ERROR; + } + *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport); + if (!*ldap) + { + ereport(LOG, + (errmsg("could not initialize LDAP: %m"))); + + return STATUS_ERROR; + } +#endif +#endif + + if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS) + { + ereport(LOG, + (errmsg("could not set LDAP protocol version: %s", + ldap_err2string(r)), + errdetail_for_ldap(*ldap))); + ldap_unbind(*ldap); + return STATUS_ERROR; + } + + if (port->hba->ldaptls) + { +#ifndef WIN32 + if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS) +#else + static __ldap_start_tls_sA _ldap_start_tls_sA = NULL; + + if (_ldap_start_tls_sA == NULL) + { + /* + * Need to load this function dynamically because it may not exist + * on Windows, and causes a load error for the whole exe if + * referenced. + */ + HANDLE ldaphandle; + + ldaphandle = LoadLibrary("WLDAP32.DLL"); + if (ldaphandle == NULL) + { + /* + * should never happen since we import other files from + * wldap32, but check anyway + */ + ereport(LOG, + (errmsg("could not load library \"%s\": error code %lu", + "WLDAP32.DLL", GetLastError()))); + ldap_unbind(*ldap); + return STATUS_ERROR; + } + _ldap_start_tls_sA = (__ldap_start_tls_sA) (pg_funcptr_t) GetProcAddress(ldaphandle, "ldap_start_tls_sA"); + if (_ldap_start_tls_sA == NULL) + { + ereport(LOG, + (errmsg("could not load function _ldap_start_tls_sA in wldap32.dll"), + errdetail("LDAP over SSL is not supported on this platform."))); + ldap_unbind(*ldap); + FreeLibrary(ldaphandle); + return STATUS_ERROR; + } + + /* + * Leak LDAP handle on purpose, because we need the library to + * stay open. This is ok because it will only ever be leaked once + * per process and is automatically cleaned up on process exit. + */ + } + if ((r = _ldap_start_tls_sA(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS) +#endif + { + ereport(LOG, + (errmsg("could not start LDAP TLS session: %s", + ldap_err2string(r)), + errdetail_for_ldap(*ldap))); + ldap_unbind(*ldap); + return STATUS_ERROR; + } + } + + return STATUS_OK; +} + +/* Placeholders recognized by FormatSearchFilter. For now just one. */ +#define LPH_USERNAME "$username" +#define LPH_USERNAME_LEN (sizeof(LPH_USERNAME) - 1) + +/* Not all LDAP implementations define this. */ +#ifndef LDAP_NO_ATTRS +#define LDAP_NO_ATTRS "1.1" +#endif + +/* Not all LDAP implementations define this. */ +#ifndef LDAPS_PORT +#define LDAPS_PORT 636 +#endif + +/* + * Return a newly allocated C string copied from "pattern" with all + * occurrences of the placeholder "$username" replaced with "user_name". + */ +static char * +FormatSearchFilter(const char *pattern, const char *user_name) +{ + StringInfoData output; + + initStringInfo(&output); + while (*pattern != '\0') + { + if (strncmp(pattern, LPH_USERNAME, LPH_USERNAME_LEN) == 0) + { + appendStringInfoString(&output, user_name); + pattern += LPH_USERNAME_LEN; + } + else + appendStringInfoChar(&output, *pattern++); + } + + return output.data; +} + +/* + * Perform LDAP authentication + */ +static int +CheckLDAPAuth(Port *port) +{ + char *passwd; + LDAP *ldap; + int r; + char *fulluser; + const char *server_name; + +#ifdef HAVE_LDAP_INITIALIZE + + /* + * For OpenLDAP, allow empty hostname if we have a basedn. We'll look for + * servers with DNS SRV records via OpenLDAP library facilities. + */ + if ((!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') && + (!port->hba->ldapbasedn || port->hba->ldapbasedn[0] == '\0')) + { + ereport(LOG, + (errmsg("LDAP server not specified, and no ldapbasedn"))); + return STATUS_ERROR; + } +#else + if (!port->hba->ldapserver || port->hba->ldapserver[0] == '\0') + { + ereport(LOG, + (errmsg("LDAP server not specified"))); + return STATUS_ERROR; + } +#endif + + /* + * If we're using SRV records, we don't have a server name so we'll just + * show an empty string in error messages. + */ + server_name = port->hba->ldapserver ? port->hba->ldapserver : ""; + + if (port->hba->ldapport == 0) + { + if (port->hba->ldapscheme != NULL && + strcmp(port->hba->ldapscheme, "ldaps") == 0) + port->hba->ldapport = LDAPS_PORT; + else + port->hba->ldapport = LDAP_PORT; + } + + sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); + + passwd = recv_password_packet(port); + if (passwd == NULL) + return STATUS_EOF; /* client wouldn't send password */ + + if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR) + { + /* Error message already sent */ + pfree(passwd); + return STATUS_ERROR; + } + + if (port->hba->ldapbasedn) + { + /* + * First perform an LDAP search to find the DN for the user we are + * trying to log in as. + */ + char *filter; + LDAPMessage *search_message; + LDAPMessage *entry; + char *attributes[] = {LDAP_NO_ATTRS, NULL}; + char *dn; + char *c; + int count; + + /* + * Disallow any characters that we would otherwise need to escape, + * since they aren't really reasonable in a username anyway. Allowing + * them would make it possible to inject any kind of custom filters in + * the LDAP filter. + */ + for (c = port->user_name; *c; c++) + { + if (*c == '*' || + *c == '(' || + *c == ')' || + *c == '\\' || + *c == '/') + { + ereport(LOG, + (errmsg("invalid character in user name for LDAP authentication"))); + ldap_unbind(ldap); + pfree(passwd); + return STATUS_ERROR; + } + } + + /* + * Bind with a pre-defined username/password (if available) for + * searching. If none is specified, this turns into an anonymous bind. + */ + r = ldap_simple_bind_s(ldap, + port->hba->ldapbinddn ? port->hba->ldapbinddn : "", + port->hba->ldapbindpasswd ? port->hba->ldapbindpasswd : ""); + if (r != LDAP_SUCCESS) + { + ereport(LOG, + (errmsg("could not perform initial LDAP bind for ldapbinddn \"%s\" on server \"%s\": %s", + port->hba->ldapbinddn ? port->hba->ldapbinddn : "", + server_name, + ldap_err2string(r)), + errdetail_for_ldap(ldap))); + ldap_unbind(ldap); + pfree(passwd); + return STATUS_ERROR; + } + + /* Build a custom filter or a single attribute filter? */ + if (port->hba->ldapsearchfilter) + filter = FormatSearchFilter(port->hba->ldapsearchfilter, port->user_name); + else if (port->hba->ldapsearchattribute) + filter = psprintf("(%s=%s)", port->hba->ldapsearchattribute, port->user_name); + else + filter = psprintf("(uid=%s)", port->user_name); + + r = ldap_search_s(ldap, + port->hba->ldapbasedn, + port->hba->ldapscope, + filter, + attributes, + 0, + &search_message); + + if (r != LDAP_SUCCESS) + { + ereport(LOG, + (errmsg("could not search LDAP for filter \"%s\" on server \"%s\": %s", + filter, server_name, ldap_err2string(r)), + errdetail_for_ldap(ldap))); + ldap_unbind(ldap); + pfree(passwd); + pfree(filter); + return STATUS_ERROR; + } + + count = ldap_count_entries(ldap, search_message); + if (count != 1) + { + if (count == 0) + ereport(LOG, + (errmsg("LDAP user \"%s\" does not exist", port->user_name), + errdetail("LDAP search for filter \"%s\" on server \"%s\" returned no entries.", + filter, server_name))); + else + ereport(LOG, + (errmsg("LDAP user \"%s\" is not unique", port->user_name), + errdetail_plural("LDAP search for filter \"%s\" on server \"%s\" returned %d entry.", + "LDAP search for filter \"%s\" on server \"%s\" returned %d entries.", + count, + filter, server_name, count))); + + ldap_unbind(ldap); + pfree(passwd); + pfree(filter); + ldap_msgfree(search_message); + return STATUS_ERROR; + } + + entry = ldap_first_entry(ldap, search_message); + dn = ldap_get_dn(ldap, entry); + if (dn == NULL) + { + int error; + + (void) ldap_get_option(ldap, LDAP_OPT_ERROR_NUMBER, &error); + ereport(LOG, + (errmsg("could not get dn for the first entry matching \"%s\" on server \"%s\": %s", + filter, server_name, + ldap_err2string(error)), + errdetail_for_ldap(ldap))); + ldap_unbind(ldap); + pfree(passwd); + pfree(filter); + ldap_msgfree(search_message); + return STATUS_ERROR; + } + fulluser = pstrdup(dn); + + pfree(filter); + ldap_memfree(dn); + ldap_msgfree(search_message); + + /* Unbind and disconnect from the LDAP server */ + r = ldap_unbind_s(ldap); + if (r != LDAP_SUCCESS) + { + ereport(LOG, + (errmsg("could not unbind after searching for user \"%s\" on server \"%s\"", + fulluser, server_name))); + pfree(passwd); + pfree(fulluser); + return STATUS_ERROR; + } + + /* + * Need to re-initialize the LDAP connection, so that we can bind to + * it with a different username. + */ + if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR) + { + pfree(passwd); + pfree(fulluser); + + /* Error message already sent */ + return STATUS_ERROR; + } + } + else + fulluser = psprintf("%s%s%s", + port->hba->ldapprefix ? port->hba->ldapprefix : "", + port->user_name, + port->hba->ldapsuffix ? port->hba->ldapsuffix : ""); + + r = ldap_simple_bind_s(ldap, fulluser, passwd); + + if (r != LDAP_SUCCESS) + { + ereport(LOG, + (errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s", + fulluser, server_name, ldap_err2string(r)), + errdetail_for_ldap(ldap))); + ldap_unbind(ldap); + pfree(passwd); + pfree(fulluser); + return STATUS_ERROR; + } + + /* Save the original bind DN as the authenticated identity. */ + set_authn_id(port, fulluser); + + ldap_unbind(ldap); + pfree(passwd); + pfree(fulluser); + + return STATUS_OK; +} + +/* + * Add a detail error message text to the current error if one can be + * constructed from the LDAP 'diagnostic message'. + */ +static int +errdetail_for_ldap(LDAP *ldap) +{ + char *message; + int rc; + + rc = ldap_get_option(ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, &message); + if (rc == LDAP_SUCCESS && message != NULL) + { + errdetail("LDAP diagnostics: %s", message); + ldap_memfree(message); + } + + return 0; +} + +#endif /* USE_LDAP */ + + +/*---------------------------------------------------------------- + * SSL client certificate authentication + *---------------------------------------------------------------- + */ +#ifdef USE_SSL +static int +CheckCertAuth(Port *port) +{ + int status_check_usermap = STATUS_ERROR; + char *peer_username = NULL; + + Assert(port->ssl); + + /* select the correct field to compare */ + switch (port->hba->clientcertname) + { + case clientCertDN: + peer_username = port->peer_dn; + break; + case clientCertCN: + peer_username = port->peer_cn; + } + + /* Make sure we have received a username in the certificate */ + if (peer_username == NULL || + strlen(peer_username) <= 0) + { + ereport(LOG, + (errmsg("certificate authentication failed for user \"%s\": client certificate contains no user name", + port->user_name))); + return STATUS_ERROR; + } + + if (port->hba->auth_method == uaCert) + { + /* + * For cert auth, the client's Subject DN is always our authenticated + * identity, even if we're only using its CN for authorization. Set + * it now, rather than waiting for check_usermap() below, because + * authentication has already succeeded and we want the log file to + * reflect that. + */ + if (!port->peer_dn) + { + /* + * This should not happen as both peer_dn and peer_cn should be + * set in this context. + */ + ereport(LOG, + (errmsg("certificate authentication failed for user \"%s\": unable to retrieve subject DN", + port->user_name))); + return STATUS_ERROR; + } + + set_authn_id(port, port->peer_dn); + } + + /* Just pass the certificate cn/dn to the usermap check */ + status_check_usermap = check_usermap(port->hba->usermap, port->user_name, peer_username, false); + if (status_check_usermap != STATUS_OK) + { + /* + * If clientcert=verify-full was specified and the authentication + * method is other than uaCert, log the reason for rejecting the + * authentication. + */ + if (port->hba->clientcert == clientCertFull && port->hba->auth_method != uaCert) + { + switch (port->hba->clientcertname) + { + case clientCertDN: + ereport(LOG, + (errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": DN mismatch", + port->user_name))); + break; + case clientCertCN: + ereport(LOG, + (errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch", + port->user_name))); + } + } + } + return status_check_usermap; +} +#endif + + +/*---------------------------------------------------------------- + * RADIUS authentication + *---------------------------------------------------------------- + */ + +/* + * RADIUS authentication is described in RFC2865 (and several others). + */ + +#define RADIUS_VECTOR_LENGTH 16 +#define RADIUS_HEADER_LENGTH 20 +#define RADIUS_MAX_PASSWORD_LENGTH 128 + +/* Maximum size of a RADIUS packet we will create or accept */ +#define RADIUS_BUFFER_SIZE 1024 + +typedef struct +{ + uint8 attribute; + uint8 length; + uint8 data[FLEXIBLE_ARRAY_MEMBER]; +} radius_attribute; + +typedef struct +{ + uint8 code; + uint8 id; + uint16 length; + uint8 vector[RADIUS_VECTOR_LENGTH]; + /* this is a bit longer than strictly necessary: */ + char pad[RADIUS_BUFFER_SIZE - RADIUS_VECTOR_LENGTH]; +} radius_packet; + +/* RADIUS packet types */ +#define RADIUS_ACCESS_REQUEST 1 +#define RADIUS_ACCESS_ACCEPT 2 +#define RADIUS_ACCESS_REJECT 3 + +/* RADIUS attributes */ +#define RADIUS_USER_NAME 1 +#define RADIUS_PASSWORD 2 +#define RADIUS_SERVICE_TYPE 6 +#define RADIUS_NAS_IDENTIFIER 32 + +/* RADIUS service types */ +#define RADIUS_AUTHENTICATE_ONLY 8 + +/* Seconds to wait - XXX: should be in a config variable! */ +#define RADIUS_TIMEOUT 3 + +static void +radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len) +{ + radius_attribute *attr; + + if (packet->length + len > RADIUS_BUFFER_SIZE) + { + /* + * With remotely realistic data, this can never happen. But catch it + * just to make sure we don't overrun a buffer. We'll just skip adding + * the broken attribute, which will in the end cause authentication to + * fail. + */ + elog(WARNING, + "adding attribute code %d with length %d to radius packet would create oversize packet, ignoring", + type, len); + return; + } + + attr = (radius_attribute *) ((unsigned char *) packet + packet->length); + attr->attribute = type; + attr->length = len + 2; /* total size includes type and length */ + memcpy(attr->data, data, len); + packet->length += attr->length; +} + +static int +CheckRADIUSAuth(Port *port) +{ + char *passwd; + ListCell *server, + *secrets, + *radiusports, + *identifiers; + + /* Make sure struct alignment is correct */ + Assert(offsetof(radius_packet, vector) == 4); + + /* Verify parameters */ + if (list_length(port->hba->radiusservers) < 1) + { + ereport(LOG, + (errmsg("RADIUS server not specified"))); + return STATUS_ERROR; + } + + if (list_length(port->hba->radiussecrets) < 1) + { + ereport(LOG, + (errmsg("RADIUS secret not specified"))); + return STATUS_ERROR; + } + + /* Send regular password request to client, and get the response */ + sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); + + passwd = recv_password_packet(port); + if (passwd == NULL) + return STATUS_EOF; /* client wouldn't send password */ + + if (strlen(passwd) > RADIUS_MAX_PASSWORD_LENGTH) + { + ereport(LOG, + (errmsg("RADIUS authentication does not support passwords longer than %d characters", RADIUS_MAX_PASSWORD_LENGTH))); + pfree(passwd); + return STATUS_ERROR; + } + + /* + * Loop over and try each server in order. + */ + secrets = list_head(port->hba->radiussecrets); + radiusports = list_head(port->hba->radiusports); + identifiers = list_head(port->hba->radiusidentifiers); + foreach(server, port->hba->radiusservers) + { + int ret = PerformRadiusTransaction(lfirst(server), + lfirst(secrets), + radiusports ? lfirst(radiusports) : NULL, + identifiers ? lfirst(identifiers) : NULL, + port->user_name, + passwd); + + /*------ + * STATUS_OK = Login OK + * STATUS_ERROR = Login not OK, but try next server + * STATUS_EOF = Login not OK, and don't try next server + *------ + */ + if (ret == STATUS_OK) + { + set_authn_id(port, port->user_name); + + pfree(passwd); + return STATUS_OK; + } + else if (ret == STATUS_EOF) + { + pfree(passwd); + return STATUS_ERROR; + } + + /* + * secret, port and identifiers either have length 0 (use default), + * length 1 (use the same everywhere) or the same length as servers. + * So if the length is >1, we advance one step. In other cases, we + * don't and will then reuse the correct value. + */ + if (list_length(port->hba->radiussecrets) > 1) + secrets = lnext(port->hba->radiussecrets, secrets); + if (list_length(port->hba->radiusports) > 1) + radiusports = lnext(port->hba->radiusports, radiusports); + if (list_length(port->hba->radiusidentifiers) > 1) + identifiers = lnext(port->hba->radiusidentifiers, identifiers); + } + + /* No servers left to try, so give up */ + pfree(passwd); + return STATUS_ERROR; +} + +static int +PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd) +{ + radius_packet radius_send_pack; + radius_packet radius_recv_pack; + radius_packet *packet = &radius_send_pack; + radius_packet *receivepacket = &radius_recv_pack; + char *radius_buffer = (char *) &radius_send_pack; + char *receive_buffer = (char *) &radius_recv_pack; + int32 service = pg_hton32(RADIUS_AUTHENTICATE_ONLY); + uint8 *cryptvector; + int encryptedpasswordlen; + uint8 encryptedpassword[RADIUS_MAX_PASSWORD_LENGTH]; + uint8 *md5trailer; + int packetlength; + pgsocket sock; + +#ifdef HAVE_IPV6 + struct sockaddr_in6 localaddr; + struct sockaddr_in6 remoteaddr; +#else + struct sockaddr_in localaddr; + struct sockaddr_in remoteaddr; +#endif + struct addrinfo hint; + struct addrinfo *serveraddrs; + int port; + ACCEPT_TYPE_ARG3 addrsize; + fd_set fdset; + struct timeval endtime; + int i, + j, + r; + + /* Assign default values */ + if (portstr == NULL) + portstr = "1812"; + if (identifier == NULL) + identifier = "postgresql"; + + MemSet(&hint, 0, sizeof(hint)); + hint.ai_socktype = SOCK_DGRAM; + hint.ai_family = AF_UNSPEC; + port = atoi(portstr); + + r = pg_getaddrinfo_all(server, portstr, &hint, &serveraddrs); + if (r || !serveraddrs) + { + ereport(LOG, + (errmsg("could not translate RADIUS server name \"%s\" to address: %s", + server, gai_strerror(r)))); + if (serveraddrs) + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + return STATUS_ERROR; + } + /* XXX: add support for multiple returned addresses? */ + + /* Construct RADIUS packet */ + packet->code = RADIUS_ACCESS_REQUEST; + packet->length = RADIUS_HEADER_LENGTH; + if (!pg_strong_random(packet->vector, RADIUS_VECTOR_LENGTH)) + { + ereport(LOG, + (errmsg("could not generate random encryption vector"))); + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + return STATUS_ERROR; + } + packet->id = packet->vector[0]; + radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (const unsigned char *) &service, sizeof(service)); + radius_add_attribute(packet, RADIUS_USER_NAME, (const unsigned char *) user_name, strlen(user_name)); + radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (const unsigned char *) identifier, strlen(identifier)); + + /* + * RADIUS password attributes are calculated as: e[0] = p[0] XOR + * MD5(secret + Request Authenticator) for the first group of 16 octets, + * and then: e[i] = p[i] XOR MD5(secret + e[i-1]) for the following ones + * (if necessary) + */ + encryptedpasswordlen = ((strlen(passwd) + RADIUS_VECTOR_LENGTH - 1) / RADIUS_VECTOR_LENGTH) * RADIUS_VECTOR_LENGTH; + cryptvector = palloc(strlen(secret) + RADIUS_VECTOR_LENGTH); + memcpy(cryptvector, secret, strlen(secret)); + + /* for the first iteration, we use the Request Authenticator vector */ + md5trailer = packet->vector; + for (i = 0; i < encryptedpasswordlen; i += RADIUS_VECTOR_LENGTH) + { + memcpy(cryptvector + strlen(secret), md5trailer, RADIUS_VECTOR_LENGTH); + + /* + * .. and for subsequent iterations the result of the previous XOR + * (calculated below) + */ + md5trailer = encryptedpassword + i; + + if (!pg_md5_binary(cryptvector, strlen(secret) + RADIUS_VECTOR_LENGTH, encryptedpassword + i)) + { + ereport(LOG, + (errmsg("could not perform MD5 encryption of password"))); + pfree(cryptvector); + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + return STATUS_ERROR; + } + + for (j = i; j < i + RADIUS_VECTOR_LENGTH; j++) + { + if (j < strlen(passwd)) + encryptedpassword[j] = passwd[j] ^ encryptedpassword[j]; + else + encryptedpassword[j] = '\0' ^ encryptedpassword[j]; + } + } + pfree(cryptvector); + + radius_add_attribute(packet, RADIUS_PASSWORD, encryptedpassword, encryptedpasswordlen); + + /* Length needs to be in network order on the wire */ + packetlength = packet->length; + packet->length = pg_hton16(packet->length); + + sock = socket(serveraddrs[0].ai_family, SOCK_DGRAM, 0); + if (sock == PGINVALID_SOCKET) + { + ereport(LOG, + (errmsg("could not create RADIUS socket: %m"))); + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + return STATUS_ERROR; + } + + memset(&localaddr, 0, sizeof(localaddr)); +#ifdef HAVE_IPV6 + localaddr.sin6_family = serveraddrs[0].ai_family; + localaddr.sin6_addr = in6addr_any; + if (localaddr.sin6_family == AF_INET6) + addrsize = sizeof(struct sockaddr_in6); + else + addrsize = sizeof(struct sockaddr_in); +#else + localaddr.sin_family = serveraddrs[0].ai_family; + localaddr.sin_addr.s_addr = INADDR_ANY; + addrsize = sizeof(struct sockaddr_in); +#endif + + if (bind(sock, (struct sockaddr *) &localaddr, addrsize)) + { + ereport(LOG, + (errmsg("could not bind local RADIUS socket: %m"))); + closesocket(sock); + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + return STATUS_ERROR; + } + + if (sendto(sock, radius_buffer, packetlength, 0, + serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0) + { + ereport(LOG, + (errmsg("could not send RADIUS packet: %m"))); + closesocket(sock); + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + return STATUS_ERROR; + } + + /* Don't need the server address anymore */ + pg_freeaddrinfo_all(hint.ai_family, serveraddrs); + + /* + * Figure out at what time we should time out. We can't just use a single + * call to select() with a timeout, since somebody can be sending invalid + * packets to our port thus causing us to retry in a loop and never time + * out. + * + * XXX: Using WaitLatchOrSocket() and doing a CHECK_FOR_INTERRUPTS() if + * the latch was set would improve the responsiveness to + * timeouts/cancellations. + */ + gettimeofday(&endtime, NULL); + endtime.tv_sec += RADIUS_TIMEOUT; + + while (true) + { + struct timeval timeout; + struct timeval now; + int64 timeoutval; + + gettimeofday(&now, NULL); + timeoutval = (endtime.tv_sec * 1000000 + endtime.tv_usec) - (now.tv_sec * 1000000 + now.tv_usec); + if (timeoutval <= 0) + { + ereport(LOG, + (errmsg("timeout waiting for RADIUS response from %s", + server))); + closesocket(sock); + return STATUS_ERROR; + } + timeout.tv_sec = timeoutval / 1000000; + timeout.tv_usec = timeoutval % 1000000; + + FD_ZERO(&fdset); + FD_SET(sock, &fdset); + + r = select(sock + 1, &fdset, NULL, NULL, &timeout); + if (r < 0) + { + if (errno == EINTR) + continue; + + /* Anything else is an actual error */ + ereport(LOG, + (errmsg("could not check status on RADIUS socket: %m"))); + closesocket(sock); + return STATUS_ERROR; + } + if (r == 0) + { + ereport(LOG, + (errmsg("timeout waiting for RADIUS response from %s", + server))); + closesocket(sock); + return STATUS_ERROR; + } + + /* + * Attempt to read the response packet, and verify the contents. + * + * Any packet that's not actually a RADIUS packet, or otherwise does + * not validate as an explicit reject, is just ignored and we retry + * for another packet (until we reach the timeout). This is to avoid + * the possibility to denial-of-service the login by flooding the + * server with invalid packets on the port that we're expecting the + * RADIUS response on. + */ + + addrsize = sizeof(remoteaddr); + packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0, + (struct sockaddr *) &remoteaddr, &addrsize); + if (packetlength < 0) + { + ereport(LOG, + (errmsg("could not read RADIUS response: %m"))); + closesocket(sock); + return STATUS_ERROR; + } + +#ifdef HAVE_IPV6 + if (remoteaddr.sin6_port != pg_hton16(port)) +#else + if (remoteaddr.sin_port != pg_hton16(port)) +#endif + { +#ifdef HAVE_IPV6 + ereport(LOG, + (errmsg("RADIUS response from %s was sent from incorrect port: %d", + server, pg_ntoh16(remoteaddr.sin6_port)))); +#else + ereport(LOG, + (errmsg("RADIUS response from %s was sent from incorrect port: %d", + server, pg_ntoh16(remoteaddr.sin_port)))); +#endif + continue; + } + + if (packetlength < RADIUS_HEADER_LENGTH) + { + ereport(LOG, + (errmsg("RADIUS response from %s too short: %d", server, packetlength))); + continue; + } + + if (packetlength != pg_ntoh16(receivepacket->length)) + { + ereport(LOG, + (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)", + server, pg_ntoh16(receivepacket->length), packetlength))); + continue; + } + + if (packet->id != receivepacket->id) + { + ereport(LOG, + (errmsg("RADIUS response from %s is to a different request: %d (should be %d)", + server, receivepacket->id, packet->id))); + continue; + } + + /* + * Verify the response authenticator, which is calculated as + * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret) + */ + cryptvector = palloc(packetlength + strlen(secret)); + + memcpy(cryptvector, receivepacket, 4); /* code+id+length */ + memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request + * authenticator, from + * original packet */ + if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no + * attributes at all */ + memcpy(cryptvector + RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength - RADIUS_HEADER_LENGTH); + memcpy(cryptvector + packetlength, secret, strlen(secret)); + + if (!pg_md5_binary(cryptvector, + packetlength + strlen(secret), + encryptedpassword)) + { + ereport(LOG, + (errmsg("could not perform MD5 encryption of received packet"))); + pfree(cryptvector); + continue; + } + pfree(cryptvector); + + if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) + { + ereport(LOG, + (errmsg("RADIUS response from %s has incorrect MD5 signature", + server))); + continue; + } + + if (receivepacket->code == RADIUS_ACCESS_ACCEPT) + { + closesocket(sock); + return STATUS_OK; + } + else if (receivepacket->code == RADIUS_ACCESS_REJECT) + { + closesocket(sock); + return STATUS_EOF; + } + else + { + ereport(LOG, + (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"", + server, receivepacket->code, user_name))); + continue; + } + } /* while (true) */ +} |