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