1 # Copyright (c) 1999-2001 Jason Gunthorpe <jgg@debian.org>
2 # Copyright (c) 2005 Joey Schulze <joey@infodrom.org>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 # - gpgm with a status FD being fed keymaterial and other interesting
20 # things does nothing.. If it could ID the keys and stuff over the
21 # status-fd I could decide what to do with them. I would also like it
22 # to report which key it selected for encryption (also if there
23 # were multi-matches..) Being able to detect a key-revoke cert would be
25 # - I would like to be able to fetch the comment and version fields from the
26 # packets so I can tell if a signature is made by pgp2 to enable the
27 # pgp2 encrypting mode.
29 import mimetools, multifile, sys, StringIO, os, tempfile, re;
30 import rfc822, time, fcntl, anydbm
34 # "--load-extension","rsa",
38 "--no-default-keyring",
39 "--secret-keyring", "/dev/null",
42 GPGSigOptions = ["--output","-"];
43 GPGSearchOptions = ["--dry-run","--with-colons","--fingerprint"];
44 GPGEncryptOptions = ["--output","-","--quiet","--always-trust",\
45 "--armor","--encrypt"];
46 GPGEncryptPGP2Options = ["--set-filename","","--rfc1991",\
47 "--load-extension","idea",\
48 "--cipher-algo","idea"] + GPGEncryptOptions;
50 # Replay cutoff times in seconds
51 CleanCutOff = 7*24*60*60;
52 AgeCutOff = 4*24*60*60;
53 FutureCutOff = 3*24*60*60;
58 # Set the keyrings, the input is a list of keyrings
59 def SetKeyrings(Rings):
61 GPGKeyRings.append("--keyring");
62 GPGKeyRings.append(x);
64 # GetClearSig takes an un-seekable email message stream (mimetools.Message)
65 # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded
67 # If this is fed to gpg/pgp it will verify the signature and spit out the
68 # signed text component. Email headers and PGP mime (RFC 2015) is understood
69 # but no effort is made to cull any information outside the PGP boundaries
70 # Please note that in the event of a mime decode the mime headers will be
71 # present in the signature text! The return result is a tuple, the first
72 # element is the text itself the second is a mime flag indicating if the
73 # result should be mime processed after sig checking.
75 # Paranoid will check the message text to make sure that all the plaintext is
76 # in fact signed (bounded by a PGP packet)
77 def GetClearSig(Msg,Paranoid = 0):
79 # See if this is a MIME encoded multipart signed message
80 if Msg.gettype() == "multipart/signed":
81 Boundary = Msg.getparam("boundary");
83 raise Error, "multipart/* without a boundary parameter";
85 # Create the multipart handler. Regrettably their implementation
87 SkMessage = StringIO.StringIO();
88 SkMessage.write(Msg.fp.read());
90 mf = multifile.MultiFile(SkMessage)
91 mf.push(Msg.getparam("boundary"));
93 # Check the first bit of the message..
99 if len(x.strip()) != 0:
100 raise Error,"Unsigned text in message (at start)";
103 # Get the first part of the multipart message
105 raise Error, "Invalid pgp/mime encoding [no section]";
107 # Get the part as a safe seekable stream
108 Signed = StringIO.StringIO();
109 Signed.write(mf.read());
110 InnerMsg = mimetools.Message(Signed);
112 # Make sure it is the right type
113 if InnerMsg.gettype() != "text/plain":
114 raise Error, "Invalid pgp/mime encoding [wrong plaintext type]";
116 # Get the next part of the multipart message
118 raise Error, "Invalid pgp/mime encoding [no section]";
119 InnerMsg = mimetools.Message(mf);
120 if InnerMsg.gettype() != "application/pgp-signature":
121 raise Error, "Invalid pgp/mime encoding [wrong signature type]";
122 Signature = ''.join(mf.readlines())
124 # Check the last bit of the message..
131 if len(x.strip()) != 0:
132 raise Error,"Unsigned text in message (at end)";
135 # Append the PGP boundary header and the signature text to re-form the
136 # original signed block [needs to convert to \r\n]
137 Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
138 # Semi-evil hack to get the proper hash type inserted in the message
139 if Msg.getparam('micalg') != None:
140 Output = Output + "Hash: MD5,SHA1,%s\r\n"%(Msg.getparam('micalg')[4:].upper())
141 Output = Output + "\r\n";
142 Output = Output + Signed.getvalue().replace("\n-","\n- -") + Signature
146 # Just return the message body
147 return (''.join(Msg.fp.readlines()),0);
151 for x in Msg.fp.readlines():
157 # Leading up to the signature
159 if Tmp == "-----BEGIN PGP SIGNED MESSAGE-----":
162 raise Error,"Unsigned text in message (at start)";
165 # In the signature plain text
167 if Tmp == "-----BEGIN PGP SIGNATURE-----":
173 if Tmp == "-----END PGP SIGNATURE-----":
179 raise Error,"Unsigned text in message (at end)";
182 # This opens GPG in 'write filter' mode. It takes Message and sends it
183 # to GPGs standard input, pipes the standard output to a temp file along
184 # with the status FD. The two tempfiles are passed to GPG by fd and are
185 # accessible from the filesystem for only a short period. Message may be
186 # None in which case GPGs stdin is closed directly after forking. This
187 # is best used for sig checking and encryption.
188 # The return result is a tuple (Exit,StatusFD,OutputFD), both fds are
189 # fully rewound and readable.
190 def GPGWriteFilter(Program,Options,Message):
191 # Make sure the tmp files we open are unreadable, there is a short race
192 # between when the temp file is opened and unlinked that some one else
193 # could open it or hard link it. This is not important however as no
194 # Secure data is fed through the temp files.
195 OldMask = os.umask(0777);
197 Output = tempfile.TemporaryFile("w+b");
198 GPGText = tempfile.TemporaryFile("w+b");
200 InPipe = [InPipe[0],InPipe[1]];
205 # Fork off GPG in a horrible way, we redirect most of its FDs
206 # Input comes from a pipe and its two outputs are spooled to unlinked
207 # temp files (ie private)
211 os.dup2(InPipe[0],0);
213 os.dup2(Output.fileno(),1);
214 os.dup2(os.open("/dev/null",os.O_WRONLY),2);
215 os.dup2(GPGText.fileno(),3);
217 Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
218 os.execvp(Program,Args);
222 # Get rid of the other end of the pipe
229 os.write(InPipe[1],Message);
235 # Wait for GPG to finish
236 Exit = os.waitpid(Child,0);
238 # Create the result including the new readable file descriptors
239 Result = (Exit,os.fdopen(os.dup(GPGText.fileno()),"r"), \
240 os.fdopen(os.dup(Output.fileno()),"r"));
255 # This takes a text passage, a destination and a flag indicating the
256 # compatibility to use and returns an encrypted message to the recipient.
257 # It is best if the recipient is specified using the hex key fingerprint
258 # of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
259 def GPGEncrypt(Message,To,PGP2):
260 Error = "KeyringError"
261 # Encrypt using the PGP5 block encoding and with the PGP5 option set.
262 # This will handle either RSA or DSA/DH asymetric keys.
263 # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
264 # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
265 # can read a message encrypted with blowfish and RSA.
266 searchkey = GPGKeySearch(To);
267 if len(searchkey) == 0:
268 raise Error, "No key found matching %s"%(To);
269 elif len(searchkey) > 1:
270 raise Error, "Multiple keys found matching %s"%(To);
271 if searchkey[0][4].find("E") < 0:
272 raise Error, "Key %s has no encryption capability - are all encryption subkeys expired or revoked? Are there any encryption subkeys?"%(To);
277 Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
280 Text = Res[2].read();
287 # We have to call gpg with a filename or it will create a packet that
288 # PGP2 cannot understand.
289 TmpName = tempfile.mktemp();
292 MsgFile = open(TmpName,"wc");
293 MsgFile.write(Message);
295 Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
298 Text = Res[2].read();
309 # Checks the signature of a standard PGP message, like that returned by
310 # GetClearSig. It returns a large tuple of the form:
311 # (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,Length,PGP2),Text);
313 # Why = None if checking was OK otherwise an error string.
314 # SigID+Date represent something suitable for use in a replay cache. The
315 # date is returned as the number of seconds since the UTC epoch.
316 # The keyID is also in this tuple for easy use of the replay
318 # KeyID, KeyFinger and Owner represent the Key used to sign this message
319 # PGP2 indicates if the message was created using PGP 2.x
320 # Text is the full byte-for-byte signed text in a string
321 def GPGCheckSig(Message):
324 Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
327 # Parse the GPG answer
338 # Grab and split up line
339 Line = Strm.readline();
342 Split = re.split("[ \n]",Line);
343 if Split[0] != "[GNUPG:]":
346 # We only process the first occurance of any tag.
347 if TagMap.has_key(Split[1]):
349 TagMap[Split[1]] = None;
351 # Good signature response
352 if Split[1] == "GOODSIG":
353 # Just in case GPG returned a bad signal before this (bug?)
357 Owner = ' '.join(Split[3:])
358 # If this message is signed with a subkey which has not yet
359 # expired, GnuPG will say GOODSIG here, even if the primary
360 # key already has expired. This came up in discussion of
361 # bug #489225. GPGKeySearch only returns non-expired keys.
362 Verify = GPGKeySearch(KeyID);
365 Why = "Key has expired (no unexpired key found in keyring matching %s)"%(KeyId);
367 # Bad signature response
368 if Split[1] == "BADSIG":
371 Why = "Verification of signature failed";
373 # Bad signature response
374 if Split[1] == "ERRSIG":
378 Why = "GPG error, ERRSIG status tag is invalid";
379 elif Split[7] == '9':
380 Why = "Unable to verify signature, signing key missing.";
381 elif Split[7] == '4':
382 Why = "Unable to verify signature, unknown packet format/key type";
384 Why = "Unable to verify signature, unknown reason";
386 if Split[1] == "NO_PUBKEY":
388 Why = "Unable to verify signature, signing key missing.";
391 if Split[1] == "EXPSIG":
393 Why = "Signature has expired";
396 if Split[1] == "EXPKEYSIG":
398 Why = "Signing key (%s, %s) has expired"%(Split[2], Split[3]);
401 if Split[1] == "KEYREVOKED" or Split[1] == "REVKEYSIG":
403 Why = "Signing key has been revoked";
406 if Split[1] == "NODATA" or Split[1] == "BADARMOR":
408 Why = "The packet was corrupted or contained no data";
411 if Split[1] == "SIG_ID":
413 Date = long(Split[4]);
415 # ValidSig has the key finger print
416 if Split[1] == "VALIDSIG":
417 # Use the fingerprint of the primary key when available
419 KeyFinger = Split[11];
421 KeyFinger = Split[2];
423 # Reopen the stream as a readable stream
424 Text = Res[2].read();
426 # A gpg failure is an automatic bad signature
427 if Exit[1] != 0 and Why == None:
429 Why = "GPG execution returned non-zero exit status: " + str(Exit[1]);
431 if GoodSig == 0 and (Why == None or len(Why) == 0):
432 Why = "Checking Failed";
434 # Try to decide if this message was sent using PGP2
436 if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) != None):
439 return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
445 # Search for keys given a search pattern. The pattern is passed directly
446 # to GPG for processing. The result is a list of tuples of the form:
447 # (KeyID,KeyFinger,Owner,Length)
448 # Which is similar to the key identification tuple output by GPGChecksig
450 # Do not return keys where the primary key has expired
451 def GPGKeySearch(SearchCriteria):
452 Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
453 [SearchCriteria," 2> /dev/null"]
462 dir = os.path.expanduser("~/.gnupg")
463 if not os.path.isdir(dir):
467 Strm = os.popen(" ".join(Args),"r")
470 # Grab and split up line
471 Line = Strm.readline();
474 Split = Line.split(":")
476 # Store some of the key fields
477 if Split[0] == 'pub':
480 Length = int(Split[2])
481 Capabilities = Split[11]
482 Expired = Split[1] == 'e'
485 if Split[0] == 'fpr':
486 if Hits.has_key(Split[9]):
488 Hits[Split[9]] = None;
490 Result.append( (KeyID,Split[9],Owner,Length,Capabilities) );
496 # Print the available key information in a format similar to GPG's output
497 # We do not know the values of all the feilds so they are just replaced
499 def GPGPrintKeyInfo(Ident):
500 print "pub %u?/%s ??-??-?? %s" % (Ident[3],Ident[0][-8:],Ident[2]);
501 print " key fingerprint = 0x%s" % (Ident[1]);
503 # Perform a substition of template
504 def TemplateSubst(Map,Template):
506 Template = Template.replace(x, Map[x])
509 # The replay class uses a python DB (BSD db if avail) to implement
510 # protection against replay. Replay is an attacker capturing the
511 # plain text signed message and sending it back to the victim at some
512 # later date. Each signature has a unique signature ID (and signing
513 # Key Fingerprint) as well as a timestamp. The first stage of replay
514 # protection is to ensure that the timestamp is reasonable, in particular
515 # not to far ahead or too far behind the current system time. The next
516 # step is to look up the signature + key fingerprint in the replay database
517 # and determine if it has been recived. The database is cleaned out
518 # periodically and old signatures are discarded. By using a timestamp the
519 # database size is bounded to being within the range of the allowed times
520 # plus a little fuzz. The cache is serialized with a flocked lock file
522 def __init__(self,Database):
523 self.Lock = open(Database + ".lock","w",0600);
524 fcntl.flock(self.Lock.fileno(),fcntl.LOCK_EX);
525 self.DB = anydbm.open(Database,"c",0600);
526 self.CleanCutOff = CleanCutOff;
527 self.AgeCutOff = AgeCutOff;
528 self.FutureCutOff = FutureCutOff;
530 # Close the cache and lock
537 # Clean out any old signatures
539 CutOff = time.time() - self.CleanCutOff;
540 for x in self.DB.keys():
541 if int(self.DB[x]) <= CutOff:
544 # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
547 if Sig[0] == None or Sig[1] == None or Sig[2] == None:
548 return "Invalid signature";
549 if int(Sig[1]) > time.time() + self.FutureCutOff:
550 return "Signature has a time too far in the future";
551 if self.DB.has_key(Sig[0] + '-' + Sig[2]):
552 return "Signature has already been received";
553 if int(Sig[1]) < time.time() - self.AgeCutOff:
554 return "Signature has passed the age cut off ";
555 # + str(int(Sig[1])) + ',' + str(time.time()) + "," + str(Sig);
558 # Add a signature, the sig is the same as is given to Check
560 if Sig[0] == None or Sig[1] == None:
561 raise RuntimeError,"Invalid signature";
562 if Sig[1] < time.time() - self.CleanCutOff:
564 Key = Sig[0] + '-' + Sig[2]
565 if self.DB.has_key(Key):
566 if int(self.DB[Key]) < Sig[1]:
567 self.DB[Key] = str(int(Sig[1]));
569 self.DB[Key] = str(int(Sig[1]));