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