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