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