# 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
#
# 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
InPipe[0] = -1;
# Send the message
- if Message != None:
+ if Message is not None:
try:
os.write(InPipe[1],Message);
except:
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
# 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:
Text = Res[2].read();
return Text;
finally:
- if Res != None:
+ if Res is not None:
Res[1].close();
Res[2].close();
else:
os.unlink(TmpName);
except:
pass;
- if Res != None:
+ if Res is not None:
Res[1].close();
Res[2].close();
# 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:])
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();
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
self.CleanCutOff = CleanCutOff;
self.AgeCutOff = AgeCutOff;
self.FutureCutOff = FutureCutOff;
+ self.Clean()
# Close the cache and lock
def __del__(self):
# 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";
# 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;
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: