Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic

Comment on


Recently, i played around with (time based) RFC conform one time password tokens. You know, these small fobs that display a different 6 digit PIN every minute. Recently, these have been RFC'ed, and some vendors produce them very cheap.

To use them, RADIUS servers are a good start. With a running RADIUS server, you can use them with openssh, openvpn and a bunch of other network services.

After looking into the documentations of pre-existing RADIUS servers, i developed a rather nasty headache. So, in true PerlMonks style, i boldly stepped forward, asked the world "How hard can it be???" and spent the next few hours developing an even bigger headache. But finally, a minimal, working RADIUS server/client pair came into being. Here is how i did it:

First, start off with a rather simple test client:

#!/usr/bin/perl -w #---AUTOPRAGMASTART--- use 5.012; use strict; use warnings; use diagnostics; use mro 'c3'; use English qw( -no_match_vars ); use Carp; our $VERSION = 0.996; #---AUTOPRAGMAEND--- use Authen::Radius; use Term::ReadKey; my $username = 'cavac'; my $password; unless (defined $password) { print 'Password: '; ReadMode('noecho'); chomp($password = ReadLine(0)); ReadMode('restore'); print "\n"; } my $r = new Authen::Radius(Host => '', Secret => 'mysecret', +TimeOut=>10); if($r->check_pwd($username, $password)) { print "Welcome!\n"; } else { print "***** FAIL *****\n"; }

This does nothing more that to ask you for a password and send try to authenticate on the RADIUS server with that password.

Depending on your installed Authen::Radius version and the implementation of the Radius server, you might want to patch that module to silence a warning:

diff -rupN Authen-Radius-0.20_old/ Authen-Radius-0.20/Radius. +pm --- Authen-Radius-0.20_old/ 2012-03-18 20:17:49.313819745 ++0100 +++ Authen-Radius-0.20/ 2012-03-18 20:18:20.054569761 +010 +0 @@ -429,7 +429,7 @@ sub vendorID ($) { } else { # look up vendor by attribute name my $vendor_name = $dict_name{$attr->{'Name'}}{'vendor'}; - my $vendor_id = defined ($dict_vendor_name{$vendor_name}{'id'}) ? + my $vendor_id = (defined $vendor_name && defined ($dict_vendor_nam +e{$vendor_name}{'id'})) ? $dict_vendor_name{$vendor_name}{'id'} : 'not defined'; return $vendor_id; }

Next, we are going to use RADIUS::Packet to write our server. This module has a rather unfortunate design decision (i would call it a bug): If it encounters a chunk of unknown type (one that is not in the dictionary), it dies. Since you never find out which type is missing in your dictionary, this is a bit ugly to debug. The following patch will remedy that problem:

diff -rupN RADIUS-1.0_old/RADIUS/ RADIUS-1.0/RADIUS/ --- RADIUS-1.0_old/RADIUS/ 2012-03-18 19:37:52.083326134 +010 +0 +++ RADIUS-1.0/RADIUS/ 2012-03-18 19:40:54.467770068 +0100 @@ -160,6 +160,12 @@ sub unpack { while (length($attrdat)) { my $length = unpack "x C", $attrdat; my ($type, $value) = unpack "C x a${\($length-2)}", $attrdat; + if(!defined($dict->attr_numtype($type))) { + print STDERR "Unknown type $type! Skipping this one - might h +ave dire consequences)!\n"; + print STDERR " Please add type $type to your dictionary!\n" +; + substr($attrdat, 0, $length) = ""; + next; + } my $val = &{$unpacker{$dict->attr_numtype($type)}}($value, $type) +; $self->set_attr($dict->attr_name($type), $val); substr($attrdat, 0, $length) = "";

Here we go. We need a server that run UDP on port 1812 on localhost. We also want to change the name displayed in "ps" (in linux/unix). The server itself will be handled by a package named Rader, and we'll hardcode it to a single authentification package "OATHusers", which in turn hold the hardcoded usernames/hashvalues pairs for all tokens. Lets start with the startscript:

#!/usr/bin/perl -w #---AUTOPRAGMASTART--- use 5.012; use strict; use warnings; use diagnostics; use mro 'c3'; use English qw( -no_match_vars ); use Carp; our $VERSION = 0.996; #---AUTOPRAGMAEND--- BEGIN { # So we find the rest of our code unshift @INC, "."; }; use Rader; our $APPNAME = "Simple Radius Server"; my $psname = "simpleradius"; print "Changing application name to '$psname'\n\n"; $0 = $psname; my $server = Rader->new( host => 'localhost', port => 1812, proto => 'udp', ); $server->run();

We could, of course, do all the low-level UDP handling ourself, like we all learned from the Perl Cookbook. But we wont. Let's just use the very good Net::Server::Single package as base class (exchange it for Net::Server::PreFork if you got hundreds of requests per second). As for the rest: We take the packet data, turn it into a RADIUS::Packet, get username/password, check against OATHusers and then either return a 'Access-Accept' or 'Access-Reject' RADIUS::Packet.

package Rader; #---AUTOPRAGMASTART--- use 5.012; use strict; use warnings; use diagnostics; use mro 'c3'; use English qw( -no_match_vars ); use Carp; our $VERSION = 0.996; #---AUTOPRAGMAEND--- use base qw(Net::Server::Single); use RADIUS::Dictionary; use RADIUS::Packet; use OATHusers; sub process_request { my $self = shift; my $prop = $self->{'server'}; # This is a VERY simple RADIUS authentication server which respond +s # to Access-Request packets with Access-Accept/Access-reject. my $secret = "mysecret"; # Shared secret on the term server # Parse the RADIUS dictionary file (must have dictionary in curren +t dir) my $dict = new RADIUS::Dictionary "dictionary" or die "Couldn't read dictionary: $!"; my $um = OATHusers->new(); # Get the data my $rec = $prop->{udp_data}; # Unpack it my $p = RADIUS::Packet->new($dict, $rec); if ($p->code eq 'Access-Request') { # Print some details about the incoming request (try ->dump here +) #print $p->attr('User-Name'), " logging in with password ", # $p->password($secret), "\n"; #$p->dump; # Create a response packet my $rp = new RADIUS::Packet $dict; if($um->validate($p->attr('User-Name'), $p->password($secret))) +{ $rp->set_code('Access-Accept'); print "Password OK\n"; } else { $rp->set_code('Access-Reject'); print "Password FAIL\n"; } $rp->set_identifier($p->identifier); $rp->set_authenticator($p->authenticator); # (No attributes are needed.. but you could set IP addr, etc. he +re) # Authenticate with the secret and send to the client. my $outpacket = auth_resp($rp->pack, $secret); $prop->{'client'}->send($outpacket, 0); #$s->sendto(auth_resp($rp->pack, $secret), $whence); } else { # It's not an Access-Request print "***** Unexpected packet type recieved. ******"; $p->dump; } } 1;

Finally, we need some way to validate keys. All users also know a fixed, numeric PIN code (which is actually alphanumeric in this implementation) with varying length they have to prepend to the PIN of time based token. For reference, we also add the serial number (key_id) of each token to our list.

The correct time based token PIN is calculated from its seed plus the current time. Since the token clock and the computer clock might differ a bit (the clock drift usually gets worse with token age) and also the user might take some time to click "connect", we search a limited timespace for a matching key. This opens - in theory - a larger attack window, but this can't really be avoided (but mitigated through some methods like only accepting newer keys than the last valid one, tracking the time difference between token and computer, etc. This is not implemented here). Also, an attacker would need the users fixed PIN in addition to the generated PIN, even if (s)he has access to the token ("something you know and something you have").

The code is rather simple:

package OATHusers; #---AUTOPRAGMASTART--- use 5.012; use strict; use warnings; use diagnostics; use mro 'c3'; use English qw( -no_match_vars ); use Carp; our $VERSION = 0.996; #---AUTOPRAGMAEND--- use Authen::OATH; sub new { my $class = shift; my $self = bless {}, $class; my %seeds = ( 'tye' => { key_id => '000001', pin => '2412', seed => uc('aaaaaaaaabbbbbbbbbbbbbbcccc +ccccccddddddd'), }, 'browseruk'=> { key_id => '222222', pin => '4242', seed => uc('010101010101010101010101010 +1010101010101'), }, 'reaper' => { key_id => '9', pin => 'SIGKILL', seed => uc('123123123123123123123123123 +1231231231231'), }, ); $self->{seeds} = \%seeds; return $self; } sub validate { my ($self, $username, $password) = @_; # Missing fields if(!defined($username) || !defined($password) || $username eq '' || $password eq '') { return 0; } # Unknown username if(!defined($self->{seeds}->{$username})) { return 0; } # For the easy part: Check length of password if(length($password) != (length($self->{seeds}->{$username}->{pin} +) + 6)) { return 0; } my $oath = Authen::OATH->new(timestep => 60); my $valid = 0; my $now = time; my $userseed = $self->{seeds}->{$username}->{seed}; for(my $i = -300; $i <= 300; $i += 60) { # Search +/- 5 minutes my $totp = $oath->totp($userseed, $now + $i); my $fullpass = $self->{seeds}->{$username}->{pin} . $totp; if($fullpass eq $password) { print "$username key drift $i\n"; $valid = 1; last; } } return $valid; } 1;

I can't give you the specifics on how to set up openssh or similar to use RADIUS. This depends too much on your operating system and distribution. But it should be easy enough.

I tested this with real keys (and without the probable typos i introduced by converting the code to a PM article) with some "OTP c200" keys i bought online from Gooze.

Note: I'm in no way affiliated with that company, i just bought a bunch of keys from them and found pricing and delivery times rather acceptable. From my experience, this is rather unusual for cryptographic stuff bought online in europe...


In reply to Radius with RFC conform OTP tokens by cavac

and:  <code> code here </code>

  • Please read these before you post! —
            For:     Use:
    & &amp;
    < &lt;
    > &gt;
    [ &#91;
    ] &#93;
  • Log In?

    What's my password?
    Create A New User
    and the web crawler heard nothing...

    How do I use this? | Other CB clients
    Other Users?
    Others imbibing at the Monastery: (8)
    As of 2014-12-27 22:30 GMT
    Find Nodes?
      Voting Booth?

      Is guessing a good strategy for surviving in the IT business?

      Results (177 votes), past polls