ud-mailgate: fix gpg result usage
[mirror/userdir-ldap.git] / userdir_gpg.py
index b181abe..abe1708 100644 (file)
@@ -1,5 +1,19 @@
- #!/usr/bin/env python
-# -*- mode: python -*-
+#   Copyright (c) 1999-2001  Jason Gunthorpe <jgg@debian.org>
+#   Copyright (c) 2005       Joey Schulze <joey@infodrom.org>
+#
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation; either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
 # GPG issues - 
 #  - gpgm with a status FD being fed keymaterial and other interesting
 #    packets so I can tell if a signature is made by pgp2 to enable the
 #    pgp2 encrypting mode.
 
-import string, mimetools, multifile, sys, StringIO, os, tempfile, re;
-import rfc822, time, fcntl, FCNTL, anydbm
+import mimetools, multifile, sys, StringIO, os, tempfile, re;
+import rfc822, time, fcntl, anydbm
 
 # General GPG options
 GPGPath = "gpg"
-GPGBasicOptions = ["--no-options","--batch","--load-extension","rsa",\
-          "--no-default-keyring","--always-trust"];
+# "--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"];
@@ -33,6 +52,9 @@ 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:
@@ -49,7 +71,10 @@ def SetKeyrings(Rings):
 # present in the signature text! The return result is a tuple, the first
 # element is the text itself the second is a mime flag indicating if the
 # result should be mime processed after sig checking.
-def GetClearSig(Msg):
+#
+# 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';
    # See if this is a MIME encoded multipart signed message
    if Msg.gettype() == "multipart/signed":
@@ -65,6 +90,16 @@ def GetClearSig(Msg):
       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]";
@@ -73,7 +108,7 @@ def GetClearSig(Msg):
       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]";
@@ -84,20 +119,65 @@ def GetClearSig(Msg):
       InnerMsg = mimetools.Message(mf);
       if InnerMsg.gettype() != "application/pgp-signature":
          raise Error, "Invalid pgp/mime encoding [wrong signature type]";
-      Signature = string.joinfields(mf.readlines(),'');
-
+      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);
+      
       # 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: %s\r\n"%(string.upper(Msg.getparam('micalg')[4:]));
+          Output = Output + "Hash: MD5,SHA1,%s\r\n"%(Msg.getparam('micalg')[4:].upper())
       Output = Output + "\r\n";
-      Output = Output +  string.replace(Signed.getvalue(),"\n---","\n- ---") + Signature;
+      Output = Output + Signed.getvalue().replace("\n-","\n- -") + Signature
       return (Output,1);
    else:
-      # Just return the message body
-      return (string.joinfields(Msg.fp.readlines(),''),0);
+      if Paranoid == 0:
+         # Just return the message body
+         return (''.join(Msg.fp.readlines()),0);
+     
+      Body = "";
+      State = 1;
+      for x in Msg.fp.readlines():
+         Body = Body + x;
+         Tmp = x.strip()
+         if len(Tmp) == 0:
+            continue;
+        
+         # Leading up to the signature
+         if State == 1:
+            if Tmp == "-----BEGIN PGP SIGNED MESSAGE-----":
+               State = 2;
+            else:
+               raise Error,"Unsigned text in message (at start)";
+            continue;
+        
+         # In the signature plain text
+         if State == 2:
+            if Tmp == "-----BEGIN PGP SIGNATURE-----":
+               State = 3;
+            continue;
+               
+         # In the signature
+         if State == 3:
+            if Tmp == "-----END PGP SIGNATURE-----":
+               State = 4;
+            continue;
+               
+          # Past the end
+         if State == 4:
+            raise Error,"Unsigned text in message (at end)";
+      return (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
@@ -177,11 +257,20 @@ def GPGWriteFilter(Program,Options,Message):
 # 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"
    # 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
    # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
    # can read a message encrypted with blowfish and RSA.
+   searchkey = GPGKeySearch(To);
+   if len(searchkey) == 0:
+      raise Error, "No key found matching %s"%(To);
+   elif len(searchkey) > 1:
+      raise Error, "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);
+
    if PGP2 == 0:
       try:
          Res = None;
@@ -265,8 +354,16 @@ def GPGCheckSig(Message):
            if Why == None:
               GoodSig = 1;
            KeyID = Split[2];
-           Owner = string.join(Split[3:],' ');
-           
+           Owner = ' '.join(Split[3:])
+           # If this message is signed with a subkey which has not yet
+           # expired, GnuPG will say GOODSIG here, even if the primary
+           # key already has expired.  This came up in discussion of
+           # bug #489225.  GPGKeySearch only returns non-expired keys.
+           Verify = GPGKeySearch(KeyID);
+           if len(Verify) == 0:
+              GoodSig = 0
+              Why = "Key has expired (no unexpired key found in keyring matching %s)"%(KeyId);
+
         # Bad signature response
         if Split[1] == "BADSIG":
            GoodSig = 0;
@@ -291,12 +388,17 @@ def GPGCheckSig(Message):
             Why = "Unable to verify signature, signing key missing.";
 
         # Expired signature
-        if Split[1] == "SIGEXPIRED":
+        if Split[1] == "EXPSIG":
            GoodSig = 0;
             Why = "Signature has expired";
-           
+
+        # Expired signature
+        if Split[1] == "EXPKEYSIG":
+           GoodSig = 0;
+            Why = "Signing key (%s, %s) has expired"%(Split[2], Split[3]);
+
         # Revoked key
-        if Split[1] == "KEYREVOKED":
+        if Split[1] == "KEYREVOKED" or Split[1] == "REVKEYSIG":
            GoodSig = 0;
             Why = "Signing key has been revoked";
 
@@ -312,7 +414,11 @@ def GPGCheckSig(Message):
 
          # ValidSig has the key finger print
         if Split[1] == "VALIDSIG":
-           KeyFinger = Split[2];
+           # Use the fingerprint of the primary key when available
+           if len(Split) >= 12:
+               KeyFinger = Split[11];
+            else:
+              KeyFinger = Split[2];
 
       # Reopen the stream as a readable stream
       Text = Res[2].read();
@@ -320,7 +426,7 @@ def GPGCheckSig(Message):
       # A gpg failure is an automatic bad signature
       if Exit[1] != 0 and Why == None:
          GoodSig = 0;
-         Why = "GPG execution failed " + str(Exit[0]);
+         Why = "GPG execution returned non-zero exit status: " + str(Exit[1]);
 
       if GoodSig == 0 and (Why == None or len(Why) == 0):
          Why = "Checking Failed";
@@ -336,10 +442,32 @@ def GPGCheckSig(Message):
          Res[1].close();
          Res[2].close();
 
+class GPGCheckSig2:
+       def __init__(self, msg):
+               res = GPGCheckSig(msg)
+               self.why = res[0]
+               self.sig_info = res[1]
+               self.key_info = res[2]
+               self.text = res[3]
+
+               self.ok = self.why is None
+
+               self.sig_id = self.sig_info[0]
+               self.sig_date = self.sig_info[1]
+               self.sig_fpr = self.sig_info[2]
+
+               self.key_id = self.key_info[0]
+               self.key_fpr = self.key_info[1]
+               self.key_owner = self.key_info[2]
+
+               self.is_pgp2 = self.key_info[4]
+
 # Search for keys given a search pattern. The pattern is passed directly
 # to GPG for processing. The result is a list of tuples of the form:
 #   (KeyID,KeyFinger,Owner,Length)
 # Which is similar to the key identification tuple output by GPGChecksig
+#
+# Do not return keys where the primary key has expired
 def GPGKeySearch(SearchCriteria):
    Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
           [SearchCriteria," 2> /dev/null"]
@@ -347,29 +475,39 @@ def GPGKeySearch(SearchCriteria):
    Result = [];
    Owner = "";
    KeyID = "";
+   Capabilities = ""
+   Expired = None;
    Hits = {};
+
+   dir = os.path.expanduser("~/.gnupg")
+   if not os.path.isdir(dir):
+      os.mkdir(dir, 0700)
+                      
    try:
-      Strm = os.popen(string.join(Args," "),"r");
+      Strm = os.popen(" ".join(Args),"r")
       
       while(1):
          # Grab and split up line
          Line = Strm.readline();
          if Line == "":
             break;
-        Split = string.split(Line,":");
-        
-        # Store some of the key fields
+         Split = Line.split(":")
+
+         # Store some of the key fields
          if Split[0] == 'pub':
             KeyID = Split[4];
             Owner = Split[9];
-           Length = int(Split[2]);
+            Length = int(Split[2])
+            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;
-            Result.append( (KeyID,Split[9],Owner,Length) );
+            if not Expired:
+               Result.append( (KeyID,Split[9],Owner,Length,Capabilities) );
    finally:
       if Strm != None:
          Strm.close();
@@ -385,7 +523,7 @@ def GPGPrintKeyInfo(Ident):
 # Perform a substition of template 
 def TemplateSubst(Map,Template):
    for x in Map.keys():
-      Template = string.replace(Template,x,Map[x]);
+      Template = Template.replace(x, Map[x])
    return Template;
 
 # The replay class uses a python DB (BSD db if avail) to implement
@@ -403,7 +541,7 @@ def TemplateSubst(Map,Template):
 class ReplayCache:
    def __init__(self,Database):
       self.Lock = open(Database + ".lock","w",0600);
-      fcntl.flock(self.Lock.fileno(),FCNTL.LOCK_EX);
+      fcntl.flock(self.Lock.fileno(),fcntl.LOCK_EX);
       self.DB = anydbm.open(Database,"c",0600);
       self.CleanCutOff = CleanCutOff;
       self.AgeCutOff = AgeCutOff;
@@ -450,3 +588,6 @@ class ReplayCache:
       else:
          self.DB[Key] = str(int(Sig[1]));
         
+# vim:set et:
+# vim:set ts=3:
+# vim:set shiftwidth=3: