prettified
[plcapi.git] / PLC / GPG.py
1 #
2 # Python "binding" for GPG. I'll write GPGME bindings eventually. The
3 # intent is to use GPG to sign method calls, as a way of identifying
4 # and authenticating peers. Calls should still go over an encrypted
5 # transport such as HTTPS, with certificate checking.
6 #
7 # Mark Huang <mlhuang@cs.princeton.edu>
8 # Copyright (C) 2006 The Trustees of Princeton University
9 #
10
11 import os
12 import xmlrpclib
13 import shutil
14 from types import StringTypes
15 from StringIO import StringIO
16 from subprocess import Popen, PIPE, call
17 from tempfile import NamedTemporaryFile, mkdtemp
18 from lxml import etree
19
20 from PLC.Faults import *
21
22 def canonicalize(args, methodname = None, methodresponse = False):
23     """
24     Returns a canonicalized XML-RPC representation of the specified
25     method call (methodname != None) or response (methodresponse =
26     True).
27     """
28
29     xml = xmlrpclib.dumps(args, methodname, methodresponse, encoding = 'utf-8', allow_none = 1)
30     dom = etree.fromstring(xml)
31     canonical=etree.tostring(dom)
32     # pre-f20 version was using Canonicalize from PyXML 
33     # from xml.dom.ext import Canonicalize
34     # Canonicalize(), though it claims to, does not encode unicode
35     # nodes to UTF-8 properly and throws an exception unless you write
36     # the stream to a file object, so just encode it ourselves.
37     return canonical.encode('utf-8')
38
39 def gpg_export(keyring, armor = True):
40     """
41     Exports the specified public keyring file.
42     """
43
44     homedir = mkdtemp()
45     args = ["gpg", "--batch", "--no-tty",
46             "--homedir", homedir,
47             "--no-default-keyring",
48             "--keyring", keyring,
49             "--export"]
50     if armor:
51         args.append("--armor")
52
53     p = Popen(args, stdin = PIPE, stdout = PIPE, stderr = PIPE, close_fds = True)
54     export = p.stdout.read()
55     err = p.stderr.read()
56     rc = p.wait()
57
58     # Clean up
59     shutil.rmtree(homedir)
60
61     if rc:
62         raise PLCAuthenticationFailure, "GPG export failed with return code %d: %s" % (rc, err)
63
64     return export
65
66 def gpg_sign(args, secret_keyring, keyring, methodname = None, methodresponse = False, detach_sign = True):
67     """
68     Signs the specified method call (methodname != None) or response
69     (methodresponse == True) using the specified GPG keyring files. If
70     args is not a tuple representing the arguments to the method call
71     or the method response value, then it should be a string
72     representing a generic message to sign (detach_sign == True) or
73     sign/encrypt (detach_sign == False) specified). Returns the
74     detached signature (detach_sign == True) or signed/encrypted
75     message (detach_sign == False).
76     """
77
78     # Accept either an opaque string blob or a Python tuple
79     if isinstance(args, StringTypes):
80         message = args
81     elif isinstance(args, tuple):
82         message = canonicalize(args, methodname, methodresponse)
83
84     # Use temporary trustdb
85     homedir = mkdtemp()
86
87     cmd = ["gpg", "--batch", "--no-tty",
88            "--homedir", homedir,
89            "--no-default-keyring",
90            "--secret-keyring", secret_keyring,
91            "--keyring", keyring,
92            "--armor"]
93
94     if detach_sign:
95         cmd.append("--detach-sign")
96     else:
97         cmd.append("--sign")
98
99     p = Popen(cmd, stdin = PIPE, stdout = PIPE, stderr = PIPE)
100     p.stdin.write(message)
101     p.stdin.close()
102     signature = p.stdout.read()
103     err = p.stderr.read()
104     rc = p.wait()
105
106     # Clean up
107     shutil.rmtree(homedir)
108
109     if rc:
110         raise PLCAuthenticationFailure, "GPG signing failed with return code %d: %s" % (rc, err)
111
112     return signature
113
114 def gpg_verify(args, key, signature = None, methodname = None, methodresponse = False):
115     """
116     Verifies the signature of the specified method call (methodname !=
117     None) or response (methodresponse = True) using the specified
118     public key material. If args is not a tuple representing the
119     arguments to the method call or the method response value, then it
120     should be a string representing a generic message to verify (if
121     signature is specified) or verify/decrypt (if signature is not
122     specified).
123     """
124
125     # Accept either an opaque string blob or a Python tuple
126     if isinstance(args, StringTypes):
127         message = args
128     else:
129         message = canonicalize(args, methodname, methodresponse)
130
131     # Write public key to temporary file
132     if os.path.exists(key):
133         keyfile = None
134         keyfilename = key
135     else:
136         keyfile = NamedTemporaryFile(suffix = '.pub')
137         keyfile.write(key)
138         keyfile.flush()
139         keyfilename = keyfile.name
140
141     # Import public key into temporary keyring
142     homedir = mkdtemp()
143     call(["gpg", "--batch", "--no-tty", "--homedir", homedir, "--import", keyfilename],
144          stdin = PIPE, stdout = PIPE, stderr = PIPE)
145
146     cmd = ["gpg", "--batch", "--no-tty",
147            "--homedir", homedir]
148
149     if signature is not None:
150         # Write detached signature to temporary file
151         sigfile = NamedTemporaryFile()
152         sigfile.write(signature)
153         sigfile.flush()
154         cmd += ["--verify", sigfile.name, "-"]
155     else:
156         # Implicit signature
157         sigfile = None
158         cmd.append("--decrypt")
159
160     p = Popen(cmd, stdin = PIPE, stdout = PIPE, stderr = PIPE)
161     p.stdin.write(message)
162     p.stdin.close()
163     if signature is None:
164         message = p.stdout.read()
165     err = p.stderr.read()
166     rc = p.wait()
167
168     # Clean up
169     shutil.rmtree(homedir)
170     if sigfile:
171         sigfile.close()
172     if keyfile:
173         keyfile.close()
174
175     if rc:
176         raise PLCAuthenticationFailure, "GPG verification failed with return code %d: %s" % (rc, err)
177
178     return message