From 871125fea8ce0370a972bf65f7d1de63f619b06c Mon Sep 17 00:00:00 2001
From: Julien Rische <jrische@redhat.com>
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
---
 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                 |  58 +++++--
 src/lib/krad/internal.h                |   7 +-
 src/lib/krad/packet.c                  | 205 +++++++++++++++++++++++--
 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, 309 insertions(+), 31 deletions(-)

diff --git a/src/include/k5-int.h b/src/include/k5-int.h
index 863d9fe9cf9..8143dbba75c 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -2404,4 +2404,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 ec024f39661..a809388549f 100644
--- a/src/lib/crypto/krb/checksum_hmac_md5.c
+++ b/src/lib/crypto/krb/checksum_hmac_md5.c
@@ -92,3 +92,31 @@ krb5_error_code krb5int_hmacmd5_checksum(const struct krb5_cksumtypes *ctp,
     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 052f4d4b510..2b27028a0f5 100644
--- a/src/lib/crypto/libk5crypto.exports
+++ b/src/lib/crypto/libk5crypto.exports
@@ -103,3 +103,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 9c13d9d7556..4ad32122a8e 100644
--- a/src/lib/krad/attr.c
+++ b/src/lib/krad/attr.c
@@ -122,6 +122,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 f309f1581c1..488bfce7bbb 100644
--- a/src/lib/krad/attrset.c
+++ b/src/lib/krad/attrset.c
@@ -164,14 +164,42 @@ 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)
+{
+    uint8_t buffer[MAX_ATTRSIZE];
+    size_t attrlen;
+    krb5_error_code retval;
+
+    retval = kr_attr_encode(ctx, secret, auth, type, data, buffer, &attrlen);
+    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)
 {
-    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) {
@@ -179,19 +207,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);
-        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);
+        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);
+        if (retval)
+            return retval;
     }
 
     *outlen = i;
diff --git a/src/lib/krad/internal.h b/src/lib/krad/internal.h
index 7619563fc56..e2a16c77a64 100644
--- a/src/lib/krad/internal.h
+++ b/src/lib/krad/internal.h
@@ -43,6 +43,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)
@@ -65,10 +67,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);
 
 /* Decode attributes from a buffer. */
diff --git a/src/lib/krad/packet.c b/src/lib/krad/packet.c
index aee830b651f..7e599ab39bd 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);
+    retval = kr_attrset_encode(set, secret, pkt_auth(pkt), msgauth_required,
+                               pkt_attr(pkt), &attrset_len);
     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,18 @@ 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);
+    retval = kr_attrset_encode(set, secret, pkt_auth(request),
+                               msgauth_required, pkt_attr(pkt), &attrset_len);
     if (retval != 0)
         goto error;
 
@@ -330,6 +447,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 +472,34 @@ krad_packet_new_response(krb5_context ctx, const char *secret, krad_code code,
     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 +551,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 +606,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 085f4b25d72..a520fe10ebd 100644
--- a/src/lib/krad/t_attrset.c
+++ b/src/lib/krad/t_attrset.c
@@ -62,7 +62,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));
     krad_attrset_free(set);
 
     /* Manually encode User-Name. */
diff --git a/src/lib/krad/t_daemon.py b/src/lib/krad/t_daemon.py
index 4a3de079c7d..647d4894eb8 100755
--- a/src/lib/krad/t_daemon.py
+++ b/src/lib/krad/t_daemon.py
@@ -40,6 +40,7 @@
 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 @@ def _HandleAuthPacket(self, pkt):
             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 c22489144f4..104b6507a26 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 c3b820a411d..dd5cdc5c26d 100755
--- a/src/tests/t_otp.py
+++ b/src/tests/t_otp.py
@@ -49,6 +49,7 @@
 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 @@ def run(self):
             reply.code = packet.AccessReject
             replyq['reply'] = False
 
+        reply.add_message_authenticator()
+
         outq.put(replyq)
         if addr is None:
             sock.send(reply.ReplyPacket())
