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