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