#!/usr/bin/perl # # This is a simple example PAM authentication agent, it implements a # simple shared secret authentication scheme. The PAM module pam_secret.so # is its counter part. Both the agent and the remote server are able to # authenticate one another, but the server is given the opportunity to # ignore a failed authentication. # $^W = 1; use strict; use IPC::Open2; $| = 1; # display extra information to STDERR my $debug = 0; if (scalar @ARGV) { $debug = 1; } # Globals my %state; my $default_key; my $next_key = $$; # loop over binary prompts for (;;) { my ($control, $data) = ReadBinaryPrompt(); my ($reply_control, $reply_data); if ($control == 0) { if ($debug) { print STDERR "agent: no packet to read\n"; } last; } elsif ($control == 0x02) { ($reply_control, $reply_data) = HandleAgentSelection($data); } elsif ($control == 0x01) { ($reply_control, $reply_data) = HandleContinuation($data); } else { if ($debug) { print STDERR "agent: unrecognized packet $control {$data} to read\n"; } ($reply_control, $reply_data) = (0x04, ""); } WriteBinaryPrompt($reply_control, $reply_data); } # Only willing to exit well if we've completed our authentication exchange if (scalar keys %state) { if ($debug) { print STDERR "The following sessions are still active:\n "; print STDERR join ', ', keys %state; print STDERR "\n"; } exit 1; } else { exit 0; } sub HandleAgentSelection ($) { my ($data) = @_; unless ( $data =~ /^([a-zA-Z0-9_]+\@?[a-zA-Z0-9_.]*)\/(.*)$/ ) { return (0x04, ""); } my ($agent_name, $payload) = ($1, $2); if ($debug) { print STDERR "agent: ". "agent=$agent_name, payload=$payload\n"; } # this agent has a defined name if ($agent_name ne "secret\@here") { if ($debug) { print STDERR "bad agent name: [$agent_name]\n"; } return (0x04, ""); } # the selection request is acompanied with a hexadecimal cookie my @tokens = split '\|', $payload; unless ((scalar @tokens) == 2) { if ($debug) { print STDERR "bad payload\n"; } return (0x04, ""); } unless ($tokens[1] =~ /^[a-z0-9]+$/) { if ($debug) { print STDERR "bad server cookie\n"; } return (0x04, ""); } my $shared_secret = IdentifyLocalSecret($tokens[0]); unless (defined $shared_secret) { # make a secret up if ($debug) { print STDERR "agent: cannot authenticate user\n"; } $shared_secret = GetRandom(); } my $local_cookie = GetRandom(); $default_key = $next_key++; $state{$default_key} = $local_cookie ."|". $tokens[1] ."|". $shared_secret; if ($debug) { print STDERR "agent: \$state{$default_key} = $state{$default_key}\n"; } return (0x01, $default_key ."|". $local_cookie); } sub HandleContinuation ($) { my ($data) = @_; my ($key, $server_digest) = split '\|', $data; unless (defined $state{$key}) { # retries and out of sequence prompts are not permitted return (0x04, ""); } my $expected_digest = CreateDigest($state{$key}); my ($local_cookie, $remote_cookie, $shared_secret) = split '\|', $state{$key}; delete $state{$key}; unless ($expected_digest eq $server_digest) { if ($debug) { print STDERR "agent: don't trust server - faking reply\n"; print STDERR "agent: got ($server_digest)\n"; print STDERR "agent: expected ($expected_digest)\n"; } ## FIXME: Agent should exchange a prompt with the client warning ## that the server is faking us out. return (0x03, CreateDigest($expected_digest . $data . GetRandom())); } if ($debug) { print STDERR "agent: server appears to know the secret\n"; } my $session_authenticated_ticket = CreateDigest($remote_cookie."|".$shared_secret."|".$local_cookie); # FIXME: Agent should set a derived session key environment # variable (available for the client (and its children) to sign # future data exchanges. if ($debug) { print STDERR "agent: should putenv(" ."\"AUTH_SESSION_TICKET=$session_authenticated_ticket\")\n"; } # return agent's authenticating digest return (0x03, CreateDigest($shared_secret."|".$remote_cookie ."|".$local_cookie)); } sub ReadBinaryPrompt { my $buffer = " "; my $count = read(STDIN, $buffer, 5); if ($count == 0) { # no more packets to read return (0, ""); } if ($count != 5) { # broken packet header return (-1, ""); } my ($length, $control) = unpack("N C", $buffer); if ($length < 5) { # broken packet length return (-1, ""); } my $data = ""; $length -= 5; while ($count = read(STDIN, $buffer, $length)) { $data .= $buffer; if ($count != $length) { $length -= $count; next; } if ($debug) { print STDERR "agent: ". "data is [$data]\n"; } return ($control, $data); } # broken packet data return (-1, ""); } sub WriteBinaryPrompt ($$) { my ($control, $data) = @_; my $length = 5 + length($data); if ($debug) { printf STDERR "agent: ". "{%d|0x%.2x|%s}\n", $length, $control, $data; } my $bp = pack("N C a*", $length, $control, $data); print STDOUT $bp; if ($debug) { printf STDERR "agent: ". "agent has replied\n"; } } ## ## Here is where we parse the simple secret file ## The format of this file is a list of lines of the following form: ## ## user@client0.host.name secret_string1 ## user@client1.host.name secret_string2 ## user@client2.host.name secret_string3 ## sub IdentifyLocalSecret ($) { my ($identifier) = @_; my $secret; if (open SECRETS, "< ". (getpwuid($<))[7] ."/.secret\@here") { my $line; while (defined ($line = )) { my ($id, $sec) = split /[\s]+/, $line; if ((defined $id) && ($id eq $identifier)) { $secret = $sec; last; } } close SECRETS; } return $secret; } ## Here is where we generate a message digest sub CreateDigest ($) { my ($data) = @_; my $pid = open2(\*MD5out, \*MD5in, "/usr/bin/md5sum -") or die "you'll need /usr/bin/md5sum installed"; my $oldfd = select MD5in; $|=1; select $oldfd; if ($debug) { print STDERR "agent: ". "telling md5: <$data>\n"; } print MD5in "$data"; close MD5in; my $reply = ; ($reply) = split /\s/, $reply; if ($debug) { print STDERR "agent: ". "md5 said: <$reply>\n"; } close MD5out; return $reply; } ## get a random number sub GetRandom { if ( -r "/dev/urandom" ) { open RANDOM, "< /dev/urandom" or die "crazy"; my $i; my $reply = ""; for ($i=0; $i<4; ++$i) { my $buffer = " "; while (read(RANDOM, $buffer, 4) != 4) { ; } $reply .= sprintf "%.8x", unpack("N", $buffer); if ($debug) { print STDERR "growing reply: [$reply]\n"; } } close RANDOM; return $reply; } else { print STDERR "agent: ". "[got linux?]\n"; return "%.8x%.8x%.8x%.8x", time, time, time, time; } }