b7fad178e424d386ab6a633d6892c89f2a302bdc
[sfa.git] / sfa / trust / gid.py
1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
13 #
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS 
21 # IN THE WORK.
22 #----------------------------------------------------------------------
23 ##
24 # Implements SFA GID. GIDs are based on certificates, and the GID class is a
25 # descendant of the certificate class.
26 ##
27
28 from __future__ import print_function
29
30 import xmlrpclib
31 import uuid
32
33 from sfa.trust.certificate import Certificate
34
35 from sfa.util.faults import GidInvalidParentHrn, GidParentHrn
36 from sfa.util.sfalogging import logger
37 from sfa.util.xrn import hrn_to_urn, urn_to_hrn, hrn_authfor_hrn
38
39 ##
40 # Create a new uuid. Returns the UUID as a string.
41
42 def create_uuid():
43     return str(uuid.uuid4().int)
44
45 ##
46 # GID is a tuple:
47 #    (uuid, urn, public_key)
48 #
49 # UUID is a unique identifier and is created by the python uuid module
50 #    (or the utility function create_uuid() in gid.py).
51 #
52 # HRN is a human readable name. It is a dotted form similar to a backward domain
53 #    name. For example, planetlab.us.arizona.bakers.
54 #
55 # URN is a human readable identifier of form:
56 #   "urn:publicid:IDN+toplevelauthority[:sub-auth.]*[\res. type]\ +object name"
57 #   For  example, urn:publicid:IDN+planetlab:us:arizona+user+bakers      
58 #
59 # PUBLIC_KEY is the public key of the principal identified by the UUID/HRN.
60 # It is a Keypair object as defined in the cert.py module.
61 #
62 # It is expected that there is a one-to-one pairing between UUIDs and HRN,
63 # but it is uncertain how this would be inforced or if it needs to be enforced.
64 #
65 # These fields are encoded using xmlrpc into the subjectAltName field of the
66 # x509 certificate. Note: Call encode() once the fields have been filled in
67 # to perform this encoding.
68
69
70 class GID(Certificate):
71     ##
72     # Create a new GID object
73     #
74     # @param create If true, create the X509 certificate
75     # @param subject If subject!=None, create the X509 cert and set the subject name
76     # @param string If string!=None, load the GID from a string
77     # @param filename If filename!=None, load the GID from a file
78     # @param lifeDays life of GID in days - default is 1825==5 years
79     # @param email Email address to put in subjectAltName - default is None
80
81     def __init__(self, create=False, subject=None, string=None, filename=None,
82                  uuid=None, hrn=None, urn=None, lifeDays=1825, email=None):
83         self.uuid = None
84         self.hrn = None
85         self.urn = None
86         self.email = None # for adding to the SubjectAltName             
87         Certificate.__init__(self, lifeDays, create, subject, string, filename)
88
89         if subject:
90             logger.debug("Creating GID for subject: %s" % subject)
91         if uuid:
92             self.uuid = int(uuid)
93         if hrn:
94             self.hrn = hrn
95             self.urn = hrn_to_urn(hrn, 'unknown')
96         if urn:
97             self.urn = urn
98             self.hrn, type = urn_to_hrn(urn)
99
100         if email:
101             logger.debug("Creating GID for subject using email: %s" % email)
102             self.set_email(email)
103
104     def set_uuid(self, uuid):
105         if isinstance(uuid, str):
106             self.uuid = int(uuid)
107         else:
108             self.uuid = uuid
109
110     def get_uuid(self):
111         if not self.uuid:
112             self.decode()
113         return self.uuid
114
115     def set_hrn(self, hrn):
116         self.hrn = hrn
117
118     def get_hrn(self):
119         if not self.hrn:
120             self.decode()
121         return self.hrn
122
123     def set_urn(self, urn):
124         self.urn = urn
125         self.hrn, type = urn_to_hrn(urn)
126  
127     def get_urn(self):
128         if not self.urn:
129             self.decode()
130         return self.urn            
131
132     # Will be stuffed into subjectAltName
133     def set_email(self, email):
134         self.email = email
135
136     def get_email(self):
137         if not self.email:
138             self.decode()
139         return self.email
140
141     def get_type(self):
142         if not self.urn:
143             self.decode()
144         _, t = urn_to_hrn(self.urn)
145         return t
146     
147     ##
148     # Encode the GID fields and package them into the subject-alt-name field
149     # of the X509 certificate. This must be called prior to signing the
150     # certificate. It may only be called once per certificate.
151
152     def encode(self):
153         if self.urn:
154             urn = self.urn
155         else:
156             urn = hrn_to_urn(self.hrn, None)
157             
158         str = "URI:" + urn
159
160         if self.uuid:
161             str += ", " + "URI:" + uuid.UUID(int=self.uuid).urn
162         
163         if self.email:
164             str += ", " + "email:" + self.email
165
166         self.set_data(str, 'subjectAltName')
167
168
169     ##
170     # Decode the subject-alt-name field of the X509 certificate into the
171     # fields of the GID. This is automatically called by the various get_*()
172     # functions in this class.
173
174     def decode(self):
175         data = self.get_data('subjectAltName')
176         dict = {}
177         if data:
178             if data.lower().startswith('uri:http://<params>'):
179                 dict = xmlrpclib.loads(data[11:])[0][0]
180             else:
181                 spl = data.split(', ')
182                 for val in spl:
183                     if val.lower().startswith('uri:urn:uuid:'):
184                         dict['uuid'] = uuid.UUID(val[4:]).int
185                     elif val.lower().startswith('uri:urn:publicid:idn+'):
186                         dict['urn'] = val[4:]
187                     elif val.lower().startswith('email:'):
188                         # FIXME: Ensure there isn't cruft in that address...
189                         # EG look for email:copy,....
190                         dict['email'] = val[6:]
191                     
192         self.uuid = dict.get("uuid", None)
193         self.urn = dict.get("urn", None)
194         self.hrn = dict.get("hrn", None)
195         self.email = dict.get("email", None)
196         if self.urn:
197             self.hrn = urn_to_hrn(self.urn)[0]
198
199     ##
200     # Dump the credential to stdout.
201     #
202     # @param indent specifies a number of spaces to indent the output
203     # @param dump_parents If true, also dump the parents of the GID
204
205     def dump(self, *args, **kwargs):
206         print(self.dump_string(*args,**kwargs))
207
208     def dump_string(self, indent=0, dump_parents=False):
209         result=" "*(indent-2) + "GID\n"
210         result += " "*indent + "hrn:" + str(self.get_hrn()) +"\n"
211         result += " "*indent + "urn:" + str(self.get_urn()) +"\n"
212         result += " "*indent + "uuid:" + str(self.get_uuid()) + "\n"
213         if self.get_email() is not None:
214             result += " "*indent + "email:" + str(self.get_email()) + "\n"
215         filename=self.get_filename()
216         if filename: result += "Filename %s\n"%filename
217
218         if self.parent and dump_parents:
219             result += " "*indent + "parent:\n"
220             result += self.parent.dump_string(indent+4, dump_parents)
221         return result
222
223     ##
224     # Verify the chain of authenticity of the GID. First perform the checks
225     # of the certificate class (verifying that each parent signs the child,
226     # etc). In addition, GIDs also confirm that the parent's HRN is a prefix
227     # of the child's HRN, and the parent is of type 'authority'.
228     #
229     # Verifying these prefixes prevents a rogue authority from signing a GID
230     # for a principal that is not a member of that authority. For example,
231     # planetlab.us.arizona cannot sign a GID for planetlab.us.princeton.foo.
232
233     def verify_chain(self, trusted_certs = None):
234         # do the normal certificate verification stuff
235         trusted_root = Certificate.verify_chain(self, trusted_certs)        
236        
237         if self.parent:
238             # make sure the parent's hrn is a prefix of the child's hrn
239             if not hrn_authfor_hrn(self.parent.get_hrn(), self.get_hrn()):
240                 raise GidParentHrn(
241                     "This cert HRN {} isn't in the namespace for parent HRN {}"
242                     .format(self.get_hrn(), self.parent.get_hrn()))
243
244             # Parent must also be an authority (of some type) to sign a GID
245             # There are multiple types of authority - accept them all here
246             if not self.parent.get_type().find('authority') == 0:
247                 raise GidInvalidParentHrn(
248                     "This cert {}'s parent {} is not an authority (is a %{})"
249                     .format(self.get_hrn(), self.parent.get_hrn(), self.parent.get_type()))
250
251             # Then recurse up the chain - ensure the parent is a trusted
252             # root or is in the namespace of a trusted root
253             self.parent.verify_chain(trusted_certs)
254         else:
255             # make sure that the trusted root's hrn is a prefix of the child's
256             trusted_gid = GID(string=trusted_root.save_to_string())
257             trusted_type = trusted_gid.get_type()
258             trusted_hrn = trusted_gid.get_hrn()
259             #if trusted_type == 'authority':
260             #    trusted_hrn = trusted_hrn[:trusted_hrn.rindex('.')]
261             cur_hrn = self.get_hrn()
262             if not hrn_authfor_hrn(trusted_hrn, cur_hrn):
263                 raise GidParentHrn(
264                     "Trusted root with HRN {} isn't a namespace authority for this cert: {}"
265                     .format(trusted_hrn, cur_hrn))
266
267             # There are multiple types of authority - accept them all here
268             if not trusted_type.find('authority') == 0:
269                 raise GidInvalidParentHrn(
270                     "This cert {}'s trusted root signer {} is not an authority (is a {})"
271                     .format(self.get_hrn(), trusted_hrn, trusted_type))