summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/glider/formats.py111
-rw-r--r--lib/glider/keys.py94
-rw-r--r--lib/glider/tests.py28
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())