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