From b3daac5a9c34b1a567a0bc3574446ee57564770c Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Wed, 27 Jan 2010 12:12:00 +0000 Subject: [PATCH] Add support for RADIUS authentication. --- doc/src/sgml/client-auth.sgml | 101 +++++++- src/backend/libpq/auth.c | 367 ++++++++++++++++++++++++++- src/backend/libpq/hba.c | 49 +++- src/backend/libpq/md5.c | 8 +- src/backend/libpq/pg_hba.conf.sample | 6 +- src/include/libpq/hba.h | 9 +- src/include/libpq/md5.h | 3 +- 7 files changed, 532 insertions(+), 11 deletions(-) diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 9ceae85644..a8360936b2 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -1,4 +1,4 @@ - + Client Authentication @@ -394,6 +394,16 @@ hostnossl database user + + radius + + + Authenticate using a RADIUS server. See for detauls. + + + + cert @@ -1331,6 +1341,95 @@ ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net" + + RADIUS authentication + + + RADIUS + + + + This authentication method operates similarly to + password except that it uses RADIUS + as the password verification method. RADIUS is used only to validate + the user name/password pairs. Therefore the user must already + exist in the database before RADIUS can be used for + authentication. + + + + When using RADIUS authentication, an Access Request message will be sent + to the configured RADIUS server. This request will be of type + Authenticate Only, and include parameters for + user name, password (encrypted) and + NAS Identifier. The request will be encrypted using + a secret shared with the server. The RADIUS server will respond to + this server with either Access Accept or + Access Reject. There is no support for RADIUS accounting. + + + + The following configuration options are supported for RADIUS: + + + radiusserver + + + The IP address of the RADIUS server to connect to. This must + be an IPV4 address and not a hostname. This parameter is required. + + + + + + radiussecret + + + The shared secret used when talking securely to the RADIUS + server. This must have exactly the same value on the PostgreSQL + and RADIUS servers. It is recommended that this is a string of + at least 16 characters. This parameter is required. + + + The encryption vector used will only be cryptographically + strong if PostgreSQL is built with support for + OpenSSL. In other cases, the transmission to the + RADIUS server should only be considered obfuscated, not secured, and + external security measures should be applied if necessary. + + + + + + + + radiusport + + + The port number on the RADIUS server to connect to. If no port + is specified, the default port 1812 will be used. + + + + + + radiusidentifier + + + The string used as NAS Identifier in the RADIUS + requests. This parameter can be used as a second parameter + identifying for example which database the user is attempting + to authenticate as, which can be used for policy matching on + the RADIUS server. If no identifier is specified, the default + postgresql will be used. + + + + + + + + Certificate authentication diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index e7d0dbb298..26d2080e6f 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/libpq/auth.c,v 1.191 2010/01/10 14:16:07 mha Exp $ + * $PostgreSQL: pgsql/src/backend/libpq/auth.c,v 1.192 2010/01/27 12:11:59 mha Exp $ * *------------------------------------------------------------------------- */ @@ -27,12 +27,14 @@ #include #include #include +#include #include "libpq/auth.h" #include "libpq/crypt.h" #include "libpq/ip.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" +#include "libpq/md5.h" #include "miscadmin.h" #include "storage/ipc.h" @@ -182,6 +184,15 @@ typedef SECURITY_STATUS static int pg_SSPI_recvauth(Port *port); #endif +/*---------------------------------------------------------------- + * RADIUS Authentication + *---------------------------------------------------------------- + */ +#ifdef USE_SSL +#include +#endif +static int CheckRADIUSAuth(Port *port); + /* * Maximum accepted size of GSS and SSPI authentication tokens. @@ -265,6 +276,9 @@ auth_failed(Port *port, int status) case uaLDAP: errstr = gettext_noop("LDAP 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; @@ -473,7 +487,9 @@ ClientAuthentication(Port *port) Assert(false); #endif break; - + case uaRADIUS: + status = CheckRADIUSAuth(port); + break; case uaTrust: status = STATUS_OK; break; @@ -2415,3 +2431,350 @@ CheckCertAuth(Port *port) } #endif + + +/*---------------------------------------------------------------- + * RADIUS authentication + *---------------------------------------------------------------- + */ + +/* + * RADIUS authentication is described in RFC2865 (and several + * others). + */ + +#define RADIUS_VECTOR_LENGTH 16 +#define RADIUS_HEADER_LENGTH 20 + +typedef struct +{ + uint8 attribute; + uint8 length; + uint8 data[1]; +} radius_attribute; + +typedef struct +{ + uint8 code; + uint8 id; + uint16 length; + uint8 vector[RADIUS_VECTOR_LENGTH]; +} radius_packet; + +/* RADIUS packet types */ +#define RADIUS_ACCESS_REQUEST 1 +#define RADIUS_ACCESS_ACCEPT 2 +#define RADIUS_ACCESS_REJECT 3 + +/* RAIDUS 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 + +/* Maximum size of a RADIUS packet we will create or accept */ +#define RADIUS_BUFFER_SIZE 1024 + +/* 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 %i with length %i 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; + char *identifier = "postgresql"; + char radius_buffer[RADIUS_BUFFER_SIZE]; + char receive_buffer[RADIUS_BUFFER_SIZE]; + radius_packet *packet = (radius_packet *)radius_buffer; + radius_packet *receivepacket = (radius_packet *)receive_buffer; + int32 service = htonl(RADIUS_AUTHENTICATE_ONLY); + uint8 *cryptvector; + uint8 encryptedpassword[RADIUS_VECTOR_LENGTH]; + int packetlength; + pgsocket sock; + struct sockaddr_in localaddr; + struct sockaddr_in remoteaddr; + socklen_t addrsize; + fd_set fdset; + struct timeval timeout; + int i,r; + + /* Make sure struct alignment is correct */ + Assert(offsetof(radius_packet, vector) == 4); + + /* Verify parameters */ + if (!port->hba->radiusserver || port->hba->radiusserver[0] == '\0') + { + ereport(LOG, + (errmsg("RADIUS server not specified"))); + return STATUS_ERROR; + } + + if (!port->hba->radiussecret || port->hba->radiussecret[0] == '\0') + { + ereport(LOG, + (errmsg("RADIUS secret not specified"))); + return STATUS_ERROR; + } + + if (port->hba->radiusport == 0) + port->hba->radiusport = 1812; + + memset(&remoteaddr, 0, sizeof(remoteaddr)); + remoteaddr.sin_family = AF_INET; + remoteaddr.sin_addr.s_addr = inet_addr(port->hba->radiusserver); + if (remoteaddr.sin_addr.s_addr == INADDR_NONE) + { + ereport(LOG, + (errmsg("RADIUS server '%s' is not a valid IP address", + port->hba->radiusserver))); + return STATUS_ERROR; + } + remoteaddr.sin_port = htons(port->hba->radiusport); + + if (port->hba->radiusidentifier && port->hba->radiusidentifier[0]) + identifier = port->hba->radiusidentifier; + + /* Send regular password request to client, and get the response */ + sendAuthRequest(port, AUTH_REQ_PASSWORD); + + passwd = recv_password_packet(port); + if (passwd == NULL) + return STATUS_EOF; /* client wouldn't send password */ + + if (strlen(passwd) == 0) + { + ereport(LOG, + (errmsg("empty password returned by client"))); + return STATUS_ERROR; + } + + if (strlen(passwd) > RADIUS_VECTOR_LENGTH) + { + ereport(LOG, + (errmsg("RADIUS authentication does not support passwords longer than 16 characters"))); + return STATUS_ERROR; + } + + /* Construct RADIUS packet */ + packet->code = RADIUS_ACCESS_REQUEST; + packet->length = RADIUS_HEADER_LENGTH; +#ifdef USE_SSL + if (RAND_bytes(packet->vector, RADIUS_VECTOR_LENGTH) != 1) + { + ereport(LOG, + (errmsg("could not generate random encryption vector"))); + return STATUS_ERROR; + } +#else + for (i = 0; i < RADIUS_VECTOR_LENGTH; i++) + /* Use a lower strengh random number of OpenSSL is not available */ + packet->vector[i] = random() % 255; +#endif + packet->id = packet->vector[0]; + radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (unsigned char *) &service, sizeof(service)); + radius_add_attribute(packet, RADIUS_USER_NAME, (unsigned char *) port->user_name, strlen(port->user_name)); + radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (unsigned char *) identifier, strlen(identifier)); + + /* + * RADIUS password attributes are calculated as: + * e[0] = p[0] XOR MD5(secret + vector) + */ + cryptvector = palloc(RADIUS_VECTOR_LENGTH + strlen(port->hba->radiussecret)); + memcpy(cryptvector, port->hba->radiussecret, strlen(port->hba->radiussecret)); + memcpy(cryptvector + strlen(port->hba->radiussecret), packet->vector, RADIUS_VECTOR_LENGTH); + if (!pg_md5_binary(cryptvector, RADIUS_VECTOR_LENGTH + strlen(port->hba->radiussecret), encryptedpassword)) + { + ereport(LOG, + (errmsg("could not perform md5 encryption of password"))); + pfree(cryptvector); + return STATUS_ERROR; + } + pfree(cryptvector); + for (i = 0; i < RADIUS_VECTOR_LENGTH; i++) + { + if (i < strlen(passwd)) + encryptedpassword[i] = passwd[i] ^ encryptedpassword[i]; + else + encryptedpassword[i] = '\0' ^ encryptedpassword[i]; + } + radius_add_attribute(packet, RADIUS_PASSWORD, encryptedpassword, RADIUS_VECTOR_LENGTH); + + /* Length need to be in network order on the wire */ + packetlength = packet->length; + packet->length = htons(packet->length); + + sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) + { + ereport(LOG, + (errmsg("could not create RADIUS socket: %m"))); + return STATUS_ERROR; + } + + memset(&localaddr, 0, sizeof(localaddr)); + localaddr.sin_family = AF_INET; + localaddr.sin_addr.s_addr = INADDR_ANY; + if (bind(sock, (struct sockaddr *) &localaddr, sizeof(localaddr))) + { + ereport(LOG, + (errmsg("could not bind local RADIUS socket: %m"))); + closesocket(sock); + return STATUS_ERROR; + } + + if (sendto(sock, radius_buffer, packetlength, 0, + (struct sockaddr *) &remoteaddr, sizeof(remoteaddr)) < 0) + { + ereport(LOG, + (errmsg("could not send RADIUS packet: %m"))); + closesocket(sock); + return STATUS_ERROR; + } + + timeout.tv_sec = RADIUS_TIMEOUT; + timeout.tv_usec = 0; + FD_ZERO(&fdset); + FD_SET(sock, &fdset); + while (true) + { + 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"))); + closesocket(sock); + return STATUS_ERROR; + } + + /* else we actually have a packet ready to read */ + break; + } + + /* Read the response packet */ + 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; + } + + closesocket(sock); + + if (remoteaddr.sin_port != htons(port->hba->radiusport)) + { + ereport(LOG, + (errmsg("RADIUS response was sent from incorrect port: %i", + ntohs(remoteaddr.sin_port)))); + return STATUS_ERROR; + } + + if (packetlength < RADIUS_HEADER_LENGTH) + { + ereport(LOG, + (errmsg("RADIUS response too short: %i", packetlength))); + return STATUS_ERROR; + } + + if (packetlength != ntohs(receivepacket->length)) + { + ereport(LOG, + (errmsg("RADIUS response has corrupt length: %i (actual length %i)", + ntohs(receivepacket->length), packetlength))); + return STATUS_ERROR; + } + + if (packet->id != receivepacket->id) + { + ereport(LOG, + (errmsg("RADIUS response is to a different request: %i (should be %i)", + receivepacket->id, packet->id))); + return STATUS_ERROR; + } + + /* + * Verify the response authenticator, which is calculated as + * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret) + */ + cryptvector = palloc(packetlength + strlen(port->hba->radiussecret)); + + 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, port->hba->radiussecret, strlen(port->hba->radiussecret)); + + if (!pg_md5_binary(cryptvector, + packetlength + strlen(port->hba->radiussecret), + encryptedpassword)) + { + ereport(LOG, + (errmsg("could not perform md5 encryption of received packet"))); + pfree(cryptvector); + return STATUS_ERROR; + } + pfree(cryptvector); + + if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) + { + ereport(LOG, + (errmsg("RADIUS response has incorrect MD5 signature"))); + return STATUS_ERROR; + } + + if (receivepacket->code == RADIUS_ACCESS_ACCEPT) + return STATUS_OK; + else if (receivepacket->code == RADIUS_ACCESS_REJECT) + return STATUS_ERROR; + else + { + ereport(LOG, + (errmsg("RADIUS response has invalid code (%i) for user '%s'", + receivepacket->code, port->user_name))); + return STATUS_ERROR; + } +} diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 98011c2822..588ce643af 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -10,7 +10,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.195 2010/01/15 09:19:02 heikki Exp $ + * $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.196 2010/01/27 12:11:59 mha Exp $ * *------------------------------------------------------------------------- */ @@ -952,6 +952,8 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline) #else unsupauth = "cert"; #endif + else if (strcmp(token, "radius")== 0) + parsedline->auth_method = uaRADIUS; else { ereport(LOG, @@ -1162,6 +1164,45 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline) else parsedline->include_realm = false; } + else if (strcmp(token, "radiusserver") == 0) + { + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusserver", "radius"); + if (inet_addr(c) == INADDR_NONE) + { + ereport(LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid RADIUS server IP address: \"%s\"", c), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + + } + parsedline->radiusserver = pstrdup(c); + } + else if (strcmp(token, "radiusport") == 0) + { + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusport", "radius"); + parsedline->radiusport = atoi(c); + if (parsedline->radiusport == 0) + { + ereport(LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid RADIUS port number: \"%s\"", c), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + } + } + else if (strcmp(token, "radiussecret") == 0) + { + REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecret", "radius"); + parsedline->radiussecret = pstrdup(c); + } + else if (strcmp(token, "radiusidentifier") == 0) + { + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifier", "radius"); + parsedline->radiusidentifier = pstrdup(c); + } else { ereport(LOG, @@ -1214,6 +1255,12 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline) } } + if (parsedline->auth_method == uaRADIUS) + { + MANDATORY_AUTH_ARG(parsedline->radiusserver, "radiusserver", "radius"); + MANDATORY_AUTH_ARG(parsedline->radiussecret, "radiussecret", "radius"); + } + /* * Enforce any parameters implied by other settings. */ diff --git a/src/backend/libpq/md5.c b/src/backend/libpq/md5.c index 002afcf0ff..e875f51ed7 100644 --- a/src/backend/libpq/md5.c +++ b/src/backend/libpq/md5.c @@ -14,7 +14,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/libpq/md5.c,v 1.38 2010/01/02 16:57:45 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/libpq/md5.c,v 1.39 2010/01/27 12:11:59 mha Exp $ */ /* This is intended to be used in both frontend and backend, so use c.h */ @@ -298,6 +298,12 @@ pg_md5_hash(const void *buff, size_t len, char *hexsum) return true; } +bool pg_md5_binary(const void *buff, size_t len, void *outbuf) +{ + if (!calculateDigestFromBuffer((uint8 *) buff, len, outbuf)) + return false; + return true; +} /* diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample index 22bb14730a..e1017cf28c 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -39,9 +39,9 @@ # any subnet that the server is directly connected to. # # METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", -# "krb5", "ident", "pam", "ldap" or "cert". Note that "password" -# sends passwords in clear text; "md5" is preferred since it sends -# encrypted passwords. +# "krb5", "ident", "pam", "ldap", "radius" or "cert". Note that +# "password" sends passwords in clear text; "md5" is preferred since +# it sends encrypted passwords. # # OPTIONS are a set of options for the authentication in the format # NAME=VALUE. The available options depend on the different diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 8ee71a7e09..54261bba61 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -4,7 +4,7 @@ * Interface to hba.c * * - * $PostgreSQL: pgsql/src/include/libpq/hba.h,v 1.60 2009/12/12 21:35:21 mha Exp $ + * $PostgreSQL: pgsql/src/include/libpq/hba.h,v 1.61 2010/01/27 12:12:00 mha Exp $ * *------------------------------------------------------------------------- */ @@ -27,7 +27,8 @@ typedef enum UserAuth uaSSPI, uaPAM, uaLDAP, - uaCert + uaCert, + uaRADIUS } UserAuth; typedef enum IPCompareMethod @@ -71,6 +72,10 @@ typedef struct char *krb_server_hostname; char *krb_realm; bool include_realm; + char *radiusserver; + char *radiussecret; + char *radiusidentifier; + int radiusport; } HbaLine; /* kluge to avoid including libpq/libpq-be.h here */ diff --git a/src/include/libpq/md5.h b/src/include/libpq/md5.h index aa36ac6fa2..decc6ddf12 100644 --- a/src/include/libpq/md5.h +++ b/src/include/libpq/md5.h @@ -9,7 +9,7 @@ * Portions Copyright (c) 1996-2010, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/libpq/md5.h,v 1.7 2010/01/02 16:58:04 momjian Exp $ + * $PostgreSQL: pgsql/src/include/libpq/md5.h,v 1.8 2010/01/27 12:12:00 mha Exp $ * *------------------------------------------------------------------------- */ @@ -23,6 +23,7 @@ extern bool pg_md5_hash(const void *buff, size_t len, char *hexsum); +extern bool pg_md5_binary(const void *buff, size_t len, void *outbuf); extern bool pg_md5_encrypt(const char *passwd, const char *salt, size_t salt_len, char *buf);