ud-generate: deal with users without loginShell
[mirror/userdir-ldap.git] / userdir_gpg.py
index e9e7538..f594cea 100644 (file)
 #    packets so I can tell if a signature is made by pgp2 to enable the
 #    pgp2 encrypting mode.
 
-import mimetools, multifile, sys, StringIO, os, tempfile, re;
-import rfc822, time, fcntl, anydbm
+import sys
+import StringIO
+import os
+import tempfile
+import re
+import time
+import fcntl
+import anydbm
+import email
+import email.message
+
+from userdir_exceptions import *
 
 # General GPG options
 GPGPath = "gpg"
-# "--load-extension","rsa",
-GPGBasicOptions = [
-   "--no-options",
-   "--batch",
-   "--no-default-keyring",
-   "--secret-keyring", "/dev/null",
-   "--always-trust"];
-GPGKeyRings = [];
-GPGSigOptions = ["--output","-"];
-GPGSearchOptions = ["--dry-run","--with-colons","--fingerprint"];
-GPGEncryptOptions = ["--output","-","--quiet","--always-trust",\
-                     "--armor","--encrypt"];
-GPGEncryptPGP2Options = ["--set-filename","","--rfc1991",\
-                         "--load-extension","idea",\
-                         "--cipher-algo","idea"] + GPGEncryptOptions;
+# "--load-extension", "rsa",
+GPGBasicOptions = ["--no-options",
+                   "--batch",
+                   "--no-default-keyring",
+                   "--secret-keyring", "/dev/null",
+                   "--always-trust"]
+GPGKeyRings = []
+GPGSigOptions = ["--output", "-"]
+GPGSearchOptions = ["--dry-run", "--with-colons", "--fingerprint",
+                    "--fingerprint", "--fixed-list-mode"]
+GPGEncryptOptions = ["--output", "-", "--quiet", "--always-trust",
+                     "--armor", "--encrypt"]
+GPGEncryptPGP2Options = ["--set-filename", "", "--rfc1991",
+                         "--load-extension", "idea",
+                         "--cipher-algo", "idea"] + GPGEncryptOptions
 
 # Replay cutoff times in seconds
-CleanCutOff = 7*24*60*60;
-AgeCutOff = 4*24*60*60;
-FutureCutOff = 3*24*60*60;
+CleanCutOff = 7 * 24 * 60 * 60
+AgeCutOff = 4 * 24 * 60 * 60
+FutureCutOff = 3 * 24 * 60 * 60
+
 
 def ClearKeyrings():
    del GPGKeyRings[:]
 
+
 # Set the keyrings, the input is a list of keyrings
 def SetKeyrings(Rings):
    for x in Rings:
-      GPGKeyRings.append("--keyring");
-      GPGKeyRings.append(x);
+      GPGKeyRings.append("--keyring")
+      GPGKeyRings.append(x)
+
 
 # GetClearSig takes an un-seekable email message stream (mimetools.Message)
 # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded
@@ -74,110 +87,89 @@ def SetKeyrings(Rings):
 #
 # Paranoid will check the message text to make sure that all the plaintext is
 # in fact signed (bounded by a PGP packet)
-def GetClearSig(Msg,Paranoid = 0):
-   Error = 'MIME Error';
+#
+# lax_multipart: treat multipart bodies other than multipart/signed
+# as one big plain text body
+def GetClearSig(Msg, Paranoid=0, lax_multipart=False):
+   if not Msg.__class__ == email.message.Message:
+      raise RuntimeError, "GetClearSign() not called with a email.message.Message"
+
+   if Paranoid and lax_multipart:
+      raise RuntimeError, "Paranoid and lax_multipart don't mix well"
+
    # See if this is a MIME encoded multipart signed message
-   if Msg.gettype() == "multipart/signed":
-      Boundary = Msg.getparam("boundary");
-      if not Boundary:
-         raise Error, "multipart/* without a boundary parameter";
-
-      # Create the multipart handler. Regrettably their implementation
-      # Needs seeking..
-      SkMessage = StringIO.StringIO();
-      SkMessage.write(Msg.fp.read());
-      SkMessage.seek(0);
-      mf = multifile.MultiFile(SkMessage)
-      mf.push(Msg.getparam("boundary"));
-
-      # Check the first bit of the message..
-      if Paranoid != 0:
-         Pos = mf.tell();
-         while 1:
-             x = mf.readline();
-             if not x: break;
-             if len(x.strip()) != 0:
-                raise Error,"Unsigned text in message (at start)";
-         mf.seek(Pos);
-
-      # Get the first part of the multipart message
-      if not mf.next():
-         raise Error, "Invalid pgp/mime encoding [no section]";
-
-      # Get the part as a safe seekable stream
-      Signed = StringIO.StringIO();
-      Signed.write(mf.read());
-      InnerMsg = mimetools.Message(Signed);
-
-      # Make sure it is the right type
-      if InnerMsg.gettype() != "text/plain":
-         raise Error, "Invalid pgp/mime encoding [wrong plaintext type]";
-
-      # Get the next part of the multipart message
-      if not mf.next():
-         raise Error, "Invalid pgp/mime encoding [no section]";
-      InnerMsg = mimetools.Message(mf);
-      if InnerMsg.gettype() != "application/pgp-signature":
-         raise Error, "Invalid pgp/mime encoding [wrong signature type]";
-      Signature = ''.join(mf.readlines())
-
-      # Check the last bit of the message..
-      if Paranoid != 0:
-         mf.pop();
-         Pos = mf.tell();
-         while 1:
-             x = mf.readline();
-             if not x: break;
-             if len(x.strip()) != 0:
-                raise Error,"Unsigned text in message (at end)";
-         mf.seek(Pos);
+   if Msg.is_multipart():
+      if not Msg.get_content_type() == "multipart/signed":
+         if lax_multipart:
+            payloads = Msg.get_payload()
+            msg = "\n".join(map( lambda p: p.get_payload(decode=True), payloads))
+            return (msg, 0)
+         raise UDFormatError, "Cannot handle multipart messages not of type multipart/signed";
+
+      if Paranoid:
+         if Msg.preamble is not None and Msg.preamble.strip() != "":
+            raise UDFormatError,"Unsigned text in message (at start)";
+         if Msg.epilogue is not None and Msg.epilogue.strip() != "":
+            raise UDFormatError,"Unsigned text in message (at end)";
+
+      payloads = Msg.get_payload()
+      if len(payloads) != 2:
+         raise UDFormatError, "multipart/signed message with number of payloads != 2";
+
+      (Signed, Signature) = payloads
+
+      if Signed.get_content_type() != "text/plain" and not lax_multipart:
+         raise UDFormatError, "Invalid pgp/mime encoding for first part[wrong plaintext type]";
+      if Signature.get_content_type() != "application/pgp-signature":
+         raise UDFormatError, "Invalid pgp/mime encoding for second part [wrong signature type]";
 
       # Append the PGP boundary header and the signature text to re-form the
       # original signed block [needs to convert to \r\n]
       Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
       # Semi-evil hack to get the proper hash type inserted in the message
-      if Msg.getparam('micalg') != None:
-          Output = Output + "Hash: MD5,SHA1,%s\r\n"%(Msg.getparam('micalg')[4:].upper())
+      if Msg.get_param('micalg') is not None:
+          Output = Output + "Hash: SHA1,%s\r\n"%(Msg.get_param('micalg')[4:].upper())
       Output = Output + "\r\n";
-      Output = Output + Signed.getvalue().replace("\n-","\n- -") + Signature
+      Output = Output + Signed.as_string().replace("\n-","\n- -") + "\n" + Signature.get_payload(decode=True)
       return (Output,1);
    else:
       if Paranoid == 0:
          # Just return the message body
-         return (''.join(Msg.fp.readlines()),0);
+         return (Msg.get_payload(decode=True), 0);
 
-      Body = "";
+      Body = [];
       State = 1;
-      for x in Msg.fp.readlines():
-          Body = Body + x;
-          Tmp = x.strip()
-          if len(Tmp) == 0:
+      for x in Msg.get_payload(decode=True).split('\n'):
+          Body.append(x)
+
+          if x == "":
              continue;
 
           # Leading up to the signature
           if State == 1:
-             if Tmp == "-----BEGIN PGP SIGNED MESSAGE-----":
+             if x == "-----BEGIN PGP SIGNED MESSAGE-----":
                 State = 2;
              else:
-                raise Error,"Unsigned text in message (at start)";
+                raise UDFormatError,"Unsigned text in message (at start)";
              continue;
 
           # In the signature plain text
           if State == 2:
-             if Tmp == "-----BEGIN PGP SIGNATURE-----":
+             if x == "-----BEGIN PGP SIGNATURE-----":
                 State = 3;
              continue;
 
           # In the signature
           if State == 3:
-             if Tmp == "-----END PGP SIGNATURE-----":
+             if x == "-----END PGP SIGNATURE-----":
                 State = 4;
              continue;
 
           # Past the end
           if State == 4:
-             raise Error,"Unsigned text in message (at end)";
-      return (Body,0);
+             raise UDFormatError,"Unsigned text in message (at end)";
+
+      return ("\n".join(Body), 0);
 
 # This opens GPG in 'write filter' mode. It takes Message and sends it
 # to GPGs standard input, pipes the standard output to a temp file along
@@ -224,7 +216,7 @@ def GPGWriteFilter(Program,Options,Message):
       InPipe[0] = -1;
 
       # Send the message
-      if Message != None:
+      if Message is not None:
          try:
             os.write(InPipe[1],Message);
          except:
@@ -252,12 +244,14 @@ def GPGWriteFilter(Program,Options,Message):
       Output.close();
       GPGText.close();
 
+
+
 # This takes a text passage, a destination and a flag indicating the
 # compatibility to use and returns an encrypted message to the recipient.
 # It is best if the recipient is specified using the hex key fingerprint
 # of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
 def GPGEncrypt(Message,To,PGP2):
-   Error = "KeyringError"
+   class KeyringError(Exception): pass
    # Encrypt using the PGP5 block encoding and with the PGP5 option set.
    # This will handle either RSA or DSA/DH asymetric keys.
    # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
@@ -265,11 +259,11 @@ def GPGEncrypt(Message,To,PGP2):
    # can read a message encrypted with blowfish and RSA.
    searchkey = GPGKeySearch(To);
    if len(searchkey) == 0:
-      raise Error, "No key found matching %s"%(To);
+      raise KeyringError("No key found matching %s"%(To))
    elif len(searchkey) > 1:
-      raise Error, "Multiple keys found matching %s"%(To);
+      raise KeyringError("Multiple keys found matching %s"%(To))
    if searchkey[0][4].find("E") < 0:
-      raise Error, "Key %s has no encryption capability - are all encryption subkeys expired or revoked?  Are there any encryption subkeys?"%(To);
+      raise KeyringError("Key %s has no encryption capability - are all encryption subkeys expired or revoked?  Are there any encryption subkeys?"%(To))
 
    if PGP2 == 0:
       try:
@@ -280,7 +274,7 @@ def GPGEncrypt(Message,To,PGP2):
          Text = Res[2].read();
          return Text;
       finally:
-         if Res != None:
+         if Res is not None:
             Res[1].close();
             Res[2].close();
    else:
@@ -302,7 +296,7 @@ def GPGEncrypt(Message,To,PGP2):
             os.unlink(TmpName);
          except:
             pass;
-         if Res != None:
+         if Res is not None:
             Res[1].close();
             Res[2].close();
 
@@ -351,7 +345,7 @@ def GPGCheckSig(Message):
          # Good signature response
          if Split[1] == "GOODSIG":
             # Just in case GPG returned a bad signal before this (bug?)
-            if Why == None:
+            if Why is None:
                GoodSig = 1;
             KeyID = Split[2];
             Owner = ' '.join(Split[3:])
@@ -424,21 +418,21 @@ def GPGCheckSig(Message):
       Text = Res[2].read();
 
       # A gpg failure is an automatic bad signature
-      if Exit[1] != 0 and Why == None:
+      if Exit[1] != 0 and Why is None:
          GoodSig = 0;
          Why = "GPG execution returned non-zero exit status: " + str(Exit[1]);
 
-      if GoodSig == 0 and (Why == None or len(Why) == 0):
+      if GoodSig == 0 and (Why is None or len(Why) == 0):
          Why = "Checking Failed";
 
       # Try to decide if this message was sent using PGP2
       PGP2Message = 0;
-      if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) != None):
+      if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) is not None):
          PGP2Message = 1;
 
       return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
    finally:
-      if Res != None:
+      if Res is not None:
          Res[1].close();
          Res[2].close();
 
@@ -471,47 +465,75 @@ class GPGCheckSig2:
 def GPGKeySearch(SearchCriteria):
    Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
           [SearchCriteria," 2> /dev/null"]
-   Strm = None;
-   Result = [];
-   Owner = "";
-   KeyID = "";
+   Strm = None
+   Result = []
+   Validity = None
+   Length = 0
+   KeyID = ""
    Capabilities = ""
-   Expired = None;
-   Hits = {};
+   Fingerprint = ""
+   Owner = ""
+   Hits = {}
 
    dir = os.path.expanduser("~/.gnupg")
    if not os.path.isdir(dir):
       os.mkdir(dir, 0700)
 
    try:
+      # The GPG output will contain zero or more stanza, one stanza per match found.
+      # Each stanza consists of the following records, in order:
+      #   tru : trust database information
+      #   pub : primary key from which we extract
+      #         field  1 - Validity
+      #         field  2 - Length
+      #         field  4 - KeyID
+      #         field 11 - Capabilities
+      #   fpr : fingerprint of primary key from which we extract
+      #         field  9 - Fingerprint
+      #   uid : first User ID attached to primary key from which we extract
+      #         Field  9 - Owner
+      #   uid : (optional) additional multiple User IDs attached to primary key
+      #   sub : (optional) secondary key
+      #   fpr : (opitonal) fingerprint of secondary key if sub is present
       Strm = os.popen(" ".join(Args),"r")
-
+      Want = "pub"
       while(1):
-         # Grab and split up line
-         Line = Strm.readline();
+         Line = Strm.readline()
          if Line == "":
-            break;
+            break
          Split = Line.split(":")
 
-         # Store some of the key fields
-         if Split[0] == 'pub':
-            KeyID = Split[4];
-            Owner = Split[9];
+         if Split[0] != Want:
+            continue
+
+         if Want == 'pub':
+            Validity = Split[1]
             Length = int(Split[2])
+            KeyID = Split[4]
             Capabilities = Split[11]
-            Expired = Split[1] == 'e'
-
-         # Output the key
-         if Split[0] == 'fpr':
-            if Hits.has_key(Split[9]):
-               continue;
-            Hits[Split[9]] = None;
-            if not Expired:
-               Result.append( (KeyID,Split[9],Owner,Length,Capabilities) );
+            Want = 'fpr'
+            continue
+
+         if Want == 'fpr':
+            Fingerprint = Split[9]
+            if Hits.has_key(Fingerprint):
+               Want = 'pub' # already seen, skip to next stanza
+            else:
+               Hits[Fingerprint] = None
+               Want = 'uid'
+            continue
+
+         if Want == 'uid':
+            Owner = Split[9]
+            if Validity != 'e': # if not expired
+               Result.append( (KeyID,Fingerprint,Owner,Length,Capabilities) )
+            Want = 'pub' # finished, skip to next stanza
+            continue
+
    finally:
-      if Strm != None:
-         Strm.close();
-   return Result;
+      if Strm is not None:
+         Strm.close()
+   return Result
 
 # Print the available key information in a format similar to GPG's output
 # We do not know the values of all the feilds so they are just replaced
@@ -546,6 +568,7 @@ class ReplayCache:
       self.CleanCutOff = CleanCutOff;
       self.AgeCutOff = AgeCutOff;
       self.FutureCutOff = FutureCutOff;
+      self.Clean()
 
    # Close the cache and lock
    def __del__(self):
@@ -564,7 +587,7 @@ class ReplayCache:
    # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
    # key ID
    def Check(self,Sig):
-      if Sig[0] == None or Sig[1] == None or Sig[2] == None:
+      if Sig[0] is None or Sig[1] is None or Sig[2] is None:
          return "Invalid signature";
       if int(Sig[1]) > time.time() + self.FutureCutOff:
          return "Signature has a time too far in the future";
@@ -577,7 +600,7 @@ class ReplayCache:
 
    # Add a signature, the sig is the same as is given to Check
    def Add(self,Sig):
-      if Sig[0] == None or Sig[1] == None:
+      if Sig[0] is None or Sig[1] is None:
          raise RuntimeError,"Invalid signature";
       if Sig[1] < time.time() - self.CleanCutOff:
          return;
@@ -588,6 +611,13 @@ class ReplayCache:
       else:
          self.DB[Key] = str(int(Sig[1]));
 
+   def process(self, sig_info):
+      r = self.Check(sig_info);
+      if r is not None:
+         raise RuntimeError, "The replay cache rejected your message: %s." % (r,)
+      self.Add(sig_info)
+      self.close()
+
 # vim:set et:
 # vim:set ts=3:
 # vim:set shiftwidth=3: