Mail::Address::Tagged - construct and validate email addresses with HMAC verification


Mail-Address-Tagged documentation Contained in the Mail-Address-Tagged distribution.

Index


Code Index:

NAME

Top

Mail::Address::Tagged - construct and validate email addresses with HMAC verification

SYNOPSIS

Top

 #---------------------------------------------------------------------------
 # methods to use when constructing an address
 #---------------------------------------------------------------------------

 my $tag = Mail::Address::Tagged->new(key   => $key,
                                      email => 'foo@bar.com');

 my $seconds = $tag->set_valid_for($period);
 my $keyword = $tag->set_keyword('wibble');

 my $tag = Mail::Address::Tagged->new(key       => $key,
                                      user      => 'foo',
                                      host      => 'bar.com',
                                      valid_for => '10d',
                                      keyword   => 'wibble');

 $my $address = $tag->make_confirm(time    => $unix_time,
                                   pid     => $pid,
                                   keyword => $keyword);

 $my $address = $tag->make_confirm({time    => $unix_time,
                                    pid     => $pid,});

 $my $address = $tag->make_dated;
 $my $address = $tag->make_sender($address_to_receive_from);




 #---------------------------------------------------------------------------
 # methods to use when validating an address
 #---------------------------------------------------------------------------

 my $tag = Mail::Address::Tagged->for_received(key      => $key,
                                               received => $address,
                                               sender   => $sender,);

 if ($tag->valid) {
   ...
 }

 my $still_valid = ! $tag->expired;




 #---------------------------------------------------------------------------
 # Methods for accessing the objects internals (these will probably
 # be mainly used internally)
 #---------------------------------------------------------------------------

 my $email   = $tag->key;
 my $address = $tag->email;
 my $user    = $tag->user;
 my $host    = $tag->host;
 my $seconds = $tag->valid_for;
 my $keyword = $tag->keyword
 my $address = $tag->wrap('text_to_wrap');

 my $hmac = $tag->conf_mac(time => $time,
                           pid  => $pid);

 my $hmac = $tag->conf_mac(time    => $time,
                           pid     => $pid,
                           keyword => $value);

 my $hmac = $tag->single_value_mac($date);
 my $hmac = $tag->single_value_mac($sender);




 # only set by for_received

 my $received_time = $tag->candidate_time
 my $received_pid  = $tag->candidate_pid
 my $received_HMAC = $tag->candidate_mac
 my $address_type  = $tag->type
 my $correspondent = $tag->sender

DESCRIPTION

Top

This module implements an object that can generate and validate tagged email addresses. These are designed to be used primarily in anti-spam applications.

The addresses generated all carry extra information, such as the date when they expire, who may use them to send you mail etc. A cryptocraphic hash of this extra information is also included in in the address. This Hashed Message Authenticaion Code (HMAC RFC 2104) is your guarantee that the information contained in the address has not been tampered with.

This module can generate and validate three different types of tagged address. They are all extensions of a users normal email address and are constructed in a similar manner. Where the normal address is user@host.com, the tagged address takes the form user-tagtype-tag@host.com.

The three supported address types are confirm, dated and sender.

Confirm addresses must contain a time (in unixtime) and the process id of the process that generated them, they may also optionally contain a keyword. They include the time and process id so that the system can deal with more than one message a second. Addresses of this type are used to request verification that a message should be delivered. The point being that automated mailers are unlikely to be able to respond in this way so spam will not get through. If a persistant spammer does reach your mailbox, then you can always black list the address. The keyword when it is supplied is the type of confirmation being asked for. All three bits of information are combined and a cryptographic hash is taken of the result, these bits of info are then combined like this.

  user-confirm-keyword.time.pid.HMAC@host.com

When mail like this is received, the bits can be separated out and a new HMAC generated, if it matches the one in the address, then this is a valid address.

Dated addresses have an expiry time and are used to accept mail up to a given time. They end up in the format

  user-dated-time.HMAC@host.com

and are validated in the same manner

The third type of supported address is sender, this takes the form

  user-sender-HMAC@host.com

the address that this will be accepted from is included in the HMAC generation. When mail of this form comes in the sending address can be checked against the HMAC if they don't match then appropriate action can be taken (disposal or confirmation request etc.).

FACTORY METHODS

Top

new

  my $tag = Mail::Address::Tagged->new(key   => $key,
                                       email => 'foo@bar.com');

  my $tag = Mail::Address::Tagged->new(key       => $key,
                                       user      => 'foo',
                                       host      => 'bar.com',
                                       valid_for => '10d',
                                       keyword   => 'wibble');

Pass this your key and email address it will constrct an object for making tagged addresses. The email address may be complete e.g. foo@bar.com or supplied as user and host.

There are also various optional parametes that may be supplied to new.

You may pass the valid_for attribute to control how long dated address will be active for, if not supplied it defaults to 5 days (see set_valid_for documentation)

The keyword parameter is used when generating dated addresses. It is included in the string and allows for the generation of addresses like this:

name-confirm-keyword.12344556.123.ABCDEF@host.org

If not supplied it defaults to the empty string.

for_received

  my $tag = Mail::Address::Tagged->for_received(key      => $key,
                                                received => $address,
                                                sender   => $sender,);

This constructs an object based on the received address. It will break it down into it's component parts, these may then be queried and checked for validity.

INSTANCE METHODS - Construction

Top

set_valid_for

  my $seconds = $tag->set_valid_for($period);

This allows one to set the time period that dated addresses will be valid for. Times periods are specified as a string which consists of a positive integer folowed by a period modifier. Valid modifiers are:

Y year
M month
w week
d day
h hours
s seconds

set_keyword

  my $keyword = $tag->set_keyword('wibble');

Set the keyword of this object. Returns the new keyword.

make_confirm

  $my $address = $tag->make_confirm(time    => $unix_time,
                                    pid     => $pid,
                                    keyword => $keyword);

  $my $address = $tag->make_confirm({time    => $unix_time,
                                     pid     => $pid,});

Return an address that will be used to confirm an email from an untrusted source. You must pass time and processid, you may also optionally pass a keyword.

make_dated

  $my $address = $tag->make_dated;

Return an address that will be allowed to send us mail for the default period of time from now.

make_sender

  $my $address = $tag->make_sender($address_to_receive_from);

Return an address that will only accept mail if it is sent from one particular sender address.

INSTANCE METHODS - querying

Top

These methods are only useful on objects constructed with the for_received method. They will tell you whether the address is genuine and whether it has expired (for dated addresses).

valid

  if ($tag->valid) {
   ...
  }

This will tell you whether the HMAC matches the details of the address.

expired

  my $still_valid = ! $tag->expired;

This will tell you whether dated addresses have expired.

VALIDATION METHODS (mostly only for internal use)

Top

conf_mac

  my $hmac = $tag->conf_mac(time => $time,
                            pid  => $pid);

  my $hmac = $tag->conf_mac(time    => $time,
                            pid     => $pid,
                            keyword => $value);

Return the HMAC for the time and pid passed in. The method may also take an optional keyword -> value pairing and if provided this will also be included in the HMAC generation.

single_value_mac

  my $hmac = $tag->single_value_mac($date);

  my $hmac = $tag->single_value_mac($sender);

Return the HMAC for the value passed in.

key

  my $email = $tag->key;

Return the cryptographic key that this address is using.

email

  my $address = $tag->email;

This returns the unaltered email address that this object is manipulating.

user

  my $user = $tag->user;

This returns the user portion of the email address that this object is manipulating.

host

  my $host = $tag->host;

This returns the host portion of the email address that this object is manipulating.

valid_for

  my $seconds = $tag->valid_for;

The number of seconds that a dated email address will be valid for.

keyword

  my $keyword = $tag->keyword

If a keyword was supplied to the constructor, this method returns its value.

wrap

  my $address = $hmac->wrap('text_to_wrap');

When you call this method it constructs an email address of the form

  nametext_to_wrap@host

that is it wraps its argument in the user and host

candidate_time

  my $time = $tag->candidate_time

The time in the address. This is only valid when an address of type confirm or dated is being validated. In all other cases it will return undef.

candidate_pid

  my $received_pid = $tag->candidate_pid

the pid in the address. this is only valid when a confirmation address is bing validated. it will return undef at all other times.

candidate_mac

  my $tag->candidate_mac

The HMAC of the address used to construct this object, this will only being valid for objects that have been instantiated to validate an address, it returns undef at all other times.

type

  $tag->type

The type of mail address used to create this object, it is only valid when this object is being used for validation, it will return undef at all other times.

sender

  $tag->sender

The sender argument passed to for_address when constructing an object to validate an address. If the object was not constructed to validate an addrss it will return undef.

BUGS

Top

Nothing Known

TODO

Top

Nothing Known


Mail-Address-Tagged documentation Contained in the Mail-Address-Tagged distribution.
package Mail::Address::Tagged;

#------------------------------------------------------------------------------
#
# Copyright  (c) Andrew Wilson 2001
#
#------------------------------------------------------------------------------
#
# Modification History
#
# Auth    Date       AECN        Description
# ------  ---------  ----------  ----------------------------------------------
# andrew  16 Sep 01  1.0.1001    Expanded the documentation
# andrew  12 Sep 01  1.0.1000    Wrote this.
#------------------------------------------------------------------------------

$Mail::Address::Tagged::VERSION = '0.01';

use strict;
use Digest::HMAC;
use Digest::SHA1;

sub new {
  my $class = shift;

  my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  exists $arg{key} or return undef;
  exists $arg{email} or (exists $arg{user} and exists $arg{host}) or return undef;

  my $self = bless {}, $class;
  $self->_init(%arg) or return undef;

  return $self;
}

sub _init {
  my $self = shift;

  %{$self} = (@_);

  if (exists $self->{email}) {
    @{$self}{'user', 'host'} = split "@", $self->{email};
  } else {
    $self->{email} = $self->{user} . '@' . $self->{host};
  }

  $self->{keyword} ||= "";
  $self->set_valid_for($self->{valid_for});
}

sub for_received {
  my $class = shift;

  my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  exists $arg{key} and
  exists $arg{address} and
  exists $arg{sender} or return undef;

  my ($user_part, $host)   = split "@", $arg{address};
  my ($user, $type, $data) = split "-", $user_part;

  # if we don't know what type of address this is then there's no
  # point continuing
  defined $type and $type =~ /^(confirm|dated|sender)$/ or return undef;

  # need to have an HMAC
  defined $data or return undef;

  # now set up the object
  my $self = $class->new(key  => $arg{key},
                         user => $user,
                         host => $host,);
  defined $self or return undef;

  $self->_set_type($type);
  $self->_set_sender($arg{sender});

  if ($type eq 'confirm') {

    my ($keyword, $date, $pid, $mac) = split m#\.#, $data;
    $self->set_keyword($keyword);
    $self->_set_candidate_time($date);
    $self->_set_candidate_pid($pid);
    $self->_set_candidate_mac($mac);

  } elsif ($type eq 'dated') {

    my ($date, $mac) = split m#\.#, $data;
    $self->_set_candidate_time($date);
    $self->_set_candidate_mac($mac);

  } else {
    $self->_set_candidate_mac($data);
  }

  return $self;
}

my %Conv = ('Y' => 60 * 60 * 24 * 365,
            'M' => 60 * 60 * 24 * 30,
            'w' => 60 * 60 * 24 * 7,
            'd' => 60 * 60 * 24,
            'h' => 60 * 60,
            'm' => 60,
            's' => 1,);

sub set_valid_for {
  my $self = shift;

  my $period  = shift;
  if (defined $period and $period =~  /^(\d+)([YMwdhms])/) {
    my $num = $1;
    my $unit = $2;

    $self->{valid_for} = $Conv{$unit} * $num;
  } else {
    $self->{valid_for} = $Conv{d} * 5;
  }
  return $self->{valid_for};
}

sub set_keyword {
  my $self = shift;

  my $new = shift;
  defined $new or return $self->keyword;
  $self->{keyword} = $new;
}

sub make_confirm {
  my $self = shift;

  my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;

  exists $arg{time} or carp('no time passed to make_confirm');
  exists $arg{pid}  or carp('no pid passed to make_confirm');

  $self->set_keyword($arg{keyword}) if exists $arg{keyword};
  my $keyword = $self->keyword;

  # generate the HMAC
  my $details;
  @{$details}{'time', 'pid'} = @arg{'time', 'pid'};
  my $mac = $self->conf_mac($details);

  return $self->wrap("-confirm-$keyword.$arg{time}.$arg{pid}." . $mac);
}

sub make_dated {
  my $self = shift;

  my $date = shift;
  defined $date or $date = time();
  $date += $self->valid_for;

  my $mac  = $self->single_value_mac($date);

  return $self->wrap("-dated-". $date . '.' . $mac);
}

sub make_sender {
  my $self = shift;

  my $sender = lc(shift);
  my $mac    = $self->single_value_mac($sender);

  return $self->wrap("-sender-" . $mac);
}

sub valid {
  my $self = shift;

  return undef unless $self->type;

  if ($self->type eq "confirm") {

    my $mac = $self->conf_mac({time => $self->candidate_time,
                               pid  => $self->candidate_pid});
    return $mac eq $self->candidate_mac;

  } elsif ($self->type eq "dated") {

    my $mac = $self->single_value_mac($self->candidate_time);
    return $mac eq $self->candidate_mac;

  } elsif ($self->type eq "sender") {

    my $mac = $self->single_value_mac($self->sender);
    return $mac eq $self->candidate_mac;

  }

  return 0;
}

sub expired {
  my $self = shift;

  return undef unless $self->candidate_time and $self->valid;
  return $self->candidate_time > time();
}

sub conf_mac {
  my $self = shift;

  my %arg = ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_;
  exists $arg{time} or carp('no time passed to conf_mac');
  exists $arg{pid}  or carp('no pid passed to conf_mac');

  my $digest = Digest::HMAC->new($self->key, "Digest::SHA1");
  $digest->add($arg{time});
  $digest->add($arg{pid});
  $digest->add($self->keyword) if $self->keyword;
  # we only want the first 6 hex digits of the HMAC (there are 40)
  return substr($digest->hexdigest, 0, 6);
}

sub single_value_mac {
  my $self = shift;

  my $value = shift;
  defined $value or carp('no value passed to single_value_mac');

  my $digest = Digest::HMAC->new($self->key, "Digest::SHA1");
  $digest->add($value);
  # we only want the first 6 hex digits of the HMAC (there are 40)
  return substr($digest->hexdigest, 0, 6);
}


sub email     { $_[0]->{email} }
sub host      { $_[0]->{host} }
sub key       { $_[0]->{key} };
sub user      { $_[0]->{user} }

sub valid_for { $_[0]->{valid_for} }

sub keyword { $_[0]->{keyword} }

sub wrap {
  my $self = shift;

  my $text = shift;
  return $self->user . $text . '@' . $self->host;
}

# these methods will not form part of the public interface of the module
sub _set_candidate_time {
  $_[0]->{candidate_time} = $_[1] if (defined $_[1])
};

sub _set_candidate_pid {
  $_[0]->{candidate_pid} = $_[1] if (defined $_[1])
};

sub _set_candidate_mac {
  $_[0]->{candidate_mac} = $_[1] if (defined $_[1])
};

sub _set_type {
  $_[0]->{type} = $_[1] if (defined $_[1])
};

sub _set_sender {
  $_[0]->{sender} = $_[1] if (defined $_[1])
};

sub candidate_time { $_[0]->{candidate_time} };
sub candidate_pid  { $_[0]->{candidate_pid}  };
sub candidate_mac  { $_[0]->{candidate_mac}  };
sub type   {  $_[0]->{type}   };
sub sender {  $_[0]->{sender} };

1;