BlastRADIUS mitigation
Hi,
(This discussion started on the security@ list, but was deemed
suitable for the -hackers@ list.)
PostgreSQL supports RADIUS authentication[1]https://www.postgresql.org/docs/current/auth-radius.html. It's probably not
widely used, and generally any security technology based on MD5 alone
should by now be considered weak and obsolete, but there are a few
reports of users. Recently, a paper was published[2]https://www.blastradius.fail/ showing
successful attacks on a range of RADIUS clients, though no mention of
PostgreSQL. It's not just an MD5 collision search, it's a more
specific cryptographic weakness in the protocol that allows messages
to be forged in quite practical amounts of CPU time. We might be
quite lucky though: our hard-coded RADIUS_TIMEOUT = 3s doesn't seem to
be enough time, based on my non-crypto-expert reading of the paper at
least.
Even if we assume that is true, FreeRADIUS 3.2.5 has recently[3]https://github.com/FreeRADIUS/freeradius-server/commit/6616be90346beb6050446bd00c8ed5bca1b8ef29
started spewing scary warnings when PostgreSQL sends requests to it by
default, and advising administrators to adjust the relevant setting
further to just drop unsigned messages. I assume the commercial
RADIUS implementations will do something like that too, based on
comments about the intended direction and expectations of clients in
an in-development RFC[4]https://datatracker.ietf.org/doc/draft-ietf-radext-deprecating-radius/.
The attached patch-set adds a basic TAP test for RADIUS
authentication, and then adds a Message-Authenticator attribute to all
outgoing requests (an HMAC-MD5 signature for the whole message that
was already defined by the RFCs but marked optional and often
omitted), and also *optionally* requires a Message-Authenticator to
appear in responses from the RADIUS server, and verifies it if
present. With the change, FreeRADIUS 3.2.5 is happy to talk to
PostgreSQL again.
The response requirement can be enabled by radiusrequirema=1 in
pg_hba.conf. For example, Debian stable is currently shipping
FreeRADIUS 3.2.1 which doesn't yet send the MA in its responses, but
FreeBSD and Debian "testing" have started shipping FreeRADIUS 3.2.5
which is how I noticed all this. So it doesn't seem quite right to
require it by default, yet?
It's quite easy to test this locally, if you have FreeRADIUS installed
on any Unixoid system. See the TAP test for some minimal
configuration files required to try it out.
I had originally developed the TAP test as part of a larger project[5]/messages/by-id/CA+hUKGKxNoVjkMCksnj6z3BwiS3y2v6LN6z7_CisLK+rv+0V4g@mail.gmail.com
really about something else, which is why it has Michael listed as a
reviewer already, and this version incorporates some new improvements
he recommended (thanks!). I've created this new thread and new
minimal test just to deal with the BlastRADIUS mitigation topic.
We might also consider just dropping RADIUS support in 18, if we don't
get patches to bring it up to date with modern RADIUS recommendations
beyond the mitigation (deprecating UDP, adding TLS, probably more
things). Such patches would ideally be written by someone with a more
direct interest in the protocol (ie I am not volunteering). But even
if we decide to drop it instead. I think we'd still want the change
I'm proposing here for released branches.
Since PostgreSQL v12 and v13 don't have the modern "common/hmac.h"
API, I came up with a cheap kludge: locally #define those interfaces
to point directly to the OpenSSL HMAC API, or just give up and drop
Message-Authenticator support if you didn't build with OpenSSL support
(in practice everyone does). Better ideas?
[1]: https://www.postgresql.org/docs/current/auth-radius.html
[2]: https://www.blastradius.fail/
[3]: https://github.com/FreeRADIUS/freeradius-server/commit/6616be90346beb6050446bd00c8ed5bca1b8ef29
[4]: https://datatracker.ietf.org/doc/draft-ietf-radext-deprecating-radius/
[5]: /messages/by-id/CA+hUKGKxNoVjkMCksnj6z3BwiS3y2v6LN6z7_CisLK+rv+0V4g@mail.gmail.com
Attachments:
v1-0001-Add-simple-test-for-RADIUS-authentication.patchapplication/octet-stream; name=v1-0001-Add-simple-test-for-RADIUS-authentication.patchDownload
From 514693f716aede9d5bfccea94eaac5c2494f82c2 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sat, 31 Dec 2022 14:41:57 +1300
Subject: [PATCH v1 1/5] Add simple test for RADIUS authentication.
This is similar to the existing tests for other authentication methods.
It requires FreeRADIUS to be installed, and opens ports that may be
considered insecure, so users have to opt in with PG_EXTRA_TESTS=radius.
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Discussion: https://postgr.es/m/CA%2BhUKGKxNoVjkMCksnj6z3BwiS3y2v6LN6z7_CisLK%2Brv%2B0V4g%40mail.gmail.com
---
doc/src/sgml/regress.sgml | 11 ++
src/test/authentication/README | 12 ++
src/test/authentication/meson.build | 1 +
src/test/authentication/t/007_radius.pl | 185 ++++++++++++++++++++++++
4 files changed, 209 insertions(+)
create mode 100644 src/test/authentication/t/007_radius.pl
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index d1042e02228..b9dc28b8dea 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -284,6 +284,17 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>radius</literal></term>
+ <listitem>
+ <para>
+ Runs the test <filename>src/test/authentication/t/007_radius.pl</filename>.
+ This requires a <productname>FreeRADIUS</productname> installation and
+ opens UDP listen sockets.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>ssl</literal></term>
<listitem>
diff --git a/src/test/authentication/README b/src/test/authentication/README
index 602280a0713..13d7a7f4d84 100644
--- a/src/test/authentication/README
+++ b/src/test/authentication/README
@@ -26,3 +26,15 @@ Either way, this test initializes, starts, and stops a test Postgres
cluster.
See src/test/perl/README for more info about running these tests.
+
+Requirements
+============
+
+The RADIUS test is skipped unless "radius" is listed in PG_TEXT_EXTRA, because
+it requires a FreeRADIUS installation and opens extra ports. FreeRADIUS can be
+installed with:
+
+Debian: apt-get install freeradius
+Homebrew: brew install freeradius-server
+FreeBSD: pkg install freeradius3
+MacPorts: port install freeradius
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc13..59da3e64169 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -12,6 +12,7 @@ tests += {
't/004_file_inclusion.pl',
't/005_sspi.pl',
't/006_login_trigger.pl',
+ 't/007_radius.pl',
],
},
}
diff --git a/src/test/authentication/t/007_radius.pl b/src/test/authentication/t/007_radius.pl
new file mode 100644
index 00000000000..ebfdf7b36e3
--- /dev/null
+++ b/src/test/authentication/t/007_radius.pl
@@ -0,0 +1,185 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use File::Copy;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+my $radiusd_dir = "${PostgreSQL::Test::Utils::tmp_check}/radiusd_data";
+my $radiusd_conf = "radiusd.conf";
+my $radiusd_users = "users.txt";
+my $radiusd_prefix;
+my $radiusd;
+
+if ($ENV{PG_TEST_EXTRA} !~ /\bradius\b/)
+{
+ plan skip_all => 'radius not enabled in PG_TEST_EXTRA';
+}
+elsif ($^O eq 'freebsd')
+{
+ $radiusd = '/usr/local/sbin/radiusd';
+}
+elsif ($^O eq 'linux' && -f '/usr/sbin/freeradius')
+{
+ $radiusd = '/usr/sbin/freeradius';
+}
+elsif ($^O eq 'linux')
+{
+ $radiusd = '/usr/sbin/radiusd';
+}
+elsif ($^O eq 'darwin' && -d '/opt/local')
+{
+ # typical path for MacPorts
+ $radiusd = '/opt/local/sbin/radiusd';
+ $radiusd_prefix = '/opt/local';
+}
+elsif ($^O eq 'darwin' && -d '/opt/homebrew')
+{
+ # typical path for Homebrew on ARM
+ $radiusd = '/opt/homebrew/bin/radiusd';
+ $radiusd_prefix = '/opt/homebrew';
+}
+elsif ($^O eq 'darwin' && -d '/usr/local')
+{
+ # typical path for Homebrew on Intel
+ $radiusd = '/usr/local/bin/radiusd';
+ $radiusd_prefix = '/usr/local';
+}
+else
+{
+ plan skip_all =>
+ "radius tests not supported on $^O or dependencies not installed";
+}
+
+note "setting up radiusd";
+
+my $radius_port = PostgreSQL::Test::Cluster::get_free_port();
+
+mkdir $radiusd_dir or die "cannot create $radiusd_dir";
+
+append_to_file("$radiusd_dir/$radiusd_users",
+ qq{test2 Cleartext-Password := "password2"});
+
+my $conf = qq{
+client default {
+ ipaddr = "127.0.0.1"
+ secret = "shared-secret"
+}
+
+modules {
+ files {
+ filename = "$radiusd_dir/users.txt"
+ }
+ pap {
+ }
+}
+
+server default {
+ listen {
+ type = "auth"
+ ipv4addr = "127.0.0.1"
+ port = "$radius_port"
+ }
+ authenticate {
+ Auth-Type PAP {
+ pap
+ }
+ }
+ authorize {
+ files
+ pap
+ }
+}
+
+log {
+ destination = "files"
+ localstatedir = "$radiusd_dir"
+ logdir = "$radiusd_dir"
+ file = "$radiusd_dir/radius.log"
+}
+
+security {
+ require_message_authenticator = "yes"
+}
+
+pidfile = "$radiusd_dir/radiusd.pid"
+};
+
+# Note that require_message_authenticator defaulted to "no" before 3.2.5, and
+# then switched to "auto" (a new mode that fills the logs up with warning
+# messages about clients that don't send MA), and presumably a later version
+# will default to "yes".
+
+if ($radiusd_prefix)
+{
+ $conf .= "prefix=\"$radiusd_prefix\"\n";
+}
+
+append_to_file("$radiusd_dir/$radiusd_conf", $conf);
+
+system_or_bail $radiusd, '-xx', '-d', $radiusd_dir;
+
+END
+{
+ kill 'INT', `cat $radiusd_dir/radiusd.pid`
+ if -f "$radiusd_dir/radiusd.pid";
+}
+
+note "setting up PostgreSQL instance";
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER test1;');
+$node->safe_psql('postgres', 'CREATE USER test2;');
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+ 'pg_hba.conf',
+ qq{
+local all test2 radius radiusservers="127.0.0.1" radiussecrets="shared-secret" radiusports="$radius_port"
+}
+);
+$node->restart;
+
+note "running tests";
+
+sub test_access
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my ($node, $role, $expected_res, $test_name, %params) = @_;
+ my $connstr = "user=$role";
+
+ if ($expected_res eq 0)
+ {
+ $node->connect_ok($connstr, $test_name, %params);
+ }
+ else
+ {
+ # No checks of the error message, only the status code.
+ $node->connect_fails($connstr, $test_name, %params);
+ }
+}
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access(
+ $node, 'test1', 2,
+ 'authentication fails if user not found in RADIUS',
+ log_unlike => [qr/connection authenticated:/]);
+test_access(
+ $node, 'test2', 2,
+ 'authentication fails with wrong password',
+ log_unlike => [qr/connection authenticated:/]);
+
+$ENV{"PGPASSWORD"} = 'password2';
+test_access($node, 'test2', 0, 'authentication succeeds with right password',
+ log_like =>
+ [qr/connection authenticated: identity="test2" method=radius/],);
+
+done_testing();
--
2.39.3 (Apple Git-146)
v1-0002-ci-Enable-RADIUS-tests.patchapplication/octet-stream; name=v1-0002-ci-Enable-RADIUS-tests.patchDownload
From adb0e21e626af3b2cb288c488f4990e1ec56a6ff Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sat, 2 Sep 2023 10:09:47 +1200
Subject: [PATCH v1 2/5] ci: Enable RADIUS tests.
Unix targets only, because FreeRADIUS doesn't exist on Windows.
XXX Not sure it's worth running this stuff on CI. This is just for
demonstration.
---
.cirrus.tasks.yml | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.yml
index 1ce6c443a8c..b9c37440777 100644
--- a/.cirrus.tasks.yml
+++ b/.cirrus.tasks.yml
@@ -20,7 +20,7 @@ env:
MTEST_ARGS: --print-errorlogs --no-rebuild -C build
PGCTLTIMEOUT: 120 # avoids spurious failures during parallel tests
TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf
- PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance
+ PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance radius
# What files to preserve in case tests fail
@@ -164,7 +164,7 @@ task:
chown root:postgres /tmp/cores
sysctl kern.corefile='/tmp/cores/%N.%P.core'
setup_additional_packages_script: |
- #pkg install -y ...
+ pkg install -y freeradius3
# NB: Intentionally build without -Dllvm. The freebsd image size is already
# large enough to make VM startup slow, and even without llvm freebsd
@@ -313,8 +313,8 @@ task:
EOF
setup_additional_packages_script: |
- #apt-get update
- #DEBIAN_FRONTEND=noninteractive apt-get -y install ...
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get -y install freeradius
matrix:
- name: Linux - Debian Bookworm - Autoconf
@@ -473,6 +473,7 @@ task:
setup_additional_packages_script: |
sh src/tools/ci/ci_macports_packages.sh \
ccache \
+ freeradius \
icu \
kerberos5 \
lz4 \
--
2.39.3 (Apple Git-146)
v1-0003-Mitigation-for-BlastRADIUS.patchapplication/octet-stream; name=v1-0003-Mitigation-for-BlastRADIUS.patchDownload
From dbf9bab2f93607987561c6f696020a9815e8c914 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sun, 4 Aug 2024 15:58:33 +1200
Subject: [PATCH v1 3/5] Mitigation for BlastRADIUS.
Add Message-Authenticator attribute (an HMAC-MD5 signature) to outgoing
Access-Request messages, and optionally require and verify them on
incoming Access-Accept and Access-Reject messages, as recommended to
mitigate CVE-2024-3596 and VU#456537 by:
https://www.blastradius.fail/
No RADIUS server should be upset by the addition of our new
Message-Authenticator attribute in requests, so we always add that.
On the other hand, we can't require the attribute to be present in
responses yet because we can't assume that all sites are already doing
that. For example, FreeRADIUS started sending them in 3.2.5, which
isn't yet in all distributions at the time of writing. Therefore, users
have to opt in to this requirement with radiusrequirema=1 in pg_hba.conf
for now; the default could change in future.
---
doc/src/sgml/client-auth.sgml | 17 +++
src/backend/libpq/auth.c | 208 +++++++++++++++++++++++++++++++++-
src/backend/libpq/hba.c | 8 ++
src/include/libpq/hba.h | 1 +
4 files changed, 229 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 51343de7cad..6c49ba18346 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -2161,6 +2161,23 @@ host ... ldap ldapbasedn="dc=example,dc=net"
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>radiusrequirema</literal></term>
+ <listitem>
+ <para>
+ Whether to require a valid <literal>Message-Authenticator</literal>
+ attribute in messages received from RADIUS servers, and ignore messages
+ that don't contain it. The default value
+ is <literal>0</literal>, but it can be set to <literal>1</literal>
+ to enable that requirement.
+ This setting does not affect requests sent by
+ <productname>PostgreSQL</productname> to the RADIUS server, which
+ always include a <literal>Message-Authenticator</literal> attribute
+ (but didn't in earlier releases).
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2b607c52704..40f0cabf3a9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -24,6 +24,7 @@
#include <unistd.h>
#include "commands/user.h"
+#include "common/hmac.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
@@ -198,7 +199,7 @@ static int pg_SSPI_make_upn(char *accountname,
*----------------------------------------------------------------
*/
static int CheckRADIUSAuth(Port *port);
-static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd);
+static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd, bool requirema);
/*
@@ -2802,6 +2803,7 @@ typedef struct
#define RADIUS_PASSWORD 2
#define RADIUS_SERVICE_TYPE 6
#define RADIUS_NAS_IDENTIFIER 32
+#define RADIUS_MESSAGE_AUTHENTICATOR 80
/* RADIUS service types */
#define RADIUS_AUTHENTICATE_ONLY 8
@@ -2809,7 +2811,7 @@ typedef struct
/* Seconds to wait - XXX: should be in a config variable! */
#define RADIUS_TIMEOUT 3
-static void
+static uint8 *
radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len)
{
radius_attribute *attr;
@@ -2825,7 +2827,7 @@ radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *dat
elog(WARNING,
"adding attribute code %d with length %d to radius packet would create oversize packet, ignoring",
type, len);
- return;
+ return NULL;
}
attr = (radius_attribute *) ((unsigned char *) packet + packet->length);
@@ -2833,6 +2835,59 @@ radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *dat
attr->length = len + 2; /* total size includes type and length */
memcpy(attr->data, data, len);
packet->length += attr->length;
+
+ /*
+ * Return pointer into message, used to fill in Message-Authenticator
+ * contents later.
+ */
+ return attr->data;
+}
+
+/*
+ * Search for an attribute in a received message. If the attribute is found,
+ * sets *value and *length (not including the header), and returns true. If
+ * the attribute is not found but the message appears to be well-formed, sets
+ * *value to NULL and returns true. If the message is found to be mal-formed,
+ * returns false.
+ */
+static bool
+radius_find_attribute(uint8 *packet,
+ size_t packet_size,
+ uint8 type,
+ uint8 **value,
+ uint8 *length)
+{
+ uint8 *end = packet + packet_size;
+ radius_attribute *attr;
+
+ attr = (radius_attribute *) (packet + RADIUS_HEADER_LENGTH);
+
+ while ((uint8 *) attr < end)
+ {
+ /* Would this attribute overflow the buffer? */
+ if (&attr->length >= end || (uint8 *) attr + attr->length > end)
+ return false;
+
+ /* Would this attribute's length be shorter than its own header? */
+ if (attr->length < offsetof(radius_attribute, data))
+ return false;
+
+ /* Is this it? */
+ if (attr->attribute == type)
+ {
+ *value = attr->data;
+ *length = attr->length - offsetof(radius_attribute, data);
+ return true;
+ }
+
+ /* Nope, skip to the next one. */
+ attr = (radius_attribute *) ((uint8 *) attr + attr->length);
+ }
+
+ /* Well-formed, but not found. */
+ *value = NULL;
+ *length = 0;
+ return true;
}
static int
@@ -2890,7 +2945,8 @@ CheckRADIUSAuth(Port *port)
radiusports ? lfirst(radiusports) : NULL,
identifiers ? lfirst(identifiers) : NULL,
port->user_name,
- passwd);
+ passwd,
+ port->hba->radiusrequirema);
/*------
* STATUS_OK = Login OK
@@ -2931,8 +2987,13 @@ CheckRADIUSAuth(Port *port)
}
static int
-PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd)
+PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd, bool requirema)
{
+ pg_hmac_ctx *hmac_context;
+ uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH];
+ uint8 message_authenticator[RADIUS_VECTOR_LENGTH];
+ uint8 *message_authenticator_location;
+ uint8 message_authenticator_size;
radius_packet radius_send_pack;
radius_packet radius_recv_pack;
radius_packet *packet = &radius_send_pack;
@@ -2993,6 +3054,18 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
return STATUS_ERROR;
}
packet->id = packet->vector[0];
+
+ /*
+ * Add Message-Authenticator attribute first, which for now holds zeroes.
+ * We remember where it is in the message so that we can fill it in later.
+ */
+ memset(message_authenticator, 0, lengthof(message_authenticator));
+ message_authenticator_location =
+ radius_add_attribute(packet,
+ RADIUS_MESSAGE_AUTHENTICATOR,
+ message_authenticator,
+ lengthof(message_authenticator));
+
radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (const unsigned char *) &service, sizeof(service));
radius_add_attribute(packet, RADIUS_USER_NAME, (const unsigned char *) user_name, strlen(user_name));
radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (const unsigned char *) identifier, strlen(identifier));
@@ -3048,6 +3121,44 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
packetlength = packet->length;
packet->length = pg_hton16(packet->length);
+ /*
+ * We use the first 16 bytes of the shared secret, zero-padded if too
+ * short, as an HMAC-MD5 key for creating and validating
+ * Message-Authenticator attributes.
+ */
+ memset(message_authenticator_key, 0, lengthof(message_authenticator_key));
+ memcpy(message_authenticator_key,
+ secret,
+ Min(lengthof(message_authenticator_key), strlen(secret)));
+
+ /*
+ * Compute the Message-Authenticator for the whole message. The
+ * Message-Authenticator itself is one of the attributes, but it holds
+ * zeroes at this point.
+ */
+ hmac_context = pg_hmac_create(PG_MD5);
+ if (hmac_context == NULL ||
+ pg_hmac_init(hmac_context,
+ message_authenticator_key,
+ lengthof(message_authenticator_key)) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) radius_buffer, packetlength) < 0 ||
+ pg_hmac_final(hmac_context,
+ message_authenticator,
+ lengthof(message_authenticator)) < 0)
+ {
+ ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s",
+ pg_hmac_error(hmac_context))));
+ pg_hmac_free(hmac_context);
+ pg_freeaddrinfo_all(hint.ai_family, serveraddrs);
+ return STATUS_ERROR;
+ }
+ pg_hmac_free(hmac_context);
+
+ /* Overwrite the attribute with the computed signature. */
+ memcpy(message_authenticator_location, message_authenticator,
+ lengthof(message_authenticator));
+
sock = socket(serveraddrs[0].ai_family, SOCK_DGRAM, 0);
if (sock == PGINVALID_SOCKET)
{
@@ -3232,6 +3343,93 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
+ /* Search for the Message-Authenticator attribute. */
+ if (!radius_find_attribute((uint8 *) receive_buffer,
+ packetlength,
+ RADIUS_MESSAGE_AUTHENTICATOR,
+ &message_authenticator_location,
+ &message_authenticator_size))
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has malformed attributes",
+ server)));
+ continue;
+ }
+ else if (message_authenticator_location == NULL)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has no Message-Authenticator",
+ server)));
+
+ /* We'll ignore this message, unless pg_hba.conf told us not to. */
+ if (requirema)
+ continue;
+ }
+ else if (message_authenticator_size != RADIUS_VECTOR_LENGTH)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has unexpected Message-Authenticator size",
+ server)));
+ continue;
+ }
+ else
+ {
+ uint8 message_authenticator_copy[RADIUS_VECTOR_LENGTH];
+
+ /*
+ * Save a copy of the received HMAC, and zero out the one in the
+ * message so that we have input required to recompute it.
+ */
+ memcpy(message_authenticator_copy,
+ message_authenticator_location,
+ RADIUS_VECTOR_LENGTH);
+ memset(message_authenticator_location,
+ 0,
+ RADIUS_VECTOR_LENGTH);
+
+ /*
+ * Compute the expected value. Note that the HMAC for
+ * Access-Accept and Access-Reject message uses the authenticator
+ * from the original Access-Request message, so we have to do a
+ * bit of splicing.
+ */
+ hmac_context = pg_hmac_create(PG_MD5);
+ if (hmac_context == NULL ||
+ pg_hmac_init(hmac_context,
+ message_authenticator_key,
+ lengthof(message_authenticator_key)) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) receive_buffer,
+ offsetof(radius_packet, vector)) < 0 ||
+ pg_hmac_update(hmac_context,
+ packet->vector,
+ RADIUS_VECTOR_LENGTH) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) receive_buffer + RADIUS_HEADER_LENGTH,
+ packetlength - RADIUS_HEADER_LENGTH) < 0 ||
+ pg_hmac_final(hmac_context,
+ message_authenticator,
+ lengthof(message_authenticator)) < 0)
+ {
+ ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s",
+ pg_hmac_error(hmac_context))));
+ pg_hmac_free(hmac_context);
+ closesocket(sock);
+ return STATUS_ERROR;
+ }
+ pg_hmac_free(hmac_context);
+
+ /* Verify. */
+ if (memcmp(message_authenticator_copy,
+ message_authenticator,
+ RADIUS_VECTOR_LENGTH) != 0)
+ {
+ ereport(LOG, (errmsg("RADIUS response from %s has invalid Message-Authenticator",
+ server)));
+ continue;
+ }
+ }
+
if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
{
closesocket(sock);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 75d588e36a1..7ff8e1ca6e7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2446,6 +2446,14 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
hbaline->radiusidentifiers = parsed_identifiers;
hbaline->radiusidentifiers_s = pstrdup(val);
}
+ else if (strcmp(name, "radiusrequirema") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaRADIUS, "radiusrequirema", "radius");
+ if (strcmp(val, "1") == 0)
+ hbaline->radiusrequirema = true;
+ else
+ hbaline->radiusrequirema = false;
+ }
else
{
ereport(elevel,
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8ea837ae82a..139e4bd410a 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -135,6 +135,7 @@ typedef struct HbaLine
char *radiusidentifiers_s;
List *radiusports;
char *radiusports_s;
+ bool radiusrequirema;
} HbaLine;
typedef struct IdentLine
--
2.39.3 (Apple Git-146)
v1-0004-Teach-007_radius-test-about-Message-Authenticator.patchapplication/octet-stream; name=v1-0004-Teach-007_radius-test-about-Message-Authenticator.patchDownload
From ff7830b3cca4653d7b1ffa2becf70b5909d96cf9 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Mon, 5 Aug 2024 23:06:16 +1200
Subject: [PATCH v1 4/5] Teach 007_radius test about Message-Authenticator.
From FreeRADIUS version 3.2.5, Access-Accept/Reject messages always have
a Message-Authenticator. We can therefore test our new
radiusrequirema=1 setting, if we detect that version or higher.
---
src/test/authentication/t/007_radius.pl | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/test/authentication/t/007_radius.pl b/src/test/authentication/t/007_radius.pl
index ebfdf7b36e3..d0994264c97 100644
--- a/src/test/authentication/t/007_radius.pl
+++ b/src/test/authentication/t/007_radius.pl
@@ -54,6 +54,29 @@ else
"radius tests not supported on $^O or dependencies not installed";
}
+note "inspecting FreeRADIUS version";
+
+# FreeRADIUS began sending Message-Authenticator in responses in version 3.2.5.
+# This allows us to test our feature for requiring it to be present.
+my $stdout;
+IPC::Run::run [ $radiusd, "-v" ], ">", \$stdout
+ or die "can't query FreeRADIUS version";
+print $stdout;
+my $radiusrequirema = 0;
+if ($stdout =~ /^FreeRADIUS Version ([0-9]+)\.([0-9]+)\.([0-9]+)/m)
+{
+ my ($major, $minor, $patch) = ($1, $2, $3);
+ if ( ($major > 3)
+ || ($major == 3 && $minor > 2)
+ || ($major == 3 && $minor == 2 && $patch >= 5))
+ {
+ $radiusrequirema = 1;
+ }
+}
+
+note
+ "setting radiusrequirema=$radiusrequirema based on detected version of FreeRADIUS";
+
note "setting up radiusd";
my $radius_port = PostgreSQL::Test::Cluster::get_free_port();
@@ -142,7 +165,7 @@ unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf(
'pg_hba.conf',
qq{
-local all test2 radius radiusservers="127.0.0.1" radiussecrets="shared-secret" radiusports="$radius_port"
+local all test2 radius radiusservers="127.0.0.1" radiussecrets="shared-secret" radiusports="$radius_port" radiusrequirema=$radiusrequirema
}
);
$node->restart;
--
2.39.3 (Apple Git-146)
v1-0005-XXX-BlastRADIUS-back-patch-kludge-for-12-and-13.patchapplication/octet-stream; name=v1-0005-XXX-BlastRADIUS-back-patch-kludge-for-12-and-13.patchDownload
From 849d9cb45286cb9f03111c6b503d7361068be21c Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Mon, 5 Aug 2024 17:09:57 +1200
Subject: [PATCH v1 5/5] XXX BlastRADIUS back-patch kludge for 12 and 13
---
doc/src/sgml/client-auth.sgml | 11 +++++++++
src/backend/libpq/auth.c | 42 ++++++++++++++++++++++++++++++++++-
2 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 6c49ba18346..70a55db3ec1 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -2175,6 +2175,17 @@ host ... ldap ldapbasedn="dc=example,dc=net"
always include a <literal>Message-Authenticator</literal> attribute
(but didn't in earlier releases).
</para>
+ <note>
+ <para>
+ <literal>Message-Authenticator</literal> attributes can only be
+ computed if <productname>PostgreSQL</productname> was built support for
+ <productname>OpenSSL</productname>. Otherwise, they
+ will be silently omitted from outgoing messages, and ignored on
+ incoming messages regardless of this setting, which may cause some
+ <productname>RADIUS</productname> servers to reject or ignore
+ requests.
+ </para>
+ </note>
</listitem>
</varlistentry>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 40f0cabf3a9..8a5876723e0 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -24,7 +24,6 @@
#include <unistd.h>
#include "commands/user.h"
-#include "common/hmac.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
@@ -40,6 +39,10 @@
#include "storage/ipc.h"
#include "utils/memutils.h"
+#ifdef USE_OPENSSL
+#include "openssl/hmac.h"
+#endif
+
/*----------------------------------------------------------------
* Global authentication functions
*----------------------------------------------------------------
@@ -2811,6 +2814,31 @@ typedef struct
/* Seconds to wait - XXX: should be in a config variable! */
#define RADIUS_TIMEOUT 3
+#ifdef USE_OPENSSL
+/*
+ * Locally redirect PostgreSQL 14's pg_hmac_XXX API directly to OpenSSL
+ * functions, to simplify back-patching of BlastRADIUS mitigation to
+ * PostgreSQL 12 and 13. If built without OpenSSL, skip HMAC support and
+ * disable Message-Authenticator attributes.
+ */
+#define RADIUS_USE_HMAC
+#define pg_hmac_ctx HMAC_CTX
+#define pg_hmac_create(...) HMAC_CTX_new()
+#define pg_hmac_init(ctx, key, key_len, ...) HMAC_Init_ex(ctx, key, key_len, EVP_md5(), NULL)
+#define pg_hmac_update HMAC_Update
+static inline int
+pg_hmac_final(pg_hmac_ctx *ctx, void *md, size_t len)
+{
+ unsigned int l = len;
+ int result = HMAC_Final(ctx, md, &l);
+
+ Assert(l == len);
+ return result;
+}
+#define pg_hmac_free(x) if (x) HMAC_CTX_free(x)
+#define pg_hmac_error(x) "HMAC error"
+#endif
+
static uint8 *
radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len)
{
@@ -2843,6 +2871,7 @@ radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *dat
return attr->data;
}
+#ifdef RADIUS_USE_HMAC
/*
* Search for an attribute in a received message. If the attribute is found,
* sets *value and *length (not including the header), and returns true. If
@@ -2889,6 +2918,7 @@ radius_find_attribute(uint8 *packet,
*length = 0;
return true;
}
+#endif
static int
CheckRADIUSAuth(Port *port)
@@ -2989,11 +3019,13 @@ CheckRADIUSAuth(Port *port)
static int
PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd, bool requirema)
{
+#ifdef RADIUS_USE_HMAC
pg_hmac_ctx *hmac_context;
uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH];
uint8 message_authenticator[RADIUS_VECTOR_LENGTH];
uint8 *message_authenticator_location;
uint8 message_authenticator_size;
+#endif
radius_packet radius_send_pack;
radius_packet radius_recv_pack;
radius_packet *packet = &radius_send_pack;
@@ -3055,6 +3087,8 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
}
packet->id = packet->vector[0];
+#ifdef RADIUS_USE_HMAC
+
/*
* Add Message-Authenticator attribute first, which for now holds zeroes.
* We remember where it is in the message so that we can fill it in later.
@@ -3065,6 +3099,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
RADIUS_MESSAGE_AUTHENTICATOR,
message_authenticator,
lengthof(message_authenticator));
+#endif
radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (const unsigned char *) &service, sizeof(service));
radius_add_attribute(packet, RADIUS_USER_NAME, (const unsigned char *) user_name, strlen(user_name));
@@ -3121,6 +3156,8 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
packetlength = packet->length;
packet->length = pg_hton16(packet->length);
+#ifdef RADIUS_USE_HMAC
+
/*
* We use the first 16 bytes of the shared secret, zero-padded if too
* short, as an HMAC-MD5 key for creating and validating
@@ -3158,6 +3195,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
/* Overwrite the attribute with the computed signature. */
memcpy(message_authenticator_location, message_authenticator,
lengthof(message_authenticator));
+#endif
sock = socket(serveraddrs[0].ai_family, SOCK_DGRAM, 0);
if (sock == PGINVALID_SOCKET)
@@ -3343,6 +3381,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
+#ifdef RADIUS_USE_HMAC
/* Search for the Message-Authenticator attribute. */
if (!radius_find_attribute((uint8 *) receive_buffer,
packetlength,
@@ -3429,6 +3468,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
}
+#endif
if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
{
--
2.39.3 (Apple Git-146)
On 05/08/2024 15:43, Thomas Munro wrote:
The response requirement can be enabled by radiusrequirema=1 in
pg_hba.conf. For example, Debian stable is currently shipping
FreeRADIUS 3.2.1 which doesn't yet send the MA in its responses, but
FreeBSD and Debian "testing" have started shipping FreeRADIUS 3.2.5
which is how I noticed all this. So it doesn't seem quite right to
require it by default, yet?
Agreed.
Since PostgreSQL v12 and v13 don't have the modern "common/hmac.h"
API, I came up with a cheap kludge: locally #define those interfaces
to point directly to the OpenSSL HMAC API, or just give up and drop
Message-Authenticator support if you didn't build with OpenSSL support
(in practice everyone does). Better ideas?
Seems reasonable. It probably wouldn't be hard to backport common/hmac.h
either, perhaps in a limited fashion with just md5 support.
Review on v1-0001-Add-simple-test-for-RADIUS-authentication.patch:
+if ($ENV{PG_TEST_EXTRA} !~ /\bradius\b/) +{ + plan skip_all => 'radius not enabled in PG_TEST_EXTRA'; +} +elsif ($^O eq 'freebsd') +{ + $radiusd = '/usr/local/sbin/radiusd'; +} +elsif ($^O eq 'linux' && -f '/usr/sbin/freeradius') +{ + $radiusd = '/usr/sbin/freeradius'; +} +elsif ($^O eq 'linux') +{ + $radiusd = '/usr/sbin/radiusd'; +} +elsif ($^O eq 'darwin' && -d '/opt/local') +{ + # typical path for MacPorts + $radiusd = '/opt/local/sbin/radiusd'; + $radiusd_prefix = '/opt/local'; +} +elsif ($^O eq 'darwin' && -d '/opt/homebrew') +{ + # typical path for Homebrew on ARM + $radiusd = '/opt/homebrew/bin/radiusd'; + $radiusd_prefix = '/opt/homebrew'; +} +elsif ($^O eq 'darwin' && -d '/usr/local') +{ + # typical path for Homebrew on Intel + $radiusd = '/usr/local/bin/radiusd'; + $radiusd_prefix = '/usr/local'; +} +else +{ + plan skip_all => + "radius tests not supported on $^O or dependencies not installed"; +}
Seems that on linux or freebsd, you'd plow ahead even if the binary is
not found, and fail later, while on macOS you'd skip the tests. I think
we should always error out if the dependencies are not found. If you
make an effort to add PG_TEST_EXTRA=radius, presumably you really do
want to run the tests, and if dependencies are missing you'd like to
know about it.
+ secret = "shared-secret"
Let's use a random value for this, and for the Cleartext-Password. This
only runs locally, and only if you explicitly add it to PG_TEST_EXTRA,
but it still seems nice to protect from other users on the system when
we can do so easily.
+security { + require_message_authenticator = "yes" +}
+# Note that require_message_authenticator defaulted to "no" before 3.2.5, and +# then switched to "auto" (a new mode that fills the logs up with warning +# messages about clients that don't send MA), and presumably a later version +# will default to "yes".
That's not quite accurate: the option didn't exist before version 3.2.5.
What happens if you set it on an older server version? /me tests: seems
that FreeRadius 3.2.1 silently accepts the setting, or any other setting
it doesn't recognize, and will do nothing with it. A little surprising,
but ok. I didn't find any mention in the docs on that.
(Also, that will make the test fail, until the
v1-0003-Mitigation-for-BlastRADIUS.patch is also applied. You could
leave that out of the test in this first patch, and add it
v1-0003-Mitigation-for-BlastRADIUS.patch)
Review on v1-0003-Mitigation-for-BlastRADIUS.patch:
+ <varlistentry> + <term><literal>radiusrequirema</literal></term> + <listitem> + <para> + Whether to require a valid <literal>Message-Authenticator</literal> + attribute in messages received from RADIUS servers, and ignore messages + that don't contain it. The default value + is <literal>0</literal>, but it can be set to <literal>1</literal> + to enable that requirement. + This setting does not affect requests sent by + <productname>PostgreSQL</productname> to the RADIUS server, which + always include a <literal>Message-Authenticator</literal> attribute + (but didn't in earlier releases). + </para> + </listitem> + </varlistentry>
I think this should include some advise on why and when you should set
it. Something like:
Enabling this mitigates the RADIUS protocol vulnerability described at
blastradius.fail. It is recommended to always set this to 1, unless you
are running an older RADIUS server version that does include the
mitigation to include Message-Authenticator in all replies. The default
will be changed to 1 in a future PostgreSQL version.
attr = (radius_attribute *) (packet + RADIUS_HEADER_LENGTH);
while ((uint8 *) attr < end)
{
/* Would this attribute overflow the buffer? */
if (&attr->length >= end || (uint8 *) attr + attr->length > end)
return false;
Is this kind of pointer arithmetic safe? In theory, if a packet is
malformed so that attr->length points beyond the end of the packet,
"(uint8 *) attr + attr-length" might overflow. That would require the
packet buffer to be allocated just before the end of the address space,
so probably cannot happen in practice. I don't remember if there are
some guarantees on that in C99 or other standards. Still, perhaps it
would be better to write this differently, e.g. using a separate "size_t
position" variable to track the current position in the buffer.
(This also relies on the fact that "struct radius_attribute" doesn't
require alignment, which is valid, and radius_add_attribute() made that
assumption already. Maybe worth a comment though while we're at it; it
certainly raised my eyebrow while reading this)
What if the message contains multiple attribute of the same type? If
there's a duplicate Message-Authenticator, we should surely reject the
packet. I don't know if duplicate attributes are legal in general.
+ /* + * Add Message-Authenticator attribute first, which for now holds zeroes. + * We remember where it is in the message so that we can fill it in later. + */
Let's mention Blast-RADIUS here as the reason to put this first. Reading
the paper though, I think it's only important in the server->client
messages, but I'm not sure, and shouldn't hurt anyway.
+ else if (message_authenticator_location == NULL) + { + ereport(LOG, + (errmsg("RADIUS response from %s has no Message-Authenticator", + server))); + + /* We'll ignore this message, unless pg_hba.conf told us not to. */ + if (requirema) + continue; + }
This is going to be very noisy if you are running an older server.
+ uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH]; + uint8 message_authenticator[RADIUS_VECTOR_LENGTH];
Perhaps use MD5_DIGEST_LENGTH for these. The Message-Authenticator is an
HMAC-MD5, which indeed has the same length as the MD5 hash used on the
password, so it's just pro forma, but it seems a little coincidental.
There's no fundamental reason they would have to be the same length, if
the RFC author's had chosen to use a different hash algorithm for
Message-Authenticator, for example.
+ /* + * We use the first 16 bytes of the shared secret, zero-padded if too + * short, as an HMAC-MD5 key for creating and validating + * Message-Authenticator attributes. + */ + memset(message_authenticator_key, 0, lengthof(message_authenticator_key)); + memcpy(message_authenticator_key, + secret, + Min(lengthof(message_authenticator_key), strlen(secret)));
If the secret is longer than 16 bytes, this truncates it. Is that
correct? According to https://en.wikipedia.org/wiki/HMAC, you're
supposed derive the suitably-sized key by calling the hash function on
the longer key in that case.
+ else if (strcmp(name, "radiusrequirema") == 0) + { + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusrequirema", "radius"); + if (strcmp(val, "1") == 0) + hbaline->radiusrequirema = true; + else + hbaline->radiusrequirema = false; + }
I was going to suggest throwing an error on any other val than "1" or
"0", but I see that we're doing the same in many other boolean options,
like ldaptls. So I guess that's fine, but would be nice to tighten that
up for all the options as a separate patch.
# v1-0004-Teach-007_radius-test-about-Message-Authenticator.patch
Looks good to me, although I'm not sure it's worthwhile to do this.
We're not reaching the codepath where we'd reject a message because of a
missing Message-Authenticator anyway. If the radiusrequirema option was
broken and had no effect, for example, the test would still pass.
# v1-0005-XXX-BlastRADIUS-back-patch-kludge-for-12-and-13.patch
Perhaps throw an error if you set "radiusrequirema=1", but the server is
compiled without OpenSSL.
--
Heikki Linnakangas
Neon (https://neon.tech)
Heikki Linnakangas <hlinnaka@iki.fi> writes:
On 05/08/2024 15:43, Thomas Munro wrote:
The response requirement can be enabled by radiusrequirema=1 in
pg_hba.conf. For example, Debian stable is currently shipping
FreeRADIUS 3.2.1 which doesn't yet send the MA in its responses, but
FreeBSD and Debian "testing" have started shipping FreeRADIUS 3.2.5
which is how I noticed all this. So it doesn't seem quite right to
require it by default, yet?
Agreed.
We should think about that not in terms of the situation today,
but the situation when we ship this fix, possibly as much as
three months from now. (There was some mention in the security-list
discussion of maybe making an off-cycle release to get this out
sooner; but nothing was decided, and I doubt we'll do that unless
we start getting user complaints.) It seems likely to me that
most up-to-date systems will have BlastRADIUS mitigation in place
by then, so maybe we should lean towards secure-by-default.
We don't necessarily have to make that decision today, either.
We could start with not-secure-by-default but reconsider
whenever the release is imminent.
regards, tom lane
On Tue, Aug 6, 2024 at 2:41 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
Seems that on linux or freebsd, you'd plow ahead even if the binary is
not found, and fail later, while on macOS you'd skip the tests. I think
we should always error out if the dependencies are not found. If you
make an effort to add PG_TEST_EXTRA=radius, presumably you really do
want to run the tests, and if dependencies are missing you'd like to
know about it.
Fixed.
+ secret = "shared-secret"
Let's use a random value for this, and for the Cleartext-Password. This
only runs locally, and only if you explicitly add it to PG_TEST_EXTRA,
but it still seems nice to protect from other users on the system when
we can do so easily.
OK, done.
+security { + require_message_authenticator = "yes" +}+# Note that require_message_authenticator defaulted to "no" before 3.2.5, and +# then switched to "auto" (a new mode that fills the logs up with warning +# messages about clients that don't send MA), and presumably a later version +# will default to "yes".That's not quite accurate: the option didn't exist before version 3.2.5.
What happens if you set it on an older server version? /me tests: seems
that FreeRadius 3.2.1 silently accepts the setting, or any other setting
it doesn't recognize, and will do nothing with it. A little surprising,
but ok. I didn't find any mention in the docs on that.
Huh. Thanks, I was confused by that. Fixed.
(Also, that will make the test fail, until the
v1-0003-Mitigation-for-BlastRADIUS.patch is also applied. You could
leave that out of the test in this first patch, and add it
v1-0003-Mitigation-for-BlastRADIUS.patch)
Yeah, done.
Review on v1-0003-Mitigation-for-BlastRADIUS.patch:
+ <varlistentry> + <term><literal>radiusrequirema</literal></term> + <listitem> + <para> + Whether to require a valid <literal>Message-Authenticator</literal> + attribute in messages received from RADIUS servers, and ignore messages + that don't contain it. The default value + is <literal>0</literal>, but it can be set to <literal>1</literal> + to enable that requirement. + This setting does not affect requests sent by + <productname>PostgreSQL</productname> to the RADIUS server, which + always include a <literal>Message-Authenticator</literal> attribute + (but didn't in earlier releases). + </para> + </listitem> + </varlistentry>I think this should include some advise on why and when you should set
it. Something like:Enabling this mitigates the RADIUS protocol vulnerability described at
blastradius.fail. It is recommended to always set this to 1, unless you
are running an older RADIUS server version that does include the
mitigation to include Message-Authenticator in all replies. The default
will be changed to 1 in a future PostgreSQL version.
Done, with small tweaks.
attr = (radius_attribute *) (packet + RADIUS_HEADER_LENGTH);
while ((uint8 *) attr < end)
{
/* Would this attribute overflow the buffer? */
if (&attr->length >= end || (uint8 *) attr + attr->length > end)
return false;Is this kind of pointer arithmetic safe? In theory, if a packet is
malformed so that attr->length points beyond the end of the packet,
"(uint8 *) attr + attr-length" might overflow. That would require the
packet buffer to be allocated just before the end of the address space,
so probably cannot happen in practice. I don't remember if there are
some guarantees on that in C99 or other standards. Still, perhaps it
would be better to write this differently, e.g. using a separate "size_t
position" variable to track the current position in the buffer.
Done.
(This also relies on the fact that "struct radius_attribute" doesn't
require alignment, which is valid, and radius_add_attribute() made that
assumption already. Maybe worth a comment though while we're at it; it
certainly raised my eyebrow while reading this)
Comment added.
What if the message contains multiple attribute of the same type? If
there's a duplicate Message-Authenticator, we should surely reject the
packet. I don't know if duplicate attributes are legal in general.
Duplicate attributes are legal in general per RFC 2865, which has a
table of attributes and their possible quantity; unfortunately this
one is an extension from RFC 2869, and I didn't find where it pins
that down. I suppose we could try to detect an unexpected duplicate,
which might have the side benefit of checking the rest of the
attributes for well-formedness (though in practice there aren't any).
Is it worth bothering with that?
I suppose if we wanted to be extra fastidious, we could also test with
a gallery of malformed packets crafted by a Perl script, but that
feels like overkill. On the other hand it would be bad if you could
crash a server by lobbing UDP packets at it because of some dumb
thinko.
+ /* + * Add Message-Authenticator attribute first, which for now holds zeroes. + * We remember where it is in the message so that we can fill it in later. + */Let's mention Blast-RADIUS here as the reason to put this first. Reading
the paper though, I think it's only important in the server->client
messages, but I'm not sure, and shouldn't hurt anyway.
Done.
+ else if (message_authenticator_location == NULL) + { + ereport(LOG, + (errmsg("RADIUS response from %s has no Message-Authenticator", + server))); + + /* We'll ignore this message, unless pg_hba.conf told us not to. */ + if (requirema) + continue; + }This is going to be very noisy if you are running an older server.
Silenced.
+ uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH]; + uint8 message_authenticator[RADIUS_VECTOR_LENGTH];Perhaps use MD5_DIGEST_LENGTH for these. The Message-Authenticator is an
HMAC-MD5, which indeed has the same length as the MD5 hash used on the
password, so it's just pro forma, but it seems a little coincidental.
There's no fundamental reason they would have to be the same length, if
the RFC author's had chosen to use a different hash algorithm for
Message-Authenticator, for example.
The first one is now gone (see next).
Now I have message_authenticator[MD5_DIGEST_LENGTH], and then the
other places that need that number use
lengthof(message_authenticator).
If the secret is longer than 16 bytes, this truncates it. Is that
correct? According to https://en.wikipedia.org/wiki/HMAC, you're
supposed derive the suitably-sized key by calling the hash function on
the longer key in that case.
Oh, actually I don't think we need that step at all: the HMAC init
function takes a variable length key and does the required
padding/hashing itself.
# v1-0004-Teach-007_radius-test-about-Message-Authenticator.patch
Looks good to me, although I'm not sure it's worthwhile to do this.
We're not reaching the codepath where we'd reject a message because of a
missing Message-Authenticator anyway. If the radiusrequirema option was
broken and had no effect, for example, the test would still pass.
Dropped.
# v1-0005-XXX-BlastRADIUS-back-patch-kludge-for-12-and-13.patch
Perhaps throw an error if you set "radiusrequirema=1", but the server is
compiled without OpenSSL.
Done.
I don't think I would turn this on in the build farm, because of the 3
second timeout which might cause noise. Elsewhere I had a patch to
make the timeout configurable, so it could be set long for positive
tests and short for negative tests, so we could maybe do that in
master and think about turning the test on somewhere.
Attachments:
v2-0001-Add-simple-test-for-RADIUS-authentication.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Add-simple-test-for-RADIUS-authentication.patchDownload
From 2435882d82cc91f0b4ac73bf6149fbacf08a547c Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sat, 31 Dec 2022 14:41:57 +1300
Subject: [PATCH v2 1/4] Add simple test for RADIUS authentication.
This is similar to the existing tests for other authentication methods.
It requires FreeRADIUS to be installed, and opens ports that may be
considered insecure, so users have to opt in with PG_EXTRA_TESTS=radius.
Reviewed-by: Heikki Linnakangas <hlinnaka@iki.fi>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Discussion: https://postgr.es/m/CA%2BhUKGLRSPTOC_ygx4_sJjWeKOkOpWGCBCJiRq8cPNuMisuzgw%40mail.gmail.com
---
doc/src/sgml/regress.sgml | 11 ++
src/test/authentication/README | 12 ++
src/test/authentication/meson.build | 1 +
src/test/authentication/t/007_radius.pl | 187 ++++++++++++++++++++++++
4 files changed, 211 insertions(+)
create mode 100644 src/test/authentication/t/007_radius.pl
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index d1042e02228..b9dc28b8dea 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -284,6 +284,17 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>radius</literal></term>
+ <listitem>
+ <para>
+ Runs the test <filename>src/test/authentication/t/007_radius.pl</filename>.
+ This requires a <productname>FreeRADIUS</productname> installation and
+ opens UDP listen sockets.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>ssl</literal></term>
<listitem>
diff --git a/src/test/authentication/README b/src/test/authentication/README
index 602280a0713..13d7a7f4d84 100644
--- a/src/test/authentication/README
+++ b/src/test/authentication/README
@@ -26,3 +26,15 @@ Either way, this test initializes, starts, and stops a test Postgres
cluster.
See src/test/perl/README for more info about running these tests.
+
+Requirements
+============
+
+The RADIUS test is skipped unless "radius" is listed in PG_TEXT_EXTRA, because
+it requires a FreeRADIUS installation and opens extra ports. FreeRADIUS can be
+installed with:
+
+Debian: apt-get install freeradius
+Homebrew: brew install freeradius-server
+FreeBSD: pkg install freeradius3
+MacPorts: port install freeradius
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc13..59da3e64169 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -12,6 +12,7 @@ tests += {
't/004_file_inclusion.pl',
't/005_sspi.pl',
't/006_login_trigger.pl',
+ 't/007_radius.pl',
],
},
}
diff --git a/src/test/authentication/t/007_radius.pl b/src/test/authentication/t/007_radius.pl
new file mode 100644
index 00000000000..c683d1d5579
--- /dev/null
+++ b/src/test/authentication/t/007_radius.pl
@@ -0,0 +1,187 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+my $radiusd_dir = "${PostgreSQL::Test::Utils::tmp_check}/radiusd_data";
+my $radiusd_conf = "radiusd.conf";
+my $radiusd_users = "users.txt";
+my $radiusd_prefix;
+my $radiusd;
+
+if ($ENV{PG_TEST_EXTRA} !~ /\bradius\b/)
+{
+ plan skip_all => 'radius not enabled in PG_TEST_EXTRA';
+}
+elsif ($^O eq 'freebsd')
+{
+ $radiusd = '/usr/local/sbin/radiusd';
+}
+elsif ($^O eq 'linux' && -f '/usr/sbin/freeradius')
+{
+ $radiusd = '/usr/sbin/freeradius';
+}
+elsif ($^O eq 'linux')
+{
+ $radiusd = '/usr/sbin/radiusd';
+}
+elsif ($^O eq 'darwin' && -d '/opt/local')
+{
+ # typical path for MacPorts
+ $radiusd = '/opt/local/sbin/radiusd';
+ $radiusd_prefix = '/opt/local';
+}
+elsif ($^O eq 'darwin' && -d '/opt/homebrew')
+{
+ # typical path for Homebrew on ARM
+ $radiusd = '/opt/homebrew/bin/radiusd';
+ $radiusd_prefix = '/opt/homebrew';
+}
+elsif ($^O eq 'darwin' && -d '/usr/local')
+{
+ # typical path for Homebrew on Intel
+ $radiusd = '/usr/local/bin/radiusd';
+ $radiusd_prefix = '/usr/local';
+}
+
+if (!defined($radiusd) || ! -f $radiusd)
+{
+ plan skip_all =>
+ "radius tests not supported on $^O or dependencies not installed";
+}
+
+sub make_random_string
+{
+ my $length = 8 + int(rand(16));
+ my $s = "";
+ $s .= chr(ord("A") + rand(26)) for 1..$length;
+ return $s;
+}
+
+my $shared_secret = make_random_string();
+my $password = make_random_string();
+
+note "setting up radiusd";
+
+my $radius_port = PostgreSQL::Test::Cluster::get_free_port();
+
+mkdir $radiusd_dir or die "cannot create $radiusd_dir";
+
+append_to_file("$radiusd_dir/$radiusd_users",
+ qq{test2 Cleartext-Password := "$password"});
+
+my $conf = qq{
+client default {
+ ipaddr = "127.0.0.1"
+ secret = "$shared_secret"
+}
+
+modules {
+ files {
+ filename = "$radiusd_dir/users.txt"
+ }
+ pap {
+ }
+}
+
+server default {
+ listen {
+ type = "auth"
+ ipv4addr = "127.0.0.1"
+ port = "$radius_port"
+ }
+ authenticate {
+ Auth-Type PAP {
+ pap
+ }
+ }
+ authorize {
+ files
+ pap
+ }
+}
+
+log {
+ destination = "files"
+ localstatedir = "$radiusd_dir"
+ logdir = "$radiusd_dir"
+ file = "$radiusd_dir/radius.log"
+}
+
+pidfile = "$radiusd_dir/radiusd.pid"
+};
+
+if ($radiusd_prefix)
+{
+ $conf .= "prefix=\"$radiusd_prefix\"\n";
+}
+
+append_to_file("$radiusd_dir/$radiusd_conf", $conf);
+
+system_or_bail $radiusd, '-xx', '-d', $radiusd_dir;
+
+END
+{
+ kill 'INT', `cat $radiusd_dir/radiusd.pid`
+ if -f "$radiusd_dir/radiusd.pid";
+}
+
+note "setting up PostgreSQL instance";
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE USER test1;');
+$node->safe_psql('postgres', 'CREATE USER test2;');
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf(
+ 'pg_hba.conf',
+ qq{
+local all test2 radius radiusservers="127.0.0.1" radiussecrets="$shared_secret" radiusports="$radius_port"
+}
+);
+$node->restart;
+
+note "running tests";
+
+sub test_access
+{
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my ($node, $role, $expected_res, $test_name, %params) = @_;
+ my $connstr = "user=$role";
+
+ if ($expected_res eq 0)
+ {
+ $node->connect_ok($connstr, $test_name, %params);
+ }
+ else
+ {
+ # No checks of the error message, only the status code.
+ $node->connect_fails($connstr, $test_name, %params);
+ }
+}
+
+$ENV{"PGPASSWORD"} = 'wrong';
+test_access(
+ $node, 'test1', 2,
+ 'authentication fails if user not found in RADIUS',
+ log_unlike => [qr/connection authenticated:/]);
+test_access(
+ $node, 'test2', 2,
+ 'authentication fails with wrong password',
+ log_unlike => [qr/connection authenticated:/]);
+
+$ENV{"PGPASSWORD"} = $password;
+test_access($node, 'test2', 0, 'authentication succeeds with right password',
+ log_like =>
+ [qr/connection authenticated: identity="test2" method=radius/],);
+
+done_testing();
--
2.46.0
v2-0002-ci-Enable-RADIUS-tests.patchtext/x-patch; charset=US-ASCII; name=v2-0002-ci-Enable-RADIUS-tests.patchDownload
From 62d315c47563dcd771dc9a3034a0df7094587656 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sat, 2 Sep 2023 10:09:47 +1200
Subject: [PATCH v2 2/4] ci: Enable RADIUS tests.
Unix targets only, because FreeRADIUS doesn't exist on Windows.
XXX Not sure it's worth running this stuff on CI. This is just for
demonstration.
---
.cirrus.tasks.yml | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.yml
index 1ce6c443a8c..b9c37440777 100644
--- a/.cirrus.tasks.yml
+++ b/.cirrus.tasks.yml
@@ -20,7 +20,7 @@ env:
MTEST_ARGS: --print-errorlogs --no-rebuild -C build
PGCTLTIMEOUT: 120 # avoids spurious failures during parallel tests
TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf
- PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance
+ PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance radius
# What files to preserve in case tests fail
@@ -164,7 +164,7 @@ task:
chown root:postgres /tmp/cores
sysctl kern.corefile='/tmp/cores/%N.%P.core'
setup_additional_packages_script: |
- #pkg install -y ...
+ pkg install -y freeradius3
# NB: Intentionally build without -Dllvm. The freebsd image size is already
# large enough to make VM startup slow, and even without llvm freebsd
@@ -313,8 +313,8 @@ task:
EOF
setup_additional_packages_script: |
- #apt-get update
- #DEBIAN_FRONTEND=noninteractive apt-get -y install ...
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get -y install freeradius
matrix:
- name: Linux - Debian Bookworm - Autoconf
@@ -473,6 +473,7 @@ task:
setup_additional_packages_script: |
sh src/tools/ci/ci_macports_packages.sh \
ccache \
+ freeradius \
icu \
kerberos5 \
lz4 \
--
2.46.0
v2-0003-Mitigation-for-BlastRADIUS.patchtext/x-patch; charset=US-ASCII; name=v2-0003-Mitigation-for-BlastRADIUS.patchDownload
From fc225f1421e7838b480c0293a6b5dcac35375387 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Sun, 4 Aug 2024 15:58:33 +1200
Subject: [PATCH v2 3/4] Mitigation for BlastRADIUS.
Add an RFC 2869 Message-Authenticator attribute (an HMAC-MD5 signature)
to outgoing Access-Request messages, and optionally require and verify
them on incoming Access-Accept and Access-Reject messages, as
recommended to mitigate CVE-2024-3596 and VU#456537 by:
https://www.blastradius.fail/
No RADIUS server should be upset by the addition of our new
Message-Authenticator attribute in requests, so we always add that.
On the other hand, we can't require the attribute to be present in
responses yet because we can't assume that all sites are already doing
that. For example, FreeRADIUS started sending them in 3.2.5, which
isn't yet in all distributions at the time of writing. Therefore, users
have to opt in to this requirement with radiusrequirema=1 in pg_hba.conf
for now; the default could change in future.
Reviewed-by: Heikki Linnakangas <hlinnaka@iki.fi>
Discussion: https://postgr.es/m/CA%2BhUKGLRSPTOC_ygx4_sJjWeKOkOpWGCBCJiRq8cPNuMisuzgw%40mail.gmail.com
---
doc/src/sgml/client-auth.sgml | 27 ++++
src/backend/libpq/auth.c | 205 +++++++++++++++++++++++-
src/backend/libpq/hba.c | 8 +
src/include/libpq/hba.h | 1 +
src/test/authentication/t/007_radius.pl | 5 +
5 files changed, 241 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 51343de7cad..37ee2833d37 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -2161,6 +2161,33 @@ host ... ldap ldapbasedn="dc=example,dc=net"
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>radiusrequirema</literal></term>
+ <listitem>
+ <para>
+ Whether to require a valid
+ <ulink url="https://datatracker.ietf.org/doc/html/rfc2869#section-5.14">RFC 2869</ulink>
+ <literal>Message-Authenticator</literal>
+ attribute in messages received from RADIUS servers, and ignore messages
+ that don't contain it. The default value
+ is <literal>0</literal>, but it can be set to <literal>1</literal>.
+ This setting does not affect requests sent by
+ <productname>PostgreSQL</productname> to the RADIUS server, which
+ always include a <literal>Message-Authenticator</literal> attribute
+ (but didn't in earlier releases).
+ </para>
+ <para>
+ Two-way <literal>Message-Authenticator</literal> attributes are a
+ mitigation for the security vulnerability known as
+ <ulink url="https://blastradius.fail">Blast-RADIUS</ulink>. It is
+ recommended to always set this to 1, unless you are running an older RADIUS
+ server version that has not been upgraded to include
+ <literal>Message-Authenticator</literal> in all replies. The default will be
+ changed to 1 in a future PostgreSQL version.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2b607c52704..07dc1e93791 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -24,6 +24,7 @@
#include <unistd.h>
#include "commands/user.h"
+#include "common/hmac.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
@@ -198,7 +199,7 @@ static int pg_SSPI_make_upn(char *accountname,
*----------------------------------------------------------------
*/
static int CheckRADIUSAuth(Port *port);
-static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd);
+static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd, bool requirema);
/*
@@ -2802,6 +2803,7 @@ typedef struct
#define RADIUS_PASSWORD 2
#define RADIUS_SERVICE_TYPE 6
#define RADIUS_NAS_IDENTIFIER 32
+#define RADIUS_MESSAGE_AUTHENTICATOR 80
/* RADIUS service types */
#define RADIUS_AUTHENTICATE_ONLY 8
@@ -2809,7 +2811,7 @@ typedef struct
/* Seconds to wait - XXX: should be in a config variable! */
#define RADIUS_TIMEOUT 3
-static void
+static uint8 *
radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len)
{
radius_attribute *attr;
@@ -2825,7 +2827,7 @@ radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *dat
elog(WARNING,
"adding attribute code %d with length %d to radius packet would create oversize packet, ignoring",
type, len);
- return;
+ return NULL;
}
attr = (radius_attribute *) ((unsigned char *) packet + packet->length);
@@ -2833,6 +2835,65 @@ radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *dat
attr->length = len + 2; /* total size includes type and length */
memcpy(attr->data, data, len);
packet->length += attr->length;
+
+ /*
+ * Return pointer into message, used to fill in Message-Authenticator
+ * contents later.
+ */
+ return attr->data;
+}
+
+/*
+ * Search for an attribute in a received message. If the attribute is found,
+ * sets *value and *length (not including the header), and returns true. If
+ * the attribute is not found but the message appears to be well-formed, sets
+ * *value to NULL and returns true. If the message is found to be mal-formed,
+ * returns false.
+ */
+static bool
+radius_find_attribute(uint8 *packet,
+ size_t packet_size,
+ uint8 type,
+ uint8 **value,
+ uint8 *length)
+{
+ size_t index = RADIUS_HEADER_LENGTH;
+
+ while (index < packet_size)
+ {
+ radius_attribute *attr;
+
+ /* Are there enough bytes left for the attribute header? */
+ if (index + offsetof(radius_attribute, data) >= packet_size)
+ return false;
+
+ /* No alignment requirement, members are all uint8. */
+ attr = (radius_attribute *) &packet[index];
+
+ /* Would this attribute overflow the packet? */
+ if (index + attr->length > packet_size)
+ return false;
+
+ /* Is this attribute's length shorter than its own header? */
+ if (attr->length < offsetof(radius_attribute, data))
+ return false;
+
+ /* Is this it? */
+ if (attr->attribute == type)
+ {
+ *value = attr->data;
+ *length = attr->length - offsetof(radius_attribute, data);
+ return true;
+ }
+
+ /* Nope, skip to the next one. */
+ index += attr->length;
+ }
+
+ /* Well-formed, but not found. */
+ *value = NULL;
+ *length = 0;
+ return true;
}
static int
@@ -2890,7 +2951,8 @@ CheckRADIUSAuth(Port *port)
radiusports ? lfirst(radiusports) : NULL,
identifiers ? lfirst(identifiers) : NULL,
port->user_name,
- passwd);
+ passwd,
+ port->hba->radiusrequirema);
/*------
* STATUS_OK = Login OK
@@ -2931,8 +2993,12 @@ CheckRADIUSAuth(Port *port)
}
static int
-PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd)
+PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd, bool requirema)
{
+ pg_hmac_ctx *hmac_context;
+ uint8 message_authenticator[MD5_DIGEST_LENGTH];
+ uint8 *message_authenticator_location;
+ uint8 message_authenticator_size;
radius_packet radius_send_pack;
radius_packet radius_recv_pack;
radius_packet *packet = &radius_send_pack;
@@ -2993,6 +3059,19 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
return STATUS_ERROR;
}
packet->id = packet->vector[0];
+
+ /*
+ * Add Message-Authenticator attribute first, per recommendations for
+ * Blast-RADIUS mitigation. Initially it holds zeroes, but we remember
+ * where it is in the message so that we can fill it in later.
+ */
+ memset(message_authenticator, 0, lengthof(message_authenticator));
+ message_authenticator_location =
+ radius_add_attribute(packet,
+ RADIUS_MESSAGE_AUTHENTICATOR,
+ message_authenticator,
+ lengthof(message_authenticator));
+
radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (const unsigned char *) &service, sizeof(service));
radius_add_attribute(packet, RADIUS_USER_NAME, (const unsigned char *) user_name, strlen(user_name));
radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (const unsigned char *) identifier, strlen(identifier));
@@ -3048,6 +3127,32 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
packetlength = packet->length;
packet->length = pg_hton16(packet->length);
+ /*
+ * Compute the Message-Authenticator for the whole message. The
+ * Message-Authenticator itself is one of the attributes, but it holds
+ * zeroes at this point.
+ */
+ hmac_context = pg_hmac_create(PG_MD5);
+ if (hmac_context == NULL ||
+ pg_hmac_init(hmac_context, (uint8 *) secret, strlen(secret)) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) radius_buffer, packetlength) < 0 ||
+ pg_hmac_final(hmac_context,
+ message_authenticator,
+ lengthof(message_authenticator)) < 0)
+ {
+ ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s",
+ pg_hmac_error(hmac_context))));
+ pg_hmac_free(hmac_context);
+ pg_freeaddrinfo_all(hint.ai_family, serveraddrs);
+ return STATUS_ERROR;
+ }
+ pg_hmac_free(hmac_context);
+
+ /* Overwrite the attribute with the computed signature. */
+ memcpy(message_authenticator_location, message_authenticator,
+ lengthof(message_authenticator));
+
sock = socket(serveraddrs[0].ai_family, SOCK_DGRAM, 0);
if (sock == PGINVALID_SOCKET)
{
@@ -3232,6 +3337,96 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
+ /* Search for the Message-Authenticator attribute. */
+ if (!radius_find_attribute((uint8 *) receive_buffer,
+ packetlength,
+ RADIUS_MESSAGE_AUTHENTICATOR,
+ &message_authenticator_location,
+ &message_authenticator_size))
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has malformed attributes",
+ server)));
+ continue;
+ }
+ else if (message_authenticator_location == NULL)
+ {
+ /*
+ * If configured to require a Message-Authenticator, ignore this
+ * message.
+ */
+ if (requirema)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has no Message-Authenticator",
+ server)));
+ continue;
+ }
+ }
+ else if (message_authenticator_size != lengthof(message_authenticator))
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has unexpected Message-Authenticator size",
+ server)));
+ continue;
+ }
+ else
+ {
+ uint8 message_authenticator_copy[lengthof(message_authenticator)];
+
+ /*
+ * Save a copy of the received HMAC, and zero out the one in the
+ * message so that we have input required to recompute it.
+ */
+ memcpy(message_authenticator_copy,
+ message_authenticator_location,
+ lengthof(message_authenticator));
+ memset(message_authenticator_location,
+ 0,
+ lengthof(message_authenticator));
+
+ /*
+ * Compute the expected value. Note that the HMAC for
+ * Access-Accept and Access-Reject message uses the authenticator
+ * from the original Access-Request message, so we have to do a
+ * bit of splicing.
+ */
+ hmac_context = pg_hmac_create(PG_MD5);
+ if (hmac_context == NULL ||
+ pg_hmac_init(hmac_context, (uint8 *) secret,
+ strlen(secret)) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) receive_buffer,
+ offsetof(radius_packet, vector)) < 0 ||
+ pg_hmac_update(hmac_context,
+ packet->vector,
+ RADIUS_VECTOR_LENGTH) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) receive_buffer + RADIUS_HEADER_LENGTH,
+ packetlength - RADIUS_HEADER_LENGTH) < 0 ||
+ pg_hmac_final(hmac_context,
+ message_authenticator,
+ lengthof(message_authenticator)) < 0)
+ {
+ ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s",
+ pg_hmac_error(hmac_context))));
+ pg_hmac_free(hmac_context);
+ closesocket(sock);
+ return STATUS_ERROR;
+ }
+ pg_hmac_free(hmac_context);
+
+ /* Verify. */
+ if (memcmp(message_authenticator_copy,
+ message_authenticator,
+ lengthof(message_authenticator)) != 0)
+ {
+ ereport(LOG, (errmsg("RADIUS response from %s has invalid Message-Authenticator",
+ server)));
+ continue;
+ }
+ }
+
if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
{
closesocket(sock);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 75d588e36a1..7ff8e1ca6e7 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2446,6 +2446,14 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
hbaline->radiusidentifiers = parsed_identifiers;
hbaline->radiusidentifiers_s = pstrdup(val);
}
+ else if (strcmp(name, "radiusrequirema") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaRADIUS, "radiusrequirema", "radius");
+ if (strcmp(val, "1") == 0)
+ hbaline->radiusrequirema = true;
+ else
+ hbaline->radiusrequirema = false;
+ }
else
{
ereport(elevel,
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8ea837ae82a..139e4bd410a 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -135,6 +135,7 @@ typedef struct HbaLine
char *radiusidentifiers_s;
List *radiusports;
char *radiusports_s;
+ bool radiusrequirema;
} HbaLine;
typedef struct IdentLine
diff --git a/src/test/authentication/t/007_radius.pl b/src/test/authentication/t/007_radius.pl
index c683d1d5579..4f66f039310 100644
--- a/src/test/authentication/t/007_radius.pl
+++ b/src/test/authentication/t/007_radius.pl
@@ -112,6 +112,11 @@ log {
file = "$radiusd_dir/radius.log"
}
+# FreeRADIUS < 3.2.5 doesn't understand this, but ignores it.
+security {
+ require_message_authenticator = "yes"
+}
+
pidfile = "$radiusd_dir/radiusd.pid"
};
--
2.46.0
v2-0004-XXX-BlastRADIUS-back-patch-kludge-for-12-and-13.patchtext/x-patch; charset=US-ASCII; name=v2-0004-XXX-BlastRADIUS-back-patch-kludge-for-12-and-13.patchDownload
From 9dd68610b8931ebd6d0969040daeb58d85edb7be Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Mon, 5 Aug 2024 17:09:57 +1200
Subject: [PATCH v2 4/4] XXX BlastRADIUS back-patch kludge for 12 and 13
---
doc/src/sgml/client-auth.sgml | 9 ++++++++
src/backend/libpq/auth.c | 42 ++++++++++++++++++++++++++++++++++-
src/backend/libpq/hba.c | 10 +++++++++
3 files changed, 60 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 37ee2833d37..bc88cad132c 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -2185,6 +2185,15 @@ host ... ldap ldapbasedn="dc=example,dc=net"
<literal>Message-Authenticator</literal> in all replies. The default will be
changed to 1 in a future PostgreSQL version.
</para>
+ <note>
+ <para>
+ <literal>Message-Authenticator</literal> attributes can only be
+ computed if <productname>PostgreSQL</productname> was built support for
+ <productname>OpenSSL</productname>. Otherwise, they
+ will be silently omitted from outgoing messages, this setting
+ cannot be turned on, and incoming messages will not be verified.
+ </para>
+ </note>
</listitem>
</varlistentry>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 07dc1e93791..72a9c0a4e52 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -24,7 +24,6 @@
#include <unistd.h>
#include "commands/user.h"
-#include "common/hmac.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
@@ -40,6 +39,10 @@
#include "storage/ipc.h"
#include "utils/memutils.h"
+#ifdef USE_OPENSSL
+#include "openssl/hmac.h"
+#endif
+
/*----------------------------------------------------------------
* Global authentication functions
*----------------------------------------------------------------
@@ -2811,6 +2814,31 @@ typedef struct
/* Seconds to wait - XXX: should be in a config variable! */
#define RADIUS_TIMEOUT 3
+#ifdef USE_OPENSSL
+/*
+ * Locally redirect PostgreSQL 14's pg_hmac_XXX API directly to OpenSSL
+ * functions, to simplify back-patching of BlastRADIUS mitigation to
+ * PostgreSQL 12 and 13. If built without OpenSSL, skip HMAC support and
+ * disable Message-Authenticator attributes.
+ */
+#define RADIUS_USE_HMAC
+#define pg_hmac_ctx HMAC_CTX
+#define pg_hmac_create(...) HMAC_CTX_new()
+#define pg_hmac_init(ctx, key, key_len, ...) HMAC_Init_ex(ctx, key, key_len, EVP_md5(), NULL)
+#define pg_hmac_update HMAC_Update
+static inline int
+pg_hmac_final(pg_hmac_ctx *ctx, void *md, size_t len)
+{
+ unsigned int l = len;
+ int result = HMAC_Final(ctx, md, &l);
+
+ Assert(l == len);
+ return result;
+}
+#define pg_hmac_free(x) if (x) HMAC_CTX_free(x)
+#define pg_hmac_error(x) "HMAC error"
+#endif
+
static uint8 *
radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len)
{
@@ -2843,6 +2871,7 @@ radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *dat
return attr->data;
}
+#ifdef RADIUS_USE_HMAC
/*
* Search for an attribute in a received message. If the attribute is found,
* sets *value and *length (not including the header), and returns true. If
@@ -2895,6 +2924,7 @@ radius_find_attribute(uint8 *packet,
*length = 0;
return true;
}
+#endif
static int
CheckRADIUSAuth(Port *port)
@@ -2995,10 +3025,12 @@ CheckRADIUSAuth(Port *port)
static int
PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd, bool requirema)
{
+#ifdef RADIUS_USE_HMAC
pg_hmac_ctx *hmac_context;
uint8 message_authenticator[MD5_DIGEST_LENGTH];
uint8 *message_authenticator_location;
uint8 message_authenticator_size;
+#endif
radius_packet radius_send_pack;
radius_packet radius_recv_pack;
radius_packet *packet = &radius_send_pack;
@@ -3060,6 +3092,8 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
}
packet->id = packet->vector[0];
+#ifdef RADIUS_USE_HMAC
+
/*
* Add Message-Authenticator attribute first, per recommendations for
* Blast-RADIUS mitigation. Initially it holds zeroes, but we remember
@@ -3071,6 +3105,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
RADIUS_MESSAGE_AUTHENTICATOR,
message_authenticator,
lengthof(message_authenticator));
+#endif
radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (const unsigned char *) &service, sizeof(service));
radius_add_attribute(packet, RADIUS_USER_NAME, (const unsigned char *) user_name, strlen(user_name));
@@ -3127,6 +3162,8 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
packetlength = packet->length;
packet->length = pg_hton16(packet->length);
+#ifdef RADIUS_USE_HMAC
+
/*
* Compute the Message-Authenticator for the whole message. The
* Message-Authenticator itself is one of the attributes, but it holds
@@ -3152,6 +3189,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
/* Overwrite the attribute with the computed signature. */
memcpy(message_authenticator_location, message_authenticator,
lengthof(message_authenticator));
+#endif
sock = socket(serveraddrs[0].ai_family, SOCK_DGRAM, 0);
if (sock == PGINVALID_SOCKET)
@@ -3337,6 +3375,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
+#ifdef RADIUS_USE_HMAC
/* Search for the Message-Authenticator attribute. */
if (!radius_find_attribute((uint8 *) receive_buffer,
packetlength,
@@ -3426,6 +3465,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
}
+#endif
if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
{
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 7ff8e1ca6e7..abe0c194d88 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2450,7 +2450,17 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
{
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusrequirema", "radius");
if (strcmp(val, "1") == 0)
+ {
+#ifdef USE_OPENSSL
hbaline->radiusrequirema = true;
+#else
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("this build does not support radiusrequirema=1"),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, file_name)));
+#endif
+ }
else
hbaline->radiusrequirema = false;
}
--
2.46.0
On Mon, Aug 05, 2024 at 05:41:21PM +0300, Heikki Linnakangas wrote:
On 05/08/2024 15:43, Thomas Munro wrote:
Since PostgreSQL v12 and v13 don't have the modern "common/hmac.h"
API, I came up with a cheap kludge: locally #define those interfaces
to point directly to the OpenSSL HMAC API, or just give up and drop
Message-Authenticator support if you didn't build with OpenSSL support
(in practice everyone does). Better ideas?Seems reasonable. It probably wouldn't be hard to backport common/hmac.h
either, perhaps in a limited fashion with just md5 support.
It's a bit more than just backporting hmac.h and hmac.c.
hmac_openssl.c only depends on OpenSSL to do its business, but the
non-OpenSSL fallback implementation depends also on the cryptohash
fallbacks for SHA-NNN and MD5. So you would also need the parts
related to cryptohash.c, sha{1,2}.c, etc. Not really complex as these
could be dropped as-is into the stable branches of 12 and 13, but not
that straight-forward either as we had the bad idea to use the
fallback MD5 implementation even if linking to OpenSSL in v12 and v13,
meaning that you may need some tweaks to avoid API conflicts.
Requiring OpenSSL and its HMAC APIs to do the job is much safer for a
stable branch, IMO.
--
Michael
On 06/08/2024 03:58, Thomas Munro wrote:
On Tue, Aug 6, 2024 at 2:41 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
What if the message contains multiple attribute of the same type? If
there's a duplicate Message-Authenticator, we should surely reject the
packet. I don't know if duplicate attributes are legal in general.Duplicate attributes are legal in general per RFC 2865, which has a
table of attributes and their possible quantity; unfortunately this
one is an extension from RFC 2869, and I didn't find where it pins
that down. I suppose we could try to detect an unexpected duplicate,
which might have the side benefit of checking the rest of the
attributes for well-formedness (though in practice there aren't any).
Is it worth bothering with that?
Hmm, it does feel sloppy to not verify the format of the rest of the
attributes. That's not new with this patch though. Maybe have a separate
function to verify the packet's format, and call that before
radius_find_attribute().
I suppose if we wanted to be extra fastidious, we could also test with
a gallery of malformed packets crafted by a Perl script, but that
feels like overkill. On the other hand it would be bad if you could
crash a server by lobbing UDP packets at it because of some dumb
thinko.
This would also be a easy target for fuzz testing. I'm not too worried
though, the packet format is pretty simple. Still, bugs happen. (Not a
requirement for this patch in any case)
+my $radius_port = PostgreSQL::Test::Cluster::get_free_port();
This isn't quite right because get_free_port() finds a free TCP port,
while radius uses UDP.
+#else + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("this build does not support radiusrequirema=1"), + errcontext("line %d of configuration file \"%s\"", + line_num, file_name))); +#endif
Maybe something like:
errmsg("radiusrequirema=1 is not supported because the server was
built without OpenSSL")
to give the user a hint what they need to do to enable it.
Other than those little things, looks good to me.
--
Heikki Linnakangas
Neon (https://neon.tech)
On Wed, Aug 7, 2024 at 5:55 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
On 06/08/2024 03:58, Thomas Munro wrote:
On Tue, Aug 6, 2024 at 2:41 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
What if the message contains multiple attribute of the same type? If
there's a duplicate Message-Authenticator, we should surely reject the
packet. I don't know if duplicate attributes are legal in general.Duplicate attributes are legal in general per RFC 2865, which has a
table of attributes and their possible quantity; unfortunately this
one is an extension from RFC 2869, and I didn't find where it pins
that down.
There's a similar table near the end of 2869:
https://datatracker.ietf.org/doc/html/rfc2869#section-5.19
I suppose if we wanted to be extra fastidious, we could also test with
a gallery of malformed packets crafted by a Perl script, but that
feels like overkill. On the other hand it would be bad if you could
crash a server by lobbing UDP packets at it because of some dumb
thinko.This would also be a easy target for fuzz testing. I'm not too worried
though, the packet format is pretty simple. Still, bugs happen. (Not a
requirement for this patch in any case)
<tangent>
I've been working on fuzzing JSON, and I spent some time adapting that
to test this RADIUS code. No pressure to use any of it (the
refactoring to pull out the response validation is cowboy-quality at
best), but it might help anyone who wants to pursue it in the future?
This fuzzer hasn't been able to find anything in the response parser.
(But the modifications I made make that claim a little weaker, since
I'm not testing what's actually shipping.) The attached response
corpus could be used to seed a malformed-packet gallery like Thomas
mentioned; it's built with the assumption that the request packet was
all zeroes and the shared secret is `secret`.
I was impressed with how quickly LLVM was able to find the packet
shape, including valid signatures. The only thing I had to eventually
add manually was a valid Message-Authenticator case; I assume the
interdependency between the authenticator and the packet checksum was
a little too much for libfuzzer to figure out on its own.
</tangent>
--Jacob
Attachments:
WIP-try-to-fuzz-RADIUS-implementation.patch.txttext/plain; charset=US-ASCII; name=WIP-try-to-fuzz-RADIUS-implementation.patch.txtDownload
From f04087abbe3381ee3a546750a217b980143441d9 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 8 Aug 2024 11:12:05 -0700
Subject: [PATCH] WIP: try to fuzz RADIUS implementation
- extract validate_radius_response()
- add LLVM fuzzer implementation
- suppress logging by default, let --log_min_messages override it
- support const response buffer (and improve fuzzability) by computing
Message-Authenticator piecewise
---
meson-clang.ini | 4 +
src/backend/libpq/auth.c | 336 +++++++++++++------------
src/backend/meson.build | 18 ++
src/common/meson.build | 16 ++
src/include/libpq/auth.h | 21 ++
src/test/modules/fuzzers/fuzz_radius.c | 74 ++++++
src/test/modules/fuzzers/meson.build | 24 ++
src/test/modules/meson.build | 1 +
8 files changed, 330 insertions(+), 164 deletions(-)
create mode 100644 meson-clang.ini
create mode 100644 src/test/modules/fuzzers/fuzz_radius.c
create mode 100644 src/test/modules/fuzzers/meson.build
diff --git a/meson-clang.ini b/meson-clang.ini
new file mode 100644
index 0000000000..560cffe8e7
--- /dev/null
+++ b/meson-clang.ini
@@ -0,0 +1,4 @@
+[binaries]
+c = 'clang-15'
+cpp = 'clang++-15'
+llvm-config = 'llvm-config-15'
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 40f0cabf3a..d97fd6e3f2 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2769,13 +2769,9 @@ CheckCertAuth(Port *port)
* RADIUS authentication is described in RFC2865 (and several others).
*/
-#define RADIUS_VECTOR_LENGTH 16
#define RADIUS_HEADER_LENGTH 20
#define RADIUS_MAX_PASSWORD_LENGTH 128
-/* Maximum size of a RADIUS packet we will create or accept */
-#define RADIUS_BUFFER_SIZE 1024
-
typedef struct
{
uint8 attribute;
@@ -2783,16 +2779,6 @@ typedef struct
uint8 data[FLEXIBLE_ARRAY_MEMBER];
} radius_attribute;
-typedef struct
-{
- uint8 code;
- uint8 id;
- uint16 length;
- uint8 vector[RADIUS_VECTOR_LENGTH];
- /* this is a bit longer than strictly necessary: */
- char pad[RADIUS_BUFFER_SIZE - RADIUS_VECTOR_LENGTH];
-} radius_packet;
-
/* RADIUS packet types */
#define RADIUS_ACCESS_REQUEST 1
#define RADIUS_ACCESS_ACCEPT 2
@@ -2993,11 +2979,9 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH];
uint8 message_authenticator[RADIUS_VECTOR_LENGTH];
uint8 *message_authenticator_location;
- uint8 message_authenticator_size;
radius_packet radius_send_pack;
radius_packet radius_recv_pack;
radius_packet *packet = &radius_send_pack;
- radius_packet *receivepacket = &radius_recv_pack;
char *radius_buffer = (char *) &radius_send_pack;
char *receive_buffer = (char *) &radius_recv_pack;
int32 service = pg_hton32(RADIUS_AUTHENTICATE_ONLY);
@@ -3216,7 +3200,7 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
struct timeval timeout;
struct timeval now;
int64 timeoutval;
- const char *errstr = NULL;
+ int status;
gettimeofday(&now, NULL);
timeoutval = (endtime.tv_sec * 1000000 + endtime.tv_usec) - (now.tv_sec * 1000000 + now.tv_usec);
@@ -3285,167 +3269,191 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
continue;
}
- if (packetlength < RADIUS_HEADER_LENGTH)
+ if (validate_radius_response(&status, receive_buffer, packet, packetlength, secret, server, user_name, requirema))
{
- ereport(LOG,
- (errmsg("RADIUS response from %s too short: %d", server, packetlength)));
- continue;
+ closesocket(sock);
+ return status;
}
- if (packetlength != pg_ntoh16(receivepacket->length))
- {
- ereport(LOG,
- (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)",
- server, pg_ntoh16(receivepacket->length), packetlength)));
- continue;
- }
+ /* otherwise, keep trying */
+ } /* while (true) */
+}
- if (packet->id != receivepacket->id)
- {
- ereport(LOG,
- (errmsg("RADIUS response from %s is to a different request: %d (should be %d)",
- server, receivepacket->id, packet->id)));
- continue;
- }
+bool
+validate_radius_response(int *status,
+ const char *receive_buffer, radius_packet *packet,
+ size_t packetlength, const char *secret,
+ const char *server, const char *user_name, bool requirema)
+{
+ radius_packet *receivepacket = (radius_packet *) receive_buffer;
+ uint8 *cryptvector;
+ uint8 encryptedpassword[RADIUS_MAX_PASSWORD_LENGTH];
+ uint8 *message_authenticator_location;
+ uint8 message_authenticator_size;
+ uint8 message_authenticator[RADIUS_VECTOR_LENGTH];
+ uint8 message_authenticator_key[RADIUS_VECTOR_LENGTH] = {0};
+ pg_hmac_ctx *hmac_context;
+ const char *errstr = NULL;
- /*
- * Verify the response authenticator, which is calculated as
- * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret)
- */
- cryptvector = palloc(packetlength + strlen(secret));
-
- memcpy(cryptvector, receivepacket, 4); /* code+id+length */
- memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request
- * authenticator, from
- * original packet */
- if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no
- * attributes at all */
- memcpy(cryptvector + RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength - RADIUS_HEADER_LENGTH);
- memcpy(cryptvector + packetlength, secret, strlen(secret));
-
- if (!pg_md5_binary(cryptvector,
- packetlength + strlen(secret),
- encryptedpassword, &errstr))
- {
- ereport(LOG,
- (errmsg("could not perform MD5 encryption of received packet: %s",
- errstr)));
- pfree(cryptvector);
- continue;
- }
- pfree(cryptvector);
+ if (packetlength < RADIUS_HEADER_LENGTH)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s too short: %d", server, (int) packetlength)));
+ return false;
+ }
- if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0)
- {
- ereport(LOG,
- (errmsg("RADIUS response from %s has incorrect MD5 signature",
- server)));
- continue;
- }
+ if (packetlength != pg_ntoh16(receivepacket->length))
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)",
+ server, pg_ntoh16(receivepacket->length), (int) packetlength)));
+ return false;
+ }
- /* Search for the Message-Authenticator attribute. */
- if (!radius_find_attribute((uint8 *) receive_buffer,
- packetlength,
- RADIUS_MESSAGE_AUTHENTICATOR,
- &message_authenticator_location,
- &message_authenticator_size))
- {
- ereport(LOG,
- (errmsg("RADIUS response from %s has malformed attributes",
- server)));
- continue;
- }
- else if (message_authenticator_location == NULL)
- {
- ereport(LOG,
- (errmsg("RADIUS response from %s has no Message-Authenticator",
- server)));
+ if (packet->id != receivepacket->id)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s is to a different request: %d (should be %d)",
+ server, receivepacket->id, packet->id)));
+ return false;
+ }
- /* We'll ignore this message, unless pg_hba.conf told us not to. */
- if (requirema)
- continue;
- }
- else if (message_authenticator_size != RADIUS_VECTOR_LENGTH)
- {
- ereport(LOG,
- (errmsg("RADIUS response from %s has unexpected Message-Authenticator size",
- server)));
- continue;
- }
- else
- {
- uint8 message_authenticator_copy[RADIUS_VECTOR_LENGTH];
+ /*
+ * Verify the response authenticator, which is calculated as
+ * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret)
+ */
+ cryptvector = palloc(packetlength + strlen(secret));
- /*
- * Save a copy of the received HMAC, and zero out the one in the
- * message so that we have input required to recompute it.
- */
- memcpy(message_authenticator_copy,
- message_authenticator_location,
- RADIUS_VECTOR_LENGTH);
- memset(message_authenticator_location,
- 0,
- RADIUS_VECTOR_LENGTH);
+ memcpy(cryptvector, receivepacket, 4); /* code+id+length */
+ memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request
+ * authenticator, from
+ * original packet */
+ if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no
+ * attributes at all */
+ memcpy(cryptvector + RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength - RADIUS_HEADER_LENGTH);
+ memcpy(cryptvector + packetlength, secret, strlen(secret));
- /*
- * Compute the expected value. Note that the HMAC for
- * Access-Accept and Access-Reject message uses the authenticator
- * from the original Access-Request message, so we have to do a
- * bit of splicing.
- */
- hmac_context = pg_hmac_create(PG_MD5);
- if (hmac_context == NULL ||
- pg_hmac_init(hmac_context,
- message_authenticator_key,
- lengthof(message_authenticator_key)) < 0 ||
- pg_hmac_update(hmac_context,
- (uint8 *) receive_buffer,
- offsetof(radius_packet, vector)) < 0 ||
- pg_hmac_update(hmac_context,
- packet->vector,
- RADIUS_VECTOR_LENGTH) < 0 ||
- pg_hmac_update(hmac_context,
- (uint8 *) receive_buffer + RADIUS_HEADER_LENGTH,
- packetlength - RADIUS_HEADER_LENGTH) < 0 ||
- pg_hmac_final(hmac_context,
- message_authenticator,
- lengthof(message_authenticator)) < 0)
- {
- ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s",
- pg_hmac_error(hmac_context))));
- pg_hmac_free(hmac_context);
- closesocket(sock);
- return STATUS_ERROR;
- }
- pg_hmac_free(hmac_context);
+ if (!pg_md5_binary(cryptvector,
+ packetlength + strlen(secret),
+ encryptedpassword, &errstr))
+ {
+ ereport(LOG,
+ (errmsg("could not perform MD5 encryption of received packet: %s",
+ errstr)));
+ pfree(cryptvector);
+ return false;
+ }
+ pfree(cryptvector);
- /* Verify. */
- if (memcmp(message_authenticator_copy,
- message_authenticator,
- RADIUS_VECTOR_LENGTH) != 0)
- {
- ereport(LOG, (errmsg("RADIUS response from %s has invalid Message-Authenticator",
- server)));
- continue;
- }
- }
+ if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has incorrect MD5 signature",
+ server)));
+ return false;
+ }
- if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
- {
- closesocket(sock);
- return STATUS_OK;
- }
- else if (receivepacket->code == RADIUS_ACCESS_REJECT)
+ /* Search for the Message-Authenticator attribute. */
+ if (!radius_find_attribute((uint8 *) receive_buffer,
+ packetlength,
+ RADIUS_MESSAGE_AUTHENTICATOR,
+ &message_authenticator_location,
+ &message_authenticator_size))
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has malformed attributes",
+ server)));
+ return false;
+ }
+ else if (message_authenticator_location == NULL)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has no Message-Authenticator",
+ server)));
+
+ /* We'll ignore this message, unless pg_hba.conf told us not to. */
+ if (requirema)
+ return false;
+ }
+ else if (message_authenticator_size != RADIUS_VECTOR_LENGTH)
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has unexpected Message-Authenticator size",
+ server)));
+ return false;
+ }
+ else
+ {
+ uint8 message_authenticator_zero[RADIUS_VECTOR_LENGTH] = {0};
+ const uint8 *attrs = (uint8 *) receive_buffer + RADIUS_HEADER_LENGTH;
+ const uint8 *end = (uint8 *) receive_buffer + packetlength;
+
+ strncpy((char *) message_authenticator_key, secret, sizeof(message_authenticator_key));
+
+ /*
+ * Compute the expected value. Note that the HMAC for
+ * Access-Accept and Access-Reject message uses the authenticator
+ * from the original Access-Request message, so we have to do a
+ * bit of splicing.
+ */
+ hmac_context = pg_hmac_create(PG_MD5);
+ if (hmac_context == NULL ||
+ pg_hmac_init(hmac_context,
+ message_authenticator_key,
+ lengthof(message_authenticator_key)) < 0 ||
+ pg_hmac_update(hmac_context,
+ (uint8 *) receive_buffer,
+ offsetof(radius_packet, vector)) < 0 ||
+ pg_hmac_update(hmac_context,
+ packet->vector,
+ RADIUS_VECTOR_LENGTH) < 0 ||
+ pg_hmac_update(hmac_context,
+ attrs,
+ message_authenticator_location - attrs) < 0 ||
+ pg_hmac_update(hmac_context,
+ message_authenticator_zero,
+ RADIUS_VECTOR_LENGTH) < 0 ||
+ pg_hmac_update(hmac_context,
+ message_authenticator_location + RADIUS_VECTOR_LENGTH,
+ end - (message_authenticator_location + RADIUS_VECTOR_LENGTH)) < 0 ||
+ pg_hmac_final(hmac_context,
+ message_authenticator,
+ lengthof(message_authenticator)) < 0)
{
- closesocket(sock);
- return STATUS_EOF;
+ ereport(LOG, (errmsg("could not compute RADIUS Message-Authenticator: %s",
+ pg_hmac_error(hmac_context))));
+ pg_hmac_free(hmac_context);
+ *status = STATUS_ERROR;
+ return true;
}
- else
+ pg_hmac_free(hmac_context);
+
+ /* Verify. */
+ if (memcmp(message_authenticator_location,
+ message_authenticator,
+ RADIUS_VECTOR_LENGTH) != 0)
{
- ereport(LOG,
- (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"",
- server, receivepacket->code, user_name)));
- continue;
+ ereport(LOG, (errmsg("RADIUS response from %s has invalid Message-Authenticator",
+ server)));
+ return false;
}
- } /* while (true) */
+ }
+
+ if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
+ {
+ *status = STATUS_OK;
+ return true;
+ }
+ else if (receivepacket->code == RADIUS_ACCESS_REJECT)
+ {
+ *status = STATUS_EOF;
+ return true;
+ }
+ else
+ {
+ ereport(LOG,
+ (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"",
+ server, receivepacket->code, user_name)));
+ return false;
+ }
}
diff --git a/src/backend/meson.build b/src/backend/meson.build
index 78c5726814..3db6a64baf 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -70,6 +70,24 @@ postgres_lib = static_library('postgres_lib',
kwargs: internal_lib_args,
)
+fuzzer_backend_sources = []
+foreach item : backend_sources
+ if item not in main_file
+ fuzzer_backend_sources += item
+ endif
+endforeach
+
+postgres_lib_fuzzer = static_library('postgres_lib_fuzzer',
+ fuzzer_backend_sources + timezone_sources + generated_backend_sources,
+ link_whole: backend_link_with,
+ dependencies: backend_build_deps,
+ c_pch: pch_postgres_h,
+ kwargs: internal_lib_args + {
+ 'c_args': ['-fsanitize=address,undefined,fuzzer-no-link'],
+ 'link_args': ['-fsanitize=address,undefined,fuzzer-no-link'],
+ },
+)
+
if cc.get_id() == 'msvc'
postgres_def = custom_target('postgres.def',
command: [perl, files('../tools/msvc_gendef.pl'),
diff --git a/src/common/meson.build b/src/common/meson.build
index de68e408fa..dee9beff1f 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -145,6 +145,17 @@ pgcommon_variants = {
},
}
+pgcommon_variants += {
+ '_fuzzer': pgcommon_variants[''] + {
+ 'c_args': pgcommon_variants['']['c_args'] + [
+ '-fsanitize=address,undefined,fuzzer-no-link',
+ ],
+ 'link_args': [
+ '-fsanitize=address,undefined,fuzzer-no-link',
+ ],
+ },
+}
+
foreach name, opts : pgcommon_variants
# Build internal static libraries for sets of files that need to be built
@@ -182,4 +193,9 @@ common_srv = pgcommon['_srv']
common_shlib = pgcommon['_shlib']
common_static = pgcommon['']
+common_fuzzer = declare_dependency(
+ link_with: pgcommon['_fuzzer'],
+ link_args: ['-fsanitize=address,undefined,fuzzer-no-link']
+)
+
subdir('unicode')
diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h
index 227b41daf6..580f8422b3 100644
--- a/src/include/libpq/auth.h
+++ b/src/include/libpq/auth.h
@@ -34,4 +34,25 @@ typedef char *(*auth_password_hook_typ) (char *input);
/* Default LDAP password mutator hook, can be overridden by a shared library */
extern PGDLLIMPORT auth_password_hook_typ ldap_password_hook;
+#define RADIUS_VECTOR_LENGTH 16
+
+/* Maximum size of a RADIUS packet we will create or accept */
+#define RADIUS_BUFFER_SIZE 1024
+
+typedef struct
+{
+ uint8 code;
+ uint8 id;
+ uint16 length;
+ uint8 vector[RADIUS_VECTOR_LENGTH];
+ /* this is a bit longer than strictly necessary: */
+ char pad[RADIUS_BUFFER_SIZE - RADIUS_VECTOR_LENGTH];
+} radius_packet;
+
+extern bool
+validate_radius_response(int *status,
+ const char *receive_buffer, radius_packet *packet,
+ size_t packetlength, const char *secret,
+ const char *server, const char *user_name, bool requirema);
+
#endif /* AUTH_H */
diff --git a/src/test/modules/fuzzers/fuzz_radius.c b/src/test/modules/fuzzers/fuzz_radius.c
new file mode 100644
index 0000000000..7955f76e42
--- /dev/null
+++ b/src/test/modules/fuzzers/fuzz_radius.c
@@ -0,0 +1,74 @@
+#include "postgres.h"
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "libpq/auth.h"
+#include "postmaster/postmaster.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "utils/resowner.h"
+
+const char *progname; /* to replace the export from main.c */
+
+int
+LLVMFuzzerInitialize(int *pargc, char ***pargv)
+{
+ static const struct option opts[] = {
+ { "log_min_messages", required_argument, NULL, 1000 },
+ { 0 },
+ };
+
+ int c;
+ char *log_min_messages = "fatal";
+
+ opterr = 0;
+ while ((c = getopt_long(*pargc, *pargv, "", opts, NULL)) != -1)
+ {
+ char *arg;
+
+ if (c == 1000) /* log_min_messages */
+ {
+ log_min_messages = optarg;
+ continue;
+ }
+ else if (c != '?' || optopt != 0)
+ {
+ /* Ignore other valid options and unrecognized short options. */
+ continue;
+ }
+
+ arg = (*pargv)[optind - 1];
+ fprintf(stderr, "unrecognized argument: %s\n", arg);
+ exit(1);
+ }
+
+ /* Fake server startup. */
+ progname = "postgres";
+ MemoryContextInit();
+ CreateAuxProcessResourceOwner();
+ InitializeGUCOptions();
+ SetConfigOption("log_min_messages", log_min_messages,
+ PGC_POSTMASTER, PGC_S_ARGV);
+
+ return 0;
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *data, size_t len)
+{
+ int status;
+ radius_packet packet = {0};
+
+ validate_radius_response(&status,
+ (const char *) data, /* input response */
+ &packet, /* request packet already sent */
+ len, /* length of input response */
+ "secret", /* shared secret */
+ NULL, /* server name */
+ NULL, /* user name */
+ false /* require Message-Authenticator? */
+ );
+
+ return 0;
+}
diff --git a/src/test/modules/fuzzers/meson.build b/src/test/modules/fuzzers/meson.build
new file mode 100644
index 0000000000..3dfc257fc1
--- /dev/null
+++ b/src/test/modules/fuzzers/meson.build
@@ -0,0 +1,24 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+can_fuzz = cc.has_argument('-fsanitize=fuzzer')
+if can_fuzz
+ fuzz_radius_sources = files(
+ 'fuzz_radius.c',
+ )
+
+ fuzz_radius = executable('fuzz_radius',
+ fuzz_radius_sources + generated_headers,
+ c_args: [
+ '-fsanitize=address,fuzzer',
+ '-Wno-missing-prototypes',
+ '-fprofile-instr-generate', '-fcoverage-mapping'
+ ],
+ link_args: ['-fsanitize=address,fuzzer'],
+ include_directories: [postgres_inc],
+ link_with: [postgres_lib_fuzzer, pgport_static],
+ dependencies: [os_deps, libintl],
+ kwargs: default_bin_args + {
+ 'install': false,
+ },
+ )
+endif
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index d8fe059d23..9765e0522a 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -5,6 +5,7 @@ subdir('commit_ts')
subdir('delay_execution')
subdir('dummy_index_am')
subdir('dummy_seclabel')
+subdir('fuzzers')
subdir('gin')
subdir('injection_points')
subdir('ldap_password_func')
--
2.34.1