ud-generate: deal with users without loginShell
[mirror/userdir-ldap.git] / userdir_gpg.py
1 #   Copyright (c) 1999-2001  Jason Gunthorpe <jgg@debian.org>
2 #   Copyright (c) 2005       Joey Schulze <joey@infodrom.org>
3 #
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.
8 #
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.
13 #
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.
17
18 # GPG issues -
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
24 #    good too.
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.
28
29 import sys
30 import StringIO
31 import os
32 import tempfile
33 import re
34 import time
35 import fcntl
36 import anydbm
37 import email
38 import email.message
39
40 from userdir_exceptions import *
41
42 # General GPG options
43 GPGPath = "gpg"
44 # "--load-extension", "rsa",
45 GPGBasicOptions = ["--no-options",
46                    "--batch",
47                    "--no-default-keyring",
48                    "--secret-keyring", "/dev/null",
49                    "--always-trust"]
50 GPGKeyRings = []
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
59
60 # Replay cutoff times in seconds
61 CleanCutOff = 7 * 24 * 60 * 60
62 AgeCutOff = 4 * 24 * 60 * 60
63 FutureCutOff = 3 * 24 * 60 * 60
64
65
66 def ClearKeyrings():
67    del GPGKeyRings[:]
68
69
70 # Set the keyrings, the input is a list of keyrings
71 def SetKeyrings(Rings):
72    for x in Rings:
73       GPGKeyRings.append("--keyring")
74       GPGKeyRings.append(x)
75
76
77 # GetClearSig takes an un-seekable email message stream (mimetools.Message)
78 # and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded
79 # clear signed text.
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.
87 #
88 # Paranoid will check the message text to make sure that all the plaintext is
89 # in fact signed (bounded by a PGP packet)
90 #
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"
96
97    if Paranoid and lax_multipart:
98       raise RuntimeError, "Paranoid and lax_multipart don't mix well"
99
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":
103          if lax_multipart:
104             payloads = Msg.get_payload()
105             msg = "\n".join(map( lambda p: p.get_payload(decode=True), payloads))
106             return (msg, 0)
107          raise UDFormatError, "Cannot handle multipart messages not of type multipart/signed";
108
109       if Paranoid:
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)";
114
115       payloads = Msg.get_payload()
116       if len(payloads) != 2:
117          raise UDFormatError, "multipart/signed message with number of payloads != 2";
118
119       (Signed, Signature) = payloads
120
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]";
125
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)
134       return (Output,1);
135    else:
136       if Paranoid == 0:
137          # Just return the message body
138          return (Msg.get_payload(decode=True), 0);
139
140       Body = [];
141       State = 1;
142       for x in Msg.get_payload(decode=True).split('\n'):
143           Body.append(x)
144
145           if x == "":
146              continue;
147
148           # Leading up to the signature
149           if State == 1:
150              if x == "-----BEGIN PGP SIGNED MESSAGE-----":
151                 State = 2;
152              else:
153                 raise UDFormatError,"Unsigned text in message (at start)";
154              continue;
155
156           # In the signature plain text
157           if State == 2:
158              if x == "-----BEGIN PGP SIGNATURE-----":
159                 State = 3;
160              continue;
161
162           # In the signature
163           if State == 3:
164              if x == "-----END PGP SIGNATURE-----":
165                 State = 4;
166              continue;
167
168           # Past the end
169           if State == 4:
170              raise UDFormatError,"Unsigned text in message (at end)";
171
172       return ("\n".join(Body), 0);
173
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);
188    try:
189       Output = tempfile.TemporaryFile("w+b");
190       GPGText = tempfile.TemporaryFile("w+b");
191       InPipe = os.pipe();
192       InPipe = [InPipe[0],InPipe[1]];
193    finally:
194       os.umask(OldMask);
195
196    try:
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)
200       Child = os.fork();
201       if Child == 0:
202          try:
203             os.dup2(InPipe[0],0);
204             os.close(InPipe[1]);
205             os.dup2(Output.fileno(),1);
206             os.dup2(os.open("/dev/null",os.O_WRONLY),2);
207             os.dup2(GPGText.fileno(),3);
208
209             Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
210             os.execvp(Program,Args);
211          finally:
212             os._exit(100);
213
214       # Get rid of the other end of the pipe
215       os.close(InPipe[0])
216       InPipe[0] = -1;
217
218       # Send the message
219       if Message is not None:
220          try:
221             os.write(InPipe[1],Message);
222          except:
223            pass;
224       os.close(InPipe[1]);
225       InPipe[1] = -1;
226
227       # Wait for GPG to finish
228       Exit = os.waitpid(Child,0);
229
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"));
233       Result[1].seek(0);
234       Result[2].seek(0);
235
236       Output.close();
237       GPGText.close();
238       return Result;
239    finally:
240       if InPipe[0] != -1:
241          os.close(InPipe[0]);
242       if InPipe[1] != -1:
243          os.close(InPipe[1]);
244       Output.close();
245       GPGText.close();
246
247
248
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))
267
268    if PGP2 == 0:
269       try:
270          Res = None;
271          Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
272          if Res[0][1] != 0:
273             return None;
274          Text = Res[2].read();
275          return Text;
276       finally:
277          if Res is not None:
278             Res[1].close();
279             Res[2].close();
280    else:
281       # We have to call gpg with a filename or it will create a packet that
282       # PGP2 cannot understand.
283       TmpName = tempfile.mktemp();
284       try:
285          Res = None;
286          MsgFile = open(TmpName,"wc");
287          MsgFile.write(Message);
288          MsgFile.close();
289          Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
290          if Res[0][1] != 0:
291             return None;
292          Text = Res[2].read();
293          return Text;
294       finally:
295          try:
296             os.unlink(TmpName);
297          except:
298             pass;
299          if Res is not None:
300             Res[1].close();
301             Res[2].close();
302
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);
306 # Where,
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
311 #             cache
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):
316    Res = None;
317    try:
318       Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
319       Exit = Res[0];
320
321       # Parse the GPG answer
322       Strm = Res[1];
323       GoodSig = 0;
324       SigId = None;
325       KeyFinger = None;
326       KeyID = None;
327       Owner = None;
328       Date = None;
329       Why = None;
330       TagMap = {};
331       while(1):
332          # Grab and split up line
333          Line = Strm.readline();
334          if Line == "":
335             break;
336          Split = re.split("[ \n]",Line);
337          if Split[0] != "[GNUPG:]":
338             continue;
339
340          # We only process the first occurance of any tag.
341          if TagMap.has_key(Split[1]):
342             continue;
343          TagMap[Split[1]] = None;
344
345          # Good signature response
346          if Split[1] == "GOODSIG":
347             # Just in case GPG returned a bad signal before this (bug?)
348             if Why is None:
349                GoodSig = 1;
350             KeyID = Split[2];
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);
357             if len(Verify) == 0:
358                GoodSig = 0
359                Why = "Key has expired (no unexpired key found in keyring matching %s)"%(KeyId);
360
361          # Bad signature response
362          if Split[1] == "BADSIG":
363             GoodSig = 0;
364             KeyID = Split[2];
365             Why = "Verification of signature failed";
366
367          # Bad signature response
368          if Split[1] == "ERRSIG":
369             GoodSig = 0;
370             KeyID = Split[2];
371             if len(Split) <= 7:
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";
377             else:
378                Why = "Unable to verify signature, unknown reason";
379
380          if Split[1] == "NO_PUBKEY":
381             GoodSig = 0;
382             Why = "Unable to verify signature, signing key missing.";
383
384          # Expired signature
385          if Split[1] == "EXPSIG":
386             GoodSig = 0;
387             Why = "Signature has expired";
388
389          # Expired signature
390          if Split[1] == "EXPKEYSIG":
391             GoodSig = 0;
392             Why = "Signing key (%s, %s) has expired"%(Split[2], Split[3]);
393
394          # Revoked key
395          if Split[1] == "KEYREVOKED" or Split[1] == "REVKEYSIG":
396             GoodSig = 0;
397             Why = "Signing key has been revoked";
398
399          # Corrupted packet
400          if Split[1] == "NODATA" or Split[1] == "BADARMOR":
401             GoodSig = 0;
402             Why = "The packet was corrupted or contained no data";
403
404          # Signature ID
405          if Split[1] == "SIG_ID":
406             SigId = Split[2];
407             Date = long(Split[4]);
408
409          # ValidSig has the key finger print
410          if Split[1] == "VALIDSIG":
411             # Use the fingerprint of the primary key when available
412             if len(Split) >= 12:
413                KeyFinger = Split[11];
414             else:
415                KeyFinger = Split[2];
416
417       # Reopen the stream as a readable stream
418       Text = Res[2].read();
419
420       # A gpg failure is an automatic bad signature
421       if Exit[1] != 0 and Why is None:
422          GoodSig = 0;
423          Why = "GPG execution returned non-zero exit status: " + str(Exit[1]);
424
425       if GoodSig == 0 and (Why is None or len(Why) == 0):
426          Why = "Checking Failed";
427
428       # Try to decide if this message was sent using PGP2
429       PGP2Message = 0;
430       if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) is not None):
431          PGP2Message = 1;
432
433       return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
434    finally:
435       if Res is not None:
436          Res[1].close();
437          Res[2].close();
438
439 class GPGCheckSig2:
440         def __init__(self, msg):
441                 res = GPGCheckSig(msg)
442                 self.why = res[0]
443                 self.sig_info = res[1]
444                 self.key_info = res[2]
445                 self.text = res[3]
446
447                 self.ok = self.why is None
448
449                 self.sig_id = self.sig_info[0]
450                 self.sig_date = self.sig_info[1]
451                 self.sig_fpr = self.sig_info[2]
452
453                 self.key_id = self.key_info[0]
454                 self.key_fpr = self.key_info[1]
455                 self.key_owner = self.key_info[2]
456
457                 self.is_pgp2 = self.key_info[4]
458
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
463 #
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"]
468    Strm = None
469    Result = []
470    Validity = None
471    Length = 0
472    KeyID = ""
473    Capabilities = ""
474    Fingerprint = ""
475    Owner = ""
476    Hits = {}
477
478    dir = os.path.expanduser("~/.gnupg")
479    if not os.path.isdir(dir):
480       os.mkdir(dir, 0700)
481
482    try:
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
487       #         field  1 - Validity
488       #         field  2 - Length
489       #         field  4 - KeyID
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
494       #         Field  9 - Owner
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")
499       Want = "pub"
500       while(1):
501          Line = Strm.readline()
502          if Line == "":
503             break
504          Split = Line.split(":")
505
506          if Split[0] != Want:
507             continue
508
509          if Want == 'pub':
510             Validity = Split[1]
511             Length = int(Split[2])
512             KeyID = Split[4]
513             Capabilities = Split[11]
514             Want = 'fpr'
515             continue
516
517          if Want == 'fpr':
518             Fingerprint = Split[9]
519             if Hits.has_key(Fingerprint):
520                Want = 'pub' # already seen, skip to next stanza
521             else:
522                Hits[Fingerprint] = None
523                Want = 'uid'
524             continue
525
526          if Want == 'uid':
527             Owner = Split[9]
528             if Validity != 'e': # if not expired
529                Result.append( (KeyID,Fingerprint,Owner,Length,Capabilities) )
530             Want = 'pub' # finished, skip to next stanza
531             continue
532
533    finally:
534       if Strm is not None:
535          Strm.close()
536    return Result
537
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
540 # with ?'s
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]);
544
545 # Perform a substition of template
546 def TemplateSubst(Map,Template):
547    for x in Map.keys():
548       Template = Template.replace(x, Map[x])
549    return Template;
550
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
563 class ReplayCache:
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;
571       self.Clean()
572
573    # Close the cache and lock
574    def __del__(self):
575       self.close();
576    def close(self):
577       self.DB.close();
578       self.Lock.close();
579
580    # Clean out any old signatures
581    def Clean(self):
582       CutOff = time.time() - self.CleanCutOff;
583       for x in self.DB.keys():
584          if int(self.DB[x]) <= CutOff:
585             del self.DB[x];
586
587    # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
588    # key ID
589    def Check(self,Sig):
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);
599       return None;
600
601    # Add a signature, the sig is the same as is given to Check
602    def Add(self,Sig):
603       if Sig[0] is None or Sig[1] is None:
604          raise RuntimeError,"Invalid signature";
605       if Sig[1] < time.time() - self.CleanCutOff:
606          return;
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]));
611       else:
612          self.DB[Key] = str(int(Sig[1]));
613
614    def process(self, sig_info):
615       r = self.Check(sig_info);
616       if r is not None:
617          raise RuntimeError, "The replay cache rejected your message: %s." % (r,)
618       self.Add(sig_info)
619       self.close()
620
621 # vim:set et:
622 # vim:set ts=3:
623 # vim:set shiftwidth=3: