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