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