skip to content

How does a RADIUS access device check that an Access-Accept (Code 2) really came from the server holding the shared secret?

level: middleimportance: must knowfreq 50%

answer

  1. recompute, then compare sixteen octets
  2. the reply is digested with the request's nonce
  3. secret appended, not keyed in
  4. mismatch means silent discard, not reject
  5. zeroed field on Accounting-Request (Code 4)

basics

~20 s

It recomputes the Response Authenticator as MD5 over the reply's Code, Identifier and Length, the Request Authenticator from its own outstanding request, the reply's attributes and the shared secret appended, then compares that digest with the reply's Authenticator field.

solid answer

~40 s

The reply's 16-octet `Authenticator` field carries the **Response Authenticator**, defined as `MD5(Code + Identifier + Length + RequestAuth + Attributes + Secret)`. `Code`, `Identifier`, `Length` and `Attributes` are the reply's own; `RequestAuth` is the 16-octet `Request Authenticator` the access device put in the matching `Access-Request (Code 1)`; the secret is appended last. The access device matches the reply to an outstanding request by `Identifier`, substitutes the saved `Request Authenticator` into the digest input in place of the field it received, recomputes, and compares. A mismatch means the packet is discarded silently — it is not a reject. Note the construction: this is an **unkeyed MD5 digest with the secret glued on the end**, not an HMAC, which is the weakness the protocol's whole security story turns on.

code

pseudocode · 16 lines
pseudocode
function verify_reply(reply, outstanding, secret):
    request = outstanding.lookup(reply.Identifier)
    if request is absent:
        discard reply

    input = reply.Code
          + reply.Identifier
          + reply.Length
          + request.RequestAuthenticator   // the saved nonce, not the received field
          + reply.Attributes
          + secret                         // appended last

    if MD5(input) equals reply.Authenticator:
        accept reply
    else:
        discard reply silently             // no error packet is defined

go deeper

for a junior

Remember the shape: the reply carries sixteen octets the access device can recompute only because it knows the same secret, and a mismatch means the reply is thrown away.

for a middle

Be able to list the digest's inputs in order and say which come from the reply and which from the request; that ordering is the question behind the question.

for a senior

Explain why an appended secret is not a keyed MAC, and what an on-path attacker gains from a broken hash when Message-Authenticator (80) is absent.

for a principal

Treat it as a design lesson: a homegrown keyed-hash construction from 1997 aged into an estate-wide exposure, and the fix had to be a transport, not a patch.

## The field, and what goes in it Every RADIUS packet ends its header with a 16-octet `Authenticator`, the last of `Code`, `Identifier`, `Length`, `Authenticator`. What that field holds depends on the packet. In an `Access-Request (Code 1)` it is the **Request Authenticator**: a value the access device must make **unpredictable and non-repeating**, because it also seeds the keystream that hides `User-Password (2)`. In a reply — `Access-Accept (Code 2)`, `Access-Reject (Code 3)`, `Access-Challenge (Code 11)` — it is the **Response Authenticator**: `Response Authenticator = MD5(Code + Identifier + Length + RequestAuth + Attributes + Secret)` where `Code`, `Identifier`, `Length` and `Attributes` are the **reply's**, `RequestAuth` is the Request Authenticator copied from the request being answered, and `Secret` is the shared secret appended to the end of the input. ## How the access device checks it 1. Read the reply's `Identifier` and find the outstanding request it belongs to. If none matches, drop the packet. 2. Retrieve the `Request Authenticator` saved with that request. 3. Build the digest input from the reply's `Code`, `Identifier` and `Length`, then the **saved** `Request Authenticator` in place of the 16 octets actually received in the field, then the reply's attributes, then the shared secret. 4. Compute MD5 over that input and compare it with the `Authenticator` octets the reply carried. 5. On a mismatch, discard silently. There is no error packet for this; a forged or corrupted reply simply never happened, and the request will be retransmitted until it times out. The binding to the request's nonce is what makes a reply usable exactly once for exactly one request: an old `Access-Accept (Code 2)` replayed later will not verify, because the `Request Authenticator` it was computed over is not the one the device is now waiting on. ## The same field, computed differently, on other codes Some packets originate a conversation but have no `Access-Request` to borrow a nonce from. There, the field is **zeroed during computation** and then overwritten with the result. | Packet | Authenticator field while computing | What the field finally holds | |---|---|---| | `Access-Request (Code 1)` | not computed | an unpredictable, non-repeating nonce | | `Access-Accept (Code 2)` / `Access-Reject (Code 3)` / `Access-Challenge (Code 11)` | the request's Request Authenticator | the Response Authenticator digest | | `Accounting-Request (Code 4)` | 16 zero octets | the resulting MD5 digest | | `CoA-Request (Code 43)` / `Disconnect-Request (Code 40)` | 16 zero octets | the resulting MD5 digest | | `Accounting-Response (Code 5)`, `CoA-ACK (44)`, `Disconnect-ACK (41)` and the NAKs | the request's Authenticator value | the Response Authenticator digest | The zeroing is not an optimisation; it is the only way to define a deterministic input when there is no earlier nonce in the exchange. It also means these request packets carry no freshness of their own, which is why a `CoA-Request (Code 43)` receiver is expected to identify the session precisely rather than trust the packet's novelty. ## Why this is weaker than it looks - **It is `MD5(data || secret)`, not HMAC.** Appending a key to a hash input is an obsolete construction; HMAC exists precisely because appending or prepending a key to a raw hash has no proof behind it. - **MD5's collision resistance is broken.** Demonstrated in 2024: an on-path attacker between the access device and the server, able to inject a chosen `Proxy-State (33)` blob, can arrange a collision that lets a genuine `Access-Reject (Code 3)` be presented as an `Access-Accept (Code 2)`. The mitigation is `Message-Authenticator (80)` on every packet, because HMAC-MD5 is a keyed construction that collisions of this kind do not break. - **Each digest is an offline oracle.** An observer holding the request and the reply has everything but the secret, so candidate secrets can be tested locally at full speed. - **Silent discard hides attacks.** A verification failure produces no packet and, in many deployments, no distinctive log line, so a mismatched-secret condition and a forgery attempt look the same from the outside. ## What to take away The Response Authenticator answers a real question — *did the party that holds this secret write this reply, in answer to this request?* — and it answers it cheaply, with no state beyond the outstanding request. What it does not do is survive contact with a broken hash. That is the reason the standards response to RADIUS's security was not a stronger digest bolted onto the same packet, but moving the whole exchange onto a transport that authenticates and encrypts it: RADIUS over TLS on TCP port 2083 (RFC 6614) and RADIUS/DTLS on UDP port 2083 (RFC 7360), with RADIUS/1.1 (RFC 9765) removing the construction altogether.

  • Why is the Authenticator field zeroed while an Accounting-Request (Code 4) is computed?
    Because there is no earlier request in the exchange to borrow a Request Authenticator from. Zeroing the 16 octets gives a deterministic input both sides can reproduce; the sender then writes the resulting digest into the field, and the `Accounting-Response (Code 5)` is computed over that value in turn.
  • What does an access device do when a reply's Response Authenticator does not verify?
    It discards the packet silently. RADIUS defines no error reply for this condition, so the outstanding request stays outstanding and is retransmitted until the retry budget is spent. That is why a mismatched secret typically surfaces as a timeout rather than as an authentication failure.
  • Does the Response Authenticator protect against replay of an old Access-Accept (Code 2)?
    Against straightforward replay, yes: the digest is computed over the Request Authenticator of the specific request being answered, so an old reply will not verify against a new nonce. That holds only while the access device genuinely makes each Request Authenticator unpredictable and non-repeating.

saying these in an interview costs you the question

  • Calls the Response Authenticator an HMAC over the reply
  • Says it is computed over the reply's own Authenticator field
  • Thinks a mismatch produces an Access-Reject (Code 3)
  • Assumes MD5 here is safe because nothing is being encrypted
  • Believes every packet type puts a fresh nonce in the field
  • Claims the digest also protects the request that preceded it