X-Git-Url: https://git.adam-barratt.org.uk/?a=blobdiff_plain;f=userdir_gpg.py;h=abe1708cbf1f604a6dbd62597a45d26722a43f33;hb=9074893cdd1396b213882989b1b2c0f698af7e56;hp=21bc1387dbf32975fd979fdbbf7714a1ec9bf925;hpb=37b0b5875ff83d8e9a9e8ca918d42fc32b720f06;p=mirror%2Fuserdir-ldap.git diff --git a/userdir_gpg.py b/userdir_gpg.py index 21bc138..abe1708 100644 --- a/userdir_gpg.py +++ b/userdir_gpg.py @@ -1,5 +1,19 @@ - #!/usr/bin/env python -# -*- mode: python -*- +# Copyright (c) 1999-2001 Jason Gunthorpe +# Copyright (c) 2005 Joey Schulze +# +# 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 @@ -12,15 +26,19 @@ # 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"]; -GPGKeyRings = ["--keyring","/usr/share/keyrings/debian-keyring.pgp",\ - "--keyring","/usr/share/keyrings/debian-keyring.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",\ @@ -34,6 +52,15 @@ 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); + # GetClearSig takes an un-seekable email message stream (mimetools.Message) # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded # clear signed text. @@ -44,7 +71,10 @@ FutureCutOff = 3*24*60*60; # 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": @@ -60,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]"; @@ -68,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]"; @@ -79,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 + Signed.getvalue() + 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 @@ -172,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; @@ -260,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; @@ -286,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"; @@ -307,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(); @@ -315,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"; @@ -331,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"] @@ -342,25 +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': - Result.append( (KeyID,Split[9],Owner,Length) ); + if Hits.has_key(Split[9]): + continue; + Hits[Split[9]] = None; + if not Expired: + Result.append( (KeyID,Split[9],Owner,Length,Capabilities) ); finally: if Strm != None: Strm.close(); @@ -376,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 @@ -394,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; @@ -441,3 +588,6 @@ class ReplayCache: else: self.DB[Key] = str(int(Sig[1])); +# vim:set et: +# vim:set ts=3: +# vim:set shiftwidth=3: