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.
40 from userdir_exceptions import *
44 # "--load-extension", "rsa",
45 GPGBasicOptions = ["--no-options",
47 "--no-default-keyring",
48 "--secret-keyring", "/dev/null",
51 GPGSigOptions = ["--output", "-"]
52 GPGSearchOptions = ["--dry-run", "--with-colons", "--fingerprint",
53 "--fingerprint", "--fixed-list-mode"]
54 GPGEncryptOptions = ["--output", "-", "--quiet", "--always-trust",
55 "--armor", "--encrypt"]
56 GPGEncryptPGP2Options = ["--set-filename", "", "--rfc1991",
57 "--load-extension", "idea",
58 "--cipher-algo", "idea"] + GPGEncryptOptions
60 # Replay cutoff times in seconds
61 CleanCutOff = 7 * 24 * 60 * 60
62 AgeCutOff = 4 * 24 * 60 * 60
63 FutureCutOff = 3 * 24 * 60 * 60
70 # Set the keyrings, the input is a list of keyrings
71 def SetKeyrings(Rings):
73 GPGKeyRings.append("--keyring")
77 # GetClearSig takes an un-seekable email message stream (mimetools.Message)
78 # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded
80 # If this is fed to gpg/pgp it will verify the signature and spit out the
81 # signed text component. Email headers and PGP mime (RFC 2015) is understood
82 # but no effort is made to cull any information outside the PGP boundaries
83 # Please note that in the event of a mime decode the mime headers will be
84 # present in the signature text! The return result is a tuple, the first
85 # element is the text itself the second is a mime flag indicating if the
86 # result should be mime processed after sig checking.
88 # Paranoid will check the message text to make sure that all the plaintext is
89 # in fact signed (bounded by a PGP packet)
91 # lax_multipart: treat multipart bodies other than multipart/signed
92 # as one big plain text body
93 def GetClearSig(Msg, Paranoid=0, lax_multipart=False):
94 if not Msg.__class__ == email.message.Message:
95 raise RuntimeError, "GetClearSign() not called with a email.message.Message"
97 if Paranoid and lax_multipart:
98 raise RuntimeError, "Paranoid and lax_multipart don't mix well"
100 # See if this is a MIME encoded multipart signed message
101 if Msg.is_multipart():
102 if not Msg.get_content_type() == "multipart/signed":
104 payloads = Msg.get_payload()
105 msg = "\n".join(map( lambda p: p.get_payload(decode=True), payloads))
107 raise UDFormatError, "Cannot handle multipart messages not of type multipart/signed";
110 if Msg.preamble is not None and Msg.preamble.strip() != "":
111 raise UDFormatError,"Unsigned text in message (at start)";
112 if Msg.epilogue is not None and Msg.epilogue.strip() != "":
113 raise UDFormatError,"Unsigned text in message (at end)";
115 payloads = Msg.get_payload()
116 if len(payloads) != 2:
117 raise UDFormatError, "multipart/signed message with number of payloads != 2";
119 (Signed, Signature) = payloads
121 if Signed.get_content_type() != "text/plain" and not lax_multipart:
122 raise UDFormatError, "Invalid pgp/mime encoding for first part[wrong plaintext type]";
123 if Signature.get_content_type() != "application/pgp-signature":
124 raise UDFormatError, "Invalid pgp/mime encoding for second part [wrong signature type]";
126 # Append the PGP boundary header and the signature text to re-form the
127 # original signed block [needs to convert to \r\n]
128 Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
129 # Semi-evil hack to get the proper hash type inserted in the message
130 if Msg.get_param('micalg') is not None:
131 Output = Output + "Hash: SHA1,%s\r\n"%(Msg.get_param('micalg')[4:].upper())
132 Output = Output + "\r\n";
133 Output = Output + Signed.as_string().replace("\n-","\n- -") + "\n" + Signature.get_payload(decode=True)
137 # Just return the message body
138 return (Msg.get_payload(decode=True), 0);
142 for x in Msg.get_payload(decode=True).split('\n'):
148 # Leading up to the signature
150 if x == "-----BEGIN PGP SIGNED MESSAGE-----":
153 raise UDFormatError,"Unsigned text in message (at start)";
156 # In the signature plain text
158 if x == "-----BEGIN PGP SIGNATURE-----":
164 if x == "-----END PGP SIGNATURE-----":
170 raise UDFormatError,"Unsigned text in message (at end)";
172 return ("\n".join(Body), 0);
174 # This opens GPG in 'write filter' mode. It takes Message and sends it
175 # to GPGs standard input, pipes the standard output to a temp file along
176 # with the status FD. The two tempfiles are passed to GPG by fd and are
177 # accessible from the filesystem for only a short period. Message may be
178 # None in which case GPGs stdin is closed directly after forking. This
179 # is best used for sig checking and encryption.
180 # The return result is a tuple (Exit,StatusFD,OutputFD), both fds are
181 # fully rewound and readable.
182 def GPGWriteFilter(Program,Options,Message):
183 # Make sure the tmp files we open are unreadable, there is a short race
184 # between when the temp file is opened and unlinked that some one else
185 # could open it or hard link it. This is not important however as no
186 # Secure data is fed through the temp files.
187 OldMask = os.umask(0777);
189 Output = tempfile.TemporaryFile("w+b");
190 GPGText = tempfile.TemporaryFile("w+b");
192 InPipe = [InPipe[0],InPipe[1]];
197 # Fork off GPG in a horrible way, we redirect most of its FDs
198 # Input comes from a pipe and its two outputs are spooled to unlinked
199 # temp files (ie private)
203 os.dup2(InPipe[0],0);
205 os.dup2(Output.fileno(),1);
206 os.dup2(os.open("/dev/null",os.O_WRONLY),2);
207 os.dup2(GPGText.fileno(),3);
209 Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
210 os.execvp(Program,Args);
214 # Get rid of the other end of the pipe
219 if Message is not None:
221 os.write(InPipe[1],Message);
227 # Wait for GPG to finish
228 Exit = os.waitpid(Child,0);
230 # Create the result including the new readable file descriptors
231 Result = (Exit,os.fdopen(os.dup(GPGText.fileno()),"r"), \
232 os.fdopen(os.dup(Output.fileno()),"r"));
249 # This takes a text passage, a destination and a flag indicating the
250 # compatibility to use and returns an encrypted message to the recipient.
251 # It is best if the recipient is specified using the hex key fingerprint
252 # of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
253 def GPGEncrypt(Message,To,PGP2):
254 class KeyringError(Exception): pass
255 # Encrypt using the PGP5 block encoding and with the PGP5 option set.
256 # This will handle either RSA or DSA/DH asymetric keys.
257 # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
258 # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
259 # can read a message encrypted with blowfish and RSA.
260 searchkey = GPGKeySearch(To);
261 if len(searchkey) == 0:
262 raise KeyringError("No key found matching %s"%(To))
263 elif len(searchkey) > 1:
264 raise KeyringError("Multiple keys found matching %s"%(To))
265 if searchkey[0][4].find("E") < 0:
266 raise KeyringError("Key %s has no encryption capability - are all encryption subkeys expired or revoked? Are there any encryption subkeys?"%(To))
271 Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
274 Text = Res[2].read();
281 # We have to call gpg with a filename or it will create a packet that
282 # PGP2 cannot understand.
283 TmpName = tempfile.mktemp();
286 MsgFile = open(TmpName,"wc");
287 MsgFile.write(Message);
289 Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
292 Text = Res[2].read();
303 # Checks the signature of a standard PGP message, like that returned by
304 # GetClearSig. It returns a large tuple of the form:
305 # (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,Length,PGP2),Text);
307 # Why = None if checking was OK otherwise an error string.
308 # SigID+Date represent something suitable for use in a replay cache. The
309 # date is returned as the number of seconds since the UTC epoch.
310 # The keyID is also in this tuple for easy use of the replay
312 # KeyID, KeyFinger and Owner represent the Key used to sign this message
313 # PGP2 indicates if the message was created using PGP 2.x
314 # Text is the full byte-for-byte signed text in a string
315 def GPGCheckSig(Message):
318 Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
321 # Parse the GPG answer
332 # Grab and split up line
333 Line = Strm.readline();
336 Split = re.split("[ \n]",Line);
337 if Split[0] != "[GNUPG:]":
340 # We only process the first occurance of any tag.
341 if TagMap.has_key(Split[1]):
343 TagMap[Split[1]] = None;
345 # Good signature response
346 if Split[1] == "GOODSIG":
347 # Just in case GPG returned a bad signal before this (bug?)
351 Owner = ' '.join(Split[3:])
352 # If this message is signed with a subkey which has not yet
353 # expired, GnuPG will say GOODSIG here, even if the primary
354 # key already has expired. This came up in discussion of
355 # bug #489225. GPGKeySearch only returns non-expired keys.
356 Verify = GPGKeySearch(KeyID);
359 Why = "Key has expired (no unexpired key found in keyring matching %s)"%(KeyId);
361 # Bad signature response
362 if Split[1] == "BADSIG":
365 Why = "Verification of signature failed";
367 # Bad signature response
368 if Split[1] == "ERRSIG":
372 Why = "GPG error, ERRSIG status tag is invalid";
373 elif Split[7] == '9':
374 Why = "Unable to verify signature, signing key missing.";
375 elif Split[7] == '4':
376 Why = "Unable to verify signature, unknown packet format/key type";
378 Why = "Unable to verify signature, unknown reason";
380 if Split[1] == "NO_PUBKEY":
382 Why = "Unable to verify signature, signing key missing.";
385 if Split[1] == "EXPSIG":
387 Why = "Signature has expired";
390 if Split[1] == "EXPKEYSIG":
392 Why = "Signing key (%s, %s) has expired"%(Split[2], Split[3]);
395 if Split[1] == "KEYREVOKED" or Split[1] == "REVKEYSIG":
397 Why = "Signing key has been revoked";
400 if Split[1] == "NODATA" or Split[1] == "BADARMOR":
402 Why = "The packet was corrupted or contained no data";
405 if Split[1] == "SIG_ID":
407 Date = long(Split[4]);
409 # ValidSig has the key finger print
410 if Split[1] == "VALIDSIG":
411 # Use the fingerprint of the primary key when available
413 KeyFinger = Split[11];
415 KeyFinger = Split[2];
417 # Reopen the stream as a readable stream
418 Text = Res[2].read();
420 # A gpg failure is an automatic bad signature
421 if Exit[1] != 0 and Why is None:
423 Why = "GPG execution returned non-zero exit status: " + str(Exit[1]);
425 if GoodSig == 0 and (Why is None or len(Why) == 0):
426 Why = "Checking Failed";
428 # Try to decide if this message was sent using PGP2
430 if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) is not None):
433 return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
440 def __init__(self, msg):
441 res = GPGCheckSig(msg)
443 self.sig_info = res[1]
444 self.key_info = res[2]
447 self.ok = self.why is None
449 self.sig_id = self.sig_info[0]
450 self.sig_date = self.sig_info[1]
451 self.sig_fpr = self.sig_info[2]
453 self.key_id = self.key_info[0]
454 self.key_fpr = self.key_info[1]
455 self.key_owner = self.key_info[2]
457 self.is_pgp2 = self.key_info[4]
459 # Search for keys given a search pattern. The pattern is passed directly
460 # to GPG for processing. The result is a list of tuples of the form:
461 # (KeyID,KeyFinger,Owner,Length)
462 # Which is similar to the key identification tuple output by GPGChecksig
464 # Do not return keys where the primary key has expired
465 def GPGKeySearch(SearchCriteria):
466 Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
467 [SearchCriteria," 2> /dev/null"]
478 dir = os.path.expanduser("~/.gnupg")
479 if not os.path.isdir(dir):
483 # The GPG output will contain zero or more stanza, one stanza per match found.
484 # Each stanza consists of the following records, in order:
485 # tru : trust database information
486 # pub : primary key from which we extract
490 # field 11 - Capabilities
491 # fpr : fingerprint of primary key from which we extract
492 # field 9 - Fingerprint
493 # uid : first User ID attached to primary key from which we extract
495 # uid : (optional) additional multiple User IDs attached to primary key
496 # sub : (optional) secondary key
497 # fpr : (opitonal) fingerprint of secondary key if sub is present
498 Strm = os.popen(" ".join(Args),"r")
501 Line = Strm.readline()
504 Split = Line.split(":")
511 Length = int(Split[2])
513 Capabilities = Split[11]
518 Fingerprint = Split[9]
519 if Hits.has_key(Fingerprint):
520 Want = 'pub' # already seen, skip to next stanza
522 Hits[Fingerprint] = None
528 if Validity != 'e': # if not expired
529 Result.append( (KeyID,Fingerprint,Owner,Length,Capabilities) )
530 Want = 'pub' # finished, skip to next stanza
538 # Print the available key information in a format similar to GPG's output
539 # We do not know the values of all the feilds so they are just replaced
541 def GPGPrintKeyInfo(Ident):
542 print "pub %u?/%s ??-??-?? %s" % (Ident[3],Ident[0][-8:],Ident[2]);
543 print " key fingerprint = 0x%s" % (Ident[1]);
545 # Perform a substition of template
546 def TemplateSubst(Map,Template):
548 Template = Template.replace(x, Map[x])
551 # The replay class uses a python DB (BSD db if avail) to implement
552 # protection against replay. Replay is an attacker capturing the
553 # plain text signed message and sending it back to the victim at some
554 # later date. Each signature has a unique signature ID (and signing
555 # Key Fingerprint) as well as a timestamp. The first stage of replay
556 # protection is to ensure that the timestamp is reasonable, in particular
557 # not to far ahead or too far behind the current system time. The next
558 # step is to look up the signature + key fingerprint in the replay database
559 # and determine if it has been recived. The database is cleaned out
560 # periodically and old signatures are discarded. By using a timestamp the
561 # database size is bounded to being within the range of the allowed times
562 # plus a little fuzz. The cache is serialized with a flocked lock file
564 def __init__(self,Database):
565 self.Lock = open(Database + ".lock","w",0600);
566 fcntl.flock(self.Lock.fileno(),fcntl.LOCK_EX);
567 self.DB = anydbm.open(Database,"c",0600);
568 self.CleanCutOff = CleanCutOff;
569 self.AgeCutOff = AgeCutOff;
570 self.FutureCutOff = FutureCutOff;
573 # Close the cache and lock
580 # Clean out any old signatures
582 CutOff = time.time() - self.CleanCutOff;
583 for x in self.DB.keys():
584 if int(self.DB[x]) <= CutOff:
587 # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
590 if Sig[0] is None or Sig[1] is None or Sig[2] is None:
591 return "Invalid signature";
592 if int(Sig[1]) > time.time() + self.FutureCutOff:
593 return "Signature has a time too far in the future";
594 if self.DB.has_key(Sig[0] + '-' + Sig[2]):
595 return "Signature has already been received";
596 if int(Sig[1]) < time.time() - self.AgeCutOff:
597 return "Signature has passed the age cut off ";
598 # + str(int(Sig[1])) + ',' + str(time.time()) + "," + str(Sig);
601 # Add a signature, the sig is the same as is given to Check
603 if Sig[0] is None or Sig[1] is None:
604 raise RuntimeError,"Invalid signature";
605 if Sig[1] < time.time() - self.CleanCutOff:
607 Key = Sig[0] + '-' + Sig[2]
608 if self.DB.has_key(Key):
609 if int(self.DB[Key]) < Sig[1]:
610 self.DB[Key] = str(int(Sig[1]));
612 self.DB[Key] = str(int(Sig[1]));
614 def process(self, sig_info):
615 r = self.Check(sig_info);
617 raise RuntimeError, "The replay cache rejected your message: %s." % (r,)
623 # vim:set shiftwidth=3: