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