diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/glider/formats.py | 111 | ||||
-rw-r--r-- | lib/glider/keys.py | 94 | ||||
-rw-r--r-- | lib/glider/tests.py | 28 |
3 files changed, 192 insertions, 41 deletions
diff --git a/lib/glider/formats.py b/lib/glider/formats.py index b579848..4659e10 100644 --- a/lib/glider/formats.py +++ b/lib/glider/formats.py @@ -1,28 +1,12 @@ -import OpenSSL.crypto - import sexp.access import sexp.encode import time import re -class UnknownMethod(Exception): +class FormatException(Exception): pass -class PublicKey: - def format(self): - raise NotImplemented() - def sign(self, data): - # returns a list of method,signature tuples. - raise NotImplemented() - def checkSignature(self, method, data, signature): - # returns True, False, or raises UnknownMethod. - raise NotImplemented() - def getKeyID(self): - raise NotImplemented() - def getRoles(self): - raise NotImplemented() - class KeyDB: def __init__(self): self.keys = {} @@ -31,18 +15,31 @@ class KeyDB: def getKey(self, keyid): return self.keys[keyid] +_rolePathCache = {} def rolePathMatches(rolePath, path): """ - >>> rolePath.matches("a/b/c/", "a/b/c/") + >>> rolePathMatches("a/b/c/", "a/b/c/") + True + >>> rolePathMatches("**/c.*", "a/b/c.txt") True - >>> rolePath.matches("**/c.*", "a/b/c.txt") + >>> rolePathMatches("**/c.*", "a/b/c.txt/foo") + False + >>> rolePathMatches("a/*/c", "a/b/c") True + >>> rolePathMatches("a/*/c", "a/b/c.txt") + False + >>> rolePathMatches("a/*/c", "a/b/c.txt") #Check cache + False """ - rolePath = re.escape(rolePath).replace(r'\*\*', r'.*') - rolePath = rolePath.replace(r'\*', r'[^/]*') - rolePath += "$" - return re.match(rolePath, path) != None + try: + regex = _rolePathCache[rolePath] + except KeyError: + rolePath = re.escape(rolePath).replace(r'\*\*', r'.*') + rolePath = rolePath.replace(r'\*', r'[^/]*') + rolePath += "$" + regex = _rolePathCache[rolePath] = re.compile(rolePath) + return regex.match(path) != None def checkSignatures(signed, keyDB, role, path): goodSigs = [] @@ -86,21 +83,41 @@ def sign(signed, key): signed[2:] = oldsignatures for method, sig in key.sign(s): - signed.append(['signature', [['keyid', keyid], ['method', method]] + signed.append(['signature', [['keyid', keyid], ['method', method]], sig]) def formatTime(t): + """ + >>> formatTime(1221265172) + '2008-09-13 00:19:32' + """ return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(t)) def parseTime(s): return time.timegm(time.strptime(s, "%Y-%m-%d %H:%M:%S")) +def _parseSchema(s, t=None): + sexpr = sexp.parse.parse(s) + schema = sexp.access.parseSchema(sexpr, t) + return schema + +SCHEMA_TABLE = { } + +PUBKEY_TEMPLATE = r""" + (=pubkey ((:unordered (=type .) (:anyof (. _)))) _) +""" + +SCHEMA_TABLE['PUBKEY'] = _parseSchema(PUBKEY_TEMPLATE) -TIME_SCHEMA = r"""/\{d}4-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/""" +TIME_TEMPLATE = r"""/\{d}4-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/""" -ATTRS_SCHEMA = r"""(:anyof (_ *))""" +SCHEMA_TABLE['TIME'] = sexp.access.parseSchema(TIME_TEMPLATE) -SIGNED_SCHEMA = r""" +ATTRS_TEMPLATE = r"""(:anyof (_ *))""" + +SCHEMA_TABLE['ATTRS'] = _parseSchema(ATTRS_TEMPLATE) + +SIGNED_TEMPLATE = r""" (=signed _ (:someof @@ -109,7 +126,9 @@ SIGNED_SCHEMA = r""" ) )""" -KEYFILE_SCHEMA = r""" +SIGNED_SCHEMA = _parseSchema(SIGNED_TEMPLATE, SCHEMA_TABLE) + +KEYFILE_TEMPLATE = r""" (=keylist (=ts .TIME) (=keys @@ -119,7 +138,9 @@ KEYFILE_SCHEMA = r""" * )""" -MIRRORLIST_SCHEMA = r""" +KEYFILE_SCHEMA = _parseSchema(KEYFILE_TEMPLATE, SCHEMA_TABLE) + +MIRRORLIST_TEMPLATE = r""" (=mirrorlist (=ts .TIME) (=mirrors (:anyof @@ -128,13 +149,17 @@ MIRRORLIST_SCHEMA = r""" *) """ -TIMESTAMP_SCHEMA = r""" +MIRRORLIST_SCHEMA = _parseSchema(MIRRORLIST_TEMPLATE, SCHEMA_TABLE) + +TIMESTAMP_TEMPLATE = r""" (=ts ((:unordered (=at .TIME) (=m .TIME .) (=k .TIME .) (:anyof (=b . . .TIME . .)) .ATTRS)) )""" -BUNDLE_SCHEMA = r""" +TIMESTAMP_SCHEMA = _parseSchema(TIMESTAMP_TEMPLATE, SCHEMA_TABLE) + +BUNDLE_TEMPLATE = r""" (=bundle (=at .TIME) (=os .) @@ -152,15 +177,19 @@ BUNDLE_SCHEMA = r""" * )""" -PACKAGE_SCHEMA = r""" +BUNDLE_SCHEMA = _parseSchema(BUNDLE_TEMPLATE, SCHEMA_TABLE) + +PACKAGE_TEMPLATE = r""" (=package - ((:unordred (=name .) - (=version .) - (=format . (.ATTRS)) - (=path .) - (=ts .TIME) - (=digest .) - (:anyof (=shortdesc . .)) - (:anyof (=longdesc . .)) - .ATTRS))) + ((:unordered (=name .) + (=version .) + (=format . (.ATTRS)) + (=path .) + (=ts .TIME) + (=digest .) + (:anyof (=shortdesc . .)) + (:anyof (=longdesc . .)) + .ATTRS))) """ + +PACKAGE_SCHEMA = _parseSchema(PACKAGE_TEMPLATE, SCHEMA_TABLE) diff --git a/lib/glider/keys.py b/lib/glider/keys.py new file mode 100644 index 0000000..1ff96f1 --- /dev/null +++ b/lib/glider/keys.py @@ -0,0 +1,94 @@ + +# These require PyCrypto. +import Crypto.PublicKey.RSA +import Crypto.Hash.SHA256 + +import sexp.access +import sexp.encode +import sexp.parse + +import binascii +import os + +class CryptoError(Exception): + pass + +class PubkeyFormatException(Exception): + pass + +class UnknownMethod(Exception): + pass + +class PublicKey: + def format(self): + raise NotImplemented() + def sign(self, data): + # returns a list of method,signature tuples. + raise NotImplemented() + def checkSignature(self, method, data, signature): + # returns True, False, or raises UnknownMethod. + raise NotImplemented() + def getKeyID(self): + raise NotImplemented() + def getRoles(self): + raise NotImplemented() + +def intToBinary(number): + h = hex(number) + assert h[:2] == '0x' + return binascii.a2b_hex(h[2:]) + +def binaryToInt(binary): + return int(binascii.b2a_hex(binary), 16) + +def _pkcs1_padding(m, size): + + # I'd rather use OAEP+, but apparently PyCrypto barely supports + # signature verification, and doesn't seem to support signature + # verification with nondeterministic padding. "argh." + + s = [ "\x00\x01", "\xff"* (size-3-len(m)), "\x00", m ] + r = s.join() + return r + +class RSAKey(PublicKey): + def __init__(self, key): + self.key = key + + @staticmethod + def generate(bits=2048): + key = Crypto.PublicKey.RSA.generate(bits=bits, randfunc=os.urandom) + return RSAKey(key) + + @staticmethod + def fromSExpression(sexpr): + # sexpr must match PUBKEY_SCHEMA + typeattr = s_child(sexpr[1], "type")[1] + if typeattr[1] != "rsa": + return None + if len(sexpr[2]) != 2: + raise PubkeyFormatException("RSA keys must have an e,n pair") + e,n = sexpr[2] + key = Crypto.PublicKey.RSA.construct((binaryToInt(n), binaryToInt(e))) + return RSAKey(key) + + def format(self): + n = intToBinary(self.key.n) + e = intToBinary(self.key.e) + return ("pubkey", [("type", "rsa")], (e, n)) + + def sign(self, sexpr): + d_obj = Crypto.Digest.SHA256.new() + sexpr.encode.hash_canonical(sexpr, d_obj) + m = _pkcs1_padding(d_obj.digest(), (self.key.size()+1) // 8) + return ("sha256-pkcs1", self.key.sign(m, "")[0]) + + def checkSignature(self, method, sexpr, sig): + if method != "sha256-pkcs1": + raise UnknownMethod("method") + d_obj = Crypto.Digest.SHA256.new() + sexpr.encode.hash_canonical(sexpr, d_obj) + m = _pkcs1_padding(d_obj.digest(), (self.key.size()+1) // 8) + return self.key.verify(sig, m) + + diff --git a/lib/glider/tests.py b/lib/glider/tests.py new file mode 100644 index 0000000..074e0be --- /dev/null +++ b/lib/glider/tests.py @@ -0,0 +1,28 @@ + +import unittest +import doctest + +import glider.keys +import glider.formats +import glider.tests + +class EncodingTest(unittest.TestCase): + def testQuotedString(self): + self.assertEquals(1,1) + +def suite(): + import sexp.tests + suite = unittest.TestSuite() + + suite.addTest(doctest.DocTestSuite(glider.formats)) + #suite.addTest(doctest.DocTestSuite(sexp.parse)) + + loader = unittest.TestLoader() + suite.addTest(loader.loadTestsFromModule(glider.tests)) + + return suite + + +if __name__ == '__main__': + + unittest.TextTestRunner(verbosity=1).run(suite()) |