1 # Thierry Parmentelat -- INRIA
3 a minimal library for writing "lightweight" SFA clients
6 from __future__ import print_function
9 # this library should probably check for the expiration date of the various
10 # certificates and automatically retrieve fresh ones when expired
16 from datetime import datetime
17 from sfa.util.xrn import Xrn
19 import sfa.util.sfalogging
20 # importing sfa.utils.faults does pull a lot of stuff
21 # OTOH it's imported from Certificate anyways, so..
22 from sfa.util.faults import RecordNotFound
24 from sfa.client.sfaserverproxy import SfaServerProxy
26 # see optimizing dependencies below
27 from sfa.trust.certificate import Keypair, Certificate
28 from sfa.trust.credential import Credential
29 from sfa.trust.gid import GID
31 # a helper class to implement the bootstrapping of cryptoa. material
32 # assuming we are starting from scratch on the client side
33 # what's needed to complete a full slice creation cycle
35 # (*) a local private key
36 # (*) the corresp. public key in the registry
37 # (**) step1: a self-signed certificate
38 # default filename is <hrn>.sscert
39 # (**) step2: a user credential
40 # obtained at the registry with GetSelfCredential
41 # using the self-signed certificate as the SSL cert
42 # default filename is <hrn>.user.cred
43 # (**) step3: a registry-provided certificate (i.e. a GID)
44 # obtained at the registry using Resolve
45 # using the step2 credential as credential
46 # default filename is <hrn>.user.gid
48 # From that point on, the GID is used as the SSL certificate
49 # and the following can be done
51 # (**) retrieve a slice (or authority) credential
52 # obtained at the registry with GetCredential
53 # using the (step2) user-credential as credential
54 # default filename is <hrn>.<type>.cred
55 # (**) retrieve a slice (or authority) GID
56 # obtained at the registry with Resolve
57 # using the (step2) user-credential as credential
58 # default filename is <hrn>.<type>.cred
60 # (**) additionnally, it might make sense to upgrade a GID file
61 # into a pkcs12 certificate usable in a browser
62 # this bundled format allows for embedding the private key
65 # Implementation notes
69 # this implementation is designed as a guideline for
70 # porting to other languages
72 # the decision to go for decorators aims at focusing
73 # on the core of what needs to be done when everything
74 # works fine, and to take caching and error management
77 # for non-pythonic developers, it should be enough to
78 # implement the bulk of this code, namely the _produce methods
79 # and to add caching and error management by whichever means
80 # is available, including inline
82 # (*) self-signed certificates
84 # still with other languages in mind, we've tried to keep the
85 # dependencies to the rest of the code as low as possible
87 # however this still relies on the sfa.trust.certificate module
88 # for the initial generation of a self-signed-certificate that
89 # is associated to the user's ssh-key
90 # (for user-friendliness, and for smooth operations with planetlab,
91 # the usage model is to reuse an existing keypair)
93 # there might be a more portable, i.e. less language-dependant way, to
94 # implement this step by exec'ing the openssl command.
95 # a known successful attempt at this approach that worked
96 # for Java is documented below
97 # http://nam.ece.upatras.gr/fstoolkit/trac/wiki/JavaSFAClient
101 # the implementation of the pkcs12 wrapping, which is a late addition,
102 # is done through direct calls to openssl
107 class SfaClientException(Exception):
111 class SfaClientBootstrap:
113 # dir is mandatory but defaults to '.'
114 def __init__(self, user_hrn, registry_url, dir=None,
115 verbose=False, timeout=None, logger=None):
117 self.registry_url = registry_url
121 self.verbose = verbose
122 self.timeout = timeout
123 # default for the logger is to use the global sfa logger
125 logger = sfa.util.sfalogging.logger
130 # unconditionnally create a self-signed certificate
131 def self_signed_cert_produce(self, output):
132 self.assert_private_key()
133 private_key_filename = self.private_key_filename()
134 keypair = Keypair(filename=private_key_filename)
135 self_signed = Certificate(subject=self.hrn)
136 self_signed.set_pubkey(keypair)
137 self_signed.set_issuer(keypair, self.hrn)
139 self_signed.save_to_file(output)
140 self.logger.debug("SfaClientBootstrap: Created self-signed certificate for {} in {}"
141 .format(self.hrn, output))
145 # unconditionnally retrieve my credential (GetSelfCredential)
146 # we always use the self-signed-cert as the SSL cert
147 def my_credential_produce(self, output):
148 self.assert_self_signed_cert()
149 certificate_filename = self.self_signed_cert_filename()
150 certificate_string = self.plain_read(certificate_filename)
151 self.assert_private_key()
152 registry_proxy = SfaServerProxy(self.registry_url,
153 self.private_key_filename(),
154 certificate_filename)
156 credential_string = registry_proxy.GetSelfCredential(
157 certificate_string, self.hrn, "user")
159 # some urns hrns may replace non hierarchy delimiters '.' with an
160 # '_' instead of escaping the '.'
161 hrn = Xrn(self.hrn).get_hrn().replace('\.', '_')
162 credential_string = registry_proxy.GetSelfCredential(
163 certificate_string, hrn, "user")
164 self.plain_write(output, credential_string)
166 "SfaClientBootstrap: Wrote result of GetSelfCredential in {}".format(output))
170 # unconditionnally retrieve my GID - use the general form
171 def my_gid_produce(self, output):
172 return self.gid_produce(output, self.hrn, "user")
174 # retrieve any credential (GetCredential) unconditionnal form
175 # we always use the GID as the SSL cert
176 def credential_produce(self, output, hrn, type):
178 certificate_filename = self.my_gid_filename()
179 self.assert_private_key()
180 registry_proxy = SfaServerProxy(self.registry_url, self.private_key_filename(),
181 certificate_filename)
182 self.assert_my_credential()
183 my_credential_string = self.my_credential_string()
184 credential_string = registry_proxy.GetCredential(
185 my_credential_string, hrn, type)
186 self.plain_write(output, credential_string)
188 "SfaClientBootstrap: Wrote result of GetCredential in {}".format(output))
191 def slice_credential_produce(self, output, hrn):
192 return self.credential_produce(output, hrn, "slice")
194 def authority_credential_produce(self, output, hrn):
195 return self.credential_produce(output, hrn, "authority")
197 # retrieve any gid(Resolve) - unconditionnal form
198 # use my GID when available as the SSL cert, otherwise the self-signed
199 def gid_produce(self, output, hrn, type):
202 certificate_filename = self.my_gid_filename()
204 self.assert_self_signed_cert()
205 certificate_filename = self.self_signed_cert_filename()
207 self.assert_private_key()
208 registry_proxy = SfaServerProxy(self.registry_url, self.private_key_filename(),
209 certificate_filename)
210 credential_string = self.plain_read(self.my_credential())
211 records = registry_proxy.Resolve(hrn, credential_string)
212 records = [record for record in records if record['type'] == type]
214 raise RecordNotFound("hrn {} ({}) unknown to registry {}".format(
215 hrn, type, self.registry_url))
217 self.plain_write(output, record['gid'])
219 "SfaClientBootstrap: Wrote GID for {} ({}) in {}".format(hrn, type, output))
223 # http://trac.myslice.info/wiki/MySlice/Developer/SFALogin
224 # produce a pkcs12 bundled certificate from GID and private key
225 # xxx for now we put a hard-wired password that's just, well, 'password'
226 # when leaving this empty on the mac, result can't seem to be loaded in
228 def my_pkcs12_produce(self, filename):
229 password = raw_input("Enter password for p12 certificate: ")
230 openssl_command = ['openssl', 'pkcs12', "-export"]
231 openssl_command += ["-password", "pass:{}".format(password)]
232 openssl_command += ["-inkey", self.private_key_filename()]
233 openssl_command += ["-in", self.my_gid_filename()]
234 openssl_command += ["-out", filename]
235 if subprocess.call(openssl_command) == 0:
236 print("Successfully created {}".format(filename))
238 print("Failed to create {}".format(filename))
240 # Returns True if credential file is valid. Otherwise return false.
241 def validate_credential(self, filename):
243 cred = Credential(filename=filename)
244 # check if credential is expires
245 if cred.get_expiration() < datetime.utcnow():
251 # return my_gid, run all missing steps in the bootstrap sequence
252 def bootstrap_my_gid(self):
253 self.self_signed_cert()
257 # once we've bootstrapped we can use this object to issue any other SFA call
259 def server_proxy(self, url):
261 return SfaServerProxy(url, self.private_key_filename(), self.my_gid_filename(),
262 verbose=self.verbose, timeout=self.timeout)
264 # now in some cases the self-signed is enough
265 def server_proxy_simple(self, url):
266 self.assert_self_signed_cert()
267 return SfaServerProxy(url, self.private_key_filename(), self.self_signed_cert_filename(),
268 verbose=self.verbose, timeout=self.timeout)
270 # this method can optionnally be invoked to ensure proper
271 # installation of the private key that belongs to this user
272 # installs private_key in working dir with expected name -- preserve mode
273 # typically user_private_key would be ~/.ssh/id_rsa
274 # xxx should probably check the 2 files are identical
275 def init_private_key_if_missing(self, user_private_key):
276 private_key_filename = self.private_key_filename()
277 if not os.path.isfile(private_key_filename):
278 key = self.plain_read(user_private_key)
279 self.plain_write(private_key_filename, key)
280 os.chmod(private_key_filename, os.stat(user_private_key).st_mode)
281 self.logger.debug("SfaClientBootstrap: Copied private key from {} into {}"
282 .format(user_private_key, private_key_filename))
286 def fullpath(self, file):
287 return os.path.join(self.dir, file)
289 # the expected filenames for the various pieces
290 def private_key_filename(self):
291 return self.fullpath("{}.pkey".format(Xrn.unescape(self.hrn)))
293 def self_signed_cert_filename(self):
294 return self.fullpath("{}.sscert".format(self.hrn))
296 def my_credential_filename(self):
297 return self.credential_filename(self.hrn, "user")
298 # the tests use sfi -u <pi-user>; meaning that the slice credential filename
299 # needs to keep track of the user too
301 def credential_filename(self, hrn, type):
303 basename = "{}.{}.cred".format(hrn, type)
305 basename = "{}-{}.{}.cred".format(self.hrn, hrn, type)
306 return self.fullpath(basename)
308 def slice_credential_filename(self, hrn):
309 return self.credential_filename(hrn, 'slice')
311 def authority_credential_filename(self, hrn):
312 return self.credential_filename(hrn, 'authority')
314 def my_gid_filename(self):
315 return self.gid_filename(self.hrn, "user")
317 def gid_filename(self, hrn, type):
318 return self.fullpath("{}.{}.gid".format(hrn, type))
320 def my_pkcs12_filename(self):
321 return self.fullpath("{}.p12".format(self.hrn))
323 # optimizing dependencies
324 # originally we used classes GID or Credential or Certificate
326 # return Credential(filename=self.my_credential()).save_to_string()
327 # but in order to make it simpler to other implementations/languages..
328 def plain_read(self, filename):
329 with open(filename) as infile:
332 def plain_write(self, filename, contents):
333 with open(filename, "w") as outfile:
334 outfile.write(contents)
336 def assert_filename(self, filename, kind):
337 if not os.path.isfile(filename):
338 raise IOError("Missing {} file {}".format(kind, filename))
341 def assert_private_key(self):
342 return self.assert_filename(self.private_key_filename(), "private key")
344 def assert_self_signed_cert(self):
345 return self.assert_filename(self.self_signed_cert_filename(), "self-signed certificate")
347 def assert_my_credential(self):
348 return self.assert_filename(self.my_credential_filename(), "user's credential")
350 def assert_my_gid(self):
351 return self.assert_filename(self.my_gid_filename(), "user's GID")
353 # decorator to make up the other methods
354 def get_or_produce(filename_method, produce_method, validate_method=None):
355 # default validator returns true
357 def wrapped(self, *args, **kw):
358 filename = filename_method(self, *args, **kw)
359 if os.path.isfile(filename):
360 if not validate_method:
362 elif validate_method(self, filename):
365 # remove invalid file
367 "Removing {} - has expired".format(filename))
370 produce_method(self, filename, *args, **kw)
375 error = sys.exc_info()[:2]
376 message = "Could not produce/retrieve {} ({} -- {})"\
377 .format(filename, error[0], error[1])
378 self.logger.log_exc(message)
379 raise Exception(message)
383 @get_or_produce(self_signed_cert_filename, self_signed_cert_produce)
384 def self_signed_cert(self): pass
386 @get_or_produce(my_credential_filename, my_credential_produce, validate_credential)
387 def my_credential(self): pass
389 @get_or_produce(my_gid_filename, my_gid_produce)
390 def my_gid(self): pass
392 @get_or_produce(my_pkcs12_filename, my_pkcs12_produce)
393 def my_pkcs12(self): pass
395 @get_or_produce(credential_filename, credential_produce, validate_credential)
396 def credential(self, hrn, type): pass
398 @get_or_produce(slice_credential_filename, slice_credential_produce, validate_credential)
399 def slice_credential(self, hrn): pass
401 @get_or_produce(authority_credential_filename, authority_credential_produce, validate_credential)
402 def authority_credential(self, hrn): pass
404 @get_or_produce(gid_filename, gid_produce)
405 def gid(self, hrn, type): pass
407 # get the credentials as strings, for inserting as API arguments
408 def my_credential_string(self):
410 return self.plain_read(self.my_credential_filename())
412 def slice_credential_string(self, hrn):
413 self.slice_credential(hrn)
414 return self.plain_read(self.slice_credential_filename(hrn))
416 def authority_credential_string(self, hrn):
417 self.authority_credential(hrn)
418 return self.plain_read(self.authority_credential_filename(hrn))
421 def private_key(self):
422 self.assert_private_key()
423 return self.private_key_filename()
425 def delegate_credential_string(self, original_credential, to_hrn, to_type='authority'):
427 sign a delegation credential to someone else
429 original_credential : typically one's user- or slice- credential to be delegated to s/b else
430 to_hrn : the hrn of the person that will be allowed to do stuff on our behalf
431 to_type : goes with to_hrn, usually 'user' or 'authority'
433 returns a string with the delegated credential
435 this internally uses self.my_gid()
436 it also retrieves the gid for to_hrn/to_type
437 and uses Credential.delegate()"""
439 # the gid and hrn of the object we are delegating
440 if isinstance(original_credential, str):
441 original_credential = Credential(string=original_credential)
442 original_gid = original_credential.get_gid_object()
443 original_hrn = original_gid.get_hrn()
445 if not original_credential.get_privileges().get_all_delegate():
446 self.logger.error("delegate_credential_string: original credential {} does not have delegate bit set"
447 .format(original_hrn))
450 # the delegating user's gid
451 my_gid = self.my_gid()
453 # retrieve the GID for the entity that we're delegating to
454 to_gidfile = self.gid(to_hrn, to_type)
455 # to_gid = GID(to_gidfile )
456 # to_hrn = delegee_gid.get_hrn()
457 # print 'to_hrn', to_hrn
458 delegated_credential = original_credential.delegate(
459 to_gidfile, self.private_key(), my_gid)
460 return delegated_credential.save_to_string(save_parents=True)