More loop logic cleanup - we only handle mail for gid Debian, so we
[mirror/userdir-ldap.git] / userdir_gpg.py
1 #   Copyright (c) 1999-2001  Jason Gunthorpe <jgg@debian.org>
2 #   Copyright (c) 2005       Joey Schulze <joey@infodrom.org>
3 #
4 #   This program is free software; you can redistribute it and/or modify
5 #   it under the terms of the GNU General Public License as published by
6 #   the Free Software Foundation; either version 2 of the License, or
7 #   (at your option) any later version.
8 #
9 #   This program is distributed in the hope that it will be useful,
10 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #   GNU General Public License for more details.
13 #
14 #   You should have received a copy of the GNU General Public License
15 #   along with this program; if not, write to the Free Software
16 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 # GPG issues - 
19 #  - gpgm with a status FD being fed keymaterial and other interesting
20 #    things does nothing.. If it could ID the keys and stuff over the
21 #    status-fd I could decide what to do with them. I would also like it
22 #    to report which key it selected for encryption (also if there 
23 #    were multi-matches..) Being able to detect a key-revoke cert would be
24 #    good too.
25 #  - I would like to be able to fetch the comment and version fields from the 
26 #    packets so I can tell if a signature is made by pgp2 to enable the
27 #    pgp2 encrypting mode.
28
29 import mimetools, multifile, sys, StringIO, os, tempfile, re;
30 import rfc822, time, fcntl, anydbm
31
32 # General GPG options
33 GPGPath = "gpg"
34 # "--load-extension","rsa",
35 GPGBasicOptions = [
36    "--no-options",
37    "--batch",
38    "--no-default-keyring",
39    "--secret-keyring", "/dev/null",
40    "--always-trust"];
41 GPGKeyRings = [];
42 GPGSigOptions = ["--output","-"];
43 GPGSearchOptions = ["--dry-run","--with-colons","--fingerprint"];
44 GPGEncryptOptions = ["--output","-","--quiet","--always-trust",\
45                      "--armor","--encrypt"];
46 GPGEncryptPGP2Options = ["--set-filename","","--rfc1991",\
47                          "--load-extension","idea",\
48                          "--cipher-algo","idea"] + GPGEncryptOptions;
49
50 # Replay cutoff times in seconds
51 CleanCutOff = 7*24*60*60;
52 AgeCutOff = 4*24*60*60;
53 FutureCutOff = 3*24*60*60;
54
55 def ClearKeyrings():
56    del GPGKeyRings[:]
57
58 # Set the keyrings, the input is a list of keyrings
59 def SetKeyrings(Rings):
60    for x in Rings:
61       GPGKeyRings.append("--keyring");
62       GPGKeyRings.append(x);           
63
64 # GetClearSig takes an un-seekable email message stream (mimetools.Message) 
65 # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded 
66 # clear signed text.
67 # If this is fed to gpg/pgp it will verify the signature and spit out the
68 # signed text component. Email headers and PGP mime (RFC 2015) is understood
69 # but no effort is made to cull any information outside the PGP boundaries
70 # Please note that in the event of a mime decode the mime headers will be
71 # present in the signature text! The return result is a tuple, the first
72 # element is the text itself the second is a mime flag indicating if the
73 # result should be mime processed after sig checking.
74 #
75 # Paranoid will check the message text to make sure that all the plaintext is 
76 # in fact signed (bounded by a PGP packet)
77 def GetClearSig(Msg,Paranoid = 0):
78    Error = 'MIME Error';
79    # See if this is a MIME encoded multipart signed message
80    if Msg.gettype() == "multipart/signed":
81       Boundary = Msg.getparam("boundary");
82       if not Boundary:
83          raise Error, "multipart/* without a boundary parameter";
84
85       # Create the multipart handler. Regrettably their implementation 
86       # Needs seeking..
87       SkMessage = StringIO.StringIO();
88       SkMessage.write(Msg.fp.read());
89       SkMessage.seek(0);
90       mf = multifile.MultiFile(SkMessage)
91       mf.push(Msg.getparam("boundary"));
92
93       # Check the first bit of the message..
94       if Paranoid != 0:
95          Pos = mf.tell();
96          while 1:
97              x = mf.readline();
98              if not x: break;
99              if len(x.strip()) != 0:
100                 raise Error,"Unsigned text in message (at start)";
101          mf.seek(Pos);
102       
103       # Get the first part of the multipart message
104       if not mf.next():
105          raise Error, "Invalid pgp/mime encoding [no section]";
106
107       # Get the part as a safe seekable stream
108       Signed = StringIO.StringIO();
109       Signed.write(mf.read());
110       InnerMsg = mimetools.Message(Signed);
111       
112       # Make sure it is the right type
113       if InnerMsg.gettype() != "text/plain":
114          raise Error, "Invalid pgp/mime encoding [wrong plaintext type]";
115    
116       # Get the next part of the multipart message
117       if not mf.next():
118          raise Error, "Invalid pgp/mime encoding [no section]";
119       InnerMsg = mimetools.Message(mf);
120       if InnerMsg.gettype() != "application/pgp-signature":
121          raise Error, "Invalid pgp/mime encoding [wrong signature type]";
122       Signature = ''.join(mf.readlines())
123
124       # Check the last bit of the message..
125       if Paranoid != 0:
126          mf.pop();
127          Pos = mf.tell();
128          while 1:
129              x = mf.readline();
130              if not x: break; 
131              if len(x.strip()) != 0:
132                 raise Error,"Unsigned text in message (at end)";
133          mf.seek(Pos);
134       
135       # Append the PGP boundary header and the signature text to re-form the
136       # original signed block [needs to convert to \r\n]
137       Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
138       # Semi-evil hack to get the proper hash type inserted in the message
139       if Msg.getparam('micalg') != None:
140           Output = Output + "Hash: MD5,SHA1,%s\r\n"%(Msg.getparam('micalg')[4:].upper())
141       Output = Output + "\r\n";
142       Output = Output + Signed.getvalue().replace("\n-","\n- -") + Signature
143       return (Output,1);
144    else:
145       if Paranoid == 0:
146          # Just return the message body
147          return (''.join(Msg.fp.readlines()),0);
148      
149       Body = "";
150       State = 1;
151       for x in Msg.fp.readlines():
152           Body = Body + x;
153           Tmp = x.strip()
154           if len(Tmp) == 0:
155              continue;
156          
157           # Leading up to the signature
158           if State == 1:
159              if Tmp == "-----BEGIN PGP SIGNED MESSAGE-----":
160                 State = 2;
161              else:
162                 raise Error,"Unsigned text in message (at start)";
163              continue;
164          
165           # In the signature plain text
166           if State == 2:
167              if Tmp == "-----BEGIN PGP SIGNATURE-----":
168                 State = 3;
169              continue;
170                 
171           # In the signature
172           if State == 3:
173              if Tmp == "-----END PGP SIGNATURE-----":
174                 State = 4;
175              continue;
176                 
177           # Past the end
178           if State == 4:
179              raise Error,"Unsigned text in message (at end)";
180       return (Body,0);
181
182 # This opens GPG in 'write filter' mode. It takes Message and sends it
183 # to GPGs standard input, pipes the standard output to a temp file along
184 # with the status FD. The two tempfiles are passed to GPG by fd and are
185 # accessible from the filesystem for only a short period. Message may be
186 # None in which case GPGs stdin is closed directly after forking. This
187 # is best used for sig checking and encryption.
188 # The return result is a tuple (Exit,StatusFD,OutputFD), both fds are
189 # fully rewound and readable.
190 def GPGWriteFilter(Program,Options,Message):
191    # Make sure the tmp files we open are unreadable, there is a short race
192    # between when the temp file is opened and unlinked that some one else
193    # could open it or hard link it. This is not important however as no 
194    # Secure data is fed through the temp files.
195    OldMask = os.umask(0777);
196    try:
197       Output = tempfile.TemporaryFile("w+b");
198       GPGText = tempfile.TemporaryFile("w+b");
199       InPipe = os.pipe();
200       InPipe = [InPipe[0],InPipe[1]];
201    finally:
202       os.umask(OldMask);
203       
204    try:
205       # Fork off GPG in a horrible way, we redirect most of its FDs
206       # Input comes from a pipe and its two outputs are spooled to unlinked
207       # temp files (ie private)
208       Child = os.fork();
209       if Child == 0:
210          try:
211             os.dup2(InPipe[0],0);
212             os.close(InPipe[1]);
213             os.dup2(Output.fileno(),1);
214             os.dup2(os.open("/dev/null",os.O_WRONLY),2);
215             os.dup2(GPGText.fileno(),3);
216             
217             Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
218             os.execvp(Program,Args);
219          finally:
220             os._exit(100);
221       
222       # Get rid of the other end of the pipe
223       os.close(InPipe[0])
224       InPipe[0] = -1;
225
226       # Send the message
227       if Message != None:
228          try:
229             os.write(InPipe[1],Message);
230          except:
231            pass;
232       os.close(InPipe[1]);
233       InPipe[1] = -1;
234
235       # Wait for GPG to finish
236       Exit = os.waitpid(Child,0);
237
238       # Create the result including the new readable file descriptors
239       Result = (Exit,os.fdopen(os.dup(GPGText.fileno()),"r"), \
240                 os.fdopen(os.dup(Output.fileno()),"r"));
241       Result[1].seek(0);
242       Result[2].seek(0);
243
244       Output.close();
245       GPGText.close();
246       return Result;
247    finally:
248       if InPipe[0] != -1:
249          os.close(InPipe[0]);
250       if InPipe[1] != -1:
251          os.close(InPipe[1]);
252       Output.close();
253       GPGText.close();
254
255 # This takes a text passage, a destination and a flag indicating the 
256 # compatibility to use and returns an encrypted message to the recipient.
257 # It is best if the recipient is specified using the hex key fingerprint
258 # of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
259 def GPGEncrypt(Message,To,PGP2):
260    Error = "KeyringError"
261    # Encrypt using the PGP5 block encoding and with the PGP5 option set.
262    # This will handle either RSA or DSA/DH asymetric keys.
263    # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
264    # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
265    # can read a message encrypted with blowfish and RSA.
266    searchkey = GPGKeySearch(To);
267    if len(searchkey) == 0:
268       raise Error, "No key found matching %s"%(To);
269    elif len(searchkey) > 1:
270       raise Error, "Multiple keys found matching %s"%(To);
271    if searchkey[0][4].find("E") < 0:
272       raise Error, "Key %s has no encryption capability - are all encryption subkeys expired or revoked?  Are there any encryption subkeys?"%(To);
273
274    if PGP2 == 0:
275       try:
276          Res = None;
277          Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
278          if Res[0][1] != 0:
279             return None;
280          Text = Res[2].read();
281          return Text;
282       finally:
283          if Res != None:
284             Res[1].close();
285             Res[2].close();
286    else:
287       # We have to call gpg with a filename or it will create a packet that
288       # PGP2 cannot understand.
289       TmpName = tempfile.mktemp();
290       try:
291          Res = None;
292          MsgFile = open(TmpName,"wc");
293          MsgFile.write(Message);
294          MsgFile.close();
295          Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
296          if Res[0][1] != 0:
297             return None;
298          Text = Res[2].read();
299          return Text;
300       finally:
301          try:
302             os.unlink(TmpName);
303          except:
304             pass;
305          if Res != None:
306             Res[1].close();
307             Res[2].close();
308
309 # Checks the signature of a standard PGP message, like that returned by
310 # GetClearSig. It returns a large tuple of the form:
311 #   (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,Length,PGP2),Text);
312 # Where,
313 #  Why = None if checking was OK otherwise an error string. 
314 #  SigID+Date represent something suitable for use in a replay cache. The
315 #             date is returned as the number of seconds since the UTC epoch.
316 #             The keyID is also in this tuple for easy use of the replay 
317 #             cache
318 #  KeyID, KeyFinger and Owner represent the Key used to sign this message
319 #         PGP2 indicates if the message was created using PGP 2.x 
320 #  Text is the full byte-for-byte signed text in a string
321 def GPGCheckSig(Message):
322    Res = None;
323    try:
324       Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
325       Exit = Res[0];
326
327       # Parse the GPG answer
328       Strm = Res[1];
329       GoodSig = 0;
330       SigId = None;
331       KeyFinger = None;
332       KeyID = None;
333       Owner = None;
334       Date = None;
335       Why = None;
336       TagMap = {};
337       while(1):
338          # Grab and split up line
339          Line = Strm.readline();
340          if Line == "":
341             break;
342          Split = re.split("[ \n]",Line);
343          if Split[0] != "[GNUPG:]":
344             continue;
345
346          # We only process the first occurance of any tag.
347          if TagMap.has_key(Split[1]):
348             continue;
349          TagMap[Split[1]] = None;
350
351          # Good signature response
352          if Split[1] == "GOODSIG":
353             # Just in case GPG returned a bad signal before this (bug?)
354             if Why == None:
355                GoodSig = 1;
356             KeyID = Split[2];
357             Owner = ' '.join(Split[3:])
358             # If this message is signed with a subkey which has not yet
359             # expired, GnuPG will say GOODSIG here, even if the primary
360             # key already has expired.  This came up in discussion of
361             # bug #489225.  GPGKeySearch only returns non-expired keys.
362             Verify = GPGKeySearch(KeyID);
363             if len(Verify) == 0:
364                GoodSig = 0
365                Why = "Key has expired (no unexpired key found in keyring matching %s)"%(KeyId);
366
367          # Bad signature response
368          if Split[1] == "BADSIG":
369             GoodSig = 0;
370             KeyID = Split[2];
371             Why = "Verification of signature failed";
372
373          # Bad signature response
374          if Split[1] == "ERRSIG":
375             GoodSig = 0;
376             KeyID = Split[2];
377             if len(Split) <= 7:
378                Why = "GPG error, ERRSIG status tag is invalid";
379             elif Split[7] == '9':
380                Why = "Unable to verify signature, signing key missing.";
381             elif Split[7] == '4':
382                Why = "Unable to verify signature, unknown packet format/key type";
383             else:   
384                Why = "Unable to verify signature, unknown reason";
385
386          if Split[1] == "NO_PUBKEY":
387             GoodSig = 0;
388             Why = "Unable to verify signature, signing key missing.";
389
390          # Expired signature
391          if Split[1] == "EXPSIG":
392             GoodSig = 0;
393             Why = "Signature has expired";
394
395          # Expired signature
396          if Split[1] == "EXPKEYSIG":
397             GoodSig = 0;
398             Why = "Signing key (%s, %s) has expired"%(Split[2], Split[3]);
399
400          # Revoked key
401          if Split[1] == "KEYREVOKED" or Split[1] == "REVKEYSIG":
402             GoodSig = 0;
403             Why = "Signing key has been revoked";
404
405          # Corrupted packet
406          if Split[1] == "NODATA" or Split[1] == "BADARMOR":
407             GoodSig = 0;
408             Why = "The packet was corrupted or contained no data";
409             
410          # Signature ID
411          if Split[1] == "SIG_ID":
412             SigId = Split[2];
413             Date = long(Split[4]);
414
415          # ValidSig has the key finger print
416          if Split[1] == "VALIDSIG":
417             # Use the fingerprint of the primary key when available
418             if len(Split) >= 12:
419                KeyFinger = Split[11];
420             else:
421                KeyFinger = Split[2];
422
423       # Reopen the stream as a readable stream
424       Text = Res[2].read();
425
426       # A gpg failure is an automatic bad signature
427       if Exit[1] != 0 and Why == None:
428          GoodSig = 0;
429          Why = "GPG execution returned non-zero exit status: " + str(Exit[1]);
430
431       if GoodSig == 0 and (Why == None or len(Why) == 0):
432          Why = "Checking Failed";
433
434       # Try to decide if this message was sent using PGP2
435       PGP2Message = 0;
436       if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) != None):
437          PGP2Message = 1;
438
439       return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
440    finally:
441       if Res != None:
442          Res[1].close();
443          Res[2].close();
444
445 # Search for keys given a search pattern. The pattern is passed directly
446 # to GPG for processing. The result is a list of tuples of the form:
447 #   (KeyID,KeyFinger,Owner,Length)
448 # Which is similar to the key identification tuple output by GPGChecksig
449 #
450 # Do not return keys where the primary key has expired
451 def GPGKeySearch(SearchCriteria):
452    Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
453           [SearchCriteria," 2> /dev/null"]
454    Strm = None;
455    Result = [];
456    Owner = "";
457    KeyID = "";
458    Capabilities = ""
459    Expired = None;
460    Hits = {};
461
462    dir = os.path.expanduser("~/.gnupg")
463    if not os.path.isdir(dir):
464       os.mkdir(dir, 0700)
465                       
466    try:
467       Strm = os.popen(" ".join(Args),"r")
468       
469       while(1):
470          # Grab and split up line
471          Line = Strm.readline();
472          if Line == "":
473             break;
474          Split = Line.split(":")
475
476          # Store some of the key fields
477          if Split[0] == 'pub':
478             KeyID = Split[4];
479             Owner = Split[9];
480             Length = int(Split[2])
481             Capabilities = Split[11]
482             Expired = Split[1] == 'e'
483
484          # Output the key
485          if Split[0] == 'fpr':
486             if Hits.has_key(Split[9]):
487                continue;
488             Hits[Split[9]] = None;
489             if not Expired:
490                Result.append( (KeyID,Split[9],Owner,Length,Capabilities) );
491    finally:
492       if Strm != None:
493          Strm.close();
494    return Result;
495
496 # Print the available key information in a format similar to GPG's output
497 # We do not know the values of all the feilds so they are just replaced
498 # with ?'s
499 def GPGPrintKeyInfo(Ident):
500    print "pub  %u?/%s ??-??-?? %s" % (Ident[3],Ident[0][-8:],Ident[2]);
501    print "     key fingerprint = 0x%s" % (Ident[1]);
502
503 # Perform a substition of template 
504 def TemplateSubst(Map,Template):
505    for x in Map.keys():
506       Template = Template.replace(x, Map[x])
507    return Template;
508
509 # The replay class uses a python DB (BSD db if avail) to implement
510 # protection against replay. Replay is an attacker capturing the
511 # plain text signed message and sending it back to the victim at some
512 # later date. Each signature has a unique signature ID (and signing 
513 # Key Fingerprint) as well as a timestamp. The first stage of replay
514 # protection is to ensure that the timestamp is reasonable, in particular
515 # not to far ahead or too far behind the current system time. The next
516 # step is to look up the signature + key fingerprint in the replay database
517 # and determine if it has been recived. The database is cleaned out 
518 # periodically and old signatures are discarded. By using a timestamp the
519 # database size is bounded to being within the range of the allowed times
520 # plus a little fuzz. The cache is serialized with a flocked lock file
521 class ReplayCache:
522    def __init__(self,Database):
523       self.Lock = open(Database + ".lock","w",0600);
524       fcntl.flock(self.Lock.fileno(),fcntl.LOCK_EX);
525       self.DB = anydbm.open(Database,"c",0600);
526       self.CleanCutOff = CleanCutOff;
527       self.AgeCutOff = AgeCutOff;
528       self.FutureCutOff = FutureCutOff;
529       
530    # Close the cache and lock
531    def __del__(self):
532       self.close();
533    def close(self):
534       self.DB.close();
535       self.Lock.close();
536       
537    # Clean out any old signatures
538    def Clean(self):
539       CutOff = time.time() - self.CleanCutOff;
540       for x in self.DB.keys():
541          if int(self.DB[x]) <= CutOff:
542             del self.DB[x];
543     
544    # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
545    # key ID
546    def Check(self,Sig):
547       if Sig[0] == None or Sig[1] == None or Sig[2] == None:
548          return "Invalid signature";
549       if int(Sig[1]) > time.time() + self.FutureCutOff:
550          return "Signature has a time too far in the future";
551       if self.DB.has_key(Sig[0] + '-' + Sig[2]):
552          return "Signature has already been received";
553       if int(Sig[1]) < time.time() - self.AgeCutOff:
554          return "Signature has passed the age cut off ";
555       # + str(int(Sig[1])) + ',' + str(time.time()) + "," + str(Sig);
556       return None;
557            
558    # Add a signature, the sig is the same as is given to Check
559    def Add(self,Sig):
560       if Sig[0] == None or Sig[1] == None:
561          raise RuntimeError,"Invalid signature";
562       if Sig[1] < time.time() - self.CleanCutOff:
563          return;
564       Key = Sig[0] + '-' + Sig[2]
565       if self.DB.has_key(Key):
566          if int(self.DB[Key]) < Sig[1]:
567             self.DB[Key] = str(int(Sig[1]));
568       else:
569          self.DB[Key] = str(int(Sig[1]));
570