From 023dcf87d34e29649dd76d33ce7d896c2b6f61d2 Mon Sep 17 00:00:00 2001 From: Julien Rische Date: Thu, 22 Aug 2024 17:15:50 +0200 Subject: [PATCH] Generate and verify message MACs in libkrad Implement some of the measures specified in draft-ietf-radext-deprecating-radius-03 for mitigating the BlastRADIUS attack (CVE-2024-3596): * Include a Message-Authenticator MAC as the first attribute when generating a packet of type Access-Request, Access-Reject, Access-Accept, or Access-Challenge (sections 5.2.1 and 5.2.4), if the secret is non-empty. (An empty secret indicates the use of Unix domain socket transport.) * Validate the Message-Authenticator MAC in received packets, if present. FreeRADIUS enforces Message-Authenticator as of versions 3.2.5 and 3.0.27. libkrad must generate Message-Authenticator attributes in order to remain compatible with these implementations. [ghudson@mit.edu: adjusted style and naming; simplified some functions; edited commit message] ticket: 9142 (new) tags: pullup target_version: 1.21-next (cherry picked from commit 871125fea8ce0370a972bf65f7d1de63f619b06c) --- src/include/k5-int.h | 5 + src/lib/crypto/krb/checksum_hmac_md5.c | 28 ++++ src/lib/crypto/libk5crypto.exports | 1 + src/lib/krad/attr.c | 17 ++ src/lib/krad/attrset.c | 59 +++++-- src/lib/krad/internal.h | 7 +- src/lib/krad/packet.c | 206 +++++++++++++++++++++++-- src/lib/krad/t_attrset.c | 2 +- src/lib/krad/t_daemon.py | 3 +- src/lib/krad/t_packet.c | 11 ++ src/tests/t_otp.py | 3 + 11 files changed, 311 insertions(+), 31 deletions(-) diff --git a/src/include/k5-int.h b/src/include/k5-int.h index 69d6a6f569..b7789a2dd8 100644 --- a/src/include/k5-int.h +++ b/src/include/k5-int.h @@ -2403,4 +2403,9 @@ krb5_boolean k5_sname_compare(krb5_context context, krb5_const_principal sname, krb5_const_principal princ); +/* Generate an HMAC-MD5 keyed checksum as specified by RFC 2104. */ +krb5_error_code +k5_hmac_md5(const krb5_data *key, const krb5_crypto_iov *data, size_t num_data, + krb5_data *output); + #endif /* _KRB5_INT_H */ diff --git a/src/lib/crypto/krb/checksum_hmac_md5.c b/src/lib/crypto/krb/checksum_hmac_md5.c index ec024f3966..a809388549 100644 --- a/src/lib/crypto/krb/checksum_hmac_md5.c +++ b/src/lib/crypto/krb/checksum_hmac_md5.c @@ -92,3 +92,31 @@ cleanup: free(hash_iov); return ret; } + +krb5_error_code +k5_hmac_md5(const krb5_data *key, const krb5_crypto_iov *data, size_t num_data, + krb5_data *output) +{ + krb5_error_code ret; + const struct krb5_hash_provider *hash = &krb5int_hash_md5; + krb5_keyblock keyblock = { 0 }; + krb5_data hashed_key; + uint8_t hkeybuf[16]; + krb5_crypto_iov iov; + + /* Hash the key if it is longer than the block size. */ + if (key->length > hash->blocksize) { + hashed_key = make_data(hkeybuf, sizeof(hkeybuf)); + iov.flags = KRB5_CRYPTO_TYPE_DATA; + iov.data = *key; + ret = hash->hash(&iov, 1, &hashed_key); + if (ret) + return ret; + key = &hashed_key; + } + + keyblock.magic = KV5M_KEYBLOCK; + keyblock.length = key->length; + keyblock.contents = (uint8_t *)key->data; + return krb5int_hmac_keyblock(hash, &keyblock, data, num_data, output); +} diff --git a/src/lib/crypto/libk5crypto.exports b/src/lib/crypto/libk5crypto.exports index d8ffa63304..00e0ce1812 100644 --- a/src/lib/crypto/libk5crypto.exports +++ b/src/lib/crypto/libk5crypto.exports @@ -102,3 +102,4 @@ krb5_c_prfplus krb5_c_derive_prfplus k5_enctype_to_ssf krb5int_c_deprecated_enctype +k5_hmac_md5 diff --git a/src/lib/krad/attr.c b/src/lib/krad/attr.c index 42d354a3b5..65ed1d35e7 100644 --- a/src/lib/krad/attr.c +++ b/src/lib/krad/attr.c @@ -125,6 +125,23 @@ static const attribute_record attributes[UCHAR_MAX] = { {"NAS-Port-Type", 4, 4, NULL, NULL}, {"Port-Limit", 4, 4, NULL, NULL}, {"Login-LAT-Port", 1, MAX_ATTRSIZE, NULL, NULL}, + {NULL, 0, 0, NULL, NULL}, /* Reserved for tunnelling */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for tunnelling */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for tunnelling */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for tunnelling */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for tunnelling */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for tunnelling */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for Apple Remote Access Protocol */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for Apple Remote Access Protocol */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for Apple Remote Access Protocol */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for Apple Remote Access Protocol */ + {NULL, 0, 0, NULL, NULL}, /* Reserved for Apple Remote Access Protocol */ + {NULL, 0, 0, NULL, NULL}, /* Password-Retry */ + {NULL, 0, 0, NULL, NULL}, /* Prompt */ + {NULL, 0, 0, NULL, NULL}, /* Connect-Info */ + {NULL, 0, 0, NULL, NULL}, /* Configuration-Token */ + {NULL, 0, 0, NULL, NULL}, /* EAP-Message */ + {"Message-Authenticator", MD5_DIGEST_SIZE, MD5_DIGEST_SIZE, NULL, NULL}, }; /* Encode User-Password attribute. */ diff --git a/src/lib/krad/attrset.c b/src/lib/krad/attrset.c index 6ec031e320..e5457ebfd7 100644 --- a/src/lib/krad/attrset.c +++ b/src/lib/krad/attrset.c @@ -164,15 +164,44 @@ krad_attrset_copy(const krad_attrset *set, krad_attrset **copy) return 0; } +/* Place an encoded attributes into outbuf at position *i. Increment *i by the + * length of the encoding. */ +static krb5_error_code +append_attr(krb5_context ctx, const char *secret, + const uint8_t *auth, krad_attr type, const krb5_data *data, + uint8_t outbuf[MAX_ATTRSETSIZE], size_t *i, krb5_boolean *is_fips) +{ + uint8_t buffer[MAX_ATTRSIZE]; + size_t attrlen; + krb5_error_code retval; + + retval = kr_attr_encode(ctx, secret, auth, type, data, buffer, &attrlen, + is_fips); + if (retval) + return retval; + + if (attrlen > MAX_ATTRSETSIZE - *i - 2) + return EMSGSIZE; + + outbuf[(*i)++] = type; + outbuf[(*i)++] = attrlen + 2; + memcpy(outbuf + *i, buffer, attrlen); + *i += attrlen; + + return 0; +} + krb5_error_code kr_attrset_encode(const krad_attrset *set, const char *secret, - const unsigned char *auth, + const uint8_t *auth, krb5_boolean add_msgauth, unsigned char outbuf[MAX_ATTRSETSIZE], size_t *outlen, krb5_boolean *is_fips) { - unsigned char buffer[MAX_ATTRSIZE]; krb5_error_code retval; - size_t i = 0, attrlen; + krad_attr msgauth_type = krad_attr_name2num("Message-Authenticator"); + const uint8_t zeroes[MD5_DIGEST_SIZE] = { 0 }; + krb5_data zerodata; + size_t i = 0; attr *a; if (set == NULL) { @@ -180,19 +209,21 @@ kr_attrset_encode(const krad_attrset *set, const char *secret, return 0; } - K5_TAILQ_FOREACH(a, &set->list, list) { - retval = kr_attr_encode(set->ctx, secret, auth, a->type, &a->attr, - buffer, &attrlen, is_fips); - if (retval != 0) + if (add_msgauth) { + /* Encode Message-Authenticator as the first attribute, per + * draft-ietf-radext-deprecating-radius-03 section 5.2. */ + zerodata = make_data((uint8_t *)zeroes, MD5_DIGEST_SIZE); + retval = append_attr(set->ctx, secret, auth, msgauth_type, &zerodata, + outbuf, &i, is_fips); + if (retval) return retval; + } - if (i + attrlen + 2 > MAX_ATTRSETSIZE) - return EMSGSIZE; - - outbuf[i++] = a->type; - outbuf[i++] = attrlen + 2; - memcpy(&outbuf[i], buffer, attrlen); - i += attrlen; + K5_TAILQ_FOREACH(a, &set->list, list) { + retval = append_attr(set->ctx, secret, auth, a->type, &a->attr, + outbuf, &i, is_fips); + if (retval) + return retval; } *outlen = i; diff --git a/src/lib/krad/internal.h b/src/lib/krad/internal.h index a17b6f39b1..ca66f3ec68 100644 --- a/src/lib/krad/internal.h +++ b/src/lib/krad/internal.h @@ -49,6 +49,8 @@ #define UCHAR_MAX 255 #endif +#define MD5_DIGEST_SIZE 16 + /* RFC 2865 */ #define MAX_ATTRSIZE (UCHAR_MAX - 2) #define MAX_ATTRSETSIZE (KRAD_PACKET_SIZE_MAX - 20) @@ -79,10 +81,11 @@ kr_attr_decode(krb5_context ctx, const char *secret, const unsigned char *auth, krad_attr type, const krb5_data *in, unsigned char outbuf[MAX_ATTRSIZE], size_t *outlen); -/* Encode the attributes into the buffer. */ +/* Encode set into outbuf. If add_msgauth is true, include a zeroed + * Message-Authenticator as the first attribute. */ krb5_error_code kr_attrset_encode(const krad_attrset *set, const char *secret, - const unsigned char *auth, + const uint8_t *auth, krb5_boolean add_msgauth, unsigned char outbuf[MAX_ATTRSETSIZE], size_t *outlen, krb5_boolean *is_fips); diff --git a/src/lib/krad/packet.c b/src/lib/krad/packet.c index c5446b890c..3c1a4d507e 100644 --- a/src/lib/krad/packet.c +++ b/src/lib/krad/packet.c @@ -36,6 +36,7 @@ typedef unsigned char uchar; /* RFC 2865 */ +#define MSGAUTH_SIZE (2 + MD5_DIGEST_SIZE) #define OFFSET_CODE 0 #define OFFSET_ID 1 #define OFFSET_LENGTH 2 @@ -222,6 +223,106 @@ packet_set_attrset(krb5_context ctx, const char *secret, krad_packet *pkt) return kr_attrset_decode(ctx, &tmp, secret, pkt_auth(pkt), &pkt->attrset); } +/* Determine if a packet requires a Message-Authenticator attribute. */ +static inline krb5_boolean +requires_msgauth(const char *secret, krad_code code) +{ + /* If no secret is provided, assume that the transport is a UNIX socket. + * Message-Authenticator is required only on UDP and TCP connections. */ + if (*secret == '\0') + return FALSE; + + /* + * Per draft-ietf-radext-deprecating-radius-03 sections 5.2.1 and 5.2.4, + * Message-Authenticator is required in Access-Request packets and all + * potential responses when UDP or TCP transport is used. + */ + return code == krad_code_name2num("Access-Request") || + code == krad_code_name2num("Access-Reject") || + code == krad_code_name2num("Access-Accept") || + code == krad_code_name2num("Access-Challenge"); +} + +/* Check if the packet has a Message-Authenticator attribute. */ +static inline krb5_boolean +has_pkt_msgauth(const krad_packet *pkt) +{ + krad_attr msgauth_type = krad_attr_name2num("Message-Authenticator"); + + return krad_attrset_get(pkt->attrset, msgauth_type, 0) != NULL; +} + +/* Return the beginning of the Message-Authenticator attribute in pkt, or NULL + * if no such attribute is present. */ +static const uint8_t * +lookup_msgauth_addr(const krad_packet *pkt) +{ + krad_attr msgauth_type = krad_attr_name2num("Message-Authenticator"); + size_t i; + uint8_t *p; + + i = OFFSET_ATTR; + while (i + 2 < pkt->pkt.length) { + p = (uint8_t *)offset(&pkt->pkt, i); + if (msgauth_type == *p) + return p; + i += p[1]; + } + + return NULL; +} + +/* + * Calculate the message authenticator MAC for pkt as specified in RFC 2869 + * section 5.14, placing the result in mac_out. Use the provided authenticator + * auth, which may be from pkt or from a corresponding request. + */ +static krb5_error_code +calculate_mac(const char *secret, const krad_packet *pkt, + const uint8_t auth[AUTH_FIELD_SIZE], + uint8_t mac_out[MD5_DIGEST_SIZE]) +{ + uint8_t zeroed_msgauth[MSGAUTH_SIZE]; + krad_attr msgauth_type = krad_attr_name2num("Message-Authenticator"); + const uint8_t *msgauth_attr, *msgauth_end, *pkt_end; + krb5_crypto_iov input[5]; + krb5_data ksecr, mac; + + msgauth_attr = lookup_msgauth_addr(pkt); + if (msgauth_attr == NULL) + return EINVAL; + msgauth_end = msgauth_attr + MSGAUTH_SIZE; + pkt_end = (const uint8_t *)pkt->pkt.data + pkt->pkt.length; + + /* Read code, id, and length from the packet. */ + input[0].flags = KRB5_CRYPTO_TYPE_DATA; + input[0].data = make_data(pkt->pkt.data, OFFSET_AUTH); + + /* Read the provided authenticator. */ + input[1].flags = KRB5_CRYPTO_TYPE_DATA; + input[1].data = make_data((uint8_t *)auth, AUTH_FIELD_SIZE); + + /* Read any attributes before Message-Authenticator. */ + input[2].flags = KRB5_CRYPTO_TYPE_DATA; + input[2].data = make_data(pkt_attr(pkt), msgauth_attr - pkt_attr(pkt)); + + /* Read Message-Authenticator with the data bytes all set to zero, per RFC + * 2869 section 5.14. */ + zeroed_msgauth[0] = msgauth_type; + zeroed_msgauth[1] = MSGAUTH_SIZE; + memset(zeroed_msgauth + 2, 0, MD5_DIGEST_SIZE); + input[3].flags = KRB5_CRYPTO_TYPE_DATA; + input[3].data = make_data(zeroed_msgauth, MSGAUTH_SIZE); + + /* Read any attributes after Message-Authenticator. */ + input[4].flags = KRB5_CRYPTO_TYPE_DATA; + input[4].data = make_data((uint8_t *)msgauth_end, pkt_end - msgauth_end); + + mac = make_data(mac_out, MD5_DIGEST_SIZE); + ksecr = string2data((char *)secret); + return k5_hmac_md5(&ksecr, input, 5, &mac); +} + ssize_t krad_packet_bytes_needed(const krb5_data *buffer) { @@ -255,6 +356,7 @@ krad_packet_new_request(krb5_context ctx, const char *secret, krad_code code, krad_packet *pkt; uchar id; size_t attrset_len; + krb5_boolean msgauth_required; pkt = packet_new(); if (pkt == NULL) { @@ -274,9 +376,13 @@ krad_packet_new_request(krb5_context ctx, const char *secret, krad_code code, if (retval != 0) goto error; + /* Determine if Message-Authenticator is required. */ + msgauth_required = (*secret != '\0' && + code == krad_code_name2num("Access-Request")); + /* Encode the attributes. */ - retval = kr_attrset_encode(set, secret, pkt_auth(pkt), pkt_attr(pkt), - &attrset_len, &pkt->is_fips); + retval = kr_attrset_encode(set, secret, pkt_auth(pkt), msgauth_required, + pkt_attr(pkt), &attrset_len, &pkt->is_fips); if (retval != 0) goto error; @@ -285,6 +391,13 @@ krad_packet_new_request(krb5_context ctx, const char *secret, krad_code code, pkt_code_set(pkt, code); pkt_len_set(pkt, pkt->pkt.length); + if (msgauth_required) { + /* Calculate and set the Message-Authenticator MAC. */ + retval = calculate_mac(secret, pkt, pkt_auth(pkt), pkt_attr(pkt) + 2); + if (retval != 0) + goto error; + } + /* Copy the attrset for future use. */ retval = packet_set_attrset(ctx, secret, pkt); if (retval != 0) @@ -307,14 +420,19 @@ krad_packet_new_response(krb5_context ctx, const char *secret, krad_code code, krb5_error_code retval; krad_packet *pkt; size_t attrset_len; + krb5_boolean msgauth_required; pkt = packet_new(); if (pkt == NULL) return ENOMEM; + /* Determine if Message-Authenticator is required. */ + msgauth_required = requires_msgauth(secret, code); + /* Encode the attributes. */ - retval = kr_attrset_encode(set, secret, pkt_auth(request), pkt_attr(pkt), - &attrset_len, &pkt->is_fips); + retval = kr_attrset_encode(set, secret, pkt_auth(request), + msgauth_required, pkt_attr(pkt), &attrset_len, + &pkt->is_fips); if (retval != 0) goto error; @@ -330,6 +448,18 @@ krad_packet_new_response(krb5_context ctx, const char *secret, krad_code code, if (retval != 0) goto error; + if (msgauth_required) { + /* + * Calculate and replace the Message-Authenticator MAC. Per RFC 2869 + * section 5.14, use the authenticator from the request, not from the + * response. + */ + retval = calculate_mac(secret, pkt, pkt_auth(request), + pkt_attr(pkt) + 2); + if (retval != 0) + goto error; + } + /* Copy the attrset for future use. */ retval = packet_set_attrset(ctx, secret, pkt); if (retval != 0) @@ -343,6 +473,34 @@ error: return retval; } +/* Verify the Message-Authenticator value in pkt, using the provided + * authenticator (which may be from pkt or from a corresponding request). */ +static krb5_error_code +verify_msgauth(const char *secret, const krad_packet *pkt, + const uint8_t auth[AUTH_FIELD_SIZE]) +{ + uint8_t mac[MD5_DIGEST_SIZE]; + krad_attr msgauth_type = krad_attr_name2num("Message-Authenticator"); + const krb5_data *msgauth; + krb5_error_code retval; + + msgauth = krad_packet_get_attr(pkt, msgauth_type, 0); + if (msgauth == NULL) + return ENODATA; + + retval = calculate_mac(secret, pkt, auth, mac); + if (retval) + return retval; + + if (msgauth->length != MD5_DIGEST_SIZE) + return EMSGSIZE; + + if (k5_bcmp(mac, msgauth->data, MD5_DIGEST_SIZE) != 0) + return EBADMSG; + + return 0; +} + /* Decode a packet. */ static krb5_error_code decode_packet(krb5_context ctx, const char *secret, const krb5_data *buffer, @@ -394,21 +552,35 @@ krad_packet_decode_request(krb5_context ctx, const char *secret, krad_packet **reqpkt) { const krad_packet *tmp = NULL; + krad_packet *req; krb5_error_code retval; - retval = decode_packet(ctx, secret, buffer, reqpkt); - if (cb != NULL && retval == 0) { + retval = decode_packet(ctx, secret, buffer, &req); + if (retval) + return retval; + + /* Verify Message-Authenticator if present. */ + if (has_pkt_msgauth(req)) { + retval = verify_msgauth(secret, req, pkt_auth(req)); + if (retval) { + krad_packet_free(req); + return retval; + } + } + + if (cb != NULL) { for (tmp = (*cb)(data, FALSE); tmp != NULL; tmp = (*cb)(data, FALSE)) { if (pkt_id_get(*reqpkt) == pkt_id_get(tmp)) break; } - } - if (cb != NULL && (retval != 0 || tmp != NULL)) - (*cb)(data, TRUE); + if (tmp != NULL) + (*cb)(data, TRUE); + } + *reqpkt = req; *duppkt = tmp; - return retval; + return 0; } krb5_error_code @@ -435,9 +607,17 @@ krad_packet_decode_response(krb5_context ctx, const char *secret, break; } - /* If the authenticator matches, then the response is valid. */ - if (memcmp(pkt_auth(*rsppkt), auth, sizeof(auth)) == 0) - break; + /* Verify the response authenticator. */ + if (k5_bcmp(pkt_auth(*rsppkt), auth, sizeof(auth)) != 0) + continue; + + /* Verify Message-Authenticator if present. */ + if (has_pkt_msgauth(*rsppkt)) { + if (verify_msgauth(secret, *rsppkt, pkt_auth(tmp)) != 0) + continue; + } + + break; } } diff --git a/src/lib/krad/t_attrset.c b/src/lib/krad/t_attrset.c index 4cdb8b7d8e..f9c66509bd 100644 --- a/src/lib/krad/t_attrset.c +++ b/src/lib/krad/t_attrset.c @@ -63,7 +63,7 @@ main(void) noerror(krad_attrset_add(set, krad_attr_name2num("User-Password"), &tmp)); /* Encode attrset. */ - noerror(kr_attrset_encode(set, "foo", auth, buffer, &encode_len, + noerror(kr_attrset_encode(set, "foo", auth, FALSE, buffer, &encode_len, &is_fips)); krad_attrset_free(set); diff --git a/src/lib/krad/t_daemon.py b/src/lib/krad/t_daemon.py index 4a3de079c7..647d4894eb 100755 --- a/src/lib/krad/t_daemon.py +++ b/src/lib/krad/t_daemon.py @@ -40,6 +40,7 @@ DICTIONARY = """ ATTRIBUTE\tUser-Name\t1\tstring ATTRIBUTE\tUser-Password\t2\toctets ATTRIBUTE\tNAS-Identifier\t32\tstring +ATTRIBUTE\tMessage-Authenticator\t80\toctets """ class TestServer(server.Server): @@ -52,7 +53,7 @@ class TestServer(server.Server): if key == "User-Password": passwd = [pkt.PwDecrypt(x) for x in pkt[key]] - reply = self.CreateReplyPacket(pkt) + reply = self.CreateReplyPacket(pkt, message_authenticator=True) if passwd == ['accept']: reply.code = packet.AccessAccept else: diff --git a/src/lib/krad/t_packet.c b/src/lib/krad/t_packet.c index c22489144f..104b6507a2 100644 --- a/src/lib/krad/t_packet.c +++ b/src/lib/krad/t_packet.c @@ -172,6 +172,9 @@ main(int argc, const char **argv) krb5_data username, password; krb5_boolean auth = FALSE; krb5_context ctx; + const krad_packet *dupreq; + const krb5_data *encpkt; + krad_packet *decreq; username = string2data("testUser"); @@ -184,9 +187,17 @@ main(int argc, const char **argv) password = string2data("accept"); noerror(make_packet(ctx, &username, &password, &packets[ACCEPT_PACKET])); + encpkt = krad_packet_encode(packets[ACCEPT_PACKET]); + noerror(krad_packet_decode_request(ctx, "foo", encpkt, NULL, NULL, + &dupreq, &decreq)); + krad_packet_free(decreq); password = string2data("reject"); noerror(make_packet(ctx, &username, &password, &packets[REJECT_PACKET])); + encpkt = krad_packet_encode(packets[REJECT_PACKET]); + noerror(krad_packet_decode_request(ctx, "foo", encpkt, NULL, NULL, + &dupreq, &decreq)); + krad_packet_free(decreq); memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; diff --git a/src/tests/t_otp.py b/src/tests/t_otp.py index c3b820a411..dd5cdc5c26 100755 --- a/src/tests/t_otp.py +++ b/src/tests/t_otp.py @@ -49,6 +49,7 @@ ATTRIBUTE User-Name 1 string ATTRIBUTE User-Password 2 octets ATTRIBUTE Service-Type 6 integer ATTRIBUTE NAS-Identifier 32 string +ATTRIBUTE Message-Authenticator 80 octets ''' class RadiusDaemon(Process): @@ -97,6 +98,8 @@ class RadiusDaemon(Process): reply.code = packet.AccessReject replyq['reply'] = False + reply.add_message_authenticator() + outq.put(replyq) if addr is None: sock.send(reply.ReplyPacket()) -- 2.46.0