Skip to content

Commit 4271830

Browse files
tverlaanbearice
authored andcommitted
fix verifying message authenticator on replies
1 parent 03ef2a3 commit 4271830

6 files changed

Lines changed: 247 additions & 86 deletions

File tree

example.exs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ attrs = [
3838
{255, "123456"}
3939
]
4040

41-
# for request packets, leave auth=nil will generate with random bytes
42-
p = %Radius.Packet{code: "Access-Request", id: 12, auth: nil, secret: secret, attrs: attrs}
41+
# for request packets, authenticator will generate with random bytes
42+
p = %Radius.Packet{code: "Access-Request", id: 12, secret: secret, attrs: attrs}
4343
# will return an iolist
44-
data = Radius.Packet.encode(p)
45-
Logger.debug("data=#{inspect(data)}")
44+
%{raw: data} = Radius.Packet.encode_request(p)
45+
Logger.debug("data=#{inspect(packet.raw)}")
4646

4747
p = Radius.Packet.decode(:erlang.iolist_to_binary(data), secret)
4848
Logger.debug(inspect(p, pretty: true))
4949

50-
# for response packets, set auth=request.auth to generate new HMAC-hash with it.
51-
p = %Radius.Packet{code: "Access-Accept", id: 12, auth: p.auth, secret: secret, attrs: p.attrs}
52-
data = Radius.Packet.encode(p)
50+
# for response packets, provide request.auth to generate new HMAC-hash with it.
51+
p2 = %Radius.Packet{code: "Access-Accept", id: 12, secret: secret, attrs: p.attrs}
52+
%{raw: data} = Radius.Packet.encode_reply(p2, p.auth)
5353
Logger.debug("data=#{inspect(data)}")
5454
# password decoding SHOULD FAIL here, guess why?
55-
p = Radius.Packet.decode(:erlang.iolist_to_binary(data), p.secret)
55+
p = Radius.Packet.decode(:erlang.iolist_to_binary(data), p2.secret)
5656
Logger.debug(inspect(p, pretty: true))
5757

5858
# wrapper of gen_udp

lib/radius.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ defmodule Radius do
3232
packet:: %Radius.Packet{}
3333
"""
3434
def send(sk, {host, port}, packet) do
35-
data = Packet.encode(packet)
35+
%{raw: data} = Packet.encode_reply(packet, packet.auth)
3636
:gen_udp.send(sk, host, port, data)
3737
end
3838
end

lib/radius/dict.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ defmodule Radius.Dict do
215215
|> String.to_charlist()
216216
|> tokenlize!
217217
|> parse!
218-
|> (&process_dict(ctx, &1)).()
218+
|> then(&process_dict(ctx, &1))
219219
rescue
220220
e in ParserError -> reraise %{e | file: path}, __STACKTRACE__
221221
e -> reraise e, __STACKTRACE__

lib/radius/packet.ex

Lines changed: 105 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
defmodule Radius.Packet do
22
require Logger
33

4+
alias __MODULE__
45
alias Radius.Dict.Attribute
56
alias Radius.Dict.Vendor
67
alias Radius.Dict.Value
78
alias Radius.Dict.EntryNotFoundError
89

10+
@type t :: %{
11+
code: String.t(),
12+
id: integer(),
13+
length: integer(),
14+
auth: binary(),
15+
attrs: keyword(),
16+
raw: iolist(),
17+
secret: binary()
18+
}
919
defstruct code: nil,
1020
id: nil,
1121
length: nil,
@@ -196,6 +206,7 @@ defmodule Radius.Packet do
196206
ipaddr :: {a,b,c,d} | {a,b,c,d,e,f,g,h}
197207
198208
"""
209+
@deprecated "Use encode_request/1-2 or encode_reply/1-2 instead"
199210
def encode(packet, options \\ []) do
200211
sign? = options |> Keyword.get(:sign, false)
201212
raw? = options |> Keyword.get(:raw, false)
@@ -211,11 +222,7 @@ defmodule Radius.Packet do
211222

212223
packet =
213224
if sign? do
214-
attrs =
215-
packet.attrs ++
216-
[
217-
{"Message-Authenticator", <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>}
218-
]
225+
attrs = packet.attrs ++ [{"Message-Authenticator", <<0::size(128)>>}]
219226

220227
%{packet | attrs: attrs}
221228
else
@@ -256,6 +263,77 @@ defmodule Radius.Packet do
256263
[header, attrs]
257264
end
258265

266+
@doc """
267+
Encode the request packet into an iolist and put the result in the `:raw` key. The `:auth` key will contain
268+
the authenticator used on the request.
269+
"""
270+
@spec encode_request(packet :: Packet.t(), options :: keyword()) :: Packet.t()
271+
def encode_request(packet, options \\ []) do
272+
packet = %{packet | auth: :crypto.strong_rand_bytes(16)}
273+
{header, attrs} = encode_packet(packet, options)
274+
275+
%{packet | raw: [header, attrs]}
276+
end
277+
278+
@doc """
279+
Encode the reply packet into an iolist and put the result in the `:raw` key. The `:auth` key needs
280+
to be filled with the authenticator of the request packet.
281+
"""
282+
@spec encode_reply(
283+
packet :: Packet.t(),
284+
request_authenticator :: binary(),
285+
options :: keyword()
286+
) :: Packet.t()
287+
def encode_reply(packet, request_authenticator, options \\ []) do
288+
packet = %{packet | auth: request_authenticator}
289+
{header, attrs} = encode_packet(packet, options)
290+
291+
resp_auth =
292+
:crypto.hash_init(:md5)
293+
|> :crypto.hash_update(header)
294+
|> :crypto.hash_update(attrs)
295+
|> :crypto.hash_update(packet.secret)
296+
|> :crypto.hash_final()
297+
298+
header = <<header::bytes-size(4), resp_auth::binary>>
299+
300+
%{packet | auth: resp_auth, raw: [header, attrs]}
301+
end
302+
303+
defp encode_packet(packet, options) do
304+
sign? = options |> Keyword.get(:sign, false)
305+
306+
packet =
307+
if sign? do
308+
attrs = [{"Message-Authenticator", <<0::size(128)>>} | packet.attrs]
309+
310+
%{packet | attrs: attrs}
311+
else
312+
packet
313+
end
314+
315+
attrs = encode_attrs(packet)
316+
317+
code = encode_code(packet.code)
318+
length = 20 + IO.iodata_length(attrs)
319+
header = <<code, packet.id, length::size(16), packet.auth::binary>>
320+
321+
attrs =
322+
if sign? do
323+
signature = message_authenticator(packet.secret, [header, attrs])
324+
[<<t, l, _::binary>> | rest_attrs] = attrs
325+
[<<t, l, signature::binary>> | rest_attrs]
326+
else
327+
attrs
328+
end
329+
330+
{header, attrs}
331+
end
332+
333+
defp message_authenticator(secret, msg) do
334+
:crypto.mac(:hmac, :md5, secret, msg)
335+
end
336+
259337
defp encode_attrs(%{attrs: a} = ctx) do
260338
Enum.map(a, fn x ->
261339
x |> resolve_attr(ctx) |> encode_attr
@@ -422,47 +500,38 @@ defmodule Radius.Packet do
422500
@doc """
423501
Return the value of a given attribute, if found, or default otherwise.
424502
"""
425-
def get_attr(packet, attr_name, default \\ nil) do
426-
result =
427-
packet.attrs
428-
|> Enum.find(default, fn
429-
{^attr_name, _} -> true
430-
_ -> false
431-
end)
432-
433-
case result do
434-
{_, value} -> value
435-
_ -> nil
436-
end
503+
def get_attr(packet, attr_name) do
504+
for {^attr_name, value} <- packet.attrs, do: value
437505
end
438506

439507
@doc """
440508
Verify if the packet signature is valid.
509+
510+
(https://www.ietf.org/rfc/rfc2869.txt)
441511
"""
442512
def verify(packet) do
443-
# TODO: this code is going to fail when validating replies
444-
sig1 =
445-
packet
446-
|> Radius.Packet.get_attr("Message-Authenticator")
513+
verify(packet, packet.auth)
514+
end
447515

448-
if sig1 != nil do
449-
attrs =
450-
packet.attrs
451-
|> Enum.filter(fn {k, _} -> k != "Message-Authenticator" end)
516+
def verify(packet, request_authenticator) do
517+
case Radius.Packet.get_attr(packet, "Message-Authenticator") do
518+
[sig1] ->
519+
attrs =
520+
Enum.map(packet.attrs, fn
521+
{"Message-Authenticator", _} -> {"Message-Authenticator", <<0::size(128)>>}
522+
attr -> attr
523+
end)
452524

453-
raw =
454-
%{packet | attrs: attrs}
455-
|> Radius.Packet.encode(raw: true, sign: true)
456-
|> IO.iodata_to_binary()
525+
packet = %{packet | attrs: attrs}
526+
{header, attrs} = encode_packet(packet, [])
527+
<<code, id, length::size(16), _resp_auth::binary>> = header
528+
sign_header = <<code, id, length::size(16), request_authenticator::binary>>
457529

458-
crop_len = byte_size(raw) - 16
459-
<<_::bytes-size(crop_len), sig2::binary>> = raw
530+
sig2 = message_authenticator(packet.secret, [sign_header, attrs])
531+
sig1 == sig2
460532

461-
sig1 == sig2
462-
else
463-
false
533+
_ ->
534+
false
464535
end
465536
end
466537
end
467-
468-
# defmodule Packet

test/radius_packet_test.exs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
defmodule Radius.PacketTest do
2+
use ExUnit.Case, async: true
3+
4+
@secret "mykey"
5+
@sample_req %Radius.Packet{
6+
code: "Access-Request",
7+
id: 118,
8+
length: 173,
9+
auth: <<55, 91, 232, 245, 150, 233, 11, 207, 252, 94, 50, 146, 157, 20, 39, 91>>,
10+
attrs: [
11+
{"NAS-IP-Address", {10, 62, 1, 238}},
12+
{"NAS-Port", 50001},
13+
{"NAS-Port-Type", "Ethernet"},
14+
{"User-Name", "host/drswin7tracyp.drsl.co.uk"},
15+
{"Called-Station-Id", "00-12-00-E3-41-C1"},
16+
{"Calling-Station-Id", "B4-99-BA-F2-8A-D6"},
17+
{"Service-Type", "Framed-User"},
18+
{"Framed-MTU", 1500},
19+
{"EAP-Message",
20+
<<2, 0, 0, 34, 1, 104, 111, 115, 116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97,
21+
99, 121, 112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107>>},
22+
{"Message-Authenticator",
23+
<<201, 62, 246, 40, 105, 10, 87, 139, 49, 112, 155, 11, 188, 202, 222, 65>>}
24+
],
25+
raw: nil,
26+
secret: "mykey"
27+
}
28+
@sample_binary_req <<1, 118, 0, 173, 55, 91, 232, 245, 150, 233, 11, 207, 252, 94, 50, 146, 157,
29+
20, 39, 91, 4, 6, 10, 62, 1, 238, 5, 6, 0, 0, 195, 81, 61, 6, 0, 0, 0, 15,
30+
1, 31, 104, 111, 115, 116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114,
31+
97, 99, 121, 112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107, 30,
32+
19, 48, 48, 45, 49, 50, 45, 48, 48, 45, 69, 51, 45, 52, 49, 45, 67, 49, 31,
33+
19, 66, 52, 45, 57, 57, 45, 66, 65, 45, 70, 50, 45, 56, 65, 45, 68, 54, 6,
34+
6, 0, 0, 0, 2, 12, 6, 0, 0, 5, 220, 79, 36, 2, 0, 0, 34, 1, 104, 111, 115,
35+
116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97, 99, 121, 112, 46,
36+
100, 114, 115, 108, 46, 99, 111, 46, 117, 107, 80, 18, 201, 62, 246, 40,
37+
105, 10, 87, 139, 49, 112, 155, 11, 188, 202, 222, 65>>
38+
@sample_rep %Radius.Packet{
39+
code: "Access-Accept",
40+
id: 118,
41+
length: 173,
42+
auth: nil,
43+
attrs: [
44+
{"NAS-IP-Address", {10, 62, 1, 238}},
45+
{"NAS-Port", 50001},
46+
{"NAS-Port-Type", "Ethernet"},
47+
{"User-Name", "host/drswin7tracyp.drsl.co.uk"},
48+
{"Called-Station-Id", "00-12-00-E3-41-C1"},
49+
{"Calling-Station-Id", "B4-99-BA-F2-8A-D6"},
50+
{"Service-Type", "Framed-User"},
51+
{"Framed-MTU", 1500},
52+
{"EAP-Message",
53+
<<2, 0, 0, 34, 1, 104, 111, 115, 116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97,
54+
99, 121, 112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107>>}
55+
],
56+
raw: nil,
57+
secret: "mykey"
58+
}
59+
@sample_binary_rep <<2, 118, 0, 155, 25, 149, 189, 198, 178, 14, 197, 28, 131, 240, 157, 146,
60+
150, 38, 53, 105, 4, 6, 10, 62, 1, 238, 5, 6, 0, 0, 195, 81, 61, 6, 0, 0,
61+
0, 15, 1, 31, 104, 111, 115, 116, 47, 100, 114, 115, 119, 105, 110, 55,
62+
116, 114, 97, 99, 121, 112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117,
63+
107, 30, 19, 48, 48, 45, 49, 50, 45, 48, 48, 45, 69, 51, 45, 52, 49, 45,
64+
67, 49, 31, 19, 66, 52, 45, 57, 57, 45, 66, 65, 45, 70, 50, 45, 56, 65, 45,
65+
68, 54, 6, 6, 0, 0, 0, 2, 12, 6, 0, 0, 5, 220, 79, 36, 2, 0, 0, 34, 1, 104,
66+
111, 115, 116, 47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97, 99, 121,
67+
112, 46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107>>
68+
@sample_binary_rep_signed <<2, 118, 0, 173, 132, 213, 98, 44, 33, 151, 126, 7, 160, 110, 91, 18,
69+
56, 125, 67, 245, 80, 18, 27, 203, 27, 162, 52, 156, 30, 25, 241,
70+
43, 80, 77, 28, 109, 228, 77, 4, 6, 10, 62, 1, 238, 5, 6, 0, 0, 195,
71+
81, 61, 6, 0, 0, 0, 15, 1, 31, 104, 111, 115, 116, 47, 100, 114,
72+
115, 119, 105, 110, 55, 116, 114, 97, 99, 121, 112, 46, 100, 114,
73+
115, 108, 46, 99, 111, 46, 117, 107, 30, 19, 48, 48, 45, 49, 50, 45,
74+
48, 48, 45, 69, 51, 45, 52, 49, 45, 67, 49, 31, 19, 66, 52, 45, 57,
75+
57, 45, 66, 65, 45, 70, 50, 45, 56, 65, 45, 68, 54, 6, 6, 0, 0, 0,
76+
2, 12, 6, 0, 0, 5, 220, 79, 36, 2, 0, 0, 34, 1, 104, 111, 115, 116,
77+
47, 100, 114, 115, 119, 105, 110, 55, 116, 114, 97, 99, 121, 112,
78+
46, 100, 114, 115, 108, 46, 99, 111, 46, 117, 107>>
79+
80+
test "decode request" do
81+
packet = Radius.Packet.decode(@sample_binary_req, @secret)
82+
assert packet.code == @sample_req.code
83+
assert packet.id == @sample_req.id
84+
assert packet.length == @sample_req.length
85+
86+
assert packet.auth == @sample_req.auth
87+
end
88+
89+
test "encode request" do
90+
# cut authenticator as it will be generated on each encoding
91+
<<before::size(32), _random::size(128), rest::binary>> =
92+
@sample_req |> Radius.Packet.encode_request() |> Map.get(:raw) |> IO.iodata_to_binary()
93+
94+
<<sample_before::size(32), _random::size(128), sample_rest::binary>> = @sample_binary_req
95+
assert <<before::size(32), rest::binary>> == <<sample_before::size(32), sample_rest::binary>>
96+
end
97+
98+
test "encode reply" do
99+
reply =
100+
@sample_rep
101+
|> Radius.Packet.encode_reply(@sample_req.auth)
102+
|> Map.get(:raw)
103+
|> IO.iodata_to_binary()
104+
105+
assert reply == @sample_binary_rep
106+
end
107+
108+
test "encode and sign reply" do
109+
reply =
110+
@sample_rep
111+
|> Radius.Packet.encode_reply(@sample_req.auth, sign: true)
112+
|> Map.get(:raw)
113+
|> IO.iodata_to_binary()
114+
115+
assert reply == @sample_binary_rep_signed
116+
end
117+
118+
test "verify message authenticator signature on request" do
119+
assert Radius.Packet.verify(@sample_req)
120+
refute Radius.Packet.verify(%{@sample_req | id: 14})
121+
refute Radius.Packet.verify(%{@sample_req | attrs: []})
122+
end
123+
124+
test "verify message authenticator signature on reply" do
125+
packet = Radius.Packet.decode(@sample_binary_rep_signed, @secret)
126+
assert Radius.Packet.verify(packet, @sample_req.auth)
127+
refute Radius.Packet.verify(packet)
128+
refute Radius.Packet.verify(%{packet | id: 14}, @sample_req.auth)
129+
refute Radius.Packet.verify(%{packet | attrs: []}, @sample_req.auth)
130+
end
131+
end

0 commit comments

Comments
 (0)