Update from samosa: be less conservative when looking for an encrypted message
[mirror/userdir-ldap.git] / gpgwrapper
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3 #
4 # Check and decode PGP signed emails.
5 # This script implements a wrapper around another program. It takes a mail
6 # on stdin and processes off a PGP signature, verifying it and seperating 
7 # out the checked plaintext. It then invokes a sub process and feeds it
8 # the verified plain text and sets environment vairables indicating the 
9 # result of the PGP check. If PGP checking fails then the subprocess is
10 # is never run and a bounce message is generated. The wrapper can understand
11 # PGP-MIME and all signatures supported by GPG. It completely decodes 
12 # PGP-MIME before running the subprocess. It also can do optional
13 # anti-replay checking on the signatures.
14 #
15 # If enabled it can also do LDAP checking to determine the uniq UID owner
16 # of the key.
17 #
18 # Options:
19 #  -r  Replay cache file, if unset replay checking is disabled
20 #  -e  Bounce error message template file, if unset very ugly bounces are 
21 #      made
22 #  -k  Colon seperated list of keyrings to use
23 #  -a  Reply to address (mail daemon administrator)
24 #  -d  LDAP search base DN
25 #  -l  LDAP server
26 #  -m  Email address to use when prettying up LDAP_EMAIL
27 #
28 # It exports the following environment variables:
29 #  LDAP_EMAIL="Adam Di Carlo <aph@debian.org>"
30 #  LDAP_UID="aph"
31 #  PGP_FINGERPRINT="E21E5D13FAD42A54F1AA5A00D801CE55"
32 #  PGP_KEYID="8FFC405EFD5A67CD"
33 #  PGP_KEYNAME="Adam Di Carlo <aph@debian.org> "
34 #  SENDER (from mailer - envelope sender for bounces)
35 #  REPLYTO (generated from message headers)
36 #
37 # Typical Debian invokation may look like:
38 # ./gpgwrapper -k /usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp \
39 #      -d ou=users,dc=debian,dc=org -l db.debian.org \
40 #      -m debian.org -a admin@db.debian.org \
41 #      -e /etc/userdir-ldap/templtes/error-reply -- test.sh
42       
43 import sys, traceback, time, os;
44 import string, pwd, getopt;
45 from userdir_gpg import *;
46
47 EX_TEMPFAIL = 75;
48 EX_PERMFAIL = 65;      # EX_DATAERR
49 Error = 'Message Error';
50 ReplyTo = "admin@db";
51
52 # Configuration
53 ReplayCacheFile = None;
54 ErrorTemplate = None;
55 LDAPDn = None;
56 LDAPServer = None;
57 EmailAppend = "";
58
59 # Safely get an attribute from a tuple representing a dn and an attribute
60 # list. It returns the first attribute if there are multi.
61 def GetAttr(DnRecord,Attribute,Default = ""):
62    try:
63       return DnRecord[1][Attribute][0];
64    except IndexError:
65       return Default;
66    except KeyError:
67       return Default;
68    return Default;
69
70 # Return a printable email address from the attributes.
71 def EmailAddress(DnRecord):
72    cn = GetAttr(DnRecord,"cn");
73    sn = GetAttr(DnRecord,"sn");
74    uid = GetAttr(DnRecord,"uid");
75    if cn == "" and sn == "":
76       return "<" + uid + "@" + EmailAppend + ">";
77    return cn + " " + sn + " <" + uid + "@" + EmailAppend + ">"
78
79 # Match the key fingerprint against an LDAP directory
80 def CheckLDAP(FingerPrint):
81    import ldap;
82    
83    # Connect to the ldap server
84    global ErrTyp, ErrMsg;
85    ErrType = EX_TEMPFAIL;
86    ErrMsg = "An error occured while performing the LDAP lookup";
87    global l;
88    l = ldap.open(LDAPServer);
89    l.simple_bind_s("","");
90
91    # Search for the matching key fingerprint
92    Attrs = l.search_s(LDAPDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=" + FingerPrint);
93    if len(Attrs) == 0:
94       raise Error, "Key not found"
95    if len(Attrs) != 1:
96       raise Error, "Oddly your key fingerprint is assigned to more than one account.."
97
98    os.environ["LDAP_UID"] = GetAttr(Attrs[0],"uid");
99    os.environ["LDAP_EMAIL"] = EmailAddress(Attrs[0]);
100    
101 # Start of main program
102 # Process options
103 (options, arguments) = getopt.getopt(sys.argv[1:], "r:e:k:a:d:l:m:");
104 for (switch, val) in options:
105    if (switch == '-r'):
106       ReplayCacheFile = val;
107    elif (switch == '-e'):
108       ErrorTemplate  = val;
109    elif (switch == '-k'):
110       SetKeyrings(string.split(val,":"));
111    elif (switch == '-a'):
112       ReplyTo = val;
113    elif (switch == '-d'):
114       LDAPDn = val;
115    elif (switch == '-l'):
116       LDAPServer = val;
117    elif (switch == '-m'):
118       EmailAppend = val;
119       
120 # Drop messages from a mailer daemon. (empty sender)
121 if os.environ.has_key('SENDER') == 0 or len(os.environ['SENDER']) == 0:
122    sys.exit(0);
123
124 ErrMsg = "Indeterminate Error";
125 ErrType = EX_TEMPFAIL;
126 try:
127    # Startup the replay cache
128    ErrType = EX_TEMPFAIL;
129    if ReplayCacheFile != None:
130       ErrMsg = "Failed to initialize the replay cache:";
131       RC = ReplayCache(ReplayCacheFile);
132       RC.Clean();
133    
134    # Get the email 
135    ErrType = EX_PERMFAIL;
136    ErrMsg = "Failed to understand the email or find a signature:";
137    Email = mimetools.Message(sys.stdin,0);
138    Msg = GetClearSig(Email);
139
140    ErrMsg = "Message is not PGP signed:"
141    if string.find(Msg[0],"-----BEGIN PGP SIGNED MESSAGE-----") == -1:
142       raise Error, "No PGP signature";
143    
144    # Check the signature
145    ErrMsg = "Unable to check the signature or the signature was invalid:";
146    Res = GPGCheckSig(Msg[0]);
147
148    if Res[0] != None:
149       raise Error, Res[0];
150       
151    if Res[3] == None:
152       raise Error, "Null signature text";
153
154    # Extract the plain message text in the event of mime encoding
155    global PlainText;
156    ErrMsg = "Problem stripping MIME headers from the decoded message"
157    if Msg[1] == 1:
158       try:
159          Index = string.index(Res[3],"\n\n") + 2;
160       except ValueError:
161          Index = string.index(Res[3],"\n\r\n") + 3;
162       PlainText = Res[3][Index:];
163    else:
164       PlainText = Res[3];   
165
166    # Check the signature against the replay cache
167    if ReplayCacheFile != None:
168       ErrMsg = "The replay cache rejected your message. Check your clock!";
169       Rply = RC.Check(Res[1]);
170       if Rply != None:
171          raise Error, Rply;
172       RC.Add(Res[1]);
173
174    # Do LDAP stuff
175    if LDAPDn != None:
176       CheckLDAP(Res[2][1]);
177       
178    # Determine the sender address
179    ErrType = EX_PERMFAIL;
180    ErrMsg = "A problem occured while trying to formulate the reply";
181    Sender = Email.getheader("Reply-To");
182    if Sender == None:
183       Sender = Email.getheader("From");
184    if Sender == None:
185       raise Error, "Unable to determine the sender's address";
186       
187    # Setup the environment
188    ErrType = EX_TEMPFAIL;
189    ErrMsg = "Problem calling the child process"
190    os.environ["PGP_KEYID"] = Res[2][0];
191    os.environ["PGP_FINGERPRINT"] = Res[2][1];
192    os.environ["PGP_KEYNAME"] = Res[2][2];
193    os.environ["REPLYTO"] = Sender;
194    
195    # Invoke the child
196    Child = os.popen(string.join(arguments," "),"w");
197    Child.write(PlainText);
198    if Child.close() != None:
199       raise Error, "Child gave a non-zero return code";
200    
201 except:
202    # Error Reply Header
203    Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
204    ErrReplyHead = "To: %s\nReply-To: %s\nDate: %s\n" % (os.environ['SENDER'],ReplyTo,Date);
205
206    # Error Body
207    Subst = {};
208    Subst["__ERROR__"] = ErrMsg;
209    Subst["__ADMIN__"] = ReplyTo;
210
211    Trace = "==> %s: %s\n" %(sys.exc_type,sys.exc_value);
212    List = traceback.extract_tb(sys.exc_traceback);
213    if len(List) >= 1:
214       Trace = Trace + "Python Stack Trace:\n";
215       for x in List:
216          Trace = Trace +  "   %s %s:%u: %s\n" %(x[2],x[0],x[1],x[3]);
217          
218    Subst["__TRACE__"] = Trace;
219
220    # Try to send the bounce
221    try:
222       if ErrorTemplate != None:
223          ErrReply = TemplateSubst(Subst,open(ErrorTemplate,"r").read());
224       else:
225          ErrReply = "\n"+str(Subst)+"\n";
226          
227       Child = os.popen("/usr/sbin/sendmail -t","w");
228       Child.write(ErrReplyHead);
229       Child.write(ErrReply);
230       if Child.close() != None:
231          raise Error, "Sendmail gave a non-zero return code";
232    except:
233       sys.exit(EX_TEMPFAIL);
234       
235    if ErrType != EX_PERMFAIL:
236       sys.exit(ErrType);
237    sys.exit(0);
238