RADIUS authentication
The attached patch implements RADIUS authentication (RFC2865-compatible).
The main usecase for me in this is the ability to use (token based)
one-time-password systems easily with PostgreSQL. These systems almost
always support RADIUS, and the implementation is fairly simple. RADIUS
can of course be used in many other scenarios as well (for example, it
can be used to implement "only this group"-access with at least Active
Directory, something our current LDAP doesn't support. We might
eventually want to support that in our LDAP, but it's not there now)
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Attachments:
radius.patchapplication/octet-stream; name=radius.patchDownload
*** a/doc/src/sgml/client-auth.sgml
--- b/doc/src/sgml/client-auth.sgml
***************
*** 393,398 **** hostnossl <replaceable>database</replaceable> <replaceable>user</replaceable>
--- 393,408 ----
</varlistentry>
<varlistentry>
+ <term><literal>radius</></term>
+ <listitem>
+ <para>
+ Authenticate using a RADIUS server. See <xref
+ linkend="auth-radius"> for detauls.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>cert</></term>
<listitem>
<para>
***************
*** 1329,1334 **** ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
--- 1339,1424 ----
</sect2>
+ <sect2 id="auth-radius">
+ <title>RADIUS authentication</title>
+
+ <indexterm zone="auth-radius">
+ <primary>RADIUS</primary>
+ </indexterm>
+
+ <para>
+ This authentication method operates similarly to
+ <literal>password</literal> 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 LDAP can be used for
+ authentication.
+ </para>
+
+ <para>
+ When using RADIUS authentication, an Access Request message will be sent
+ to the configured RADIUS server. This request will be of type
+ <literal>Authenticate Only</literal>, and include parameters for
+ <literal>user name</>, <literal>password</>(encrypted) and
+ <literal>NAS Identifier</>. The request will be encrypted using
+ a secret shared with the server. The RADIUS server will respond to
+ this server with either <literal>Access Accept</> or
+ <literal>Access Reject</>. There is no support for RADIUS accounting.
+ </para>
+
+ <para>
+ The following configuration options are supported for RADIUS:
+ <variablelist>
+ <varlistentry>
+ <term><literal>radiusserver</literal></term>
+ <listitem>
+ <para>
+ The IP address of the RADIUS server to connect to. This must
+ be an IP address and not a hostname. This parameter is required.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>radiussecret</literal></term>
+ <listitem>
+ <para>
+ The shared secret used when talking securely to the RAIDUS
+ 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.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>radiusport</literal></term>
+ <listitem>
+ <para>
+ The port number on the RADIUS server to connect to. If no port
+ is specified, the default port <literal>1812</> will be used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>radiusidentifier</literal></term>
+ <listitem>
+ <para>
+ The string used as <literal>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
+ <literal>postgresql</> will be used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ </para>
+ </sect2>
+
<sect2 id="auth-cert">
<title>Certificate authentication</title>
*** a/src/backend/libpq/auth.c
--- b/src/backend/libpq/auth.c
***************
*** 33,38 ****
--- 33,39 ----
#include "libpq/ip.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
+ #include "libpq/md5.h"
#include "miscadmin.h"
#include "storage/ipc.h"
***************
*** 182,187 **** typedef SECURITY_STATUS
--- 183,194 ----
static int pg_SSPI_recvauth(Port *port);
#endif
+ /*----------------------------------------------------------------
+ * RADIUS Authentication
+ *----------------------------------------------------------------
+ */
+ static int CheckRADIUSAuth(Port *port);
+
/*
* Maximum accepted size of GSS and SSPI authentication tokens.
***************
*** 265,270 **** auth_failed(Port *port, int status)
--- 272,280 ----
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,479 **** ClientAuthentication(Port *port)
Assert(false);
#endif
break;
!
case uaTrust:
status = STATUS_OK;
break;
--- 483,491 ----
Assert(false);
#endif
break;
! case uaRADIUS:
! status = CheckRADIUSAuth(port);
! break;
case uaTrust:
status = STATUS_OK;
break;
***************
*** 2415,2417 **** CheckCertAuth(Port *port)
--- 2427,2764 ----
}
#endif
+
+
+ /*----------------------------------------------------------------
+ * RADIUS authentication
+ *----------------------------------------------------------------
+ */
+
+ /*
+ * RADIUS authentication is described in RFC2865 (and several
+ * others).
+ */
+
+ #define RADIUS_VECTOR_LENGTH 16
+ #define RADIUS_HEADER_LENGTH 20
+
+ typedef struct
+ {
+ unsigned char attribute;
+ unsigned char length;
+ unsigned char data[1];
+ } radius_attribute;
+
+ typedef struct
+ {
+ unsigned char code;
+ unsigned char id;
+ unsigned short length;
+ unsigned char 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, int 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);
+ unsigned char *cryptvector;
+ unsigned char 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;
+ for (i = 0; i < RADIUS_VECTOR_LENGTH; i++)
+ /* XXX: Generate a more secure random string? */
+ packet->vector[i] = random() % 255;
+ 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;
+ }
+ }
*** a/src/backend/libpq/hba.c
--- b/src/backend/libpq/hba.c
***************
*** 947,952 **** parse_hba_line(List *line, int line_num, HbaLine *parsedline)
--- 947,954 ----
#else
unsupauth = "cert";
#endif
+ else if (strcmp(token, "radius")== 0)
+ parsedline->auth_method = uaRADIUS;
else
{
ereport(LOG,
***************
*** 1157,1162 **** parse_hba_line(List *line, int line_num, HbaLine *parsedline)
--- 1159,1203 ----
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,
***************
*** 1209,1214 **** parse_hba_line(List *line, int line_num, HbaLine *parsedline)
--- 1250,1261 ----
}
}
+ 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.
*/
*** a/src/backend/libpq/md5.c
--- b/src/backend/libpq/md5.c
***************
*** 298,303 **** pg_md5_hash(const void *buff, size_t len, char *hexsum)
--- 298,309 ----
return true;
}
+ bool pg_md5_binary(const void *buff, size_t len, void *outbuf)
+ {
+ if (!calculateDigestFromBuffer((uint8 *) buff, len, outbuf))
+ return false;
+ return true;
+ }
/*
*** a/src/backend/libpq/pg_hba.conf.sample
--- b/src/backend/libpq/pg_hba.conf.sample
***************
*** 38,45 ****
# 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.
#
# OPTIONS are a set of options for the authentication in the format
# NAME=VALUE. The available options depend on the different authentication
--- 38,46 ----
# that the server is directly connected to.
#
# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", "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 authentication
*** a/src/include/libpq/hba.h
--- b/src/include/libpq/hba.h
***************
*** 27,33 **** typedef enum UserAuth
uaSSPI,
uaPAM,
uaLDAP,
! uaCert
} UserAuth;
typedef enum IPCompareMethod
--- 27,34 ----
uaSSPI,
uaPAM,
uaLDAP,
! uaCert,
! uaRADIUS
} UserAuth;
typedef enum IPCompareMethod
***************
*** 71,76 **** typedef struct
--- 72,81 ----
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 */
*** a/src/include/libpq/md5.h
--- b/src/include/libpq/md5.h
***************
*** 23,28 ****
--- 23,29 ----
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);
On sön, 2010-01-10 at 14:25 +0100, Magnus Hagander wrote:
The attached patch implements RADIUS authentication (RFC2865-compatible).
The main usecase for me in this is the ability to use (token based)
one-time-password systems easily with PostgreSQL. These systems almost
always support RADIUS, and the implementation is fairly simple. RADIUS
can of course be used in many other scenarios as well (for example, it
can be used to implement "only this group"-access with at least Active
Directory, something our current LDAP doesn't support. We might
eventually want to support that in our LDAP, but it's not there now)
Sounds interesting; I didn't know RADIUS was still in use.
There is a copy-and-paste'o in the patch, where LDAP is mentioned
instead of RADIUS.
On Sun, Jan 10, 2010 at 18:55, Peter Eisentraut <peter_e@gmx.net> wrote:
On sön, 2010-01-10 at 14:25 +0100, Magnus Hagander wrote:
The attached patch implements RADIUS authentication (RFC2865-compatible).
The main usecase for me in this is the ability to use (token based)
one-time-password systems easily with PostgreSQL. These systems almost
always support RADIUS, and the implementation is fairly simple. RADIUS
can of course be used in many other scenarios as well (for example, it
can be used to implement "only this group"-access with at least Active
Directory, something our current LDAP doesn't support. We might
eventually want to support that in our LDAP, but it's not there now)Sounds interesting; I didn't know RADIUS was still in use.
It's very much in use. It works well for that kind of scenario, and
it's still very much in use by ISPs (for other things than database,s
of course)
There is a copy-and-paste'o in the patch, where LDAP is mentioned
instead of RADIUS.
Yeah, Stefan just pointed that out to me over IM. Thanks. There's also
a smis-spelling of the word RADIUS :-)
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Magnus,
* Magnus Hagander (magnus@hagander.net) wrote:
The attached patch implements RADIUS authentication (RFC2865-compatible).
Great! We have a few environments which use RADIUS auth, nice that PG
might be able to use that auth method in the future.
I'm not a fan of having the shared secret stored in a 'regular' config
file. Could you support, or maybe just change it to, breaking that out
into another file? Perhaps something simimlar to how pam_radius_auth
works, where you can also list multiple servers?
http://freeradius.org/pam_radius_auth/
Would also allow using the same file for multiple RADIUS-based servers..
I know pg_hba.conf can just be set to have minimal permissions (and is
on Debian), but that's the kind of file that tends to end up in things
like subversion repositories or puppet configs where they aren't
treated as carefully since, generally, what's in them doesn't come
across as super-sensetive.
Thanks,
Stephen
(2010/01/10 22:25), Magnus Hagander wrote:
The attached patch implements RADIUS authentication (RFC2865-compatible).
The main usecase for me in this is the ability to use (token based)
one-time-password systems easily with PostgreSQL. These systems almost
always support RADIUS, and the implementation is fairly simple. RADIUS
can of course be used in many other scenarios as well (for example, it
can be used to implement "only this group"-access with at least Active
Directory, something our current LDAP doesn't support. We might
eventually want to support that in our LDAP, but it's not there now)
I checked this patch.
It can be applied on the latest CVS HEAD, and built without any matter.
I tried to work it with freeradius, then it performs well in a few
configurations. (Of course, it is far from comprehensive.)
Because I'm not good at RADIUS protocol, I didn't check correctness
of the protocol.
Hmm, it introduces the format of the UDP packets.
It seems to me this patch is implemented correctly.
http://technet.microsoft.com/en-us/library/cc958030.aspx
Here is a few comments from the initial reviewing.
* Is the feature to be configurable via ./configure scripts?
Currently, we have --with-pam or --with-ldap option, and it allows
users to turn on/off the feature.
Of course, it has dependency on libraries.
* A corresponding comment. This patch implements RADIUS protocol
by itself. Is there any commonly used libraries for the purpose?
It allows us to separate a burden to manage a certain network
protocol within PostgreSQL.
* IIUC, inet_addr() takes only IPv4 address. It is used to translate
"radiusserver" parameter to netaddr format.
Could you document this parameter takes only IPv4 format.
* I think this comment is right.
+ for (i = 0; i < RADIUS_VECTOR_LENGTH; i++)
+ /* XXX: Generate a more secure random string? */
+ packet->vector[i] = random() % 255;
The random seed is initialized at BackendRun() with MyProcPid and
the time of backend process launched.
Then, PostgresMain() -> InitPostgres() -> PerformAuthentication()
will be called, and this random() shall be the first call just after
initialization of the srandom().
Do you have any good idea?
Or, do you think it should be fixed with high priority?
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
2010/1/18 KaiGai Kohei <kaigai@ak.jp.nec.com>:
(2010/01/10 22:25), Magnus Hagander wrote:
The attached patch implements RADIUS authentication (RFC2865-compatible).
The main usecase for me in this is the ability to use (token based)
one-time-password systems easily with PostgreSQL. These systems almost
always support RADIUS, and the implementation is fairly simple. RADIUS
can of course be used in many other scenarios as well (for example, it
can be used to implement "only this group"-access with at least Active
Directory, something our current LDAP doesn't support. We might
eventually want to support that in our LDAP, but it's not there now)I checked this patch.
Thanks!
Here is a few comments from the initial reviewing.
* Is the feature to be configurable via ./configure scripts?
Currently, we have --with-pam or --with-ldap option, and it allows
users to turn on/off the feature.
Of course, it has dependency on libraries.
I think not, because it doesn't rely on external libraries we might as
well always enable it. As long as you don't configure it in
pg_hba.conf, it has zero cost to the installation. Adding a configure
parameter would just make things complicated. For example, we don't
have a configure switch to enable ident or md5.
* A corresponding comment. This patch implements RADIUS protocol
by itself. Is there any commonly used libraries for the purpose?
It allows us to separate a burden to manage a certain network
protocol within PostgreSQL.
I looked briefly at it. The ones I found would require almost as much
code as doing the protocol itself, and had some compatibility issues
(mainly wrt windows)
* IIUC, inet_addr() takes only IPv4 address. It is used to translate
"radiusserver" parameter to netaddr format.
Could you document this parameter takes only IPv4 format.
Will do.
* I think this comment is right.
+ for (i = 0; i < RADIUS_VECTOR_LENGTH; i++)
+ /* XXX: Generate a more secure random string? */
+ packet->vector[i] = random() % 255;The random seed is initialized at BackendRun() with MyProcPid and
the time of backend process launched.
Then, PostgresMain() -> InitPostgres() -> PerformAuthentication()
will be called, and this random() shall be the first call just after
initialization of the srandom().Do you have any good idea?
Or, do you think it should be fixed with high priority?
It does need a fairly good random number generator there to be secure,
so it should probably be improved. OTOH, the whole thing can be more
considered obfuscation rather than encryption, and those who really
care about higher security will use ipsec or trusted networks.
Maybe switching to erand48() would make this better, and good enough?
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?
This I can't answer, I don't know this well enough. Somebody else?
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Magnus Hagander <magnus@hagander.net> writes:
2010/1/18 KaiGai Kohei <kaigai@ak.jp.nec.com>:
�The random seed is initialized at BackendRun() with MyProcPid and
�the time of backend process launched.
�Then, PostgresMain() -> InitPostgres() -> PerformAuthentication()
�will be called, and this random() shall be the first call just after
�initialization of the srandom().
Maybe switching to erand48() would make this better, and good enough?
Wouldn't help in the least. The problem is not the RNG itself but lack
of an adequately unpredictable random seed, and anything you do here
is unlikely to be more random than what we already arranged for.
regards, tom lane
(2010/01/20 0:19), Magnus Hagander wrote:
2010/1/18 KaiGai Kohei<kaigai@ak.jp.nec.com>:
(2010/01/10 22:25), Magnus Hagander wrote:
The attached patch implements RADIUS authentication (RFC2865-compatible).
The main usecase for me in this is the ability to use (token based)
one-time-password systems easily with PostgreSQL. These systems almost
always support RADIUS, and the implementation is fairly simple. RADIUS
can of course be used in many other scenarios as well (for example, it
can be used to implement "only this group"-access with at least Active
Directory, something our current LDAP doesn't support. We might
eventually want to support that in our LDAP, but it's not there now)I checked this patch.
Thanks!
Here is a few comments from the initial reviewing.
* Is the feature to be configurable via ./configure scripts?
Currently, we have --with-pam or --with-ldap option, and it allows
users to turn on/off the feature.
Of course, it has dependency on libraries.I think not, because it doesn't rely on external libraries we might as
well always enable it. As long as you don't configure it in
pg_hba.conf, it has zero cost to the installation. Adding a configure
parameter would just make things complicated. For example, we don't
have a configure switch to enable ident or md5.* A corresponding comment. This patch implements RADIUS protocol
by itself. Is there any commonly used libraries for the purpose?
It allows us to separate a burden to manage a certain network
protocol within PostgreSQL.I looked briefly at it. The ones I found would require almost as much
code as doing the protocol itself, and had some compatibility issues
(mainly wrt windows)
OK, I agreed.
* IIUC, inet_addr() takes only IPv4 address. It is used to translate
"radiusserver" parameter to netaddr format.
Could you document this parameter takes only IPv4 format.Will do.
* I think this comment is right. + for (i = 0; i< RADIUS_VECTOR_LENGTH; i++) + /* XXX: Generate a more secure random string? */ + packet->vector[i] = random() % 255;The random seed is initialized at BackendRun() with MyProcPid and
the time of backend process launched.
Then, PostgresMain() -> InitPostgres() -> PerformAuthentication()
will be called, and this random() shall be the first call just after
initialization of the srandom().Do you have any good idea?
Or, do you think it should be fixed with high priority?It does need a fairly good random number generator there to be secure,
so it should probably be improved. OTOH, the whole thing can be more
considered obfuscation rather than encryption, and those who really
care about higher security will use ipsec or trusted networks.Maybe switching to erand48() would make this better, and good enough?
As Tom pointed out, it is fundamentally same.
The matter is this random() invocation is the first time after
initialization of random seed by srandom(). It means an external observer
can estimate the random value uniquely using pid and startup time.
In other representation, the "random" value is the result of function
which takes arguments of pid and startup time, without random factor.
for (i = 0; i< RADIUS_VECTOR_LENGTH; i++)
packet->vector[i] = f(getpid(), port->SessionStartTime, i);
One idea is to modify the logic to set up random seed in BackendRun().
In most of UNIX-like operating system, we can use /dev/random as a random
seed which is well randomized.
http://en.wikipedia.org/wiki//dev/random
It seems to me PostmasterRandom() is a right place to set random seed,
and we can inject a block something like #ifdef HAVE_DEV_RANDOM ...
Instead of such kind of efforts, we can also document that PostgreSQL and
RADIUS server should have communication using enough secure connection
explicitly. IMO, it will cover most of use cases.
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?This I can't answer, I don't know this well enough. Somebody else?
What manner is applied to handle network protocol in other part?
The radius_packet is declared as follows:
+ typedef struct
+ {
+ unsigned char code; +0
+ unsigned char id; +1
+ unsigned short length; +2
+ unsigned char vector[RADIUS_VECTOR_LENGTH]; +4? +8?
+ } radius_packet;
It may be a bit nervous, except for possible alignment of the vector
on 64bit architecture.
And, one more. It seems to me uint8 and uint16 are more preferable than
unsigned char/short in this context.
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
2010/1/20 KaiGai Kohei <kaigai@ak.jp.nec.com>:
(2010/01/20 0:19), Magnus Hagander wrote:
* I think this comment is right.
+ for (i = 0; i< RADIUS_VECTOR_LENGTH; i++)
+ /* XXX: Generate a more secure random string? */
+ packet->vector[i] = random() % 255;The random seed is initialized at BackendRun() with MyProcPid and
the time of backend process launched.
Then, PostgresMain() -> InitPostgres() -> PerformAuthentication()
will be called, and this random() shall be the first call just after
initialization of the srandom().Do you have any good idea?
Or, do you think it should be fixed with high priority?It does need a fairly good random number generator there to be secure,
so it should probably be improved. OTOH, the whole thing can be more
considered obfuscation rather than encryption, and those who really
care about higher security will use ipsec or trusted networks.Maybe switching to erand48() would make this better, and good enough?
As Tom pointed out, it is fundamentally same.
The matter is this random() invocation is the first time after
initialization of random seed by srandom(). It means an external observer
can estimate the random value uniquely using pid and startup time.In other representation, the "random" value is the result of function
which takes arguments of pid and startup time, without random factor.for (i = 0; i< RADIUS_VECTOR_LENGTH; i++)
packet->vector[i] = f(getpid(), port->SessionStartTime, i);One idea is to modify the logic to set up random seed in BackendRun().
In most of UNIX-like operating system, we can use /dev/random as a random
seed which is well randomized.http://en.wikipedia.org/wiki//dev/random
It seems to me PostmasterRandom() is a right place to set random seed,
and we can inject a block something like #ifdef HAVE_DEV_RANDOM ...Instead of such kind of efforts, we can also document that PostgreSQL and
RADIUS server should have communication using enough secure connection
explicitly. IMO, it will cover most of use cases.
There is one more option here - use OpenSSL if available. It has
functions for secure random number generations
(http://www.openssl.org/docs/crypto/RAND_bytes.html). That seems easy
enough when OpenSSL is available.
The question then becomes what do we do if we don't have OpenSSL
available? Do we document that it's not secure, or refuse to run it?
I'd vote for document it.. If you don't have SSL enabled, then you
clearly don't use SSL for the libpq connection, which means the
password goes in cleartext in that stream...
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?This I can't answer, I don't know this well enough. Somebody else?
What manner is applied to handle network protocol in other part?
The radius_packet is declared as follows:
+ typedef struct + { + unsigned char code; +0 + unsigned char id; +1 + unsigned short length; +2 + unsigned char vector[RADIUS_VECTOR_LENGTH]; +4? +8? + } radius_packet;It may be a bit nervous, except for possible alignment of the vector
on 64bit architecture.And, one more. It seems to me uint8 and uint16 are more preferable than
unsigned char/short in this context.
Yeha, that is probably right. I copied that off a reference
implementation of the struct. Will change accordingly.
FWIW, I tested it on Win64 and it didn't have any issues there at least.
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
2010/1/24 Magnus Hagander <magnus@hagander.net>:
2010/1/20 KaiGai Kohei <kaigai@ak.jp.nec.com>:
As Tom pointed out, it is fundamentally same.
The matter is this random() invocation is the first time after
initialization of random seed by srandom(). It means an external observer
can estimate the random value uniquely using pid and startup time.In other representation, the "random" value is the result of function
which takes arguments of pid and startup time, without random factor.for (i = 0; i< RADIUS_VECTOR_LENGTH; i++)
packet->vector[i] = f(getpid(), port->SessionStartTime, i);One idea is to modify the logic to set up random seed in BackendRun().
In most of UNIX-like operating system, we can use /dev/random as a random
seed which is well randomized.http://en.wikipedia.org/wiki//dev/random
It seems to me PostmasterRandom() is a right place to set random seed,
and we can inject a block something like #ifdef HAVE_DEV_RANDOM ...Instead of such kind of efforts, we can also document that PostgreSQL and
RADIUS server should have communication using enough secure connection
explicitly. IMO, it will cover most of use cases.There is one more option here - use OpenSSL if available. It has
functions for secure random number generations
(http://www.openssl.org/docs/crypto/RAND_bytes.html). That seems easy
enough when OpenSSL is available.The question then becomes what do we do if we don't have OpenSSL
available? Do we document that it's not secure, or refuse to run it?
I'd vote for document it.. If you don't have SSL enabled, then you
clearly don't use SSL for the libpq connection, which means the
password goes in cleartext in that stream...
Updated patch attached.
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
Attachments:
radius.patchapplication/octet-stream; name=radius.patchDownload
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 <replaceable>database</replaceable> <replaceable>user</replaceable>
</varlistentry>
<varlistentry>
+ <term><literal>radius</></term>
+ <listitem>
+ <para>
+ Authenticate using a RADIUS server. See <xref
+ linkend="auth-radius"> for detauls.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><literal>cert</></term>
<listitem>
<para>
@@ -1331,6 +1341,95 @@ ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
</sect2>
+ <sect2 id="auth-radius">
+ <title>RADIUS authentication</title>
+
+ <indexterm zone="auth-radius">
+ <primary>RADIUS</primary>
+ </indexterm>
+
+ <para>
+ This authentication method operates similarly to
+ <literal>password</literal> 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.
+ </para>
+
+ <para>
+ When using RADIUS authentication, an Access Request message will be sent
+ to the configured RADIUS server. This request will be of type
+ <literal>Authenticate Only</literal>, and include parameters for
+ <literal>user name</>, <literal>password</> (encrypted) and
+ <literal>NAS Identifier</>. The request will be encrypted using
+ a secret shared with the server. The RADIUS server will respond to
+ this server with either <literal>Access Accept</> or
+ <literal>Access Reject</>. There is no support for RADIUS accounting.
+ </para>
+
+ <para>
+ The following configuration options are supported for RADIUS:
+ <variablelist>
+ <varlistentry>
+ <term><literal>radiusserver</literal></term>
+ <listitem>
+ <para>
+ The IP address of the RADIUS server to connect to. This must
+ be an IPV4 address and not a hostname. This parameter is required.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>radiussecret</literal></term>
+ <listitem>
+ <para>
+ 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.
+ <note>
+ <para>
+ The encryption vector used will only be cryptographically
+ strong if <productname>PostgreSQL</> is built with support for
+ <productname>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.
+ </para>
+ </note>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>radiusport</literal></term>
+ <listitem>
+ <para>
+ The port number on the RADIUS server to connect to. If no port
+ is specified, the default port <literal>1812</> will be used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>radiusidentifier</literal></term>
+ <listitem>
+ <para>
+ The string used as <literal>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
+ <literal>postgresql</> will be used.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ </para>
+ </sect2>
+
<sect2 id="auth-cert">
<title>Certificate authentication</title>
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 <openssl/rand.h>
+#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);
(2010/01/24 23:29), Magnus Hagander wrote:
2010/1/20 KaiGai Kohei<kaigai@ak.jp.nec.com>:
(2010/01/20 0:19), Magnus Hagander wrote:
* I think this comment is right. + for (i = 0; i< RADIUS_VECTOR_LENGTH; i++) + /* XXX: Generate a more secure random string? */ + packet->vector[i] = random() % 255;The random seed is initialized at BackendRun() with MyProcPid and
the time of backend process launched.
Then, PostgresMain() -> InitPostgres() -> PerformAuthentication()
will be called, and this random() shall be the first call just after
initialization of the srandom().Do you have any good idea?
Or, do you think it should be fixed with high priority?It does need a fairly good random number generator there to be secure,
so it should probably be improved. OTOH, the whole thing can be more
considered obfuscation rather than encryption, and those who really
care about higher security will use ipsec or trusted networks.Maybe switching to erand48() would make this better, and good enough?
As Tom pointed out, it is fundamentally same.
The matter is this random() invocation is the first time after
initialization of random seed by srandom(). It means an external observer
can estimate the random value uniquely using pid and startup time.In other representation, the "random" value is the result of function
which takes arguments of pid and startup time, without random factor.for (i = 0; i< RADIUS_VECTOR_LENGTH; i++)
packet->vector[i] = f(getpid(), port->SessionStartTime, i);One idea is to modify the logic to set up random seed in BackendRun().
In most of UNIX-like operating system, we can use /dev/random as a random
seed which is well randomized.http://en.wikipedia.org/wiki//dev/random
It seems to me PostmasterRandom() is a right place to set random seed,
and we can inject a block something like #ifdef HAVE_DEV_RANDOM ...Instead of such kind of efforts, we can also document that PostgreSQL and
RADIUS server should have communication using enough secure connection
explicitly. IMO, it will cover most of use cases.There is one more option here - use OpenSSL if available. It has
functions for secure random number generations
(http://www.openssl.org/docs/crypto/RAND_bytes.html). That seems easy
enough when OpenSSL is available.
In just my opinion (so, committer may have different one), it is an
option to utilize openSSL library when available. However, it should
be moved to PostmasterRandom() and used to provide more randomness
for srandom(). And, srandom() in the head of BackendRun() should be
replaced by PostmasterRandom().
I also want any opinions from others.
The question then becomes what do we do if we don't have OpenSSL
available? Do we document that it's not secure, or refuse to run it?
I'd vote for document it.. If you don't have SSL enabled, then you
clearly don't use SSL for the libpq connection, which means the
password goes in cleartext in that stream...
The seed of random is a different issue from safeness of password on
the stream between client and server. For example, if admin set up
IPsec/ESP between them, OpenSSL is not must-requirement.
Even if OpenSSL is not available, as long as both of PostgreSQL and
RADIUS server are set up in trusted network, we can consider it is
secure. So, all we can do is to introduce the risk, and the decisions
are depending on end-users.
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?This I can't answer, I don't know this well enough. Somebody else?
What manner is applied to handle network protocol in other part?
The radius_packet is declared as follows:
+ typedef struct + { + unsigned char code; +0 + unsigned char id; +1 + unsigned short length; +2 + unsigned char vector[RADIUS_VECTOR_LENGTH]; +4? +8? + } radius_packet;It may be a bit nervous, except for possible alignment of the vector
on 64bit architecture.And, one more. It seems to me uint8 and uint16 are more preferable than
unsigned char/short in this context.Yeha, that is probably right. I copied that off a reference
implementation of the struct. Will change accordingly.FWIW, I tested it on Win64 and it didn't have any issues there at least.
Just to be safe, could you inject an Assert() here?
If a minor compiler aligned it unintentionally, it will be a bug not easy
to find out.
/* check whether the compiler aligned it unintentionally, or not */
Assert(offsetof(radius_packet, vector) == 4);
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>
2010/1/25 KaiGai Kohei <kaigai@ak.jp.nec.com>:
(2010/01/24 23:29), Magnus Hagander wrote:
There is one more option here - use OpenSSL if available. It has
functions for secure random number generations
(http://www.openssl.org/docs/crypto/RAND_bytes.html). That seems easy
enough when OpenSSL is available.In just my opinion (so, committer may have different one), it is an
option to utilize openSSL library when available. However, it should
be moved to PostmasterRandom() and used to provide more randomness
for srandom(). And, srandom() in the head of BackendRun() should be
replaced by PostmasterRandom().
That is a feature separate from this.
And note that PostmasterRandom() and friends still deal with
pseudo-random numbers AFAIK. Not crytographically strong ones. Which
migh tactually be something we'd want to do in other places (like
generating salts), but again that's a completely different scope from
this.
I also want any opinions from others.
Agreed, me too. I suggest a separate thread discussing random
generations in general for that.
The question then becomes what do we do if we don't have OpenSSL
available? Do we document that it's not secure, or refuse to run it?
I'd vote for document it.. If you don't have SSL enabled, then you
clearly don't use SSL for the libpq connection, which means the
password goes in cleartext in that stream...The seed of random is a different issue from safeness of password on
the stream between client and server. For example, if admin set up
IPsec/ESP between them, OpenSSL is not must-requirement.
Exactly, which is why I suggest a note in the docs only.
Even if OpenSSL is not available, as long as both of PostgreSQL and
RADIUS server are set up in trusted network, we can consider it is
secure. So, all we can do is to introduce the risk, and the decisions
are depending on end-users.
Agreed.
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?This I can't answer, I don't know this well enough. Somebody else?
What manner is applied to handle network protocol in other part?
The radius_packet is declared as follows:
+ typedef struct + { + unsigned char code; +0 + unsigned char id; +1 + unsigned short length; +2 + unsigned char vector[RADIUS_VECTOR_LENGTH]; +4? +8? + } radius_packet;It may be a bit nervous, except for possible alignment of the vector
on 64bit architecture.And, one more. It seems to me uint8 and uint16 are more preferable than
unsigned char/short in this context.Yeha, that is probably right. I copied that off a reference
implementation of the struct. Will change accordingly.FWIW, I tested it on Win64 and it didn't have any issues there at least.
Just to be safe, could you inject an Assert() here?
If a minor compiler aligned it unintentionally, it will be a bug not easy
to find out./* check whether the compiler aligned it unintentionally, or not */
Assert(offsetof(radius_packet, vector) == 4);
Yeah, good point. I'll add that.
--
Magnus Hagander
Me: http://www.hagander.net/
Work: http://www.redpill-linpro.com/
(2010/01/26 6:30), Magnus Hagander wrote:
2010/1/25 KaiGai Kohei<kaigai@ak.jp.nec.com>:
(2010/01/24 23:29), Magnus Hagander wrote:
There is one more option here - use OpenSSL if available. It has
functions for secure random number generations
(http://www.openssl.org/docs/crypto/RAND_bytes.html). That seems easy
enough when OpenSSL is available.In just my opinion (so, committer may have different one), it is an
option to utilize openSSL library when available. However, it should
be moved to PostmasterRandom() and used to provide more randomness
for srandom(). And, srandom() in the head of BackendRun() should be
replaced by PostmasterRandom().That is a feature separate from this.
And note that PostmasterRandom() and friends still deal with
pseudo-random numbers AFAIK. Not crytographically strong ones. Which
migh tactually be something we'd want to do in other places (like
generating salts), but again that's a completely different scope from
this.I also want any opinions from others.
Agreed, me too. I suggest a separate thread discussing random
generations in general for that.
I'd like to take the issue into committer review stage.
Your patch is technically right, but I don't know whether it is on the
right direction in the community decision.
I think it is not a role in round-robin reviewer's stage.
The question then becomes what do we do if we don't have OpenSSL
available? Do we document that it's not secure, or refuse to run it?
I'd vote for document it.. If you don't have SSL enabled, then you
clearly don't use SSL for the libpq connection, which means the
password goes in cleartext in that stream...The seed of random is a different issue from safeness of password on
the stream between client and server. For example, if admin set up
IPsec/ESP between them, OpenSSL is not must-requirement.Exactly, which is why I suggest a note in the docs only.
Even if OpenSSL is not available, as long as both of PostgreSQL and
RADIUS server are set up in trusted network, we can consider it is
secure. So, all we can do is to introduce the risk, and the decisions
are depending on end-users.Agreed.
* It casts char array (such as radius_buffer) into radius_packet
structure. The radius_packet structure represents the format of
RADIUS network packet as is.
It may be preferable to give compiler a hint not to align this
structure.
In GCC, we can use "__attribute__((packed))" to suggest not to
align the member of structure. Is there any portable way for this?This I can't answer, I don't know this well enough. Somebody else?
What manner is applied to handle network protocol in other part?
The radius_packet is declared as follows:
+ typedef struct + { + unsigned char code; +0 + unsigned char id; +1 + unsigned short length; +2 + unsigned char vector[RADIUS_VECTOR_LENGTH]; +4? +8? + } radius_packet;It may be a bit nervous, except for possible alignment of the vector
on 64bit architecture.And, one more. It seems to me uint8 and uint16 are more preferable than
unsigned char/short in this context.Yeha, that is probably right. I copied that off a reference
implementation of the struct. Will change accordingly.FWIW, I tested it on Win64 and it didn't have any issues there at least.
Just to be safe, could you inject an Assert() here?
If a minor compiler aligned it unintentionally, it will be a bug not easy
to find out./* check whether the compiler aligned it unintentionally, or not */
Assert(offsetof(radius_packet, vector) == 4);Yeah, good point. I'll add that.
OK, I'll have nothing to comment on this patch any more.
Thanks,
--
OSS Platform Development Division, NEC
KaiGai Kohei <kaigai@ak.jp.nec.com>