-#!/usr/bin/env python
-# -*- mode: python -*-
-#
-# Check and decode PGP signed emails.
-# This script implements a wrapper around another program. It takes a mail
-# on stdin and processes off a PGP signature, verifying it and seperating
-# out the checked plaintext. It then invokes a sub process and feeds it
-# the verified plain text and sets environment vairables indicating the
-# result of the PGP check. If PGP checking fails then the subprocess is
-# is never run and a bounce message is generated. The wrapper can understand
-# PGP-MIME and all signatures supported by GPG. It completely decodes
-# PGP-MIME before running the subprocess. It also can do optional
-# anti-replay checking on the signatures.
-#
-# If enabled it can also do LDAP checking to determine the uniq UID owner
-# of the key.
-#
-# Options:
-# -r Replay cache file, if unset replay checking is disabled
-# -e Bounce error message template file, if unset very ugly bounces are
-# made
-# -k Colon seperated list of keyrings to use
-# -a Reply to address (mail daemon administrator)
-# -d LDAP search base DN
-# -l LDAP server
-# -m Email address to use when prettying up LDAP_EMAIL
-#
-# It exports the following environment variables:
-# LDAP_EMAIL="Adam Di Carlo <aph@debian.org>"
-# LDAP_UID="aph"
-# PGP_FINGERPRINT="E21E5D13FAD42A54F1AA5A00D801CE55"
-# PGP_KEYID="8FFC405EFD5A67CD"
-# PGP_KEYNAME="Adam Di Carlo <aph@debian.org> "
-# SENDER (from mailer - envelope sender for bounces)
-# REPLYTO (generated from message headers)
-#
-# Typical Debian invokation may look like:
-# ./gpgwrapper -k /usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp \
-# -d ou=users,dc=debian,dc=org -l db.debian.org \
-# -m debian.org -a admin@db.debian.org \
-# -e /etc/userdir-ldap/templtes/error-reply -- test.sh
-
-import sys, traceback, time, os;
-import pwd, getopt;
-from userdir_gpg import *;
-
-EX_TEMPFAIL = 75;
-EX_PERMFAIL = 65; # EX_DATAERR
-Error = 'Message Error';
-ReplyTo = "admin@db";
-
-# Configuration
-ReplayCacheFile = None;
-ErrorTemplate = None;
-LDAPDn = None;
-LDAPServer = None;
-EmailAppend = "";
-
-# Safely get an attribute from a tuple representing a dn and an attribute
-# list. It returns the first attribute if there are multi.
-def GetAttr(DnRecord,Attribute,Default = ""):
- try:
- return DnRecord[1][Attribute][0];
- except IndexError:
- return Default;
- except KeyError:
- return Default;
- return Default;
-
-# Return a printable email address from the attributes.
-def EmailAddress(DnRecord):
- cn = GetAttr(DnRecord,"cn");
- sn = GetAttr(DnRecord,"sn");
- uid = GetAttr(DnRecord,"uid");
- if cn == "" and sn == "":
- return "<" + uid + "@" + EmailAppend + ">";
- return cn + " " + sn + " <" + uid + "@" + EmailAppend + ">"
-
-# Match the key fingerprint against an LDAP directory
-def CheckLDAP(FingerPrint):
- import ldap;
-
- # Connect to the ldap server
- global ErrTyp, ErrMsg;
- ErrType = EX_TEMPFAIL;
- ErrMsg = "An error occured while performing the LDAP lookup";
- global l;
- l = connectLDAP(LDAPServer);
- l.simple_bind_s("","");
-
- # Search for the matching key fingerprint
- Attrs = l.search_s(LDAPDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=" + FingerPrint);
- if len(Attrs) == 0:
- raise Error, "Key not found"
- if len(Attrs) != 1:
- raise Error, "Oddly your key fingerprint is assigned to more than one account.."
-
- os.environ["LDAP_UID"] = GetAttr(Attrs[0],"uid");
- os.environ["LDAP_EMAIL"] = EmailAddress(Attrs[0]);
-
-# Start of main program
-# Process options
-(options, arguments) = getopt.getopt(sys.argv[1:], "r:e:k:a:d:l:m:");
-for (switch, val) in options:
- if (switch == '-r'):
- ReplayCacheFile = val;
- elif (switch == '-e'):
- ErrorTemplate = val;
- elif (switch == '-k'):
- SetKeyrings(val.split(":"));
- elif (switch == '-a'):
- ReplyTo = val;
- elif (switch == '-d'):
- LDAPDn = val;
- elif (switch == '-l'):
- LDAPServer = val;
- elif (switch == '-m'):
- EmailAppend = val;
-
-# Drop messages from a mailer daemon. (empty sender)
-if os.environ.has_key('SENDER') == 0 or len(os.environ['SENDER']) == 0:
- sys.exit(0);
-
-ErrMsg = "Indeterminate Error";
-ErrType = EX_TEMPFAIL;
-try:
- # Startup the replay cache
- ErrType = EX_TEMPFAIL;
- if ReplayCacheFile != None:
- ErrMsg = "Failed to initialize the replay cache:";
- RC = ReplayCache(ReplayCacheFile);
- RC.Clean();
-
- # Get the email
- ErrType = EX_PERMFAIL;
- ErrMsg = "Failed to understand the email or find a signature:";
- Email = mimetools.Message(sys.stdin,0);
- Msg = GetClearSig(Email);
-
- ErrMsg = "Message is not PGP signed:"
- if Msg[0].find("-----BEGIN PGP SIGNED MESSAGE-----") == -1:
- raise Error, "No PGP signature";
-
- # Check the signature
- ErrMsg = "Unable to check the signature or the signature was invalid:";
- Res = GPGCheckSig(Msg[0]);
-
- if Res[0] != None:
- raise Error, Res[0];
-
- if Res[3] == None:
- raise Error, "Null signature text";
-
- # Extract the plain message text in the event of mime encoding
- global PlainText;
- ErrMsg = "Problem stripping MIME headers from the decoded message"
- if Msg[1] == 1:
- try:
- Index = Res[3].index("\n\n") + 2;
- except ValueError:
- Index = Res[3].index("\n\r\n") + 3;
- PlainText = Res[3][Index:];
- else:
- PlainText = Res[3];
-
- # Check the signature against the replay cache
- if ReplayCacheFile != None:
- ErrMsg = "The replay cache rejected your message. Check your clock!";
- Rply = RC.Check(Res[1]);
- if Rply != None:
- raise Error, Rply;
- RC.Add(Res[1]);
-
- # Do LDAP stuff
- if LDAPDn != None:
- CheckLDAP(Res[2][1]);
-
- # Determine the sender address
- ErrType = EX_PERMFAIL;
- ErrMsg = "A problem occured while trying to formulate the reply";
- Sender = Email.getheader("Reply-To");
- if Sender == None:
- Sender = Email.getheader("From");
- if Sender == None:
- raise Error, "Unable to determine the sender's address";
-
- # Setup the environment
- ErrType = EX_TEMPFAIL;
- ErrMsg = "Problem calling the child process"
- os.environ["PGP_KEYID"] = Res[2][0];
- os.environ["PGP_FINGERPRINT"] = Res[2][1];
- os.environ["PGP_KEYNAME"] = Res[2][2];
- os.environ["REPLYTO"] = Sender;
-
- # Invoke the child
- Child = os.popen(" ".join(arguments),"w");
- Child.write(PlainText);
- if Child.close() != None:
- raise Error, "Child gave a non-zero return code";
-
-except:
- # Error Reply Header
- Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
- ErrReplyHead = "To: %s\nReply-To: %s\nDate: %s\n" % (os.environ['SENDER'],ReplyTo,Date);
-
- # Error Body
- Subst = {};
- Subst["__ERROR__"] = ErrMsg;
- Subst["__ADMIN__"] = ReplyTo;
-
- Trace = "==> %s: %s\n" %(sys.exc_type,sys.exc_value);
- List = traceback.extract_tb(sys.exc_traceback);
- if len(List) >= 1:
- Trace = Trace + "Python Stack Trace:\n";
- for x in List:
- Trace = Trace + " %s %s:%u: %s\n" %(x[2],x[0],x[1],x[3]);
-
- Subst["__TRACE__"] = Trace;
-
- # Try to send the bounce
- try:
- if ErrorTemplate != None:
- ErrReply = TemplateSubst(Subst,open(ErrorTemplate,"r").read());
- else:
- ErrReply = "\n"+str(Subst)+"\n";
-
- Child = os.popen("/usr/sbin/sendmail -t","w");
- Child.write(ErrReplyHead);
- Child.write(ErrReply);
- if Child.close() != None:
- raise Error, "Sendmail gave a non-zero return code";
- except:
- sys.exit(EX_TEMPFAIL);
-
- if ErrType != EX_PERMFAIL:
- sys.exit(ErrType);
- sys.exit(0);
-