X-Git-Url: https://git.adam-barratt.org.uk/?p=mirror%2Fuserdir-ldap.git;a=blobdiff_plain;f=userdir_gpg.py;h=da8abfc72374797e580bd163a76491426f91548d;hp=7a6862bb74f10b8639a671652c9007d37eb86e6c;hb=HEAD;hpb=bc235b8e60a9d83053900bc5abce1c125ea94bb9 diff --git a/userdir_gpg.py b/userdir_gpg.py index 7a6862b..da8abfc 100644 --- a/userdir_gpg.py +++ b/userdir_gpg.py @@ -26,40 +26,53 @@ # 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 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') != 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 @@ -471,47 +463,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; + 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 +566,7 @@ class ReplayCache: self.CleanCutOff = CleanCutOff; self.AgeCutOff = AgeCutOff; self.FutureCutOff = FutureCutOff; + self.Clean() # Close the cache and lock def __del__(self): @@ -588,6 +609,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: