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