diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 366cb26..35f6779 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -395,6 +395,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 f4fd4d6..b601454 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -33,6 +33,7 @@
#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 +183,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 +275,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 +486,9 @@ ClientAuthentication(Port *port)
Assert(false);
#endif
break;
-
+ case uaRADIUS:
+ status = CheckRADIUSAuth(port);
+ break;
case uaTrust:
status = STATUS_OK;
break;
@@ -2415,3 +2430,347 @@ 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;
+ int sock;
+ struct sockaddr_in localaddr;
+ struct sockaddr_in remoteaddr;
+ socklen_t addrsize;
+ fd_set fdset;
+ struct timeval timeout;
+ int i,r;
+
+ /* 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 4fc4215..dd7ad5c 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -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 467d19c..35d5fb4 100644
--- a/src/backend/libpq/md5.c
+++ b/src/backend/libpq/md5.c
@@ -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 54b369d..e614a17 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -38,8 +38,9 @@
# 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.
+# "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 authentication
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 10561fe..76fefcb 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -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 6e1c53b..b3d486a 100644
--- a/src/include/libpq/md5.h
+++ b/src/include/libpq/md5.h
@@ -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);