#!/usr/bin/perl # # MOZILLA THUNDERBIRD EXTENSION FOR SENDER POLICY FRAMEWORK # AND DOMAINKEYS -- QUERY SERVER # # Copyright 2004 Joshua Tauberer , but with # no rights reserved. You're free to use this as you want. # # See: http://taubz.for.net/code/spf # # INSTALLATION REQUIREMENTS # ------------------------- # This script has a number of dependencies. # # You will need to have openssl installed. If you # are running this script on an RPM-based distro, install # both the openssl and openssl-devel packages. Do # this first. # # You will also need to run these commands to make sure # you have all of the required Perl modules installed: # # cpan Digest::MD5 # cpan MIME::Base64 # cpan Mail::SPF::Query # cpan Mail::DomainKeys # cpan Crypt::OpenSSL::Bignum # cpan Crypt::RSA # # This script expects "query.log" to be a writable # file in the same directory as this script. You might have # to make a world-writable file if Apache isn't executing # scripts as you, or just disable logging. # # LOOK OUT FOR... # # If you have a "search" directive in your resolve.conf that # points to a domain that resolves all subdomains with a # wildcard, the SURBL spammer check will be confused. You'll # need to disable SURBL in the options section below or make # some modifications to the SURBL subroutine. # ############################################################ package SenderVerification; ######################## ####### OPTIONS ######## ######################## # Set this to 0 to turn off logging, or 1 to # log requests (anonymously) to 'query.log', # but make sure the file is writeable. $EnableLogging = 1; $LogFile = "/home/tauberer/www/code/spf/cgi-bin/query.log"; # Turns on and off SURBL lookups, to see # if the From: or Return-Path: addresses are # from known spamming or phishing domains. $EnableSURBL = 0; # Enable or disable SPF checking. $EnableSPF = 1; # Enable or disable DomainKeys checking. $EnableDomainKeys = 1; # Set the hostname of this machine, for informational # purposes only. $Hostname = "jt.2rad.net"; ######################## #### END OF OPTIONS #### ######################## #### BEGIN MODULES ##### # MODULES FOR SPF use Mail::SPF::Query; # MODULES FOR DOMAINKEYS use Mail::DomainKeys::Signature; use Mail::DomainKeys::Key::Public; use Crypt::OpenSSL::Bignum; use Crypt::RSA::Primitives; use Crypt::RSA::DataFormat qw(octet_len os2ip i2osp octet_xor mgf1); use Crypt::RSA::Key::Public; use MIME::Base64; # MODULES FOR LOGGING QUERIES use Digest::MD5 qw(md5_base64); # Delete this line and disable # logging if Digest::MD5 isn't # available. sub SenderVerificationCheck { #return Error("Quit with a message to all users."); my %q; # Parse The Query Parameters while ($ENV{QUERY_STRING} =~ /(^|\&)(\w+)=([^\&]+)/g) { my $name = $2; my $value = $3; $q{$name} = $value; $q{$name} =~ s/%(\d\d)/chr(hex($1))/eg; $q{$name} =~ s/\s+//g; # prevent the log from being corrupted } my $remoteaddr = $ENV{REMOTE_ADDR}; # IP and HELO reversed? if ($q{ip} =~ /^[a-z]/ && $q{helo} =~ /^\d/) { my $x = $q{ip}; $q{ip} = $q{helo}; $q{helo} = $x; } # HELO has more than the hostname? if ($q{helo} =~ /\(HELO([^)]+)\)/) { $q{helo} = $1; } # The actual checks... if ($q{agent} eq "spf:0.1" || $q{agent} eq "spf:0.2") { return Error("You're using an old version of this extension. You should upgrade."); } if ($q{agent} ne "sve:0.7") { return Error("Extension update available at http://update.mozilla.org or http://taubz.for.net/code/spf."); } my $result = 'none'; my $comment; my $domain; my $method; my $methodspfguess; # Perform an SPF check on the from address, and then the envelope address. # The envfrom parameter should only be given if it is different # from the from parameter. if ($EnableSPF) { foreach my $addr ($q{from}, $q{envfrom}) { if ($result eq "pass") { next; } # don't bother with check if ($addr eq "") { next; } # one of these may not be specified $addr = lc($addr); my $d = $addr; $d =~ s/^([\w\W]+)\@//; my $r; my $c; # Check SURBL on the domain of the address if ($EnableSURBL) { ($r, $c) = SURBL($addr); if (defined($r)) { $result = $r; $comment = $c; $domain = $d; $method = "surbl"; last; } } # Check SPF ($r, $c) = SPF(0, $q{ip}, $q{helo}, $addr); if ($r eq "neutral" || $r eq "none") { # Attempt a best guess, but only use it if it's positive. ($r2, $c2) = SPF(1, $q{ip}, $q{helo}, $addr); if ($r2 eq "pass") { $r = $r2; $c = $c2; $methodspfguess = 1; } } # If we've gotten here, the result before this check # was either none, neutral, or fail. The SPF result # is none, neutral, fail, or pass. Aside from none, # the result we get is always more informative than # the result we had, so take it. if ($r ne "none") { $result = $r; $comment = $c; $domain = $d; $method = "spf"; } } } # DomainKeys Check $diddk = 0; if (($result ne "pass" || $methodspfguess) && $q{domainkeys_hash} ne "" && $EnableDomainKeys) { # Get the expected SHA1 hash based on the DomainKeys header. # Only do this if we're not already returning a 'pass' status # and we have the DK header to check. $diddk = 1; my ($dkdomain, $expected_hash) = DK($q{domainkeys_header}, $q{from}, $q{envfrom}); # Does the expected hash match the hash? if ($expected_hash eq $q{domainkeys_hash}) { $result = "pass"; $comment = "The DomainKeys signature was verified from <$dkdomain>."; $domain = $dkdomain; $method = "dk"; } elsif ($result eq 'none') { # Report neutral to indicate the domain supports something # but verification nevertheless was not successful. $result = 'neutral'; } } if ($EnableLogging && $remoteaddr ne "") { # Log the query (anonymously) open LOG, ">>$LogFile"; flock(LOG, 2); seek(LOG, 0, 2); my $time = time; my $user = md5_base64($remoteaddr); my $emailDomain = $domain; if ($emailDomain eq "") { $q{from} =~ /\@([\w\W]+)$/; $emailDomain = $1; } if ($emailDomain eq "") { $q{envfrom} =~ /\@([\w\W]+)$/; $emailDomain = $1; } if ($emailDomain eq "") { $emailDomain = "unknown"; } print LOG "$time\t$q{agent}\t$emailDomain\t$result\t$user\t$diddk\n"; close LOG; } # Output the result # Perform a reverse DNS to pass back to the client. my ($reversedns) = gethostbyaddr(pack('C4',split('\.',$q{ip})),2); $result = Escape($result); $comment = Escape($comment); $domain = Escape($domain); my $ret = "\n\t\n"; # Repeat the query in the response foreach my $k (keys(%q)) { $ret .= "\t\t<$k>" . Escape($q{$k}) . "\n"; } $ret .= < $result $comment $domain $reversedns $method EOF return $ret; } sub SPF { my $guess = shift; my $ip = shift; my $helo = shift; my $sender = shift; my $query = new Mail::SPF::Query (ip => $ip, sender => $sender, helo=>$helo, trusted=>1, guess=>$guess); my ($result, # pass | fail | softfail | neutral | none | error | unknown [mechanism] $smtp_comment, # "please see http://spf.pobox.com/why.html?..." when rejecting, return this string to the SMTP client $header_comment, # prepend_header("Received-SPF" => "$result ($header_comment)") $spf_record, # "v=spf1 ..." original SPF record for the domain ) = $query->result(); if ($result ne "pass" && $result ne "fail" && $result ne "none") { $result = "neutral"; } my $h = quotemeta($Hostname); $header_comment =~ s/$h: (\w)/'SPF: ' . uc($1)/e; my $comment = $header_comment; if ($result eq "pass") { if (!$guess) { $comment = "The domain of the email was explicitly permitted via SPF."; } else { $comment = "The domain of the email was implicitly permitted via SPF/guess."; } } return ($result, $comment); } sub DK { my $sig = shift; my $from = shift; my $envfrom = shift; $sig = Mail::DomainKeys::Signature->parse(String => $sig); # Fetch the public key my $pubk = fetch Mail::DomainKeys::Key::Public( Protocol => $sig->protocol, Selector => $sig->selector, Domain => $sig->domain); if (!defined($pubk)) { return undef; } if ($pubk->revoked) { return undef; } # Granularity must match the local part # of either of the From: or envelope addresses. # We're only interested in verifying the domain # of the sender anyway, for now at least. if ($pubk->granularity ne "") { my $granmatch = 0; my $gran = quotemeta($pubk->granularity); foreach my $addr ($from, $envfrom) { if ($addr =~ /^$gran\@/) { $granmatch = 1; last; } } if (!$granmatch) { return undef; } } # The following is based on Crypt::RSA::SS::PSS. # If anyone reading can get this to work with # $pubk->cork directly, that'd be preferable. my ($kn, $ke) = $pubk->cork->get_key_parameters(); my $key = bless { e => $ke->to_decimal, n => $kn->to_decimal }, 'Crypt::RSA::Key::Public'; my $rsa = Crypt::RSA::Primitives->new(); my $S = MIME::Base64::decode($sig->signature); my $k = octet_len ($key->n); my $s = os2ip ($S); my $m = $rsa->core_verify (Key => $key, Signature => $s) || return undef; my $em1 = i2osp ($m, $k-1) || return undef; $em1 = substr($em1, length($em1) - 20, 20); $em1 = MIME::Base64::encode($em1); $em1 =~ s/[=\s]+$//; return ($sig->domain, $em1); } sub SURBL { # Run the domain through SURBL to check for known # phishers and spammers. # Get the domain in the format SURBL expects my $domain = $_[0]; $domain =~ /\@([\w\W]+)$/; $domain = $1; if ($domain =~ /\.(com|edu|gov|int|mil|net|org|biz|info|name|pro|aero|coop|museum)$/) { $domain =~ /([^.]+\.[a-z]+)$/; $domain = $1; } else { $domain =~ /([^.]+\.[^.]+\.[a-z]+)$/; $domain = $1; } # This comes from somewhere on the SURBL website. my $whitelist = < is known to be a scam. It should not be trusted."); } elsif ($a4 != 0) { return ("spamming", "The domain <$domain> is a known spammer. It should not be trusted."); } } return undef; } sub Escape { my $a = $_[0]; $a =~ s/\&/\&/g; $a =~ s/\/\>/g; return $a; } sub Error { my $msg = shift; $msg = Escape($msg); return < error $msg EOF };