File Coverage

blib/lib/Data/Radius/Packet.pm
Criterion Covered Total %
statement 146 213 68.5
branch 44 92 47.8
condition 21 66 31.8
subroutine 17 19 89.4
pod 4 7 57.1
total 232 397 58.4


line stmt bran cond sub pod time code
1             package Data::Radius::Packet;
2             # encode/decode RADIUS protocol messages
3              
4 1     1   1140 use v5.10;
  1         4  
5 1     1   5 use strict;
  1         2  
  1         19  
6 1     1   6 use warnings;
  1         1  
  1         23  
7 1     1   5 use Digest::MD5 ();
  1         2  
  1         13  
8 1     1   494 use Digest::HMAC_MD5 ();
  1         1446  
  1         22  
9 1     1   624 use bytes;
  1         15  
  1         4  
10              
11 1     1   35 use base qw(Class::Accessor::Fast);
  1         2  
  1         107  
12             __PACKAGE__->mk_accessors(qw(secret dict));
13              
14 1     1   6 use Data::Radius::Constants qw(:all);
  1         2  
  1         143  
15 1     1   442 use Data::Radius::Encode qw(encode);
  1         2  
  1         65  
16 1     1   443 use Data::Radius::Decode qw(decode);
  1         2  
  1         60  
17 1     1   8 use Data::Radius::Util qw(encrypt_pwd decrypt_pwd is_enum_type);
  1         2  
  1         55  
18              
19             use constant {
20             # common attributes
21 1         2273 ATTR_PASSWORD => 2,
22             ATTR_VENDOR => 26,
23             # Message-Authenticator
24             ATTR_MSG_AUTH_NAME => 'Message-Authenticator',
25             ATTR_MSG_AUTH => 80,
26             ATTR_MSG_AUTH_LEN => 18,
27              
28             # has extra byte in VSA header
29             WIMAX_VENDOR => 24757,
30 1     1   6 };
  1         2  
31              
32             my %IS_REPLY = map { $_ => 1 } (ACCESS_ACCEPT, ACCESS_REJECT, DISCONNECT_ACCEPT, DISCONNECT_REJECT, COA_ACCEPT, COA_REJECT);
33             my %IS_REQUEST = map { $_ => 1 } (ACCESS_REQUEST, ACCOUNTING_REQUEST, DISCONNECT_REQUEST, COA_REQUEST);
34              
35             my $request_id = int( rand(255) );
36              
37             # Digest::MD5 object
38             my $md5;
39              
40             sub new {
41 2     2 1 1522 my ($class, %h) = @_;
42             my $obj = {
43             secret => $h{secret},
44             dict => $h{dict},
45 2         8 };
46              
47 2         8 bless $obj, $class;
48             }
49              
50             # build new request
51             # input:
52             # type - radius code
53             # authenticator - for access request allow to override random one,
54             # for replies - value from request must be used
55             # av_list - array-ref of AV in {Name, Value} or {Id,Type,VendorId,Value} form
56             # dict - allow to override default dictionary object from constructor
57             # secret - allow to override default secret from constructor
58             # with_msg_auth - boolean, to add Message-Authenticator.
59             # This can be archieved by adding Message-Authenticator to av_list with undefined value
60             # request_id - allow to specify custom value (0..255), otherwise internal counter is used
61             sub build {
62 2     2 1 3188 my ($self, %h) = @_;
63              
64             # RADIUS code
65 2         5 my $type = $h{type};
66             # list in form of { Name => ... Value => ... [Vendor => ...]}
67 2         4 my $av_list = $h{av_list};
68             # object of Data::Radius::Dictionary or compatible
69 2   33     65 my $dict = $h{dict} // $self->dict();
70             # RADIUS secret
71 2 50       19 if($h{secret}) {
72 0         0 $self->secret($h{secret});
73             }
74 2 50       41 die 'No secret value' if(! defined $self->secret);
75             # enable adding Message-Authenticator attribute (RFC3579)
76             # enable it by defaulf if Message-Authenticator is present in av_list with empty value
77 2         28 my $with_msg_auth = $h{with_msg_auth};
78              
79 2 50 33     7 if ($self->is_reply($type) && ! $h{authenticator}) {
80 0         0 die "No authenticator value from request";
81             }
82              
83             # Authenticator required now to encode password field (if present)
84 2         5 my $authenticator;
85 2 50       28 if ($type == ACCESS_REQUEST) {
86             # random, but allow to override for testing
87 2   33     7 $authenticator = $h{authenticator} // pack 'L4', map { int(rand(2 ** 32 - 1)) } (0..3);
  0         0  
88             }
89              
90             # pack attributes
91 2         6 my @bin_av = ();
92 2         3 foreach my $av (@{$av_list}) {
  2         6  
93             # Message-Authenticator
94 5 100 66     20 if (($av->{Name} eq ATTR_MSG_AUTH_NAME) && !$av->{Value}) {
95 1         57 $with_msg_auth = 1;
96             # this AV will be calculated and added to the end of list
97 1         3 next;
98             }
99              
100 4         13 my $bin = $self->pack_attribute($av, $authenticator);
101 4 50       19 next if(! $bin);
102 4         11 push @bin_av, $bin;
103             }
104              
105 2         6 my $attributes = join('', @bin_av);
106              
107             # build packet header
108              
109 2         5 my $length = 20 + length($attributes);
110              
111             # generate new sequential id if not given (one byte size)
112 2   33     6 my $req_id = $h{request_id} // ($request_id++) & 0xff;
113              
114             # RFC3579 Message-Authenticator (EAP)
115 2 50       15 if($with_msg_auth) {
116             # calculate and append Message-Authenticator attribute
117 2         5 $length += ATTR_MSG_AUTH_LEN;
118 2         4 my $msg_auth = "\x0" x (ATTR_MSG_AUTH_LEN - 2);
119              
120 2         4 my $used_auth;
121 2 50       7 if ($type == ACCESS_REQUEST) {
    0          
122             # random-generated
123 2         4 $used_auth = $authenticator;
124             }
125             elsif ($self->is_request($type)) {
126             # Message-Authenticator should not be present in ACCOUNTING_REQUEST
127 0         0 $used_auth = "\x00" x 16;
128             }
129             else {
130             # must be passed when composing replies
131 0         0 $used_auth = $h{authenticator};
132             }
133              
134 2         9 my $data = join('',
135             pack('C C n', $type, $req_id, $length),
136             $used_auth,
137             $attributes,
138             pack('C C', ATTR_MSG_AUTH, ATTR_MSG_AUTH_LEN),
139             $msg_auth,
140             );
141              
142 2         44 my $hmac = Digest::HMAC_MD5->new($self->secret);
143 2         87 $hmac->add( $data );
144 2         17 $msg_auth = $hmac->digest;
145              
146 2         42 $attributes .= pack('C C', ATTR_MSG_AUTH, ATTR_MSG_AUTH_LEN) . $msg_auth;
147             }
148              
149             # calculate authentificator value for non-authentication request
150 2 50       7 if (! $authenticator) {
151             # calculated from content
152 0 0       0 my $used_auth = $self->is_request($type) ? "\x0" x 16 : $h{authenticator};
153              
154 0         0 my $hdr = pack('C C n', $type, $req_id, $length);
155 0   0     0 $md5 //= Digest::MD5->new;
156 0         0 $md5->add($hdr, $used_auth, $attributes, $self->secret);
157 0         0 $authenticator = $md5->digest();
158             }
159              
160             # wtf?
161 2 50       5 die "No authenticator" if(! $authenticator);
162              
163 2         9 my $packet = join('',
164             pack('C C n', $type, $req_id, $length),
165             $authenticator,
166             $attributes,
167             );
168              
169 2         11 return ($packet, $req_id, $authenticator);
170             }
171              
172             # authenticator required only for password attribute
173             # av: {Name,Value,[Tag]} or {Id,Type,Value,[VendorId],[Tag]}
174             sub pack_attribute {
175 7     7 0 1423 my ($self, $av, $authenticator) = @_;
176              
177             # optional
178 7         165 my $dict = $self->dict;
179              
180 7         37 my $attr;
181             my $vendor_id;
182              
183             # attribute not present in dictionary must be passed as {Id, Type, Value, VendorId, Tag },
184             # where VendorId and Tag are optional
185 7 100       18 if ($av->{Id}) {
186 2 50       44 if (! $av->{Type}) {
187 0         0 warn "No attribute type for " . $av->{Id};
188 0         0 return undef;
189             }
190             $attr = {
191             id => $av->{Id},
192             name => $av->{Id},
193             type => $av->{Type},
194             vendor => $av->{VendorId},
195             has_tag => defined $av->{Tag},
196 2         14 };
197 2         5 $vendor_id = $av->{VendorId};
198             }
199             else {
200             # av: {Name, Value}
201              
202 5 50       12 if (! $dict) {
203 0         0 warn 'No dictionary provided';
204 0         0 return undef;
205             }
206              
207             # tagged attribute
208 5 50       15 if ($av->{Name} =~ /^([\w-]+):(\d+)$/) {
209 0         0 ($av->{Name}, $av->{Tag}) = ($1, $2);
210             }
211              
212 5         17 $attr = $dict->attribute($av->{Name});
213 5 50       32 if (! $attr) {
214 0         0 warn "Unknown attribute ".$av->{Name};
215 0         0 return undef;
216             }
217              
218 5 50 33     23 if (defined $av->{Tag} && !$attr->{has_tag}) {
219 0         0 warn "Tag not required for attribute ".$av->{Name};
220 0         0 return undef;
221             }
222              
223 5 50 33     16 if ($attr->{has_tag} && ! defined $av->{Tag}) {
224 0         0 warn "No Tag provided for attribute ".$av->{Name};
225 0         0 return undef;
226             }
227              
228             # TODO store vendor_id in dictionary parser
229 5         14 $vendor_id = $dict->vendor_id($attr->{vendor});
230             }
231              
232 7 50       23 if (defined $av->{Tag}) {
233 0   0     0 my $tag = $av->{Tag} // 0;
234 0 0 0     0 if ($tag < 1 || $tag > 31) {
235 0   0     0 warn "Tag value is out of range [1..31] for ".($av->{Name} // $av->{Id});
236 0         0 return undef;
237             }
238             }
239              
240 7         13 my $value = $av->{Value};
241 7 50       16 if (! defined $value) {
242 0         0 warn "Undefined value for " . $attr->{name};
243 0         0 return undef;
244             }
245              
246 7 100 66     25 if ($attr->{id} == ATTR_PASSWORD && ! $vendor_id) {
247             # need an authenticator - this attribute must be present only in ACCESS REQUEST
248 2         36 $value = encrypt_pwd($value, $self->secret, $authenticator);
249             }
250              
251 7 50 66     34 if ($attr->{type} ne 'tlv' && is_enum_type($attr->{type}) && $dict) {
      66        
252             # convert constant-like values to real value
253 0   0     0 $value = $dict->value($attr->{name}, $value) // $value;
254             }
255             # else - for TVL type value is ARRAY-ref
256              
257 7 50       142 my $encoded = encode($attr, $value, $self->dict, ($attr->{has_tag} ? $av->{Tag} : undef) );
258              
259 7 50       15 if (! defined $encoded) {
260 0         0 warn "Unable to encode value for ".$av->{Name};
261 0         0 return undef;
262             }
263              
264 7         12 my $len_encoded = length($encoded);
265              
266 7 100       13 if (! $vendor_id) {
267             # tag already included into value, if any
268 5         31 return pack('C C', $attr->{id}, $len_encoded + 2) . $encoded;
269             }
270              
271             # VSA
272              
273 2         3 my $vsa_header;
274 2 100       7 if ($vendor_id == WIMAX_VENDOR) {
275 1         6 $vsa_header = pack('N C C C', $vendor_id, $attr->{id}, $len_encoded + 3, 0);
276             }
277             else {
278             # tag already included into value, if any
279 1         5 $vsa_header = pack('N C C', $vendor_id, $attr->{id}, $len_encoded + 2);
280             }
281              
282 2         16 return pack('C C', ATTR_VENDOR, length($vsa_header) + $len_encoded + 2) . $vsa_header . $encoded;
283             }
284              
285             # parse binary-encoded radius packet
286             # returns list: type, request-id, authenticator, \@AV_list
287             sub parse {
288 1     1 1 2839 my ($self, $packet, $orig_auth) = @_;
289              
290 1         29 my $dict = $self->dict;
291              
292 1         12 my($type, $req_id, $length, $auth, $attributes) = unpack('C C n a16 a*', $packet);
293              
294             # Validate authenticator field
295 1         3 my $expected_auth;
296 1 50       4 if ($type == ACCESS_REQUEST) {
297             # authenticator is random value - no validation
298             }
299             else {
300 0         0 my $used_auth;
301 0 0       0 if ($self->is_request($type)) {
302 0         0 $used_auth = "\x00" x 16;
303             }
304             else {
305             # fo replied we have to use authenticator from request:
306 0 0       0 if (! $orig_auth) {
307 0         0 warn "No original authenticator - unable to verify reply";
308 0         0 return undef;
309             }
310 0         0 $used_auth = $orig_auth;
311             }
312              
313 0   0     0 $md5 //= Digest::MD5->new;
314              
315 0         0 my $hdr = pack('C C n', $type, $req_id, $length);
316 0         0 $md5->add($hdr, $used_auth, $attributes, $self->secret);
317 0         0 $expected_auth = $md5->digest();
318              
319 0 0       0 if($auth ne $expected_auth) {
320 0         0 warn "Bad authenticator value";
321 0         0 return undef;
322             }
323             }
324              
325             # decode attributes
326 1         3 my @attr;
327             my $msg_auth;
328 1         2 my $pos = 0;
329 1         3 my $len = length($attributes);
330              
331 1         4 while ($pos < $len) {
332 3         7 my ($attr_val, $vendor_id, $vendor, $vsa_len, $attr, $tag) = ();
333             # FIXME not supported
334 3         5 my $wimax_cont;
335              
336 3         9 my ($attr_id, $attr_len) = unpack('C C', substr($attributes, $pos, 2));
337              
338 3 50       8 if ($attr_id == ATTR_VENDOR) {
339 0         0 my $vsa_header_len = 6;
340              
341 0         0 ($vendor_id, $attr_id, $vsa_len) = unpack('N C C', substr($attributes, $pos + 2, $vsa_header_len) );
342 0 0       0 if ($vendor_id == WIMAX_VENDOR) {
343             # +1 continuation byte
344 0         0 $vsa_header_len = 7;
345 0         0 $wimax_cont = unpack('C', substr($attributes, $pos + 8, 1));
346 0 0       0 warn 'continuation field is not supported' if ($wimax_cont);
347 0         0 printf "WIMAX cont: %d\n", $wimax_cont;
348             }
349              
350 0 0       0 if ($dict) {
351 0   0     0 $vendor = $dict->vendor_name($vendor_id) // $vendor_id;
352 0         0 $attr = $dict->attribute_name($vendor, $attr_id);
353             }
354              
355 0         0 $attr_val = substr($attributes, $pos + 2 + $vsa_header_len, $attr_len - 2 - $vsa_header_len);
356             }
357             else {
358 3 50       7 if ($dict) {
359 3         11 $attr = $dict->attribute_name(undef, $attr_id);
360             }
361              
362 3         32 $attr_val = substr($attributes, $pos + 2, $attr_len - 2);
363             }
364              
365 3 100 66     13 if ($attr_id == ATTR_MSG_AUTH && ! $vendor) {
366 1 50       4 die "Invalid Message-Authenticator len" if ($attr_len != 18);
367 1         2 $msg_auth = $attr_val;
368             # zero it to verify later
369 1         2 $attr_val = "\x0" x (ATTR_MSG_AUTH_LEN - 2);
370 1         3 substr($attributes, $pos + 2, $attr_len - 2, $attr_val);
371             }
372              
373 3         5 $pos += $attr_len;
374              
375 3 50       9 if (! $attr) {
376             # raw data for unknown attribute
377 0         0 push @attr, {
378             Name => $attr_id,
379             Value => $attr_val,
380             Type => undef,
381             Vendor => $vendor,
382             Tag => undef,
383             };
384 0         0 next;
385             }
386              
387 3         56 (my $decoded, $tag) = decode($attr, $attr_val, $self->dict);
388 3 50       10 if (is_enum_type($attr->{type})) {
389             # try to convert value to constants
390 0   0     0 $decoded = $dict->constant($attr->{name}, $decoded) // $decoded;
391             }
392              
393             # password is expected only in auth request
394 3 100 66     21 if ($type == ACCESS_REQUEST && $attr->{id} == ATTR_PASSWORD && ! $attr->{vendor}) {
      66        
395 1         35 $decoded = decrypt_pwd($decoded, $self->secret, $auth);
396             }
397              
398             push @attr, {
399             Name => $attr->{name},
400             Value => $decoded,
401             Type => $attr->{type},
402 3         21 Vendor => $vendor,
403             Tag => $tag,
404             };
405             }
406              
407 1 50       16 if($msg_auth) {
408             # we already replaced msg auth value to \x0...
409 1         3 my $auth_used;
410 1 50       5 if ($self->is_reply($type)) {
    50          
411 0         0 $auth_used = $orig_auth;
412             }
413             elsif ($type == ACCESS_REQUEST) {
414 1         3 $auth_used = $auth;
415             }
416             else {
417             # other type of request should use 00x16
418             # Message-Authenticator should not be present in ACCOUNTING_REQUEST
419 0         0 $auth_used = "\x00" x 16;
420             }
421              
422 1         6 my $data = join('',
423             pack('C C n', $type, $req_id, $length),
424             $auth_used,
425             $attributes,
426             );
427 1         25 my $hmac = Digest::HMAC_MD5->new($self->secret);
428 1         33 $hmac->add( $data );
429 1         7 my $exp_msg_auth = $hmac->digest;
430              
431 1 50       18 if ($msg_auth ne $exp_msg_auth) {
432 0         0 warn "Message-Authenticator not verified";
433 0         0 return undef;
434             }
435             }
436              
437 1         6 return ($type, $req_id, $auth, \@attr);
438             }
439              
440             # extract request id from packet header without parsing attributes
441             # should be used to find original authenticator value for received reply packet to pass it to decode_request()
442             sub request_id {
443 0     0 1 0 my ($self, $packet) = @_;
444 0         0 my $req_id = unpack('C', substr($packet, 1, 1));
445 0         0 return $req_id;
446             }
447              
448             sub is_reply {
449 3     3 0 7 my ($class, $type) = @_;
450 3   50     20 return $IS_REPLY{ $type } // 0;
451             }
452              
453             sub is_request {
454 0     0 0   my ($class, $type) = @_;
455 0   0       return $IS_REQUEST{ $type } // 0;
456             }
457              
458             1;
459              
460             __END__