terom@348: import base64 terom@348: import calendar terom@348: import datetime terom@348: import ipaddr terom@348: import hashlib terom@348: import M2Crypto terom@348: terom@348: import logging; log = logging.getLogger('pvl.login.pubtkt') terom@348: terom@348: def datetime2unix (dt) : terom@348: """ terom@348: datetime.datetime -> float terom@348: """ terom@348: terom@348: return calendar.timegm(dt.utctimetuple()) terom@348: terom@348: class Error (Exception) : terom@348: pass terom@348: terom@348: class ParseError (Error) : terom@348: pass terom@348: terom@348: class VerifyError (Error) : terom@348: def __init__ (self, pubtkt, error) : terom@348: self.pubtkt = pubtkt terom@348: self.error = error terom@348: terom@348: class ExpiredError (VerifyError) : terom@348: def __init__ (self, pubtkt, now) : terom@348: self.pubtkt = pubtkt terom@348: self.now = now terom@348: terom@348: class ServerKeys (object) : terom@348: @classmethod terom@348: def config (cls, public_key, private_key) : terom@348: return cls( terom@348: public = M2Crypto.RSA.load_pub_key(public_key), terom@348: private = M2Crypto.RSA.load_key(private_key), terom@348: ) terom@348: terom@348: def __init__ (self, public, private) : terom@348: self.public = public terom@348: self.private = private terom@348: terom@348: class PubTkt (object) : terom@348: @classmethod terom@348: def load (cls, cookie, public_key) : terom@348: """ terom@348: Load and verify a pubtkt from a cookie. terom@348: terom@348: Raise ParseError, VerifyError. terom@348: """ terom@348: terom@348: pubtkt, hash, sig = cls.parse(cookie) terom@348: terom@348: log.debug("parsed %s hash=%s sig=%s", pubtkt, hash.encode('hex'), sig.encode('hex')) terom@348: terom@348: try : terom@348: if not public_key.verify(hash, sig, 'sha1') : terom@348: raise VerifyError(pubtkt, "Unable to verify signature") terom@348: except M2Crypto.RSA.RSAError as ex : terom@348: raise VerifyError(pubtkt, str(ex)) terom@348: terom@348: now = datetime.datetime.now() terom@348: terom@348: log.debug("validating %s < %s", pubtkt.validuntil, now) terom@348: terom@348: if pubtkt.validuntil < now : terom@348: raise ExpiredError(pubtkt, now) terom@348: terom@348: return pubtkt terom@348: terom@348: @classmethod terom@348: def parse (cls, cookie) : terom@348: """ terom@348: Load a pubtkt from a cookie terom@348: terom@348: Raises ParseError. terom@348: """ terom@348: terom@348: if ';sig=' in cookie : terom@348: data, sig = cookie.rsplit(';sig=', 1) terom@348: else : terom@348: raise ParseError("Missing signature") terom@348: terom@348: sig = base64.b64decode(sig) terom@348: hash = hashlib.sha1(data).digest() terom@348: terom@348: try : terom@348: attrs = dict(field.split('=', 1) for field in data.split(';')) terom@348: except ValueError as ex : terom@348: raise ParseError(str(ex)) terom@348: terom@348: try : terom@348: return cls.build(**attrs), hash, sig terom@348: except (TypeError, ValueError) as ex : terom@348: raise ParseError(str(ex)) terom@348: terom@348: @classmethod terom@348: def build (cls, uid, validuntil, cip=None, tokens=None, udata=None, graceperiod=None, bauth=None) : terom@348: """ terom@348: Build a pubtkt from items. terom@348: terom@348: Raises TypeError or ValueError.. terom@348: """ terom@348: terom@348: return cls(uid, terom@348: validuntil = datetime.datetime.fromtimestamp(int(validuntil)), terom@348: cip = ipaddr.IPAddress(cip) if cip else None, terom@348: tokens = tokens.split(',') if tokens else (), terom@348: udata = udata, terom@348: graceperiod = datetime.datetime.fromtimestamp(int(graceperiod)) if graceperiod else None, terom@348: bauth = bauth, terom@348: ) terom@348: terom@348: @classmethod terom@348: def new (cls, uid, expiry, **opts) : terom@348: now = datetime.datetime.now() terom@348: terom@348: return cls(uid, now + expiry, **opts) terom@348: terom@348: def __init__ (self, uid, validuntil, cip=None, tokens=(), udata=None, graceperiod=None, bauth=None) : terom@348: self.uid = uid terom@348: self.validuntil = validuntil terom@348: self.cip = cip terom@348: self.tokens = tokens terom@348: self.udata = udata terom@348: self.graceperiod = graceperiod terom@348: self.bauth = bauth terom@348: terom@348: def iteritems (self) : terom@348: yield 'uid', self.uid terom@348: yield 'validuntil', int(datetime2unix(self.validuntil)) terom@348: terom@348: if self.cip : terom@348: yield 'cip', self.cip terom@348: terom@348: if self.tokens : terom@348: yield 'tokens', ','.join(self.tokens) terom@348: terom@348: if self.udata : terom@348: yield 'udata', self.udata terom@348: terom@348: if self.graceperiod : terom@348: yield 'graceperiod', int(datetime2unix(self.graceperiod)) terom@348: terom@348: if self.bauth : terom@348: yield 'bauth', self.bauth terom@348: terom@348: def __str__ (self) : terom@348: """ terom@348: The (unsigned) pubtkt terom@348: """ terom@348: terom@348: return ';'.join('%s=%s' % (key, value) for key, value in self.iteritems()) terom@348: terom@348: def sign (self, private_key) : terom@348: data = str(self) terom@348: hash = hashlib.sha1(data).digest() terom@348: sign = private_key.sign(hash, 'sha1') terom@348: terom@348: return '%s;sig=%s' % (self, base64.b64encode(sign)) terom@348: terom@349: def valid (self) : terom@349: """ terom@349: Return remaining ticket validity. terom@349: """ terom@349: terom@349: return self.validuntil - datetime.datetime.now() terom@349: terom@349: def grace (self) : terom@349: """ terom@349: Return remaining ticket grace. terom@349: """ terom@349: terom@349: return self.graceperiod - datetime.datetime.now() terom@349: