*** a/doc/src/sgml/client-auth.sgml --- b/doc/src/sgml/client-auth.sgml *************** *** 393,398 **** hostnossl database user --- 393,408 ---- + radius + + + Authenticate using a RADIUS server. See for detauls. + + + + + cert *************** *** 1329,1334 **** ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net" --- 1339,1424 ---- + + 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 LDAP 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 IP address and not a hostname. This parameter is required. + + + + + + radiussecret + + + 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. + + + + + + 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 *** 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);