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