5 # - gpgm with a status FD being fed keymaterial and other interesting
6 # things does nothing.. If it could ID the keys and stuff over the
7 # status-fd I could decide what to do with them. I would also like it
8 # to report which key it selected for encryption (also if there
9 # were multi-matches..) Being able to detect a key-revoke cert would be
11 # - I would like to be able to fetch the comment and version fields from the
12 # packets so I can tell if a signature is made by pgp2 to enable the
13 # pgp2 encrypting mode.
15 import string, mimetools, multifile, sys, StringIO, os, tempfile, re;
16 import rfc822, time, fcntl, FCNTL, anydbm
20 # "--load-extension","rsa",
21 GPGBasicOptions = ["--no-options","--batch",
22 "--no-default-keyring","--always-trust"];
24 GPGSigOptions = ["--output","-"];
25 GPGSearchOptions = ["--dry-run","--with-colons","--fingerprint"];
26 GPGEncryptOptions = ["--output","-","--quiet","--always-trust",\
27 "--armor","--encrypt"];
28 GPGEncryptPGP2Options = ["--set-filename","","--rfc1991",\
29 "--load-extension","idea",\
30 "--cipher-algo","idea"] + GPGEncryptOptions;
32 # Replay cutoff times in seconds
33 CleanCutOff = 7*24*60*60;
34 AgeCutOff = 4*24*60*60;
35 FutureCutOff = 3*24*60*60;
37 # Set the keyrings, the input is a list of keyrings
38 def SetKeyrings(Rings):
40 GPGKeyRings.append("--keyring");
41 GPGKeyRings.append(x);
43 # GetClearSig takes an un-seekable email message stream (mimetools.Message)
44 # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded
46 # If this is fed to gpg/pgp it will verify the signature and spit out the
47 # signed text component. Email headers and PGP mime (RFC 2015) is understood
48 # but no effort is made to cull any information outside the PGP boundaries
49 # Please note that in the event of a mime decode the mime headers will be
50 # present in the signature text! The return result is a tuple, the first
51 # element is the text itself the second is a mime flag indicating if the
52 # result should be mime processed after sig checking.
54 # Paranoid will check the message text to make sure that all the plaintext is
55 # in fact signed (bounded by a PGP packet)
56 def GetClearSig(Msg,Paranoid = 0):
58 # See if this is a MIME encoded multipart signed message
59 if Msg.gettype() == "multipart/signed":
60 Boundary = Msg.getparam("boundary");
62 raise Error, "multipart/* without a boundary parameter";
64 # Create the multipart handler. Regrettably their implementation
66 SkMessage = StringIO.StringIO();
67 SkMessage.write(Msg.fp.read());
69 mf = multifile.MultiFile(SkMessage)
70 mf.push(Msg.getparam("boundary"));
72 # Check the first bit of the message..
78 if len(string.strip(x)) != 0:
79 raise Error,"Unsigned text in message (at start)";
82 # Get the first part of the multipart message
84 raise Error, "Invalid pgp/mime encoding [no section]";
86 # Get the part as a safe seekable stream
87 Signed = StringIO.StringIO();
88 Signed.write(mf.read());
89 InnerMsg = mimetools.Message(Signed);
91 # Make sure it is the right type
92 if InnerMsg.gettype() != "text/plain":
93 raise Error, "Invalid pgp/mime encoding [wrong plaintext type]";
95 # Get the next part of the multipart message
97 raise Error, "Invalid pgp/mime encoding [no section]";
98 InnerMsg = mimetools.Message(mf);
99 if InnerMsg.gettype() != "application/pgp-signature":
100 raise Error, "Invalid pgp/mime encoding [wrong signature type]";
101 Signature = string.joinfields(mf.readlines(),'');
103 # Check the last bit of the message..
110 if len(string.strip(x)) != 0:
111 raise Error,"Unsigned text in message (at end)";
114 # Append the PGP boundary header and the signature text to re-form the
115 # original signed block [needs to convert to \r\n]
116 Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
117 # Semi-evil hack to get the proper hash type inserted in the message
118 if Msg.getparam('micalg') != None:
119 Output = Output + "Hash: MD5,SHA1,%s\r\n"%(string.upper(Msg.getparam('micalg')[4:]));
120 Output = Output + "\r\n";
121 Output = Output + string.replace(Signed.getvalue(),"\n-","\n- -") + Signature;
125 # Just return the message body
126 return (string.joinfields(Msg.fp.readlines(),''),0);
130 for x in Msg.fp.readlines():
132 Tmp = string.strip(x);
136 # Leading up to the signature
138 if Tmp == "-----BEGIN PGP SIGNED MESSAGE-----":
141 raise Error,"Unsigned text in message (at start)";
144 # In the signature plain text
146 if Tmp == "-----BEGIN PGP SIGNATURE-----":
152 if Tmp == "-----END PGP SIGNATURE-----":
158 raise Error,"Unsigned text in message (at end)";
161 # This opens GPG in 'write filter' mode. It takes Message and sends it
162 # to GPGs standard input, pipes the standard output to a temp file along
163 # with the status FD. The two tempfiles are passed to GPG by fd and are
164 # accessible from the filesystem for only a short period. Message may be
165 # None in which case GPGs stdin is closed directly after forking. This
166 # is best used for sig checking and encryption.
167 # The return result is a tuple (Exit,StatusFD,OutputFD), both fds are
168 # fully rewound and readable.
169 def GPGWriteFilter(Program,Options,Message):
170 # Make sure the tmp files we open are unreadable, there is a short race
171 # between when the temp file is opened and unlinked that some one else
172 # could open it or hard link it. This is not important however as no
173 # Secure data is fed through the temp files.
174 OldMask = os.umask(0777);
176 Output = tempfile.TemporaryFile("w+b");
177 GPGText = tempfile.TemporaryFile("w+b");
179 InPipe = [InPipe[0],InPipe[1]];
184 # Fork off GPG in a horrible way, we redirect most of its FDs
185 # Input comes from a pipe and its two outputs are spooled to unlinked
186 # temp files (ie private)
190 os.dup2(InPipe[0],0);
192 os.dup2(Output.fileno(),1);
193 os.dup2(os.open("/dev/null",os.O_WRONLY),2);
194 os.dup2(GPGText.fileno(),3);
196 Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
197 os.execvp(Program,Args);
201 # Get rid of the other end of the pipe
208 os.write(InPipe[1],Message);
214 # Wait for GPG to finish
215 Exit = os.waitpid(Child,0);
217 # Create the result including the new readable file descriptors
218 Result = (Exit,os.fdopen(os.dup(GPGText.fileno()),"r"), \
219 os.fdopen(os.dup(Output.fileno()),"r"));
234 # This takes a text passage, a destination and a flag indicating the
235 # compatibility to use and returns an encrypted message to the recipient.
236 # It is best if the recipient is specified using the hex key fingerprint
237 # of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
238 def GPGEncrypt(Message,To,PGP2):
239 # Encrypt using the PGP5 block encoding and with the PGP5 option set.
240 # This will handle either RSA or DSA/DH asymetric keys.
241 # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
242 # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
243 # can read a message encrypted with blowfish and RSA.
247 Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
250 Text = Res[2].read();
257 # We have to call gpg with a filename or it will create a packet that
258 # PGP2 cannot understand.
259 TmpName = tempfile.mktemp();
262 MsgFile = open(TmpName,"wc");
263 MsgFile.write(Message);
265 Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
268 Text = Res[2].read();
279 # Checks the signature of a standard PGP message, like that returned by
280 # GetClearSig. It returns a large tuple of the form:
281 # (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,Length,PGP2),Text);
283 # Why = None if checking was OK otherwise an error string.
284 # SigID+Date represent something suitable for use in a replay cache. The
285 # date is returned as the number of seconds since the UTC epoch.
286 # The keyID is also in this tuple for easy use of the replay
288 # KeyID, KeyFinger and Owner represent the Key used to sign this message
289 # PGP2 indicates if the message was created using PGP 2.x
290 # Text is the full byte-for-byte signed text in a string
291 def GPGCheckSig(Message):
294 Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
297 # Parse the GPG answer
308 # Grab and split up line
309 Line = Strm.readline();
312 Split = re.split("[ \n]",Line);
313 if Split[0] != "[GNUPG:]":
316 # We only process the first occurance of any tag.
317 if TagMap.has_key(Split[1]):
319 TagMap[Split[1]] = None;
321 # Good signature response
322 if Split[1] == "GOODSIG":
323 # Just in case GPG returned a bad signal before this (bug?)
327 Owner = string.join(Split[3:],' ');
329 # Bad signature response
330 if Split[1] == "BADSIG":
333 Why = "Verification of signature failed";
335 # Bad signature response
336 if Split[1] == "ERRSIG":
340 Why = "GPG error, ERRSIG status tag is invalid";
341 elif Split[7] == '9':
342 Why = "Unable to verify signature, signing key missing.";
343 elif Split[7] == '4':
344 Why = "Unable to verify signature, unknown packet format/key type";
346 Why = "Unable to verify signature, unknown reason";
348 if Split[1] == "NO_PUBKEY":
350 Why = "Unable to verify signature, signing key missing.";
353 if Split[1] == "SIGEXPIRED":
355 Why = "Signature has expired";
358 if Split[1] == "KEYREVOKED":
360 Why = "Signing key has been revoked";
363 if Split[1] == "NODATA" or Split[1] == "BADARMOR":
365 Why = "The packet was corrupted or contained no data";
368 if Split[1] == "SIG_ID":
370 Date = long(Split[4]);
372 # ValidSig has the key finger print
373 if Split[1] == "VALIDSIG":
374 KeyFinger = Split[2];
376 # Reopen the stream as a readable stream
377 Text = Res[2].read();
379 # A gpg failure is an automatic bad signature
380 if Exit[1] != 0 and Why == None:
382 Why = "GPG execution failed " + str(Exit[0]);
384 if GoodSig == 0 and (Why == None or len(Why) == 0):
385 Why = "Checking Failed";
387 # Try to decide if this message was sent using PGP2
389 if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) != None):
392 return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
398 # Search for keys given a search pattern. The pattern is passed directly
399 # to GPG for processing. The result is a list of tuples of the form:
400 # (KeyID,KeyFinger,Owner,Length)
401 # Which is similar to the key identification tuple output by GPGChecksig
402 def GPGKeySearch(SearchCriteria):
403 Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
404 [SearchCriteria," 2> /dev/null"]
411 Strm = os.popen(string.join(Args," "),"r");
414 # Grab and split up line
415 Line = Strm.readline();
418 Split = string.split(Line,":");
420 # Store some of the key fields
421 if Split[0] == 'pub':
424 Length = int(Split[2]);
427 if Split[0] == 'fpr':
428 if Hits.has_key(Split[9]):
430 Hits[Split[9]] = None;
431 Result.append( (KeyID,Split[9],Owner,Length) );
437 # Print the available key information in a format similar to GPG's output
438 # We do not know the values of all the feilds so they are just replaced
440 def GPGPrintKeyInfo(Ident):
441 print "pub %u?/%s ??-??-?? %s" % (Ident[3],Ident[0][-8:],Ident[2]);
442 print " key fingerprint = 0x%s" % (Ident[1]);
444 # Perform a substition of template
445 def TemplateSubst(Map,Template):
447 Template = string.replace(Template,x,Map[x]);
450 # The replay class uses a python DB (BSD db if avail) to implement
451 # protection against replay. Replay is an attacker capturing the
452 # plain text signed message and sending it back to the victim at some
453 # later date. Each signature has a unique signature ID (and signing
454 # Key Fingerprint) as well as a timestamp. The first stage of replay
455 # protection is to ensure that the timestamp is reasonable, in particular
456 # not to far ahead or too far behind the current system time. The next
457 # step is to look up the signature + key fingerprint in the replay database
458 # and determine if it has been recived. The database is cleaned out
459 # periodically and old signatures are discarded. By using a timestamp the
460 # database size is bounded to being within the range of the allowed times
461 # plus a little fuzz. The cache is serialized with a flocked lock file
463 def __init__(self,Database):
464 self.Lock = open(Database + ".lock","w",0600);
465 fcntl.flock(self.Lock.fileno(),FCNTL.LOCK_EX);
466 self.DB = anydbm.open(Database,"c",0600);
467 self.CleanCutOff = CleanCutOff;
468 self.AgeCutOff = AgeCutOff;
469 self.FutureCutOff = FutureCutOff;
471 # Close the cache and lock
478 # Clean out any old signatures
480 CutOff = time.time() - self.CleanCutOff;
481 for x in self.DB.keys():
482 if int(self.DB[x]) <= CutOff:
485 # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
488 if Sig[0] == None or Sig[1] == None or Sig[2] == None:
489 return "Invalid signature";
490 if int(Sig[1]) > time.time() + self.FutureCutOff:
491 return "Signature has a time too far in the future";
492 if self.DB.has_key(Sig[0] + '-' + Sig[2]):
493 return "Signature has already been received";
494 if int(Sig[1]) < time.time() - self.AgeCutOff:
495 return "Signature has passed the age cut off ";
496 # + str(int(Sig[1])) + ',' + str(time.time()) + "," + str(Sig);
499 # Add a signature, the sig is the same as is given to Check
501 if Sig[0] == None or Sig[1] == None:
502 raise RuntimeError,"Invalid signature";
503 if Sig[1] < time.time() - self.CleanCutOff:
505 Key = Sig[0] + '-' + Sig[2]
506 if self.DB.has_key(Key):
507 if int(self.DB[Key]) < Sig[1]:
508 self.DB[Key] = str(int(Sig[1]));
510 self.DB[Key] = str(int(Sig[1]));