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