diff --git a/ed25519/.gitignore b/ed25519/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/ed25519/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/ed25519/.travis.yml b/ed25519/.travis.yml new file mode 100644 index 00000000..8518ca28 --- /dev/null +++ b/ed25519/.travis.yml @@ -0,0 +1,28 @@ +language: python +python: 2.7 +env: + - TOXENV=py26 + - TOXENV=py27 + #- TOXENV=py32 + #- TOXENV=py33 + - TOXENV=pypy + +install: + # Add the PyPy repository + - "if [[ $TOXENV == 'pypy' ]]; then sudo add-apt-repository -y ppa:pypy/ppa; fi" + # Upgrade PyPy + - "if [[ $TOXENV == 'pypy' ]]; then sudo apt-get -y install pypy; fi" + # This is required because we need to get rid of the Travis installed PyPy + # or it'll take precedence over the PPA installed one. + - "if [[ $TOXENV == 'pypy' ]]; then sudo rm -rf /usr/local/pypy/bin; fi" + - pip install tox + +script: + - tox + +notifications: + irc: + channels: + - "irc.freenode.org#cryptography-dev" + use_notice: true + skip_join: true diff --git a/ed25519/__init__.py b/ed25519/__init__.py old mode 100755 new mode 100644 diff --git a/ed25519/ed25519.py b/ed25519/ed25519.py old mode 100755 new mode 100644 index 7f8613b8..b7e9ff04 --- a/ed25519/ed25519.py +++ b/ed25519/ed25519.py @@ -1,104 +1,161 @@ import hashlib + b = 256 -q = 2**255 - 19 -l = 2**252 + 27742317777372353535851937790883648493 +q = 2 ** 255 - 19 +l = 2 ** 252 + 27742317777372353535851937790883648493 + def H(m): - return hashlib.sha512(m).digest() + return hashlib.sha512(m).digest() -def expmod(b,e,m): - if e == 0: return 1 - t = expmod(b,e/2,m)**2 % m - if e & 1: t = (t*b) % m - return t -def inv(x): - return expmod(x,q-2,q) +def pow2(x, p): + """== pow(x, 2**p, q)""" + while p > 0: + x = x * x % q + p -= 1 + return x + +def inv(z): + """$= z^{-1} \mod q$, for z != 0""" + # Adapted from curve25519_athlon.c in djb's Curve25519. + z2 = z * z % q # 2 + z9 = pow2(z2, 2) * z % q # 9 + z11 = z9 * z2 % q # 11 + z2_5_0 = (z11*z11)%q * z9 % q # 31 == 2^5 - 2^0 + z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0 + z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ... + z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q + z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q + z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q + z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q + z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0 + return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2 + d = -121665 * inv(121666) -I = expmod(2,(q-1)/4,q) +I = pow(2, (q - 1) / 4, q) + def xrecover(y): - xx = (y*y-1) * inv(d*y*y+1) - x = expmod(xx,(q+3)/8,q) - if (x*x - xx) % q != 0: x = (x*I) % q - if x % 2 != 0: x = q-x - return x + xx = (y * y - 1) * inv(d * y * y + 1) + x = pow(xx, (q + 3) / 8, q) + + if (x * x - xx) % q != 0: + x = (x * I) % q + + if x % 2 != 0: + x = q-x + + return x + By = 4 * inv(5) Bx = xrecover(By) -B = [Bx % q,By % q] +B = (Bx % q, By % q) -def edwards(P,Q): - x1 = P[0] - y1 = P[1] - x2 = Q[0] - y2 = Q[1] - x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2) - y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2) - return [x3 % q,y3 % q] -def scalarmult(P,e): - if e == 0: return [0,1] - Q = scalarmult(P,e/2) - Q = edwards(Q,Q) - if e & 1: Q = edwards(Q,P) - return Q +def edwards(P, Q): + x1, y1 = P + x2, y2 = Q + x3 = (x1 * y2 + x2 * y1) * inv(1 + d * x1 * x2 * y1 * y2) + y3 = (y1 * y2 + x1 * x2) * inv(1 - d * x1 * x2 * y1 * y2) + + return (x3 % q, y3 % q) + + +def scalarmult(P, e): + if e == 0: + return (0, 1) + + Q = scalarmult(P, e / 2) + Q = edwards(Q, Q) + + if e & 1: + Q = edwards(Q, P) + + return Q + def encodeint(y): - bits = [(y >> i) & 1 for i in range(b)] - return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + bits = [(y >> i) & 1 for i in range(b)] + return ''.join([ + chr(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b/8) + ]) + def encodepoint(P): - x = P[0] - y = P[1] - bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] - return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + x = P[0] + y = P[1] + bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] + return ''.join([ + chr(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b/8) + ]) + + +def bit(h, i): + return (ord(h[i / 8]) >> (i % 8)) & 1 -def bit(h,i): - return (ord(h[i/8]) >> (i%8)) & 1 def publickey(sk): - h = H(sk) - a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) - A = scalarmult(B,a) - return encodepoint(A) + h = H(sk) + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + A = scalarmult(B, a) + return encodepoint(A) + def Hint(m): - h = H(m) - return sum(2**i * bit(h,i) for i in range(2*b)) + h = H(m) + return sum(2 ** i * bit(h, i) for i in range(2 * b)) + + +def signature(m, sk, pk): + h = H(sk) + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + r = Hint(''.join([h[j] for j in range(b / 8, b / 4)]) + m) + R = scalarmult(B, r) + S = (r + Hint(encodepoint(R) + pk + m) * a) % l + return encodepoint(R) + encodeint(S) -def signature(m,sk,pk): - h = H(sk) - a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) - r = Hint(''.join([h[i] for i in range(b/8,b/4)]) + m) - R = scalarmult(B,r) - S = (r + Hint(encodepoint(R) + pk + m) * a) % l - return encodepoint(R) + encodeint(S) def isoncurve(P): - x = P[0] - y = P[1] - return (-x*x + y*y - 1 - d*x*x*y*y) % q == 0 + x, y = P + return (-x * x + y * y - 1 - d * x * x * y * y) % q == 0 + def decodeint(s): - return sum(2**i * bit(s,i) for i in range(0,b)) + return sum(2 ** i * bit(s, i) for i in range(0, b)) + def decodepoint(s): - y = sum(2**i * bit(s,i) for i in range(0,b-1)) - x = xrecover(y) - if x & 1 != bit(s,b-1): x = q-x - P = [x,y] - if not isoncurve(P): raise Exception("decoding point that is not on curve") - return P + y = sum(2 ** i * bit(s, i) for i in range(0, b - 1)) + x = xrecover(y) -def checkvalid(s,m,pk): - if len(s) != b/4: raise Exception("signature length is wrong") - if len(pk) != b/8: raise Exception("public-key length is wrong") - R = decodepoint(s[0:b/8]) - A = decodepoint(pk) - S = decodeint(s[b/8:b/4]) - h = Hint(encodepoint(R) + pk + m) - if scalarmult(B,S) != edwards(R,scalarmult(A,h)): - raise Exception("signature does not pass verification") + if x & 1 != bit(s, b-1): + x = q-x + + P = (x, y) + + if not isoncurve(P): + raise Exception("decoding point that is not on curve") + + return P + + +def checkvalid(s, m, pk): + if len(s) != b / 4: + raise Exception("signature length is wrong") + + if len(pk) != b / 8: + raise Exception("public-key length is wrong") + + R = decodepoint(s[:b / 8]) + A = decodepoint(pk) + S = decodeint(s[b / 8:b / 4]) + h = Hint(encodepoint(R) + pk + m) + + if scalarmult(B, S) != edwards(R, scalarmult(A, h)): + raise Exception("signature does not pass verification") diff --git a/ed25519/runtests.sh b/ed25519/runtests.sh new file mode 100755 index 00000000..4dec6743 --- /dev/null +++ b/ed25519/runtests.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +python -u signfast.py < sign.input + +if [[ $TEST == 'slow' ]]; then + python -u sign.py < sign.input +fi diff --git a/ed25519/science.py b/ed25519/science.py new file mode 100644 index 00000000..18a2fcc2 --- /dev/null +++ b/ed25519/science.py @@ -0,0 +1,32 @@ +import os +import timeit + +import ed25519 + + +seed = os.urandom(32) + +data = "The quick brown fox jumps over the lazy dog" +private_key = seed +public_key = ed25519.publickey(seed) +signature = ed25519.signature(data, private_key, public_key) + + +print('Time generate') +print(timeit.timeit("ed25519.publickey(seed)", + setup="from __main__ import ed25519, seed", + number=10, +)) + +print('\nTime create signature') +print(timeit.timeit("ed25519.signature(data, private_key, public_key)", + setup="from __main__ import ed25519, data, private_key, public_key", + number=10, +)) + + +print('\nTime verify signature') +print(timeit.timeit("ed25519.checkvalid(signature, data, public_key)", + setup="from __main__ import ed25519, signature, data, public_key", + number=10, +)) diff --git a/ed25519/setup.py b/ed25519/setup.py new file mode 100644 index 00000000..d65e3d2a --- /dev/null +++ b/ed25519/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup + + +setup( + name="ed25519", + version="1.0", + + py_modules="ed25519", + + zip_safe=False, +) diff --git a/ed25519/sign.py b/ed25519/sign.py index be099ad2..18eea684 100644 --- a/ed25519/sign.py +++ b/ed25519/sign.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import sys import binascii import ed25519 @@ -16,6 +18,7 @@ while 1: line = sys.stdin.readline() if not line: break + print(".", end="") x = line.split(':') sk = binascii.unhexlify(x[0][0:64]) pk = ed25519.publickey(sk) diff --git a/ed25519/signfast.py b/ed25519/signfast.py new file mode 100644 index 00000000..630c92ca --- /dev/null +++ b/ed25519/signfast.py @@ -0,0 +1,48 @@ +from __future__ import print_function + +import sys +import binascii +import ed25519 + +# examples of inputs: see sign.input +# should produce no output: python sign.py < sign.input + +# warning: currently 37 seconds/line on a fast machine + +# fields on each input line: sk, pk, m, sm +# each field hex +# each field colon-terminated +# sk includes pk at end +# sm includes m at end + +MAX = 10 + +i = 0 +while 1: + if i >= MAX: + break + i += 1 + line = sys.stdin.readline() + if not line: break + print(".", end="") + x = line.split(':') + sk = binascii.unhexlify(x[0][0:64]) + pk = ed25519.publickey(sk) + m = binascii.unhexlify(x[2]) + s = ed25519.signature(m,sk,pk) + ed25519.checkvalid(s,m,pk) + forgedsuccess = 0 + try: + if len(m) == 0: + forgedm = "x" + else: + forgedmlen = len(m) + forgedm = ''.join([chr(ord(m[i])+(i==forgedmlen-1)) for i in range(forgedmlen)]) + ed25519.checkvalid(s,forgedm,pk) + forgedsuccess = 1 + except: + pass + assert not forgedsuccess + assert x[0] == binascii.hexlify(sk + pk) + assert x[1] == binascii.hexlify(pk) + assert x[3] == binascii.hexlify(s + m) diff --git a/ed25519/tox.ini b/ed25519/tox.ini new file mode 100644 index 00000000..1fdba166 --- /dev/null +++ b/ed25519/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py26,py27,pypy,py32,py33 + +[testenv] +commands = ./runtests.sh diff --git a/resources/images/TUF repository tools.png b/resources/images/TUF repository tools.png new file mode 100644 index 00000000..3fe9cc9d Binary files /dev/null and b/resources/images/TUF repository tools.png differ diff --git a/setup.py b/setup.py index d035942f..99422bea 100755 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ url='https://www.updateframework.com', install_requires=['pycrypto>=2.6'], packages=[ + 'ed25519', 'tuf', 'tuf.client', 'tuf.compatibility', @@ -82,6 +83,7 @@ 'tuf/repo/quickstart.py', 'tuf/pushtools/push.py', 'tuf/pushtools/receivetools/receive.py', - 'tuf/repo/signercli.py' + 'tuf/repo/signercli.py', + 'tuf/client/basic_client.py' ] ) diff --git a/tests/unit/test_ed25519_keys.py b/tests/unit/test_ed25519_keys.py new file mode 100755 index 00000000..bdda7823 --- /dev/null +++ b/tests/unit/test_ed25519_keys.py @@ -0,0 +1,123 @@ +""" + + test_ed25519_keys.py + + + Vladimir Diaz + + + October 11, 2013. + + + See LICENSE for licensing information. + + + Test cases for test_ed25519_keys.py. +""" + +import unittest +import logging + +import tuf +import tuf.log +import tuf.formats +import tuf.ed25519_keys as ed25519 + +logger = logging.getLogger('tuf.test_ed25519_keys') + +public, private = ed25519.generate_public_and_private() +FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' + + +class TestEd25519_keys(unittest.TestCase): + def setUp(self): + pass + + + def test_generate_public_and_private(self): + pub, priv = ed25519.generate_public_and_private() + + # Check format of 'pub' and 'priv'. + self.assertEqual(True, tuf.formats.ED25519PUBLIC_SCHEMA.matches(pub)) + self.assertEqual(True, tuf.formats.ED25519SEED_SCHEMA.matches(priv)) + + # Check for invalid argument. + self.assertRaises(tuf.FormatError, + ed25519.generate_public_and_private, 'True') + + self.assertRaises(tuf.FormatError, + ed25519.generate_public_and_private, 2048) + + + def test_create_signature(self): + global public + global private + data = 'The quick brown fox jumps over the lazy dog' + signature, method = ed25519.create_signature(public, private, data) + + # Verify format of returned values. + self.assertEqual(True, + tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature)) + + self.assertEqual(True, tuf.formats.NAME_SCHEMA.matches(method)) + self.assertEqual('ed25519-python', method) + + # Check for improperly formatted argument. + self.assertRaises(tuf.FormatError, + ed25519.create_signature, 123, private, data) + + self.assertRaises(tuf.FormatError, + ed25519.create_signature, public, 123, data) + + # Check for invalid 'data'. + self.assertRaises(tuf.CryptoError, + ed25519.create_signature, public, private, 123) + + + def test_verify_signature(self): + global public + global private + data = 'The quick brown fox jumps over the lazy dog' + signature, method = ed25519.create_signature(public, private, data) + + valid_signature = ed25519.verify_signature(public, method, signature, data) + self.assertEqual(True, valid_signature) + + # Check for improperly formatted arguments. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, 123, method, + signature, data) + + # Signature method improperly formatted. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, 123, + signature, data) + + # Signature not a string. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method, + 123, data) + + # Invalid signature length, which must be exactly 64 bytes.. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method, + 'bad_signature', data) + + # Check for invalid signature and data. + # Mismatched data. + self.assertEqual(False, ed25519.verify_signature(public, method, + signature, '123')) + + # Mismatched signature. + bad_signature = 'a'*64 + self.assertEqual(False, ed25519.verify_signature(public, method, + bad_signature, data)) + + # Generated signature created with different data. + new_signature, method = ed25519.create_signature(public, private, + 'mismatched data') + + self.assertEqual(False, ed25519.verify_signature(public, method, + new_signature, data)) + + + +# Run the unit tests. +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_keydb.py b/tests/unit/test_keydb.py index c7330875..fe57b776 100755 --- a/tests/unit/test_keydb.py +++ b/tests/unit/test_keydb.py @@ -13,7 +13,6 @@ Unit test for 'keydb.py'. - """ import unittest @@ -21,7 +20,7 @@ import tuf import tuf.formats -import tuf.rsa_key +import tuf.keys import tuf.keydb import tuf.log @@ -31,7 +30,7 @@ # Generate the three keys to use in our test cases. KEYS = [] for junk in range(3): - KEYS.append(tuf.rsa_key.generate(2048)) + KEYS.append(tuf.keys.generate_rsa_key(2048)) @@ -89,7 +88,7 @@ def test_get_key(self): - def test_add_rsakey(self): + def test_add_key(self): # Test conditions using valid 'keyid' arguments. rsakey = KEYS[0] keyid = KEYS[0]['keyid'] @@ -97,9 +96,9 @@ def test_add_rsakey(self): keyid2 = KEYS[1]['keyid'] rsakey3 = KEYS[2] keyid3 = KEYS[2]['keyid'] - self.assertEqual(None, tuf.keydb.add_rsakey(rsakey, keyid)) - self.assertEqual(None, tuf.keydb.add_rsakey(rsakey2, keyid2)) - self.assertEqual(None, tuf.keydb.add_rsakey(rsakey3)) + self.assertEqual(None, tuf.keydb.add_key(rsakey, keyid)) + self.assertEqual(None, tuf.keydb.add_key(rsakey2, keyid2)) + self.assertEqual(None, tuf.keydb.add_key(rsakey3)) self.assertEqual(rsakey, tuf.keydb.get_key(keyid)) self.assertEqual(rsakey2, tuf.keydb.get_key(keyid2)) @@ -109,26 +108,26 @@ def test_add_rsakey(self): tuf.keydb.clear_keydb() rsakey3['keytype'] = 'bad_keytype' - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, None, keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, '', keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, ['123'], keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, {'a': 'b'}, keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, {'keyid': ''}) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, 123) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, False) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, ['keyid']) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey3, keyid3) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, None, keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, '', keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, ['123'], keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, {'a': 'b'}, keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, {'keyid': ''}) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, 123) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, False) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, ['keyid']) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey3, keyid3) rsakey3['keytype'] = 'rsa' # Test conditions where keyid does not match the rsakey. - self.assertRaises(tuf.Error, tuf.keydb.add_rsakey, rsakey, keyid2) - self.assertRaises(tuf.Error, tuf.keydb.add_rsakey, rsakey2, keyid) + self.assertRaises(tuf.Error, tuf.keydb.add_key, rsakey, keyid2) + self.assertRaises(tuf.Error, tuf.keydb.add_key, rsakey2, keyid) # Test conditions using keyids that have already been added. - tuf.keydb.add_rsakey(rsakey, keyid) - tuf.keydb.add_rsakey(rsakey2, keyid2) - self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_rsakey, rsakey) - self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_rsakey, rsakey2) + tuf.keydb.add_key(rsakey, keyid) + tuf.keydb.add_key(rsakey2, keyid2) + self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_key, rsakey) + self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_key, rsakey2) @@ -140,12 +139,13 @@ def test_remove_key(self): keyid2 = KEYS[1]['keyid'] rsakey3 = KEYS[2] keyid3 = KEYS[2]['keyid'] - tuf.keydb.add_rsakey(rsakey, keyid) - tuf.keydb.add_rsakey(rsakey2, keyid2) - tuf.keydb.add_rsakey(rsakey3, keyid3) + tuf.keydb.add_key(rsakey, keyid) + tuf.keydb.add_key(rsakey2, keyid2) + tuf.keydb.add_key(rsakey3, keyid3) self.assertEqual(None, tuf.keydb.remove_key(keyid)) self.assertEqual(None, tuf.keydb.remove_key(keyid2)) + # Ensure the keys were actually removed. self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid) self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid2) diff --git a/tests/unit/test_keys.py b/tests/unit/test_keys.py new file mode 100755 index 00000000..758c1009 --- /dev/null +++ b/tests/unit/test_keys.py @@ -0,0 +1,193 @@ +""" + + test_keys.py + + + Vladimir Diaz + + + October 10, 2013. + + + See LICENSE for licensing information. + + + Test cases for test_keys.py. + TODO: test case for ed25519 key generation and refactor. +""" + +import unittest +import logging + +import tuf +import tuf.log +import tuf.formats +import tuf.keys + +logger = logging.getLogger('tuf.test_keys') + +KEYS = tuf.keys +FORMAT_ERROR_MSG = 'tuf.FormatError was raised! Check object\'s format.' +DATA = 'SOME DATA REQUIRING AUTHENTICITY.' + + +rsakey_dict = KEYS.generate_rsa_key() +temp_key_info_vals = rsakey_dict.values() +temp_key_vals = rsakey_dict['keyval'].values() + + +class TestKeys(unittest.TestCase): + def setUp(self): + rsakey_dict['keytype']=temp_key_info_vals[0] + rsakey_dict['keyid']=temp_key_info_vals[1] + rsakey_dict['keyval']=temp_key_info_vals[2] + rsakey_dict['keyval']['public']=temp_key_vals[0] + rsakey_dict['keyval']['private']=temp_key_vals[1] + + + def test_generate_rsa_key(self): + _rsakey_dict = KEYS.generate_rsa_key() + + # Check if the format of the object returned by generate() corresponds + # to RSAKEY_SCHEMA format. + self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict), + FORMAT_ERROR_MSG) + + # Passing a bit value that is <2048 to generate() - should raise + # 'tuf.FormatError'. + self.assertRaises(tuf.FormatError, KEYS.generate_rsa_key, 555) + + # Passing a string instead of integer for a bit value. + self.assertRaises(tuf.FormatError, KEYS.generate_rsa_key, 'bits') + + # NOTE if random bit value >=2048 (not 4096) is passed generate(bits) + # does not raise any errors and returns a valid key. + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(2048))) + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(4096))) + + + def test_format_keyval_to_metadata(self): + keyvalue = rsakey_dict['keyval'] + keytype = rsakey_dict['keytype'] + key_meta = KEYS.format_keyval_to_metadata(keytype, keyvalue) + + # Check if the format of the object returned by this function corresponds + # to KEY_SCHEMA format. + self.assertEqual(None, + tuf.formats.KEY_SCHEMA.check_match(key_meta), + FORMAT_ERROR_MSG) + key_meta = KEYS.format_keyval_to_metadata(keytype, keyvalue, private=True) + + # Check if the format of the object returned by this function corresponds + # to KEY_SCHEMA format. + self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta), + FORMAT_ERROR_MSG) + + # Supplying a 'bad' keyvalue. + self.assertRaises(tuf.FormatError, KEYS.format_keyval_to_metadata, + 'bad_keytype', keyvalue) + + del keyvalue['public'] + self.assertRaises(tuf.FormatError, KEYS.format_keyval_to_metadata, + keytype, keyvalue) + + + def test_format_metadata_to_key(self): + # Reconfiguring rsakey_dict to conform to KEY_SCHEMA + # i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}} + #keyid = rsakey_dict['keyid'] + del rsakey_dict['keyid'] + + rsakey_dict_from_meta = KEYS.format_metadata_to_key(rsakey_dict) + + # Check if the format of the object returned by this function corresponds + # to RSAKEY_SCHEMA format. + self.assertEqual(None, + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict_from_meta), + FORMAT_ERROR_MSG) + + # Supplying a wrong number of arguments. + self.assertRaises(TypeError, KEYS.format_metadata_to_key) + args = (rsakey_dict, rsakey_dict) + self.assertRaises(TypeError, KEYS.format_metadata_to_key, *args) + + # Supplying a malformed argument to the function - should get FormatError + del rsakey_dict['keyval'] + self.assertRaises(tuf.FormatError, KEYS.format_metadata_to_key, + rsakey_dict) + + + def test_helper_get_keyid(self): + keytype = rsakey_dict['keytype'] + keyvalue = rsakey_dict['keyval'] + + # Check format of 'keytype'. + self.assertEqual(None, tuf.formats.KEYTYPE_SCHEMA.check_match(keytype), + FORMAT_ERROR_MSG) + + # Check format of 'keyvalue'. + self.assertEqual(None, tuf.formats.KEYVAL_SCHEMA.check_match(keyvalue), + FORMAT_ERROR_MSG) + + keyid = KEYS._get_keyid(keytype, keyvalue) + + # Check format of 'keyid' - the output of '_get_keyid()' function. + self.assertEqual(None, tuf.formats.KEYID_SCHEMA.check_match(keyid), + FORMAT_ERROR_MSG) + + + def test_create_signature(self): + # Creating a signature for 'DATA'. + signature = KEYS.create_signature(rsakey_dict, DATA) + + # Check format of output. + self.assertEqual(None, + tuf.formats.SIGNATURE_SCHEMA.check_match(signature), + FORMAT_ERROR_MSG) + + # Removing private key from 'rsakey_dict' - should raise a TypeError. + rsakey_dict['keyval']['private'] = '' + + args = (rsakey_dict, DATA) + self.assertRaises(TypeError, KEYS.create_signature, *args) + + # Supplying an incorrect number of arguments. + self.assertRaises(TypeError, KEYS.create_signature) + + + def test_verify_signature(self): + # Creating a signature 'signature' of 'DATA' to be verified. + signature = KEYS.create_signature(rsakey_dict, DATA) + + # Verifying the 'signature' of 'DATA'. + verified = KEYS.verify_signature(rsakey_dict, signature, DATA) + self.assertTrue(verified, "Incorrect signature.") + + # Testing an invalid 'signature'. Same 'signature' is passed, with + # 'DATA' different than the original 'DATA' that was used + # in creating the 'signature'. Function should return 'False'. + + # Modifying 'DATA'. + _DATA = '1111'+DATA+'1111' + + # Verifying the 'signature' of modified '_DATA'. + verified = KEYS.verify_signature(rsakey_dict, signature, _DATA) + self.assertFalse(verified, + 'Returned \'True\' on an incorrect signature.') + + # Modifying 'signature' to pass an incorrect method since only + # 'PyCrypto-PKCS#1 PSS' + # is accepted. + signature['method'] = 'Biff' + + args = (rsakey_dict, signature, DATA) + self.assertRaises(tuf.UnknownMethodError, KEYS.verify_signature, *args) + + # Passing incorrect number of arguments. + self.assertRaises(TypeError, KEYS.verify_signature) + + + +# Run the unit tests. +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_keystore.py b/tests/unit/test_keystore.py index 5eed223a..e1a8c7f2 100755 --- a/tests/unit/test_keystore.py +++ b/tests/unit/test_keystore.py @@ -13,7 +13,6 @@ Unit test for keystore.py. - """ import unittest @@ -25,7 +24,7 @@ import tuf import tuf.repo.keystore -import tuf.rsa_key +import tuf.keys import tuf.formats import tuf.util import tuf.log @@ -56,7 +55,7 @@ for i in range(3): # Populating the original 'RSAKEYS' and 'PASSWDS' lists. - RSAKEYS.append(tuf.rsa_key.generate()) + RSAKEYS.append(tuf.keys.generate_rsa_key()) PASSWDS.append('passwd_'+str(i)) # Saving original copies of 'RSAKEYS' and 'PASSWDS' to temp variables @@ -350,6 +349,7 @@ def tearDownModule(): tuf.repo.keystore.clear_keystore() + # Run the unit tests. if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_pycrypto_keys.py b/tests/unit/test_pycrypto_keys.py new file mode 100755 index 00000000..eaa322b0 --- /dev/null +++ b/tests/unit/test_pycrypto_keys.py @@ -0,0 +1,198 @@ +""" + + test_pycrypto_keys.py + + + Vladimir Diaz + + + October 10, 2013. + + + See LICENSE for licensing information. + + + Test cases for test_pycrypto_keys.py. +""" + +import unittest +import logging + +import tuf +import tuf.log +import tuf.formats +import tuf.pycrypto_keys as pycrypto + +logger = logging.getLogger('tuf.test_pycrypto_keys') + +public_rsa, private_rsa = pycrypto.generate_rsa_public_and_private() +FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' + + +class TestPycrypto_keys(unittest.TestCase): + def setUp(self): + pass + + + def test_generate_rsa_public_and_private(self): + pub, priv = pycrypto.generate_rsa_public_and_private() + + # Check format of 'pub' and 'priv'. + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pub), + FORMAT_ERROR_MSG) + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(priv), + FORMAT_ERROR_MSG) + + # Check for invalid bits argument. bit >= 2048 and a multiple of 256. + self.assertRaises(tuf.FormatError, + pycrypto.generate_rsa_public_and_private, 1024) + + self.assertRaises(ValueError, + pycrypto.generate_rsa_public_and_private, 2049) + + self.assertRaises(tuf.FormatError, + pycrypto.generate_rsa_public_and_private, '2048') + + + def test_create_rsa_signature(self): + global private_rsa + data = 'The quick brown fox jumps over the lazy dog' + signature, method = pycrypto.create_rsa_signature(private_rsa, data) + + # Verify format of returned values. + self.assertNotEqual(None, signature) + self.assertEqual(None, tuf.formats.NAME_SCHEMA.check_match(method), + FORMAT_ERROR_MSG) + self.assertEqual('PyCrypto-PKCS#1 PSS', method) + + # Check for improperly formatted argument. + self.assertRaises(tuf.FormatError, + pycrypto.create_rsa_signature, 123, data) + + # Check for invalid 'data'. + self.assertRaises(tuf.CryptoError, + pycrypto.create_rsa_signature, private_rsa, 123) + + + def test_verify_rsa_signature(self): + global public_rsa + global private_rsa + data = 'The quick brown fox jumps over the lazy dog' + signature, method = pycrypto.create_rsa_signature(private_rsa, data) + + valid_signature = pycrypto.verify_rsa_signature(signature, method, public_rsa, + data) + self.assertEqual(True, valid_signature) + + # Check for improperly formatted arguments. + self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, signature, + 123, public_rsa, data) + + self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, signature, + method, 123, data) + + self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, 123, method, + public_rsa, data) + + # Check for invalid signature and data. + self.assertRaises(tuf.CryptoError, pycrypto.verify_rsa_signature, signature, + method, public_rsa, 123) + + self.assertEqual(False, pycrypto.verify_rsa_signature(signature, method, + public_rsa, 'mismatched data')) + + mismatched_signature, method = pycrypto.create_rsa_signature(private_rsa, + 'mismatched data') + + self.assertEqual(False, pycrypto.verify_rsa_signature(mismatched_signature, + method, public_rsa, data)) + + + + def test_create_rsa_encrypted_pem(self): + global public_rsa + global private_rsa + passphrase = 'pw' + + # Check format of 'public_rsa'. + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(public_rsa), + FORMAT_ERROR_MSG) + + # Check format of 'passphrase'. + self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), + FORMAT_ERROR_MSG) + + # Generate the encrypted PEM string of 'public_rsa'. + pem_rsakey = pycrypto.create_rsa_encrypted_pem(private_rsa, passphrase) + + # Check format of 'pem_rsakey'. + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pem_rsakey), + FORMAT_ERROR_MSG) + + # Check for invalid arguments. + self.assertRaises(tuf.FormatError, + pycrypto.create_rsa_encrypted_pem, 1, passphrase) + self.assertRaises(tuf.FormatError, + pycrypto.create_rsa_encrypted_pem, private_rsa, ['pw']) + + + def test_create_rsa_public_and_private_from_encrypted_pem(self): + global private_rsa + passphrase = 'pw' + + # Generate the encrypted PEM string of 'private_rsa'. + pem_rsakey = pycrypto.create_rsa_encrypted_pem(private_rsa, passphrase) + + # Check format of 'passphrase'. + self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), + FORMAT_ERROR_MSG) + + # Decrypt 'pem_rsakey' and verify the decrypted object is properly + # formatted. + public_decrypted, private_decrypted = \ + pycrypto.create_rsa_public_and_private_from_encrypted_pem(pem_rsakey, + passphrase) + self.assertEqual(None, + tuf.formats.PEMRSA_SCHEMA.check_match(public_decrypted), + FORMAT_ERROR_MSG) + + self.assertEqual(None, + tuf.formats.PEMRSA_SCHEMA.check_match(private_decrypted), + FORMAT_ERROR_MSG) + + # Does 'public_decrypted' and 'private_decrypted' match the originals? + self.assertEqual(public_rsa, public_decrypted) + self.assertEqual(private_rsa, private_decrypted) + + # Attempt decryption of 'pem_rsakey' using an incorrect passphrase. + self.assertRaises(tuf.CryptoError, + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + pem_rsakey, 'bad_pw') + + # Check for non-encrypted PEM strings. + # create_rsa_public_and_private_from_encrypted_pem() + # returns a tuple of tuf.formats.PEMRSA_SCHEMA objects if the PEM formatted + # string is not actually encrypted but still a valid PEM string. + pub, priv = pycrypto.create_rsa_public_and_private_from_encrypted_pem( + private_rsa, passphrase) + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pub), + FORMAT_ERROR_MSG) + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(priv), + FORMAT_ERROR_MSG) + + # Check for invalid arguments. + self.assertRaises(tuf.FormatError, + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + 123, passphrase) + self.assertRaises(tuf.FormatError, + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + pem_rsakey, ['pw']) + self.assertRaises(tuf.CryptoError, + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + 'invalid_pem', passphrase) + + + +# Run the unit tests. +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_quickstart.py b/tests/unit/test_quickstart.py index 00ddf44e..e1bca8c4 100755 --- a/tests/unit/test_quickstart.py +++ b/tests/unit/test_quickstart.py @@ -18,7 +18,6 @@ Given that all message prompts don't change - this will work pretty well for running quickstart without having to manually enter input to prompts every time you want to run quickstart. - """ import os diff --git a/tests/unit/test_roledb.py b/tests/unit/test_roledb.py index 0b0259b5..cb048854 100755 --- a/tests/unit/test_roledb.py +++ b/tests/unit/test_roledb.py @@ -13,7 +13,6 @@ Unit test for 'roledb.py'. - """ @@ -22,7 +21,7 @@ import tuf import tuf.formats -import tuf.rsa_key +import tuf.keys import tuf.roledb import tuf.log @@ -32,7 +31,7 @@ # Generate the three keys to use in our test cases. KEYS = [] for junk in range(3): - KEYS.append(tuf.rsa_key.generate(2048)) + KEYS.append(tuf.keys.generate_rsa_key(2048)) diff --git a/tests/unit/test_rsa_key.py b/tests/unit/test_rsa_key.py deleted file mode 100755 index 881ee1d7..00000000 --- a/tests/unit/test_rsa_key.py +++ /dev/null @@ -1,258 +0,0 @@ -""" - - test_rsa_key.py - - - Konstantin Andrianov - - - April 24, 2012. - - - See LICENSE for licensing information. - - - Test cases for rsa_key.py. - - - I'm using 'global rsakey_dict' - there is no harm in doing so since - in order to modify the global variable in any method, python requires - explicit indication to modify i.e. declaring 'global' in each method - that modifies the global variable 'rsakey_dict'. - -""" - -import unittest -import logging - -import tuf -import tuf.log -import tuf.formats -import tuf.rsa_key - -logger = logging.getLogger('tuf.test_rsa_key') - -RSA_KEY = tuf.rsa_key -FORMAT_ERROR_MSG = 'tuf.FormatError was raised! Check object\'s format.' -DATA = 'SOME DATA REQUIRING AUTHENTICITY.' - - -rsakey_dict = RSA_KEY.generate() -temp_key_info_vals = rsakey_dict.values() -temp_key_vals = rsakey_dict['keyval'].values() - - -class TestRsa_key(unittest.TestCase): - def setUp(self): - rsakey_dict['keytype']=temp_key_info_vals[0] - rsakey_dict['keyid']=temp_key_info_vals[1] - rsakey_dict['keyval']=temp_key_info_vals[2] - rsakey_dict['keyval']['public']=temp_key_vals[0] - rsakey_dict['keyval']['private']=temp_key_vals[1] - - - def test_generate(self): - _rsakey_dict = RSA_KEY.generate() - - # Check if the format of the object returned by generate() corresponds - # to RSAKEY_SCHEMA format. - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict), - FORMAT_ERROR_MSG) - - # Passing a bit value that is <2048 to generate() - should raise - # 'tuf.FormatError'. - self.assertRaises(tuf.FormatError, RSA_KEY.generate, 555) - - # Passing a string instead of integer for a bit value. - self.assertRaises(tuf.FormatError, RSA_KEY.generate, 'bits') - - # NOTE if random bit value >=2048 (not 4096) is passed generate(bits) - # does not raise any errors and returns a valid key. - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(RSA_KEY.generate(2048))) - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(RSA_KEY.generate(4096))) - - def test_create_in_metadata_format(self): - key_value = rsakey_dict['keyval'] - key_meta = RSA_KEY.create_in_metadata_format(key_value) - - # Check if the format of the object returned by this function corresponds - # to KEY_SCHEMA format. - self.assertEqual(None, - tuf.formats.KEY_SCHEMA.check_match(key_meta), - FORMAT_ERROR_MSG) - key_meta = RSA_KEY.create_in_metadata_format(key_value, private=True) - - # Check if the format of the object returned by this function corresponds - # to KEY_SCHEMA format. - self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta), - FORMAT_ERROR_MSG) - - # Supplying a 'bad' key_value. - del key_value['public'] - self.assertRaises(tuf.FormatError, RSA_KEY.create_in_metadata_format, - key_value) - - - def test_create_from_metadata_format(self): - # Reconfiguring rsakey_dict to conform to KEY_SCHEMA - # i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}} - #keyid = rsakey_dict['keyid'] - del rsakey_dict['keyid'] - - rsakey_dict_from_meta = RSA_KEY.create_from_metadata_format(rsakey_dict) - - # Check if the format of the object returned by this function corresponds - # to RSAKEY_SCHEMA format. - self.assertEqual(None, - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict_from_meta), - FORMAT_ERROR_MSG) - - # Supplying a wrong number of arguments. - self.assertRaises(TypeError, RSA_KEY.create_from_metadata_format) - args = (rsakey_dict, rsakey_dict) - self.assertRaises(TypeError, RSA_KEY.create_from_metadata_format, *args) - - # Supplying a malformed argument to the function - should get FormatError - del rsakey_dict['keyval'] - self.assertRaises(tuf.FormatError, RSA_KEY.create_from_metadata_format, - rsakey_dict) - - - def test_helper_get_keyid(self): - key_value = rsakey_dict['keyval'] - - # Check format of 'key_value'. - self.assertEqual(None, tuf.formats.KEYVAL_SCHEMA.check_match(key_value), - FORMAT_ERROR_MSG) - - keyid = RSA_KEY._get_keyid(key_value) - - # Check format of 'keyid' - the output of '_get_keyid()' function. - self.assertEqual(None, tuf.formats.KEYID_SCHEMA.check_match(keyid), - FORMAT_ERROR_MSG) - - - def test_createsignature(self): - # Creating a signature for 'DATA'. - signature = RSA_KEY.create_signature(rsakey_dict, DATA) - - # Check format of output. - self.assertEqual(None, - tuf.formats.SIGNATURE_SCHEMA.check_match(signature), - FORMAT_ERROR_MSG) - - # Removing private key from 'rsakey_dict' - should raise a TypeError. - rsakey_dict['keyval']['private'] = '' - - args = (rsakey_dict, DATA) - self.assertRaises(TypeError, RSA_KEY.create_signature, *args) - - # Supplying an incorrect number of arguments. - self.assertRaises(TypeError, RSA_KEY.create_signature) - - - def test_verify_signature(self): - # Creating a signature 'signature' of 'DATA' to be verified. - signature = RSA_KEY.create_signature(rsakey_dict, DATA) - - # Verifying the 'signature' of 'DATA'. - verified = RSA_KEY.verify_signature(rsakey_dict, signature, DATA) - self.assertTrue(verified, "Incorrect signature.") - - # Testing an invalid 'signature'. Same 'signature' is passed, with - # 'DATA' different than the original 'DATA' that was used - # in creating the 'signature'. Function should return 'False'. - - # Modifying 'DATA'. - _DATA = '1111'+DATA+'1111' - - # Verifying the 'signature' of modified '_DATA'. - verified = RSA_KEY.verify_signature(rsakey_dict, signature, _DATA) - self.assertFalse(verified, - 'Returned \'True\' on an incorrect signature.') - - # Modifying 'signature' to pass an incorrect method since only - # 'PyCrypto-PKCS#1 PSS' - # is accepted. - signature['method'] = 'Biff' - - args = (rsakey_dict, signature, DATA) - self.assertRaises(tuf.UnknownMethodError, RSA_KEY.verify_signature, *args) - - # Passing incorrect number of arguments. - self.assertRaises(TypeError,RSA_KEY.verify_signature) - - - def test_create_encrypted_pem(self): - passphrase = 'pw' - - # Check format of 'rsakey_dict'. - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict), - FORMAT_ERROR_MSG) - - # Check format of 'passphrase'. - self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), - FORMAT_ERROR_MSG) - - # Generate the encrypted PEM string of 'rsakey_dict'. - pem_rsakey = tuf.rsa_key.create_encrypted_pem(rsakey_dict, passphrase) - - # Check for invalid arguments. - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_encrypted_pem, 'Biff', passphrase) - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_encrypted_pem, rsakey_dict, ['pw']) - - - - def test_create_from_encrypted_pem(self): - passphrase = 'pw' - - # Check format of 'rsakey_dict'. - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict), - FORMAT_ERROR_MSG) - - # Check format of 'passphrase'. - self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), - FORMAT_ERROR_MSG) - - # Generate the encrypted PEM string of 'rsakey_dict'. - pem_rsakey = tuf.rsa_key.create_encrypted_pem(rsakey_dict, passphrase) - - # Decrypt 'pem_rsakey' and verify the decrypted object is properly - # formatted. - decrypted_rsakey = tuf.rsa_key.create_from_encrypted_pem(pem_rsakey, - passphrase) - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(decrypted_rsakey), - FORMAT_ERROR_MSG) - - # Does 'decrypted_rsakey' match the original 'rsakey_dict'. - self.assertEqual(rsakey_dict, decrypted_rsakey) - - # Attempt decryption of 'pem_rsakey' using an incorrect passphrase. - self.assertRaises(tuf.CryptoError, - tuf.rsa_key.create_from_encrypted_pem, pem_rsakey, - 'bad_pw') - # Check for non-encrypted PEM string. create_from_encrypted_pem()/PyCrypto - # returns a tuf.formats.RSAKEY_SCHEMA object if PEM formatted string is - # not actually encrypted but still a valid PEM string. - non_encrypted_private_key = rsakey_dict['keyval']['private'] - decrypted_non_encrypted = tuf.rsa_key.create_from_encrypted_pem( - non_encrypted_private_key, passphrase) - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match( - decrypted_non_encrypted), FORMAT_ERROR_MSG) - - # Check for invalid arguments. - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_from_encrypted_pem, 123, passphrase) - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_from_encrypted_pem, pem_rsakey, ['pw']) - self.assertRaises(tuf.CryptoError, - tuf.rsa_key.create_from_encrypted_pem, 'invalid_pem', - passphrase) - - - -# Run the unit tests. -if __name__ == '__main__': - unittest.main() diff --git a/tests/unit/test_sig.py b/tests/unit/test_sig.py index efdbe516..db8fe579 100755 --- a/tests/unit/test_sig.py +++ b/tests/unit/test_sig.py @@ -14,10 +14,8 @@ Test cases for for sig.py. - """ - import unittest import logging @@ -26,7 +24,7 @@ import tuf.formats import tuf.keydb import tuf.roledb -import tuf.rsa_key +import tuf.keys import tuf.sig logger = logging.getLogger('tuf.test_sig') @@ -34,7 +32,7 @@ # Setup the keys to use in our test cases. KEYS = [] for _ in range(3): - KEYS.append(tuf.rsa_key.generate(2048)) + KEYS.append(tuf.keys.generate_rsa_key(2048)) @@ -55,7 +53,7 @@ def test_get_signature_status_no_role(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) # No specific role we're considering. sig_status = tuf.sig.get_signature_status(signable, None) @@ -82,7 +80,7 @@ def test_get_signature_status_bad_sig(self): signable['signed'], KEYS[0])) signable['signed'] += 'signature no longer matches signed data' - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -112,7 +110,7 @@ def test_get_signature_status_unknown_method(self): signable['signed'], KEYS[0])) signable['signatures'][0]['method'] = 'fake-sig-method' - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -142,7 +140,7 @@ def test_get_signature_status_single_key(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -171,7 +169,7 @@ def test_get_signature_status_below_threshold(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], @@ -205,8 +203,8 @@ def test_get_signature_status_below_threshold_unrecognized_sigs(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[2])) - tuf.keydb.add_rsakey(KEYS[0]) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[0]) + tuf.keydb.add_key(KEYS[1]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], @@ -242,8 +240,8 @@ def test_get_signature_status_below_threshold_unauthorized_sigs(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[1])) - tuf.keydb.add_rsakey(KEYS[0]) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[0]) + tuf.keydb.add_key(KEYS[1]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], KEYS[2]['keyid']], threshold) @@ -278,7 +276,7 @@ def test_check_signatures_no_role(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) # No specific role we're considering. It's invalid to use the # function tuf.sig.verify() without a role specified because @@ -295,7 +293,7 @@ def test_verify_single_key(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -321,8 +319,8 @@ def test_verify_unrecognized_sig(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[2])) - tuf.keydb.add_rsakey(KEYS[0]) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[0]) + tuf.keydb.add_key(KEYS[1]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], KEYS[1]['keyid']], threshold) @@ -363,7 +361,7 @@ def test_may_need_new_keys(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[1]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[1]['keyid']], threshold) diff --git a/tuf/README.md b/tuf/README.md new file mode 100644 index 00000000..a1102983 --- /dev/null +++ b/tuf/README.md @@ -0,0 +1,312 @@ +## Create TUF Repository + +![Repo Tools Diagram 1](https://raw.github.com/SantiagoTorres/tuf/repository-tools/resources/images/TUF%20repository%20tools.png) +### Keys + +#### Create RSA Keys +```python +from tuf.libtuf import * + +# Generate and write the first of two root keys for the repository. +# The following function creates an RSA key pair, where the private key is saved to +# “path/to/root_key” and the public key to “path/to/root_key.pub”. +generate_and_write_rsa_keypair("path/to/root_key", bits=2048, password="password") + +# If the key length is unspecified, it defaults to 3072 bits. A length of less +# than 2048 bits prints an error mesage. A password may be supplied as an +# argument, otherwise a user prompt is presented. +generate_and_write_rsa_keypair("path/to/root_key2") +Enter a password for the RSA key: +Confirm: +``` +The following four files should now exist: + +1. root_key +2. root_key.pub +3. root_key2 +4. root_key2.pub + +### Import RSA Keys +```python +from tuf.libtuf import * + +# Import an existing public key. +public_root_key = import_rsa_publickey_from_file("path/to/root_key.pub") + +# Import an existing private key. Importing a private key requires a password, whereas +# importing a public key does not. +private_root_key = import_rsa_privatekey_from_file("path/to/root_key") +Enter a password for the RSA key: +Confirm: +``` +At the time of importing the private RSA, a tuf.CryptoError can be thrown if +the key/password is invalid + +### Create a new Repository + +#### Create Root +```python +# Continuing from the previous section... + +# Create a new Repository object that holds the file path to the repository and the four +# top-level role objects (Root, Targets, Release, Timestamp). Metadata files are created when +# repository.write() is called. The repository directory is created if it does not exist. +repository = create_new_repository("path/to/repository/") + +# The Repository instance, ‘repository’, initially contains top-level Metadata objects. +# Add one of the public keys, created in the previous section, to the root role. Metadata is +# considered valid if it is signed by the public key’s corresponding private key. +repository.root.add_key(public_root_key) + +# Role keys (i.e., keyid) may be queried. Other attributes include: signing_keys, version, +# signatures, expiration, threshold, and compressions. +repository.root.keys +[u'b23514431a53676595922e955c2d547293da4a7917e3ca243a175e72bbf718df'] + +# Add a second public key to the root role. Although previously generated and saved to a file, +# the second public key must be imported before it can added to a role. +public_root_key2 = import_rsa_publickey_from_file("path/to/root_key2.pub") +repository.root.add_key(public_root_key2) + +# Threshold for each role defaults to 1. Users may change the threshold value, but libtuf.py +# validates thresholds and signatures and warns users. Set the threshold of the root role to 2, +# which means the root metadata file is considered valid if it contains at least 2 valid +# signatures. +repository.root.threshold = 2 +private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2", password="pw") + +# Load the root signing keys to the repository, which write() uses to sign the root metadata. +# The load_signing_key() method SHOULD warn when the key is NOT explicitly allowed to +# sign for it. +repository.root.load_signing_key(private_root_key) +repository.root_load_signing_key(private_root_key2) + +# Print the number of valid signatures and public & private keys of the repository's metadata. +repository.status() +'root' role contains 2 / 2 signatures. +'targets' role contains 0 / 1 public keys. + +try: + repository.write() +# An exception is raised here by write() because the other top-level roles (targets, release, +# and timestamp) have not been configured with keys. +except tuf.Error, e: + print e +Not enough signatures for 'path/to/repository/metadata.staged/root.txt' + +# In the next section, update the other top-level roles and create a repository with valid metadata. +``` + +#### Create Timestamp, Release, Targets + +```python +# Continuing from the previous section . . . + +# Generate keys for the remaining top-level roles. The root keys have been set above. +# The password argument may be omitted if a password prompt is needed. +generate_and_write_rsa_keypair("path/to/targets_key", password="pw") +generate_and_write_rsa_keypair("path/to/release_key", password="pw") +generate_and_write_rsa_keypair("path/to/timestamp_key", password="pw") + +# Add the public keys of the remaining top-level roles. +repository.targets.add_key(import_rsa_publickey_from_file("path/to/targets_key.pub")) +repository.release.add_key(import_rsa_publickey_from_file("path/to/release_key.pub")) +repository.timestamp.add_key(import_rsa_publickey_from_file("path/to/timestamp_key.pub")) + +# Import the signing keys of the remaining top-level roles. Prompt for passwords. +private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key") +Enter a password for the RSA key: +Confirm: +private_release_key = import_rsa_privatekey_from_file("path/to/release_key") +Enter a password for the RSA key: +Confirm: +private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key") +Enter a password for the RSA key: +Confirm: + +# Load the signing keys of the remaining roles so that valid signatures are generated when +# repository.write() is called. +repository.targets.load_signing_key(private_targets_key) +repository.release.load_signing_key(private_release_key) +repository.timestamp.load_signing_key(private_timestamp_key) + +# Optionally set the expiration date of the timestamp role. By default, roles are set to expire +# as follows: root(1 year), targets(3 months), release(1 week), timestamp(1 day). +repository.timestamp.expiration = "2014-10-28 12:08:00" + +# Metadata files may also be compressed. Only "gz" is currently supported. +repository.targets.compressions = ["gz"] +repository.release.compressions = ["gz"] + +# Write all metadata to “path/to/repository/metadata/” +# The common case is to crawl the filesystem for all roles in +# “path/to/repository/metadata/targets/”. +repository.write() +``` + +### Targets + +#### Add Target Files +```Bash +# Create and save target files to the targets directory of the repository. +$ cd path/to/repository/targets/ +$ echo 'file1' > file1.txt +$ echo 'file2' > file2.txt +$ echo 'file3' > file3.txt +$ mkdir django; echo 'file4' > django/file4.txt +``` + +```python +# Load the repository created in the previous section. This repository contains metadata for +# the top-level roles, but no targets. +repository = load_repository("path/to/repository/") + +# Get a list of file paths in a directory, even those in sub-directories. +# This must be relative to an existing directory in the repository, otherwise throw an +# error. +list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/", + recursive_walk=False, followlinks=True) + +# Add the list of target paths to the metadata of the Targets role. Any target file paths +# that may already exist are NOT replaced. add_targets() does not create or move target files. +repository.targets.add_targets(list_of_targets) + +# Individual target files may also be added. +repository.targets.add_target("path/to/repository/targets/file3.txt") + +# The private key of the updated targets metadata must be loaded before it can be signed and +# written (Note the load_repository() call above). +private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key") +Enter a password for the RSA key: +Confirm: +repository.targets.load_signing_key(private_targets_key) + +# Due to the load_repository(), we must also load the private keys of the other top-level roles +# to generate a valid set of metadata. +private_root_key = import_rsa_privatekey_from_file("path/to/root_key") +Enter a password for the RSA key: +Confirm: +private_root_key2 = import_rsa_privatekey_from_file("path/to/root_key2") +Enter a password for the RSA key: +Confirm: +private_release_key = import_rsa_privatekey_from_file("path/to/release_key") +Enter a password for the RSA key: +Confirm: +private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key") +Enter a password for the RSA key: +Confirm: + +repository.root.load_signing_key(private_root_key) +repository.root.load_signing_key(private_root_key2) +repository.release.load_signing_key(private_release_key) +repository.timestamp.load_signing_key(private_timestamp_key) + +# Generate new versions of all the top-level metadata and increment version numbers. +repository.write() +``` + +#### Remove Target Files +```python +# Continuing from the previous section . . . + +# Remove a target file listed in the “targets” metadata. The target file is not actually deleted +# from the file system. +repository.targets.remove_target("path/to/repository/targets/file3.txt") + +# repository.write() creates any new metadata files, updates those that have changed, and any that +# need updating to make a new “release” (new release.txt and timestamp.txt). +repository.write() +``` + +### Delegations +```python +# Continuing from the previous section . . . + +# Generate a key for a new delegated role named “unclaimed”. +generate_and_write_rsa_keypair("path/to/unclaimed_key", bits=2048, password="pw") +public_unclaimed_key = import_rsa_publickey_from_file("path/to/unclaimed_key.pub") + +# Make a delegation from “targets” to “targets/unclaimed”, for all targets in “list_of_targets”. +# The delegated role’s full name is not required. +# delegated(rolename, list_of_public_keys, list_of_file_paths, threshold, restricted_paths) +repository.targets.delegate("unclaimed", [public_unclaimed_key], []) + +# Load the private key of “targets/unclaimed” so that signatures are added and valid metadata +# is created. +private_unclaimed_key = import_rsa_privatekey_from_file("path/to/unclaimed_key") +Enter a password for the RSA key: +Confirm: +repository.targets.unclaimed.load_signing_key(private_unclaimed_key) + +# Update an attribute of the unclaimed role and add a target file. +repository.targets.unclaimed.version = 2 + +# Delegations may also be nested. Create the delegated role "targets/unclaimed/django", +# where it initially contains zero targets and future targets are restricted to a +# particular directory. +repository.targets.unclaimed.delegate("django", [public_unclaimed_key], [], + restricted_paths=["path/to/repository/targets/django/"]) +repository.targets.unclaimed.django.load_signing_key(private_unclaimed_key) +repository.targets.unclaimed.django.add_target("path/to/repository/targets/django/file4.txt") +repository.targets.unclaimed.django.compressions = ["gz"] + +# Write the metadata of "targets/unclaimed", targets/unclaimed/django", targets, release, +# and timestamp. +repository.write() +``` + +#### Revoke Delegated Role +```python +# Continuing from the previous section . . . + +# Create a delegated role that will be revoked in the next step. +repository.targets.unclaimed.delegate("flask", [public_unclaimed_key], []) + +# Revoke “targets/unclaimed/flask” and write the metadata of all remaining roles. +repository.targets.unclaimed.revoke("flask") +repository.write() +``` + +```bash +# Copy the staged metadata directory changes to the live repository. +$ cp -r "path/to/repository/metadata.staged" "path/to/repository/metadata" +``` + +## Client Setup and Repository Trial + +### Using TUF Within an Example Client Updater +```python +# The following function creates a directory structure that a client +# downloading new software using tuf (via tuf/client/updater.py) will expect. +# The root.txt metadata file must exist, and also the directories that hold the metadata files +# downloaded from a repository. Software updaters integrating with TUF may use this +# directory to store TUF updates saved on the client side. create_tuf_client_directory() +# moves metadata files “path/to/repository/” to “path/to/client/”. The repository in +# “path/to/repository/” is the repository created in the “Create TUF Repository” section. +create_tuf_client_directory("path/to/repository/", "path/to/client/") +``` + +#### Test TUF Locally +```Bash +# Run the local TUF repository server. +$ cd “path/to/repository/”; python -m SimpleHTTPServer 8001 + +# Retrieve targets from the TUF repository and save them to "path/to/client/". The +# basic_client.py module is available in "tuf/client/". +# In a different command-line prompt . . . +$ cd "path/to/client/" +$ ls +metadata/ + +$ python basic_client.py --repo http://localhost:8001 + +$ ls . targets/ targets/django/ +.: +metadata targets tuf.log + +targets/: +django file1.txt file2.txt + +targets/django/: +file4.txt +``` diff --git a/tuf/__init__.py b/tuf/__init__.py index 70324314..9fc02fdf 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -18,7 +18,6 @@ The names chosen for TUF Exception classes should end in 'Error' except where there is a good reason not to, and provide that reason in those cases. - """ import urlparse @@ -117,6 +116,14 @@ class RepositoryError(Error): +class InsufficientKeysError(Error): + """Indicate that metadata role lacks a threshold of pubic or private keys.""" + pass + + + + + class ForbiddenTargetError(RepositoryError): """Indicate that a role signed for a target that it was not delegated to.""" pass diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 3b875fb3..22c80262 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -97,7 +97,6 @@ # The updated target files are saved locally to 'destination_directory'. for target in updated_targets: updater.download_target(target, destination_directory) - """ import errno @@ -111,6 +110,7 @@ import tuf.download import tuf.formats import tuf.hash +import tuf.keys import tuf.keydb import tuf.log import tuf.mirrors @@ -194,7 +194,6 @@ class Updater(object): Any files located in 'destination_directory' that were previously served by the repository but have since been removed, can be deleted from disk by the client by calling this method. - """ def __init__(self, updater_name, repository_mirrors): @@ -251,7 +250,6 @@ def __init__(self, updater_name, repository_mirrors): None. - """ # Do the arguments have the correct format? @@ -327,7 +325,6 @@ def __init__(self, updater_name, repository_mirrors): def __str__(self): """ The string representation of an Updater object. - """ return self.name @@ -370,7 +367,6 @@ def _load_metadata_from_file(self, metadata_set, metadata_role): None. - """ # Ensure we have a valid metadata set. @@ -435,7 +431,6 @@ def _rebuild_key_and_role_db(self): None. - """ # Clobbering this means all delegated metadata files are rendered outdated @@ -475,7 +470,6 @@ def _import_delegations(self, parent_role): None. - """ current_parent_metadata = self.metadata['current'][parent_role] @@ -492,13 +486,13 @@ def _import_delegations(self, parent_role): # Iterate through the keys of the delegated roles of 'parent_role' # and load them. for keyid, keyinfo in keys_info.items(): - if keyinfo['keytype'] == 'rsa': - rsa_key = tuf.rsa_key.create_from_metadata_format(keyinfo) + if keyinfo['keytype'] in ['rsa', 'ed25519']: + key = tuf.keys.format_metadata_to_key(keyinfo) # We specify the keyid to ensure that it's the correct keyid # for the key. try: - tuf.keydb.add_rsakey(rsa_key, keyid) + tuf.keydb.add_key(key, keyid) except tuf.KeyAlreadyExistsError: pass except (tuf.FormatError, tuf.Error), e: @@ -556,7 +550,6 @@ def refresh(self): None. - """ # The timestamp role does not have signed metadata about it; otherwise we @@ -617,7 +610,6 @@ def __check_hashes(self, file_object, trusted_hashes): None. - """ # Verify each trusted hash of 'trusted_hashes'. Raise exception if @@ -700,7 +692,6 @@ def __soft_check_compressed_file_length(self, file_object, None. - """ observed_length = file_object.get_compressed_length() @@ -745,7 +736,6 @@ def get_target_file(self, target_filepath, compressed_file_length, A tuf.util.TempFile file-like object containing the target. - """ def verify_uncompressed_target_file(target_file_object): @@ -801,7 +791,6 @@ def __verify_uncompressed_metadata_file(self, metadata_file_object, None. - """ metadata = metadata_file_object.read() @@ -871,7 +860,6 @@ def unsafely_get_metadata_file(self, metadata_role, metadata_filepath, A tuf.util.TempFile file-like object containing the metadata. - """ def unsafely_verify_uncompressed_metadata_file(metadata_file_object): @@ -926,7 +914,6 @@ def safely_get_metadata_file(self, metadata_role, metadata_filepath, A tuf.util.TempFile file-like object containing the metadata. - """ def safely_verify_uncompressed_metadata_file(metadata_file_object): @@ -992,7 +979,6 @@ def __get_file(self, filepath, verify_uncompressed_file, file_type, A tuf.util.TempFile file-like object containing the metadata or target. - """ file_mirrors = tuf.mirrors.get_list_of_mirrors(file_type, filepath, @@ -1085,7 +1071,6 @@ def _update_metadata(self, metadata_role, fileinfo, compression=None): None. - """ # Construct the metadata filename as expected by the download/mirror modules. @@ -1234,7 +1219,6 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas None. - """ uncompressed_metadata_filename = metadata_role + '.txt' @@ -1373,7 +1357,6 @@ def _ensure_all_targets_allowed(self, metadata_role, metadata_object): None. - """ # Return if 'metadata_role' is 'targets'. 'targets' is not @@ -1536,7 +1519,6 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): Boolean. True if the fileinfo has changed, false otherwise. - """ # If there is no fileinfo currently stored for 'metadata_filename', @@ -1595,7 +1577,6 @@ def _update_fileinfo(self, metadata_filename): None. - """ # In case we delayed loading the metadata and didn't do it in @@ -1640,7 +1621,6 @@ def _move_current_to_previous(self, metadata_role): None. - """ # Get the 'current' and 'previous' full file paths for 'metadata_role' @@ -1685,7 +1665,6 @@ def _delete_metadata(self, metadata_role): None. - """ # The root metadata role is never deleted without a replacement. @@ -1723,7 +1702,6 @@ def _ensure_not_expired(self, metadata_role): None. - """ # Construct the full metadata filename and the location of its @@ -1779,7 +1757,6 @@ def all_targets(self): A list of targets, conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ # Load the most up-to-date targets of the 'targets' role and all @@ -1834,7 +1811,6 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals None. - """ roles_to_update = [] @@ -1846,7 +1822,10 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals if metadata_path == rolename + '.txt': roles_to_update.append(metadata_path[:-len('.txt')]) elif include_delegations and metadata_path.startswith(role_prefix): - roles_to_update.append(metadata_path[:-len('.txt')]) + # Add delegated roles. Skip roles names containing compression + # extensions. + if metadata_path.endswith('.txt'): + roles_to_update.append(metadata_path[:-len('.txt')]) # Remove the 'targets' role because it gets updated when the targets.txt # file is updated in _update_metadata_if_changed('targets'). @@ -1886,7 +1865,6 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals def refresh_targets_metadata_chain(self, rolename): """ Proof-of-concept. - """ # List of parent roles to update. @@ -1993,7 +1971,6 @@ def _targets_of_role(self, rolename, targets=None, skip_refresh=False): A list of dict objects containing the target information of all the targets of 'rolename'. Conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ if targets is None: @@ -2063,7 +2040,6 @@ def targets_of_role(self, rolename='targets'): A list of targets, conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ # Does 'rolename' have the correct format? @@ -2104,7 +2080,6 @@ def target(self, target_filepath): The target information for 'target_filepath', conformant to 'tuf.formats.TARGETFILE_SCHEMA'. - """ # Does 'target_filepath' have the correct format? @@ -2152,7 +2127,6 @@ def _preorder_depth_first_walk(self, target_filepath): The target information for 'target_filepath', conformant to 'tuf.formats.TARGETFILE_SCHEMA'. - """ target = None @@ -2234,7 +2208,6 @@ def _get_target_from_targets_role(self, role_name, targets, target_filepath): The target information for 'target_filepath', conformant to 'tuf.formats.TARGETFILE_SCHEMA'. - """ target = None @@ -2295,7 +2268,6 @@ def _visit_child_role(self, child_role, target_filepath): 'target_filepath', then we return the role name of 'child_role'. Otherwise, we return None. - """ child_role_name = child_role['name'] @@ -2367,7 +2339,6 @@ def _get_target_hash(self, target_filepath, hash_function='sha256'): The hash of 'target_filepath'. - """ # Calculate the hash of the filepath to determine which bin to find the @@ -2417,7 +2388,6 @@ def remove_obsolete_targets(self, destination_directory): None. - """ # Does 'destination_directory' have the correct format? @@ -2480,7 +2450,6 @@ def updated_targets(self, targets, destination_directory): A list of targets, conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ # Do the arguments have the correct format? @@ -2545,7 +2514,6 @@ def download_target(self, target, destination_directory): None. - """ # Do the arguments have the correct format? diff --git a/tuf/conf.py b/tuf/conf.py index 249ab870..a48f1b75 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -13,10 +13,8 @@ A central location for TUF configuration settings. - """ - # Set a directory that should be used for all temporary files. If this # is None, then the system default will be used. The system default # will also be used if a directory path set here is invalid or @@ -65,3 +63,11 @@ # iteration setting used by the old '.key'. # https://en.wikipedia.org/wiki/PBKDF2 PBKDF2_ITERATIONS = 100000 + +# The user client may set the cryptography library used by The Update Framework +# updater, or the software updater integrating TUF. The repository tools may +# Supported RSA cryptography libraries: ['pycrypto'] +RSA_CRYPTO_LIBRARY = 'pycrypto' + +# Supported ed25519 cryptography libraries: ['pynacl', 'ed25519'] +ED25519_CRYPTO_LIBRARY = 'ed25519' diff --git a/tuf/ed25519_key.py b/tuf/ed25519_key.py deleted file mode 100755 index f2583f76..00000000 --- a/tuf/ed25519_key.py +++ /dev/null @@ -1,624 +0,0 @@ -""" - - ed25519_key.py - - - Vladimir Diaz - - - September 24, 2013. - - - See LICENSE for licensing information. - - - The goal of this module is to support ed25519 signatures. ed25519 is an - elliptic-curve public key signature scheme, its main strength being small - signatures (64 bytes) and small public keys (32 bytes). - http://ed25519.cr.yp.to/ - - 'tuf/ed25519_key.py' calls 'ed25519/ed25519.py', which is the pure Python - implementation of ed25519 provided by the author: - http://ed25519.cr.yp.to/software.html - Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which - provides Python bindings to the NaCl library and is much faster than the pure - python implementation. PyNaCl relies on the C library, libsodium. - - https://github.com/dstufft/pynacl - https://github.com/jedisct1/libsodium - http://nacl.cr.yp.to/ - - The ed25519-related functions included here are generate(), create_signature() - and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used - by ed25519_key.py generate the actual ed25519 keys and the functions listed - above can be viewed as an easy-to-use public interface. Additional functions - contained here include format_keyval_to_metadata() and - format_metadata_to_key(). These last two functions produce or use - ed25519 keys compatible with the key structures listed in TUF Metadata files. - The generate() function returns a dictionary containing all the information - needed of ed25519 keys, such as public/private keys and a keyID identifier. - create_signature() and verify_signature() are supplemental functions used for - generating ed25519 signatures and verifying them. - - Key IDs are used as identifiers for keys (e.g., RSA key). They are the - hexadecimal representation of the hash of key object (specifically, the key - object containing only the public key). Review 'ed25519_key.py' and the - '_get_keyid()' function to see precisely how keyids are generated. One may - get the keyid of a key object by simply accessing the dictionary's 'keyid' - key (i.e., ed25519_key_dict['keyid']). - """ - -# Help with Python 3 compatability, where the print statement is a function, an -# implicit relative import is invalid, and the '/' operator performs true -# division. Example: print 'hello world' raises a 'SyntaxError' exception. -from __future__ import print_function -from __future__ import absolute_import -from __future__ import division - -# Required for hexadecimal conversions. Signatures and public/private keys are -# hexlified. -import binascii - -# Generate OS-specific randomness (os.urandom) suitable for cryptographic use. -# http://docs.python.org/2/library/os.html#miscellaneous-functions -import os - -import tuf - -# Import the python implementation of the ed25519 algorithm that is provided by -# the author. Note: This implementation is very slow and does not include -# protection against side-channel attacks according to the author. Verifying -# signatures can take approximately 9 seconds on an intel core 2 duo @ -# 2.2 ghz x 2). Optionally, the PyNaCl module may be used to speed up ed25519 -# cryptographic operations. -# http://ed25519.cr.yp.to/software.html -# Try to import PyNaCl. The functions found in this module provide the option -# of using PyNaCl over the slower implementation of ed25519. -try: - import nacl.signing - import nacl.encoding -except (ImportError, IOError): - message = 'The PyNacl library and/or its dependencies cannot be imported.' - raise tuf.UnsupportedLibraryError(message) - -# The pure Python implementation of ed25519. -import ed25519.ed25519 - -# Digest objects needed to generate hashes. -import tuf.hash - -# Perform object format-checking. -import tuf.formats - -# The default hash algorithm to use when generating KeyIDs. -_KEY_ID_HASH_ALGORITHM = 'sha256' - -# Supported ed25519 signing methods. 'ed25519-python' is the pure Python -# implementation signing method. 'ed25519-pynacl' (i.e., 'nacl' module) is the -# (libsodium+Python bindings) implementation signing method. -_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519-python', 'ed25519-pynacl'] - - -def generate(use_pynacl=False): - """ - - Generate an ed25519 seed key ('sk') and public key ('pk'). - In addition, a keyid used as an identifier for ed25519 keys is generated. - The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and has the - form: - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are strings. An ed25519 seed key is a random - 32-byte value and public key 32 bytes, although both are hexlified to 64 - bytes. - - >>> ed25519_key = generate() - >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key) - True - >>> len(ed25519_key['keyval']['public']) - 64 - >>> len(ed25519_key['keyval']['private']) - 64 - >>> ed25519_key_pynacl = generate(use_pynacl=True) - >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_pynacl) - True - >>> len(ed25519_key_pynacl['keyval']['public']) - 64 - >>> len(ed25519_key_pynacl['keyval']['private']) - 64 - - - use_pynacl: - True, if the ed25519 keys should be generated with PyNaCl. False, if the - keys should be generated with the pure Python implementation of ed25519 - (much slower). - - - NotImplementedError, if a randomness source is not found. - - - The ed25519 keys are generated by first creating a random 32-byte value - 'sk' with os.urandom() and then calling ed25519's ed25519.25519.publickey(sk) - or PyNaCl's nacl.signing.SigningKey(). - - - A dictionary containing the ed25519 keys and other identifying information. - Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. - """ - - # Begin building the ed25519 key dictionary. - ed25519_key_dict = {} - keytype = 'ed25519' - - # Generate ed25519's seed key by calling os.urandom(). The random bytes - # returned should be suitable for cryptographic use and is OS-specific. - # Raise 'NotImplementedError' if a randomness source is not found. - # ed25519 seed keys are fixed at 32 bytes (256-bit keys). - # http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/ - seed = os.urandom(32) - public = None - - if use_pynacl: - # Generate the public key. PyNaCl (i.e., 'nacl' module) performs - # the actual key generation. - nacl_key = nacl.signing.SigningKey(seed) - public = str(nacl_key.verify_key) - - # Use the pure Python implementation of ed25519. - else: - public = ed25519.ed25519.publickey(seed) - - # Generate the keyid for the ed25519 key dict. 'key_value' corresponds to the - # 'keyval' entry of the 'ED25519KEY_SCHEMA' dictionary. The seed (private) - # key information is not included in the generation of the 'keyid' identifier. - key_value = {'public': binascii.hexlify(public), - 'private': ''} - keyid = _get_keyid(key_value) - - # Build the 'ed25519_key_dict' dictionary. Update 'key_value' with the - # ed25519 seed key prior to adding 'key_value' to 'ed25519_key_dict'. - key_value['private'] = binascii.hexlify(seed) - - ed25519_key_dict['keytype'] = keytype - ed25519_key_dict['keyid'] = keyid - ed25519_key_dict['keyval'] = key_value - - return ed25519_key_dict - - - - - -def format_keyval_to_metadata(key_value, private=False): - """ - - Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. - If 'private' is True, include the private key. The dictionary - returned has the form: - {'keytype': 'ed25519', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - or if 'private' is False: - - {'keytype': 'ed25519', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': ''}} - - The private and public keys are 32 bytes, although hexlified to 64 bytes. - - ed25519 keys are stored in Metadata files (e.g., root.txt) in the format - returned by this function. - - >>> ed25519_key = generate() - >>> key_val = ed25519_key['keyval'] - >>> ed25519_metadata = format_keyval_to_metadata(key_val, private=True) - >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) - True - - - key_value: - A dictionary containing a seed and public ed25519 key. - 'key_value' is of the form: - - {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'} - - conformat to 'tuf.formats.KEYVAL_SCHEMA'. - - private: - Indicates if the private key should be included in the - returned dictionary. - - - tuf.FormatError, if 'key_value' does not conform to - 'tuf.formats.KEYVAL_SCHEMA'. - - - None. - - - A 'KEY_SCHEMA' dictionary. - """ - - # Does 'key_value' have the correct format? - # This check will ensure 'key_value' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.KEYVAL_SCHEMA.check_match(key_value) - - if private is True and len(key_value['private']): - return {'keytype': 'ed25519', 'keyval': key_value} - else: - public_key_value = {'public': key_value['public'], 'private': ''} - return {'keytype': 'ed25519', 'keyval': public_key_value} - - - - - -def format_metadata_to_key(key_metadata): - """ - - Construct an ed25519 key dictionary (i.e., tuf.formats.ED25519KEY_SCHEMA) - from 'key_metadata'. The dict returned by this function has the exact - format as the dict returned by generate(). It is of the form: - - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are 32-byte strings, although hexlified to 64 - bytes. - - ed25519 key dictionaries in 'ED25519KEY_SCHEMA' format should be used by - modules storing a collection of keys, such as a keydb keystore. - ed25519 keys as stored in metadata files use a different format, so this - function should be called if an ed25519 key is extracted from one of these - metadata files and needs converting. Generate() creates an entirely - new key and returns it in the format appropriate for 'keydb.py' and - 'keystore.py'. - - >>> ed25519_key = generate() - >>> key_val = ed25519_key['keyval'] - >>> ed25519_metadata = format_keyval_to_metadata(key_val, private=True) - >>> ed25519_key_2 = format_metadata_to_key(ed25519_metadata) - >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) - True - >>> ed25519_key == ed25519_key_2 - True - - - key_metadata: - The ed25519 key dictionary as stored in Metadata files, conforming to - 'tuf.formats.KEY_SCHEMA'. It has the form: - - {'keytype': 'ed25519', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - - tuf.FormatError, if 'key_metadata' does not conform to - 'tuf.formats.KEY_SCHEMA'. - - - None. - - - A dictionary containing the ed25519 keys and other identifying information. - """ - - # Does 'key_metadata' have the correct format? - # This check will ensure 'key_metadata' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.KEY_SCHEMA.check_match(key_metadata) - - # Construct the dictionary to be returned. - ed25519_key_dict = {} - keytype = 'ed25519' - key_value = key_metadata['keyval'] - - # Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash - # The hash is in hexdigest form. _get_keyid() ensures the private key - # information is not included. - keyid = _get_keyid(key_value) - - # We now have all the required key values. Build 'ed25519_key_dict'. - ed25519_key_dict['keytype'] = keytype - ed25519_key_dict['keyid'] = keyid - ed25519_key_dict['keyval'] = key_value - - return ed25519_key_dict - - - - - -def _get_keyid(key_value): - """Return the keyid for 'key_value'.""" - - # 'keyid' will be generated from an object conformant to 'KEY_SCHEMA', - # which is the format Metadata files (e.g., root.txt) store keys. - # 'format_keyval_to_metadata()' returns the object needed by _get_keyid(). - ed25519_key_meta = format_keyval_to_metadata(key_value, private=False) - - # Convert the ed25519 key to JSON Canonical format suitable for adding - # to digest objects. - ed25519_key_update_data = tuf.formats.encode_canonical(ed25519_key_meta) - - # Create a digest object and call update(), using the JSON - # canonical format of 'ed25519_key_meta' as the update data. - digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) - digest_object.update(ed25519_key_update_data) - - # 'keyid' becomes the hexadecimal representation of the hash. - keyid = digest_object.hexdigest() - - return keyid - - - - - -def create_signature(ed25519_key_dict, data, use_pynacl=False): - """ - - Return a signature dictionary of the form: - {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'method': 'ed25519-python', - 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} - - Note: 'method' may also be 'ed25519-pynacl', if the signature was created - by the 'nacl' module. - - The signing process will use the public and seed key - ed25519_key_dict['keyval']['private'], - ed25519_key_dict['keyval']['public'] - - and 'data' to generate the signature. - - >>> ed25519_key_dict = generate() - >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(ed25519_key_dict, data) - >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) - True - >>> len(signature['sig']) - 128 - >>> signature_pynacl = create_signature(ed25519_key_dict, data, True) - >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature_pynacl) - True - >>> len(signature_pynacl['sig']) - 128 - - - ed25519_key_dict: - A dictionary containing the ed25519 keys and other identifying information. - 'ed25519_key_dict' has the form: - - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are 32-byte strings, although hexlified to 64 - bytes. - - data: - Data object used by create_signature() to generate the signature. - - use_pynacl: - True, if the ed25519 signature should be generated with PyNaCl. False, - if the signature should be generated with the pure Python implementation - of ed25519 (much slower). - - - TypeError, if a private key is not defined for 'ed25519_key_dict'. - - tuf.FormatError, if an incorrect format is found for 'ed25519_key_dict'. - - tuf.CryptoError, if a signature cannot be created. - - - ed25519.ed25519.signature() or nacl.signing.SigningKey.sign() called to - generate the actual signature. - - - A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. - ed25519 signatures are 64 bytes, however, the hexlified signature - (128 bytes) is stored in the dictionary returned. - """ - - # Does 'ed25519_key_dict' have the correct format? - # This check will ensure 'ed25519_key_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.ED25519KEY_SCHEMA.check_match(ed25519_key_dict) - - # Signing the 'data' object requires a seed and public key. - # 'ed25519.ed25519.py' generates the actual 64-byte signature in pure Python. - # nacl.signing.SigningKey.sign() generates the signature if 'use_pynacl' - # is True. - signature = {} - private_key = ed25519_key_dict['keyval']['private'] - public_key = ed25519_key_dict['keyval']['public'] - private_key = binascii.unhexlify(private_key) - public_key = binascii.unhexlify(public_key) - - keyid = ed25519_key_dict['keyid'] - method = None - sig = None - - # Verify the signature, but only if the private key has been set. The private - # key is a NULL string if unset. Although it may be clearer to explicit check - # that 'private_key' is not '', we can/should check for a value and not - # compare identities with the 'is' keyword. - if len(private_key): - if use_pynacl: - method = 'ed25519-pynacl' - try: - nacl_key = nacl.signing.SigningKey(private_key) - nacl_sig = nacl_key.sign(data) - sig = nacl_sig.signature - except (ValueError, nacl.signing.CryptoError): - message = 'An "ed25519-pynacl" signature could not be created.' - raise tuf.CryptoError(message) - - # Generate an "ed25519-python" (i.e., pure python implementation) signature. - else: - # ed25519.ed25519.signature() requires both the seed and public keys. - # It calculates the SHA512 of the seed key, which is 32 bytes. - method = 'ed25519-python' - try: - sig = ed25519.ed25519.signature(data, private_key, public_key) - except Exception, e: - message = 'An "ed25519-python" signature could not be generated.' - raise tuf.CryptoError(message) - - # Raise an exception since the private key is not defined. - else: - message = 'The required private key is not defined for "ed25519_key_dict".' - raise TypeError(message) - - # Build the signature dictionary to be returned. - # The hexadecimal representation of 'sig' is stored in the signature. - signature['keyid'] = keyid - signature['method'] = method - signature['sig'] = binascii.hexlify(sig) - - return signature - - - - - -def verify_signature(ed25519_key_dict, signature, data, use_pynacl=False): - """ - - Determine whether the seed key belonging to 'ed25519_key_dict' produced - 'signature'. verify_signature() will use the public key found in - 'ed25519_key_dict', the 'method' and 'sig' objects contained in 'signature', - and 'data' to complete the verification. Type-checking performed on both - 'ed25519_key_dict' and 'signature'. - - >>> ed25519_key_dict = generate() - >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(ed25519_key_dict, data) - >>> verify_signature(ed25519_key_dict, signature, data) - True - >>> verify_signature(ed25519_key_dict, signature, data, True) - True - >>> bad_data = 'The sly brown fox jumps over the lazy dog.' - >>> bad_signature = create_signature(ed25519_key_dict, bad_data) - >>> verify_signature(ed25519_key_dict, bad_signature, data, True) - False - - - ed25519_key_dict: - A dictionary containing the ed25519 keys and other identifying - information. 'ed25519_key_dict' has the form: - - {'keytype': 'ed25519', - 'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are 32-byte strings, although hexlified to - 64 bytes. - - signature: - The signature dictionary produced by tuf.ed25519_key.create_signature(). - 'signature' has the form: - - {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'method': 'ed25519-python', - 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} - - Conformant to 'tuf.formats.SIGNATURE_SCHEMA'. - - data: - Data object used by tuf.ed25519_key.create_signature() to generate - 'signature'. 'data' is needed here to verify the signature. - - use_pynacl: - True, if the ed25519 signature should be verified with PyNaCl. False, - if the signature should be verified with the pure Python implementation - of ed25519 (much slower). - - - tuf.UnknownMethodError. Raised if the signing method used by - 'signature' is not one supported by tuf.ed25519_key.create_signature(). - - tuf.FormatError. Raised if either 'ed25519_key_dict' - or 'signature' do not match their respective tuf.formats schema. - 'ed25519_key_dict' must conform to 'tuf.formats.ED25519KEY_SCHEMA'. - 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. - - - ed25519.ed25519.checkvalid() called to do the actual verification. - nacl.signing.VerifyKey.verify() called if 'use_pynacl' is True. - - - Boolean. True if the signature is valid, False otherwise. - """ - - # Does 'ed25519_key_dict' have the correct format? - # This check will ensure 'ed25519_key_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.ED25519KEY_SCHEMA.check_match(ed25519_key_dict) - - # Does 'signature' have the correct format? - tuf.formats.SIGNATURE_SCHEMA.check_match(signature) - - # Using the public key belonging to 'ed25519_key_dict' - # (i.e., ed25519_key_dict['keyval']['public']), verify whether 'signature' - # was produced by ed25519_key_dict's corresponding seed key - # ed25519_key_dict['keyval']['private']. Before returning the Boolean result, - # ensure 'ed25519-python' or 'ed25519-pynacl' was used as the signing method. - method = signature['method'] - sig = signature['sig'] - sig = binascii.unhexlify(sig) - public = ed25519_key_dict['keyval']['public'] - public = binascii.unhexlify(public) - valid_signature = False - - if method in _SUPPORTED_ED25519_SIGNING_METHODS: - if use_pynacl: - try: - nacl_verify_key = nacl.signing.VerifyKey(public) - nacl_message = nacl_verify_key.verify(data, sig) - if nacl_message == data: - valid_signature = True - except nacl.signing.BadSignatureError: - pass - - # Verify signature with 'ed25519-python' (i.e., pure Python implementation). - else: - try: - ed25519.ed25519.checkvalid(sig, data, public) - valid_signature = True - - # The pure Python implementation raises 'Exception' if 'signature' is - # invalid. - except Exception, e: - pass - else: - message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \ - 'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.' - raise tuf.UnknownMethodError(message) - - return valid_signature - - - -if __name__ == '__main__': - # The interactive sessions of the documentation strings can - # be tested by running 'ed25519_key.py' as a standalone module. - # python -B ed25519_key.py - import doctest - doctest.testmod() diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py new file mode 100755 index 00000000..7b1fdb27 --- /dev/null +++ b/tuf/ed25519_keys.py @@ -0,0 +1,408 @@ +""" + + ed25519_keys.py + + + Vladimir Diaz + + + September 24, 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to support ed25519 signatures. ed25519 is an + elliptic-curve public key signature scheme, its main strength being small + signatures (64 bytes) and small public keys (32 bytes). + http://ed25519.cr.yp.to/ + + 'tuf/ed25519_keys.py' calls 'ed25519/ed25519.py', which is the pure Python + implementation of ed25519 optimized for a faster runtime. + The Python reference implementation is concise, but very slow (verifying + signatures takes ~9 seconds on an Intel core 2 duo @ 2.2 ghz x 2). The + optimized version can verify signatures in ~2 seconds. + + http://ed25519.cr.yp.to/software.html + https://github.com/pyca/ed25519 + + Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which + is a Python binding to the NaCl library and is faster than the pure python + implementation. Verifying signatures can take approximately 0.0009 seconds. + PyNaCl relies on the libsodium C library. + + https://github.com/pyca/pynacl + https://github.com/jedisct1/libsodium + http://nacl.cr.yp.to/ + + The ed25519-related functions included here are generate(), create_signature() + and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used + by ed25519_keys.py generate the actual ed25519 keys and the functions listed + above can be viewed as an easy-to-use public interface. + """ + +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +# 'binascii' required for hexadecimal conversions. Signatures and +# public/private keys are hexlified. +import binascii + +# 'os' required to generate OS-specific randomness (os.urandom) suitable for +# cryptographic use. +# http://docs.python.org/2/library/os.html#miscellaneous-functions +import os + +# Import the python implementation of the ed25519 algorithm provided by pyca, +# which is an optimized version of the one provided by ed25519's authors. +# Note: The pure Python version do not include protection against side-channel +# attacks. Verifying signatures can take approximately 2 seconds on a intel +# core 2 duo @ 2.2 ghz x 2). Optionally, the PyNaCl module may be used to +# speed up ed25519 cryptographic operations. +# http://ed25519.cr.yp.to/software.html +# https://github.com/pyca/ed25519 +# https://github.com/pyca/pynacl +# +# PyNaCl's 'cffi' dependency may thrown an 'IOError' exception when +# importing 'nacl.signing'. +try: + import nacl.signing + import nacl.encoding +except (ImportError, IOError): + pass + +# The optimized pure Python implementation of ed25519 provided by TUF. If +# PyNaCl cannot be imported and an attempt to use is made in this module, a +# 'tuf.UnsupportedLibraryError' exception is raised. +import ed25519.ed25519 + +import tuf + +# Digest objects needed to generate hashes. +import tuf.hash + +# Perform object format-checking. +import tuf.formats + +# Supported ed25519 signing methods. 'ed25519-python' is the pure Python +# implementation signing method. 'ed25519-pynacl' (i.e., 'nacl' module) is the +# (libsodium+Python bindings) implementation signing method. +_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519-python', 'ed25519-pynacl'] + + +def generate_public_and_private(use_pynacl=False): + """ + + Generate a pair of ed25519 public and private keys. + The public and private keys returned conform to + 'tuf.formats.ED25519PULIC_SCHEMA' and 'tuf.formats.ED25519SEED_SCHEMA', + respectively, and have the form: + + '\xa2F\x99\xe0\x86\x80%\xc8\xee\x11\xb95T\xd9\...' + + An ed25519 seed key is a random 32-byte string. Public keys are also 32 + bytes. + + >>> public, private = generate_public_and_private(use_pynacl=False) + >>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public) + True + >>> tuf.formats.ED25519SEED_SCHEMA.matches(private) + True + >>> public, private = generate_public_and_private(use_pynacl=True) + >>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public) + True + >>> tuf.formats.ED25519SEED_SCHEMA.matches(private) + True + + + use_pynacl: + True, if the ed25519 keys should be generated with PyNaCl. False, if the + keys should be generated with the pure Python implementation of ed25519 + (slower). + + + tuf.FormatError, if 'use_pynacl' is not a Boolean. + + tuf.UnsupportedLibraryError, if the PyNaCl ('nacl') module is unavailable + and 'use_pynacl' is True. + + NotImplementedError, if a randomness source is not found by 'os.urandom'. + + + The ed25519 keys are generated by first creating a random 32-byte seed + with os.urandom() and then calling ed25519's + ed25519.25519.publickey(seed) or PyNaCl's nacl.signing.SigningKey(). + + + A (public, private) tuple that conform to 'tuf.formats.ED25519PUBLIC_SCHEMA' + and 'tuf.formats.ED25519SEED_SCHEMA', respectively. + """ + + # Does 'use_pynacl' have the correct format? + # This check will ensure 'use_pynacl' conforms to 'tuf.formats.TOGGLE_SCHEMA'. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) + + # Generate ed25519's seed key by calling os.urandom(). The random bytes + # returned should be suitable for cryptographic use and is OS-specific. + # Raise 'NotImplementedError' if a randomness source is not found. + # ed25519 seed keys are fixed at 32 bytes (256-bit keys). + # http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/ + seed = os.urandom(32) + public = None + + if use_pynacl: + # Generate the public key. PyNaCl (i.e., 'nacl' module) performs + # the actual key generation. + try: + nacl_key = nacl.signing.SigningKey(seed) + public = str(nacl_key.verify_key) + except NameError: + message = 'The PyNaCl library and/or its dependencies unavailable.' + raise tuf.UnsupportedLibraryError(message) + + # Use the pure Python implementation of ed25519. + else: + public = ed25519.ed25519.publickey(seed) + + return public, seed + + + + + +def create_signature(public_key, private_key, data, use_pynacl=False): + """ + + Return a (signature, method) tuple, where the method is either: + 'ed25519-python' if the signature is generated by the pure python + implemenation, or 'ed25519-pynacl' if generated by 'nacl'. + signature conforms to 'tuf.formats.ED25519SIGNATURE_SCHEMA', and has the + form: + + '\xae\xd7\x9f\xaf\x95{bP\x9e\xa8YO Z\x86\x9d...' + + A signature is a 64-byte string. + + >>> public, private = generate_public_and_private(use_pynacl=False) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = \ + create_signature(public, private, data, use_pynacl=False) + >>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature) + True + >>> method == 'ed25519-python' + True + >>> signature, method = \ + create_signature(public, private, data, use_pynacl=True) + >>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature) + True + >>> method == 'ed25519-pynacl' + True + + + public: + The ed25519 public key, which is a 32-byte string. + + private: + The ed25519 private key, which is a 32-byte string. + + data: + Data object used by create_signature() to generate the signature. + + use_pynacl: + True, if the ed25519 signature should be generated with PyNaCl. False, + if the signature should be generated with the pure Python implementation + of ed25519 (much slower). + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.CryptoError, if a signature cannot be created. + + + ed25519.ed25519.signature() or nacl.signing.SigningKey.sign() called to + generate the actual signature. + + + A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. + ed25519 signatures are 64 bytes, however, the hexlified signature is + stored in the dictionary returned. + """ + + # Does 'public_key' have the correct format? + # This check will ensure 'public_key' conforms to + # 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key) + + # Is 'private_key' properly formatted? + tuf.formats.ED25519SEED_SCHEMA.check_match(private_key) + + # Is 'use_pynacl' properly formatted? + tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) + + # Signing the 'data' object requires a seed and public key. + # 'ed25519.ed25519.py' generates the actual 64-byte signature in pure Python. + # nacl.signing.SigningKey.sign() generates the signature if 'use_pynacl' + # is True. + public = public_key + private = private_key + + method = None + signature = None + + # The private and public keys have been validated above by 'tuf.formats' and + # should be 32-byte strings. + if use_pynacl: + method = 'ed25519-pynacl' + try: + nacl_key = nacl.signing.SigningKey(private) + nacl_sig = nacl_key.sign(data) + signature = nacl_sig.signature + + except NameError: + message = 'The PyNaCl library and/or its dependencies unavailable.' + raise tuf.UnsupportedLibraryError(message) + + except (ValueError, nacl.signing.CryptoError): + message = 'An "ed25519-pynacl" signature could not be created.' + raise tuf.CryptoError(message) + + # Generate an "ed25519-python" (i.e., pure python implementation) signature. + else: + # ed25519.ed25519.signature() requires both the seed and public keys. + # It calculates the SHA512 of the seed key, which is 32 bytes. + method = 'ed25519-python' + try: + signature = ed25519.ed25519.signature(data, private, public) + + # 'Exception' raised by ed25519.py for any exception that may occur. + except Exception, e: + message = 'An "ed25519-python" signature could not be generated.' + raise tuf.CryptoError(message) + + return signature, method + + + + + +def verify_signature(public_key, method, signature, data, use_pynacl=False): + """ + + Determine whether the private key corresponding to 'public_key' produced + 'signature'. verify_signature() will use the public key, the 'method' and + 'sig', and 'data' arguments to complete the verification. + + >>> public, private = generate_public_and_private(use_pynacl=False) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = \ + create_signature(public, private, data, use_pynacl=False) + >>> verify_signature(public, method, signature, data, use_pynacl=False) + True + >>> verify_signature(public, method, signature, data, use_pynacl=True) + True + >>> bad_data = 'The sly brown fox jumps over the lazy dog' + >>> bad_signature, method = \ + create_signature(public, private, bad_data, use_pynacl=False) + >>> verify_signature(public, method, bad_signature, data, use_pynacl=False) + False + + + public_key: + The public key is a 32-byte string. + + method: + 'ed25519-python' if the signature was generated by the pure python + implementation and 'ed25519-pynacl' if generated by 'nacl'. + + signature: + The signature is a 64-byte string. + + data: + Data object used by tuf.ed25519_keys.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + use_pynacl: + True, if the ed25519 signature should be verified by PyNaCl. False, + if the signature should be verified with the pure Python implementation + of ed25519 (slower). + + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported by tuf.ed25519_keys.create_signature(). + + tuf.FormatError. Raised if the arguments are improperly formatted. + + + ed25519.ed25519.checkvalid() called to do the actual verification. + nacl.signing.VerifyKey.verify() called if 'use_pynacl' is True. + + + Boolean. True if the signature is valid, False otherwise. + """ + + # Does 'public_key' have the correct format? + # This check will ensure 'public_key' conforms to + # 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key) + + # Is 'method' properly formatted? + tuf.formats.NAME_SCHEMA.check_match(method) + + # Is 'signature' properly formatted? + tuf.formats.ED25519SIGNATURE_SCHEMA.check_match(signature) + + # Is 'use_pynacl' properly formatted? + tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) + + # Verify 'signature'. Before returning the Boolean result, + # ensure 'ed25519-python' or 'ed25519-pynacl' was used as the signing method. + # Raise 'tuf.UnsupportedLibraryError' if 'use_pynacl' is True but 'nacl' is + # unavailable. + public = public_key + valid_signature = False + + if method in _SUPPORTED_ED25519_SIGNING_METHODS: + if use_pynacl: + try: + nacl_verify_key = nacl.signing.VerifyKey(public) + nacl_message = nacl_verify_key.verify(data, signature) + if nacl_message == data: + valid_signature = True + except NameError: + message = 'The PyNaCl library and/or its dependencies unavailable.' + raise tuf.UnsupportedLibraryError(message) + except nacl.signing.BadSignatureError: + pass + + # Verify signature with 'ed25519-python' (i.e., pure Python implementation). + else: + try: + ed25519.ed25519.checkvalid(signature, data, public) + valid_signature = True + + # The pure Python implementation raises 'Exception' if 'signature' is + # invalid. + except Exception, e: + pass + else: + message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \ + 'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.' + raise tuf.UnknownMethodError(message) + + return valid_signature + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running 'ed25519_keys.py' as a standalone module. + # python -B ed25519_keys.py + import doctest + doctest.testmod() diff --git a/tuf/evp.py b/tuf/evp.py new file mode 100755 index 00000000..60895bbd --- /dev/null +++ b/tuf/evp.py @@ -0,0 +1,425 @@ +""" + + evp.py + + + Vladimir Diaz + + + October 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to support public-key cryptography using + the RSA algorithm. The RSA-related functions provided include + generate(), create_signature(), and verify_signature(). The 'evpy' package + used by 'rsa_key.py' generates the actual RSA keys and the functions listed + above can be viewed as an easy-to-use public interface. Additional functions + contained here include create_in_metadata_format() and + create_from_metadata_format(). These last two functions produce or use RSA + keys compatible with the key structures listed in TUF Metadata files. + The generate() function returns a dictionary containing all the information + needed of RSA keys, such as public and private keys, keyIDs, and an iden- + fier. create_signature() and verify_signature() are supplemental functions + used for generating RSA signatures and verifying them. + + Key IDs are used as identifiers for keys (e.g., RSA key). They are the + hexadecimal representation of the hash of key object (specifically, the key + object containing only the public key). See 'rsa_key.py' and the + '_get_keyid()' function to see precisely how keyids are generated. One may + get the keyid of a key object by simply accessing the dictionary's 'keyid' + key (i.e., rsakey['keyid']). + +""" + + +# Required for hexadecimal conversions. +import binascii + +# Needed to generate, sign, and verify RSA keys. +import evpy.signature +import evpy.envelope + +# Digest objects needed to generate hashes. +import tuf.hash + +# Perform object format-checking. +import tuf.formats + + +_KEY_ID_HASH_ALGORITHM = 'sha256' + +# Recommended RSA key sizes: http://www.rsa.com/rsalabs/node.asp?id=2004 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. +_DEFAULT_RSA_KEY_BITS = 3072 + + +def generate(bits=_DEFAULT_RSA_KEY_BITS): + """ + + Generate public and private RSA keys, with modulus length 'bits'. + In addition, a keyid used as an identifier for RSA keys is generated. + The object returned conforms to tuf.formats.RSAKEY_SCHEMA and as the form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + + bits: + The key size, or key length, of the RSA key. + + + tuf.CryptoError, if an exception occurs after calling evpy.envelope.keygen(). + + tuf.FormatError, if 'bits' does not contain the correct format. + + + The RSA keys are generated by calling evpy.envelope.keygen(). + + + A dictionary containing the RSA keys and other identifying information. + + """ + + + # Does 'bits' have the correct format? + # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. + # 'bits' must be an integer object, with a minimum value of 2048. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + + # Generate the public and private keys. 'public_key' and 'private_key' + # will both be strings containing RSA keys in PEM format. + # The evpy.envelope module performs the actual key generation. The + # evpy.envelope.keygen() function returns a (public, private) tuple. + + try: + public_key, private_key = evpy.envelope.keygen(bits, pem=True) + except (evpy.envelope.EnvelopeError, evpy.envelope.KeygenError, MemoryError), e: + raise tuf.CryptoError(e) + + # Generate the keyid for the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the RSAKEY_SCHEMA dictionary. + key_value = {'public': public_key, + 'private': ''} + + keyid = _get_keyid(key_value) + + # Build the 'rsakey_dict' dictionary. + # Update 'key_value' with the RSA private key prior to adding + # 'key_value' to 'rsakey_dict'. + key_value['private'] = private_key + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def create_in_metadata_format(key_value, private=False): + """ + + Return a dictionary conformant to tuf.formats.KEY_SCHEMA. + If 'private' is True, include the private key. The dictionary + returned has the form: + {'keytype': 'rsa', + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + or + + {'keytype': 'rsa', + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': ''}} if 'private' is False. + + The private and public keys are in PEM format. + + RSA keys are stored in Metadata files (e.g., root.txt) in the format + returned by this function. + + + key_value: + A dictionary containing a private and public RSA key. + 'key_value' is of the form: + + {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}, + conformat to tuf.formats.KEYVAL_SCHEMA. + + private: + Indicates if the private key should be included in the + returned dictionary. + + + tuf.FormatError, if 'key_value' does not conform to + tuf.formats.KEYVAL_SCHEMA. + + + None. + + + An KEY_SCHEMA dictionary. + + """ + + + # Does 'key_value' have the correct format? + # This check will ensure 'key_value' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEYVAL_SCHEMA.check_match(key_value) + + if private and key_value['private']: + return {'keytype': 'rsa', 'keyval': key_value} + else: + public_key_value = {'public': key_value['public'], 'private': ''} + return {'keytype': 'rsa', 'keyval': public_key_value} + + + + + +def create_from_metadata_format(key_metadata): + """ + + Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA) + from 'key_metadata'. The dict returned by this function has the exact + format as the dict returned by generate(). It is of the form: + + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + RSA key dictionaries in RSAKEY_SCHEMA format should be used by + modules storing a collection of keys, such as a keydb and keystore. + RSA keys as stored in metadata files use a different format, so this + function should be called if an RSA key is extracted from one of these + metadata files and needs converting. Generate() creates an entirely + new key and returns it in the format appropriate for keydb and keystore. + + + key_metadata: + The RSA key dictionary as stored in Metadata files, conforming to + tuf.formats.KEY_SCHEMA. + + + tuf.FormatError, if 'key_metadata' does not conform to + tuf.formats.KEY_SCHEMA. + + + None. + + + A dictionary containing the RSA keys and other identifying information. + + """ + + + # Does 'key_metadata' have the correct format? + # This check will ensure 'key_metadata' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEY_SCHEMA.check_match(key_metadata) + + # Construct the dictionary to be returned. + rsakey_dict = {} + keytype = 'rsa' + key_value = key_metadata['keyval'] + + keyid = _get_keyid(key_value) + + # We now have all the required key values. + # Build 'rsakey_dict'. + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def _get_keyid(key_value): + """Return the keyid for 'key_value'.""" + + # 'keyid' will be generated from an object conformant to KEY_SCHEMA, + # which is the format Metadata files (e.g., root.txt) store keys. + # 'create_in_metadata_format()' returns the object needed by _get_keyid(). + rsakey_meta = create_in_metadata_format(key_value, private=False) + + # Convert the RSA key to JSON Canonical format suitable for adding + # to digest objects. + rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta) + + # Create a digest object and call update(), using the JSON + # canonical format of 'rskey_meta' as the update data. + digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) + digest_object.update(rsakey_update_data) + + # 'keyid' becomes the hexadecimal representation of the hash. + keyid = digest_object.hexdigest() + + return keyid + + + + + +def create_signature(rsakey_dict, data): + """ + + Return a signature dictionary of the form: + {'keyid': keyid, 'method': 'evp', 'sig': sig}. + + The signing process will use the private key + rsakey_dict['keyval']['private'] and 'data' to generate the signature. + + + rsakey_dict: + A dictionary containing the RSA keys and other identifying information. + 'rsakey_dict' has the form: + + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + data: + Data object used by create_signature() to generate the signature. + + + TypeError, if a private key is not defined for 'rsakey_dict'. + + tuf.FormatError, if an incorrect format is found for the + 'rsakey_dict' object. + + + evpy.signature.sign() called to perform the actual signing. + + + A signature dictionary conformat to tuf.format.SIGNATURE_SCHEMA. + + """ + + + # Does 'rsakey_dict' have the correct format? + # This check will ensure 'rsakey_dict' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + + # Signing the 'data' object requires a private key. + # The 'evp' (i.e., evpy) signing method is the only method + # currently supported. + signature = {} + private_key = rsakey_dict['keyval']['private'] + keyid = rsakey_dict['keyid'] + method = 'evp' + + if private_key: + sig = evpy.signature.sign(data, key=private_key) + else: + raise TypeError('The required private key is not defined for rsakey_dict.') + + # Build the signature dictionary to be returned. + # The hexadecimal representation of 'sig' is stored in the signature. + signature['keyid'] = keyid + signature['method'] = method + signature['sig'] = binascii.hexlify(sig) + + return signature + + + + + +def verify_signature(rsakey_dict, signature, data): + """ + + Determine whether the private key belonging to 'rsakey_dict' produced + 'signature'. verify_signature() will use the public key found in + 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', + and 'data' to complete the verification. Type-checking performed on both + 'rsakey_dict' and 'signature'. + + + rsakey_dict: + A dictionary containing the RSA keys and other identifying information. + 'rsakey_dict' has the form: + + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + signature: + The signature dictionary produced by tuf.rsa_key.create_signature(). + 'signature' has the form: + {'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to + tuf.formats.SIGNATURE_SCHEMA. + + data: + Data object used by tuf.rsa_key.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported by tuf.rsa_key.create_signature(). + + tuf.FormatError. Raised if either 'rsakey_dict' + or 'signature' do not match their respective tuf.formats schema. + 'rsakey_dict' must conform to tuf.formats.RSAKEY_SCHEMA. + 'signature' must conform to tuf.formats.SIGNATURE_SCHEMA. + + + evpy.signature_verify() called to do the actual verification. + + + Boolean. + + """ + + + # Does 'rsakey_dict' have the correct format? + # This check will ensure 'rsakey_dict' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + + # Does 'signature' have the correct format? + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + # Using the public key belonging to 'rsakey_dict' + # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' + # was produced by rsakey_dict's corresponding private key + # rsakey_dict['keyval']['private']. Before returning the Boolean result, + # ensure 'evp' was used as the signing method. + + method = signature['method'] + sig = signature['sig'] + public_key = rsakey_dict['keyval']['public'] + + if method != 'evp': + raise tuf.UnknownMethodError(method) + return evpy.signature.verify(data, binascii.unhexlify(sig), key=public_key) diff --git a/tuf/formats.py b/tuf/formats.py index 85fc37c2..8c14b00d 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -57,10 +57,8 @@ Example: signable_object = make_signable(unsigned_object) - """ - import binascii import calendar import re @@ -76,8 +74,12 @@ # easily backwards compatible with clients that are already deployed. # A date in 'YYYY-MM-DD HH:MM:SS UTC' format. +# TODO: Support timestamps according to the ISO 8601 standard. TIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC') +# A date in 'YYYY-MM-DD HH:MM:SS UTC' format. +DATETIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}') + # A hexadecimal value in '23432df87ab..' format. HASH_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+') @@ -109,7 +111,7 @@ # A dictionary holding version information. VERSION_SCHEMA = SCHEMA.Object( - object_name='version', + object_name='VERSION_SCHEMA', major=SCHEMA.Integer(lo=0), minor=SCHEMA.Integer(lo=0), fix=SCHEMA.Integer(lo=0)) @@ -142,6 +144,9 @@ # The minimum number of bits for an RSA key. Must be 2048 bits and greater. RSAKEYBITS_SCHEMA = SCHEMA.Integer(lo=2048) +# A PyCrypto signature. +PYCRYPTOSIGNATURE_SCHEMA = SCHEMA.AnyString() + # An RSA key in PEM format. PEMRSA_SCHEMA = SCHEMA.AnyString() @@ -155,26 +160,52 @@ # key identifier ('rsa', 233df889cb). For RSA keys, the key value is a pair of # public and private keys in PEM Format stored as strings. KEYVAL_SCHEMA = SCHEMA.Object( - object_name='keyval', + object_name='KEYVAL_SCHEMA', public=SCHEMA.AnyString(), private=SCHEMA.AnyString()) -# A generic key. All TUF keys should be saved to metadata files in this format. +# Supported TUF key types. +KEYTYPE_SCHEMA = SCHEMA.OneOf( + [SCHEMA.String('rsa'), SCHEMA.String('ed25519')]) + +# A generic TUF key. All TUF keys should be saved to metadata files in this +# format. KEY_SCHEMA = SCHEMA.Object( - object_name='key', + object_name='KEY_SCHEMA', keytype=SCHEMA.AnyString(), keyval=KEYVAL_SCHEMA) -# An RSA key. +# A TUF key object. This schema simplifies validation of keys that may be +# one of the supported key types. +# Supported key types: 'rsa', 'ed25519'. +ANYKEY_SCHEMA = SCHEMA.Object( + object_name='ANYKEY_SCHEMA', + keytype=KEYTYPE_SCHEMA, + keyid=KEYID_SCHEMA, + keyval=KEYVAL_SCHEMA) + +# A list of TUF key objects. +ANYKEYLIST_SCHEMA = SCHEMA.ListOf(ANYKEY_SCHEMA) + +# An RSA TUF key. RSAKEY_SCHEMA = SCHEMA.Object( - object_name='rsakey', + object_name='RSAKEY_SCHEMA', keytype=SCHEMA.String('rsa'), keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) -# An ed25519 key. +# An ED25519 raw public key, which must be 32 bytes. +ED25519PUBLIC_SCHEMA = SCHEMA.LengthString(32) + +# An ED25519 raw seed key, which must be 32 bytes. +ED25519SEED_SCHEMA = SCHEMA.LengthString(32) + +# An ED25519 raw signature, which must be 64 bytes. +ED25519SIGNATURE_SCHEMA = SCHEMA.LengthString(64) + +# An ed25519 TUF key. ED25519KEY_SCHEMA = SCHEMA.Object( - object_name='ed25519key', + object_name='ED25519KEY_SCHEMA', keytype=SCHEMA.String('ed25519'), keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) @@ -183,7 +214,7 @@ # This schema allows the storage of multiple hashes for the same file # (e.g., sha256 and sha512 may be computed for the same file and stored). FILEINFO_SCHEMA = SCHEMA.Object( - object_name='fileinfo', + object_name='FILEINFO_SCHEMA', length=LENGTH_SCHEMA, hashes=HASHDICT_SCHEMA, custom=SCHEMA.Optional(SCHEMA.Object())) @@ -196,7 +227,7 @@ # A dict holding a target file. TARGETFILE_SCHEMA = SCHEMA.Object( - object_name='targetfile', + object_name='TARGETFILE_SCHEMA', filepath=RELPATH_SCHEMA, fileinfo=FILEINFO_SCHEMA) TARGETFILES_SCHEMA = SCHEMA.ListOf(TARGETFILE_SCHEMA) @@ -210,18 +241,21 @@ # one can imagine that maybe a key wants to sign multiple times with different # signature methods. SIGNATURE_SCHEMA = SCHEMA.Object( - object_name='signature', + object_name='SIGNATURE_SCHEMA', keyid=KEYID_SCHEMA, method=SIG_METHOD_SCHEMA, sig=HEX_SCHEMA) +# List of SIGNATURE_SCHEMA. +SIGNATURES_SCHEMA = SCHEMA.ListOf(SIGNATURE_SCHEMA) + # A schema holding the result of checking the signatures of a particular # 'SIGNABLE_SCHEMA' role. # For example, how many of the signatures for the 'Target' role are # valid? This SCHEMA holds this information. See 'sig.py' for # more information. SIGNATURESTATUS_SCHEMA = SCHEMA.Object( - object_name='signaturestatus', + object_name='SIGNATURESTATUS_SCHEMA', threshold=SCHEMA.Integer(), good_sigs=SCHEMA.ListOf(KEYID_SCHEMA), bad_sigs=SCHEMA.ListOf(KEYID_SCHEMA), @@ -231,7 +265,7 @@ # A signable object. Holds the signing role and its associated signatures. SIGNABLE_SCHEMA = SCHEMA.Object( - object_name='signable', + object_name='SIGNABLE_SCHEMA', signed=SCHEMA.Any(), signatures=SCHEMA.ListOf(SIGNATURE_SCHEMA)) @@ -255,7 +289,7 @@ # 'remote_directory' entries. See 'tuf/pushtools/pushtoolslib.py' and # 'tuf/pushtools/push.py'. SCPCONFIG_SCHEMA = SCHEMA.Object( - object_name='scp_config', + object_name='SCPCONFIG_SCHEMA', general=SCHEMA.Object( object_name='[general]', transfer_module=SCHEMA.String('scp'), @@ -275,8 +309,7 @@ # 'backup_directory' entries. # see 'tuf/pushtools/pushtoolslib.py' and 'tuf/pushtools/receive/receive.py' RECEIVECONFIG_SCHEMA = SCHEMA.Object( - object_name='receive_config', - general=SCHEMA.Object( + object_name='RECEIVECONFIG_SCHEMA', general=SCHEMA.Object( object_name='[general]', pushroots=SCHEMA.ListOf(PATH_SCHEMA), repository_directory=PATH_SCHEMA, @@ -286,15 +319,16 @@ # A path hash prefix is a hexadecimal string. PATH_HASH_PREFIX_SCHEMA = HEX_SCHEMA + # A list of path hash prefixes. PATH_HASH_PREFIXES_SCHEMA = SCHEMA.ListOf(PATH_HASH_PREFIX_SCHEMA) # Role object in {'keyids': [keydids..], 'name': 'ABC', 'threshold': 1, -# 'paths':[filepaths..]} # format. +# 'paths':[filepaths..]} format. ROLE_SCHEMA = SCHEMA.Object( - object_name='role', - keyids=SCHEMA.ListOf(KEYID_SCHEMA), + object_name='ROLE_SCHEMA', name=SCHEMA.Optional(ROLENAME_SCHEMA), + keyids=SCHEMA.ListOf(KEYID_SCHEMA), threshold=THRESHOLD_SCHEMA, paths=SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA)) @@ -308,37 +342,66 @@ # Like ROLEDICT_SCHEMA, except that ROLE_SCHEMA instances are stored in order. ROLELIST_SCHEMA = SCHEMA.ListOf(ROLE_SCHEMA) -# The root: indicates root keys and top-level roles. +# The delegated roles of a Targets role (a parent). +DELEGATIONS_SCHEMA = SCHEMA.Object( + keys=KEYDICT_SCHEMA, + roles=ROLELIST_SCHEMA) + +# The number of seconds before metadata expires. The minimum is 86400 seconds +# (= 1 day). This schema is used for the initial expiration date. Repository +# maintainers may later modify this value (TIME_SCHEMA). +EXPIRATION_SCHEMA = SCHEMA.Integer(lo=86400) + +# Supported compression extension (e.g., 'gz'). +COMPRESSION_SCHEMA = SCHEMA.OneOf([SCHEMA.String(''), SCHEMA.String('gz')]) + +# List of supported compression extensions. +COMPRESSIONS_SCHEMA = SCHEMA.ListOf( + SCHEMA.OneOf([SCHEMA.String(''), SCHEMA.String('gz')])) + +# tuf.roledb +ROLEDB_SCHEMA = SCHEMA.Object( + object_name='ROLEDB_SCHEMA', + keyids=SCHEMA.ListOf(KEYID_SCHEMA), + signing_keyids=SCHEMA.Optional(SCHEMA.ListOf(KEYID_SCHEMA)), + threshold=THRESHOLD_SCHEMA, + version=SCHEMA.Optional(METADATAVERSION_SCHEMA), + expires=SCHEMA.Optional(SCHEMA.OneOf([EXPIRATION_SCHEMA, TIME_SCHEMA])), + signatures=SCHEMA.Optional(SCHEMA.ListOf(SIGNATURE_SCHEMA)), + compressions=SCHEMA.Optional(COMPRESSIONS_SCHEMA), + paths=SCHEMA.Optional(RELPATHS_SCHEMA), + path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA), + delegations=SCHEMA.Optional(DELEGATIONS_SCHEMA)) + +# Root role: indicates root keys and top-level roles. ROOT_SCHEMA = SCHEMA.Object( - object_name='root', + object_name='ROOT_SCHEMA', _type=SCHEMA.String('Root'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, keys=KEYDICT_SCHEMA, roles=ROLEDICT_SCHEMA) -# Targets. Indicates targets and delegates target paths to other roles. +# Targets role: Indicates targets and delegates target paths to other roles. TARGETS_SCHEMA = SCHEMA.Object( - object_name='targets', + object_name='TARGETS_SCHEMA', _type=SCHEMA.String('Targets'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, targets=FILEDICT_SCHEMA, - delegations=SCHEMA.Optional(SCHEMA.Object( - keys=KEYDICT_SCHEMA, - roles=ROLELIST_SCHEMA))) + delegations=SCHEMA.Optional(DELEGATIONS_SCHEMA)) -# A Release: indicates the latest versions of all metadata (except timestamp). +# Release role: indicates the latest versions of all metadata (except timestamp). RELEASE_SCHEMA = SCHEMA.Object( - object_name='release', + object_name='RELEASE_SCHEMA', _type=SCHEMA.String('Release'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, meta=FILEDICT_SCHEMA) -# A Timestamp: indicates the latest version of the release file. +# Timestamp role: indicates the latest version of the release file. TIMESTAMP_SCHEMA = SCHEMA.Object( - object_name='timestamp', + object_name='TIMESTAMP_SCHEMA', _type=SCHEMA.String('Timestamp'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, @@ -347,7 +410,7 @@ # A schema containing information a repository mirror may require, # such as a url, the path of the directory metadata files, etc. MIRROR_SCHEMA = SCHEMA.Object( - object_name='mirror', + object_name='MIRROR_SCHEMA', url_prefix=URL_SCHEMA, metadata_path=RELPATH_SCHEMA, targets_path=RELPATH_SCHEMA, @@ -365,7 +428,7 @@ # A Mirrorlist: indicates all the live mirrors, and what documents they # serve. MIRRORLIST_SCHEMA = SCHEMA.Object( - object_name='mirrorlist', + object_name='MIRRORLIST_SCHEMA', _type=SCHEMA.String('Mirrors'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, @@ -383,7 +446,6 @@ class MetaFile(object): and ReleaseFile all inherit from MetaFile. The __eq__, __ne__, perform 'equal' and 'not equal' comparisons between Metadata File objects. - """ info = None @@ -401,7 +463,6 @@ def __getattr__(self, name): Allow all metafile objects to have their interesting attributes referred to directly without the info dict. The info dict is just to be able to do the __eq__ comparison generically. - """ if name in self.info: @@ -473,14 +534,10 @@ def from_metadata(object): @staticmethod - def make_metadata(version, expiration_seconds, keydict, roledict): - # Is 'expiration_seconds' properly formatted? - # Raise 'tuf.FormatError' if not. - LENGTH_SCHEMA.check_match(expiration_seconds) - + def make_metadata(version, expiration_date, keydict, roledict): result = {'_type' : 'Root'} result['version'] = version - result['expires'] = format_time(time.time() + expiration_seconds) + result['expires'] = expiration_date result['keys'] = keydict result['roles'] = roledict @@ -645,7 +702,6 @@ def format_time(timestamp): A string in 'YYYY-MM-DD HH:MM:SS UTC' format. - """ try: @@ -677,7 +733,6 @@ def parse_time(string): A timestamp (e.g., 499137660). - """ # Is 'string' properly formatted? @@ -715,7 +770,6 @@ def format_base64(data): A base64-encoded string. - """ try: @@ -746,7 +800,6 @@ def parse_base64(base64_string): A byte string representing the parsed based64 encoding of 'base64_string'. - """ if not isinstance(base64_string, basestring): @@ -791,7 +844,6 @@ def make_signable(object): A dict in 'SIGNABLE_SCHEMA' format. - """ if not isinstance(object, dict) or 'signed' not in object: @@ -832,7 +884,6 @@ def make_fileinfo(length, hashes, custom=None): A dictionary conformant to 'FILEINFO_SCHEMA', representing the file information of a metadata or target file. - """ fileinfo = {'length' : length, 'hashes' : hashes} @@ -889,7 +940,6 @@ def make_role_metadata(keyids, threshold, name=None, paths=None, A properly formatted role meta dict, conforming to 'ROLE_SCHEMA'. - """ role_meta = {} @@ -950,7 +1000,6 @@ def get_role_class(expected_rolename): The class corresponding to 'expected_rolename'. E.g., 'Release' as an argument to this function causes 'ReleaseFile' to be returned. - """ # Does 'expected_rolename' have the correct type? @@ -993,7 +1042,6 @@ def expected_meta_rolename(meta_rolename): A string (e.g., 'Root', 'Targets'). - """ # Does 'meta_rolename' have the correct type? @@ -1033,7 +1081,6 @@ def check_signable_object_format(object): A string representing the signing role (e.g., 'root', 'targets'). The role string is returned with characters all lower case. - """ # Does 'object' have the correct type? @@ -1077,7 +1124,6 @@ def _canonical_string_encoder(string): A string with the canonical-encoded 'string' embedded. - """ string = '"%s"' % re.sub(r'(["\\])', r'\\\1', string) @@ -1182,7 +1228,6 @@ def encode_canonical(object, output_function=None): A string representing the 'object' encoded in canonical JSON form. - """ result = None diff --git a/tuf/keydb.py b/tuf/keydb.py index 601993b3..023a3184 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -25,15 +25,14 @@ and the '_get_keyid()' function to learn precisely how keyids are generated. One may get the keyid of a key object by simply accessing the dictionary's 'keyid' key (i.e., rsakey['keyid']). - """ - import logging +import copy import tuf import tuf.formats -import tuf.rsa_key +import tuf.keys # See 'log.py' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.keydb') @@ -62,13 +61,12 @@ def create_keydb_from_root_metadata(root_metadata): A function to add the key to the database is called. In the case of RSA - keys, this function is add_rsakey(). + keys, this function is add_key(). The old keydb key database is replaced. None. - """ # Does 'root_metadata' have the correct format? @@ -87,10 +85,10 @@ def create_keydb_from_root_metadata(root_metadata): if key_metadata['keytype'] == 'rsa': # 'key_metadata' is stored in 'KEY_SCHEMA' format. Call # create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA' - # format, which is the format expected by 'add_rsakey()'. - rsakey_dict = tuf.rsa_key.create_from_metadata_format(key_metadata) + # format, which is the format expected by 'add_key()'. + rsakey_dict = tuf.keys.format_metadata_to_key(key_metadata) try: - add_rsakey(rsakey_dict, keyid) + add_key(rsakey_dict, keyid) # 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'. except tuf.Error, e: logger.error(e) @@ -105,7 +103,7 @@ def create_keydb_from_root_metadata(root_metadata): -def add_rsakey(rsakey_dict, keyid=None): +def add_key(key_dict, keyid=None): """ Add 'rsakey_dict' to the key database while avoiding duplicates. @@ -113,8 +111,8 @@ def add_rsakey(rsakey_dict, keyid=None): and raise an exception if it is not. - rsakey_dict: - A dictionary conformant to 'tuf.formats.RSAKEY_SCHEMA'. + key_dict: + A dictionary conformant to 'tuf.formats.ANYKEY_SCHEMA'. It has the form: {'keytype': 'rsa', 'keyid': keyid, @@ -138,15 +136,13 @@ def add_rsakey(rsakey_dict, keyid=None): None. - """ - # Does 'rsakey_dict' have the correct format? # This check will ensure 'rsakey_dict' has the appropriate number of objects # and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) # Does 'keyid' have the correct format? if keyid is not None: @@ -154,16 +150,16 @@ def add_rsakey(rsakey_dict, keyid=None): tuf.formats.KEYID_SCHEMA.check_match(keyid) # Check if the keyid found in 'rsakey_dict' matches 'keyid'. - if keyid != rsakey_dict['keyid']: - raise tuf.Error('Incorrect keyid '+rsakey_dict['keyid']+' expected '+keyid) + if keyid != key_dict['keyid']: + raise tuf.Error('Incorrect keyid '+key_dict['keyid']+' expected '+keyid) # Check if the keyid belonging to 'rsakey_dict' is not already # available in the key database before returning. - keyid = rsakey_dict['keyid'] + keyid = key_dict['keyid'] if keyid in _keydb_dict: raise tuf.KeyAlreadyExistsError('Key: '+keyid) - _keydb_dict[keyid] = rsakey_dict + _keydb_dict[keyid] = copy.deepcopy(key_dict) @@ -190,7 +186,6 @@ def get_key(keyid): The key matching 'keyid'. In the case of RSA keys, a dictionary conformant to 'tuf.formats.RSAKEY_SCHEMA' is returned. - """ # Does 'keyid' have the correct format? @@ -201,7 +196,7 @@ def get_key(keyid): # Return the key belonging to 'keyid', if found in the key database. try: - return _keydb_dict[keyid] + return copy.deepcopy(_keydb_dict[keyid]) except KeyError: raise tuf.UnknownKeyError('Key: '+keyid) @@ -229,7 +224,6 @@ def remove_key(keyid): None. - """ # Does 'keyid' have the correct format? @@ -264,7 +258,6 @@ def clear_keydb(): None. - """ _keydb_dict.clear() diff --git a/tuf/keys.py b/tuf/keys.py new file mode 100755 index 00000000..f0a94e87 --- /dev/null +++ b/tuf/keys.py @@ -0,0 +1,1021 @@ +""" + + keys.py + + + Vladimir Diaz + + + October 4, 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to centralize cryptographic key routines and their + supported operations (e.g., creating and verifying signatures). This module + is designed to support multiple public-key algorithms, such as RSA and + ED25519, and multiple cryptography libraries. Which cryptography library to + use is determined by the default, or user modified, values set in + 'tuf.conf.py' + + The (RSA and ED25519)-related functions provided include generate_rsa_key(), + generate_ed5519_key(), create_signature(), and verify_signature(). + The cryptography libraries called by 'tuf.keys.py' generate the actual TUF + keys and the functions listed above can be viewed as an easy-to-use public + interface. + + Additional functions contained here include format_keyval_to_metadata() and + format_metadata_to_key(). These last two functions produce or use TUF keys + compatible with the key structures listed in TUF Metadata files. The key + generation functions return a dictionary containing all the information needed + of TUF keys, such as public and private keys and a keyID. create_signature() + and verify_signature() are supplemental functions used for generating + signatures and verifying them. + + https://en.wikipedia.org/wiki/RSA_(algorithm) + http://ed25519.cr.yp.to/ + + Key IDs are used as identifiers for keys (e.g., RSA key). They are the + hexadecimal representation of the hash of key object (specifically, the key + object containing only the public key). Review 'rsa_key.py' and the + '_get_keyid()' function to see precisely how keyids are generated. One may + get the keyid of a key object by simply accessing the dictionary's 'keyid' + key (i.e., rsakey['keyid']). + """ + +# Required for hexadecimal conversions. Signatures and public/private keys are +# hexlified. +import binascii + +# 'pycrypto' is the only currently supported library for the creation of RSA +# keys. https://github.com/dlitz/pycrypto +_SUPPORTED_RSA_CRYPTO_LIBRARIES = ['pycrypto'] + +# The currently supported libraries for the creation of ed25519 keys and +# signatures. The 'pynacl' library should be installed and used over the slower +# python implementation of ed25519. The python implementation will be used +# if 'pynacl' is unavailable. +_SUPPORTED_ED25519_CRYPTO_LIBRARIES = ['ed25519', 'pynacl'] + +# Track which libraries are imported and thus available. A optimized version +# of the ed25519 python implementation is provided by TUF and avaialable by +# default. https://github.com/pyca/ed25519 +_available_crypto_libraries = ['ed25519'] + +# Import the PyCrypto library so that RSA keys are supported. +try: + import Crypto + import tuf.pycrypto_keys + _available_crypto_libraries.append('pycrypto') +except ImportError: + pass + +# Import the PyNaCl library, if available. It is recommended this library be +# used over the pure python implementation of ed25519, due to its speedier +# routines and side-channel protections available in the libsodium library. +try: + import nacl + _available_crypto_libraries.append('pynacl') +except ImportError: + pass + +# The optimized version of the ed25519 library provided by default is imported +# regardless of the availability of PyNaCl. +import tuf.ed25519_keys + + +# Import the TUF package and TUF-defined exceptions in __init__.py. +import tuf + +# Import the cryptography library settings. +import tuf.conf + +# Digest objects needed to generate hashes. +import tuf.hash + +# Perform format checks of argument objects. +import tuf.formats + +# The hash algorithm to use in the generation of keyids. +_KEY_ID_HASH_ALGORITHM = 'sha256' + +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. +_DEFAULT_RSA_KEY_BITS = 3072 + +# The crypto libraries to use in 'keys.py', set by default or by the user. +# The following cryptography libraries are currently supported: +# ['pycrypto', 'pynacl', 'ed25519'] +_RSA_CRYPTO_LIBRARY = tuf.conf.RSA_CRYPTO_LIBRARY +_ED25519_CRYPTO_LIBRARY = tuf.conf.ED25519_CRYPTO_LIBRARY + + +def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): + """ + + Generate public and private RSA keys, with modulus length 'bits'. In + addition, a keyid identifier for the RSA key is generated. The object + returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and has the + form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are strings in PEM format. + + Although the PyCrypto crytography library called sets a 1024-bit minimum + key size, generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + >>> rsa_key = generate_rsa_key(bits=2048) + >>> tuf.formats.RSAKEY_SCHEMA.matches(rsa_key) + True + >>> public = rsa_key['keyval']['public'] + >>> private = rsa_key['keyval']['private'] + >>> tuf.formats.PEMRSA_SCHEMA.matches(public) + True + >>> tuf.formats.PEMRSA_SCHEMA.matches(private) + True + + + bits: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + + tuf.FormatError, if 'bits' is improperly or invalid (i.e., not an integer + and not at least 2048). + + tuf.UnsupportedLibraryError, if any of the cryptography libraries specified + in 'tuf.conf.py' are unsupported or unavailable. + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'bits' have the correct format? + # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. + # 'bits' must be an integer object, with a minimum value of 2048. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified in + # 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. + _check_crypto_libraries() + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + public = None + private = None + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + public, private = tuf.pycrypto_keys.generate_rsa_public_and_private(bits) + else: + message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + # Generate the keyid of the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': public, + 'private': ''} + keyid = _get_keyid(keytype, key_value) + + # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA + # private key prior to adding 'key_value' to 'rsakey_dict'. + key_value['private'] = private + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def generate_ed25519_key(): + """ + + Generate public and private ED25519 keys, both of length 32-bytes, although + they are hexlified to 64 bytes. + In addition, a keyid identifier generated for the returned ED25519 object. + The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and as the + form: + {'keytype': 'ed25519', + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyval': {'public': '9ccf3f02b17f82febf5dd3bab878b767d8408...', + 'private': 'ab310eae0e229a0eceee3947b6e0205dfab3...'}} + + The public and private keys are strings in PEM format and stored in the + 'keyval' field of the returned dictionary. + + >>> ed25519_key = generate_ed25519_key() + >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key) + True + >>> len(ed25519_key['keyval']['public']) + 64 + >>> len(ed25519_key['keyval']['private']) + 64 + + + None. + + + tuf.UnsupportedLibraryError, if an unsupported or unavailable library is + detected. + + + The ED25519 keys are generated by calling either the optimized pure Python + implementation of ed25519, or the ed25519 routines provided by 'pynacl'. + + + A dictionary containing the ED25519 keys and other identifying information. + Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. + """ + + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified + # in 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. + _check_crypto_libraries() + + # Begin building the ED25519 key dictionary. + ed25519_key = {} + keytype = 'ed25519' + public = None + private = None + + # Generate the public and private ED25519 keys. Use the 'pynacl' library + # if available, otherwise fall back to optimized pure python implementation + # provided by pyca and available in TUF. + if 'pynacl' in _available_crypto_libraries: + public, private = \ + tuf.ed25519_keys.generate_public_and_private(use_pynacl=True) + else: + public, private = \ + tuf.ed25519_keys.generate_public_and_private(use_pynacl=False) + + # Generate the keyid of the ED25519 key. 'key_value' corresponds to the + # 'keyval' entry of the 'ED25519KEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': binascii.hexlify(public), + 'private': ''} + keyid = _get_keyid(keytype, key_value) + + # Build the 'ed25519_key' dictionary. Update 'key_value' with the ED25519 + # private key prior to adding 'key_value' to 'ed25519_key'. + key_value['private'] = binascii.hexlify(private) + + ed25519_key['keytype'] = keytype + ed25519_key['keyid'] = keyid + ed25519_key['keyval'] = key_value + + return ed25519_key + + + + + +def format_keyval_to_metadata(keytype, key_value, private=False): + """ + + Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. + If 'private' is True, include the private key. The dictionary + returned has the form: + {'keytype': keytype, + 'keyval': {'public': '...', + 'private': '...'}} + + or if 'private' is False: + + {'keytype': keytype, + 'keyval': {'public': '...', + 'private': ''}} + + TUF keys are stored in Metadata files (e.g., root.txt) in the format + returned by this function. + + >>> ed25519_key = generate_ed25519_key() + >>> key_val = ed25519_key['keyval'] + >>> keytype = ed25519_key['keytype'] + >>> ed25519_metadata = \ + format_keyval_to_metadata(keytype, key_val, private=True) + >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) + True + + + key_type: + The 'rsa' or 'ed25519' strings. + + key_value: + A dictionary containing a private and public keys. + 'key_value' is of the form: + + {'public': '...', + 'private': '...'}}, + + conformant to 'tuf.formats.KEYVAL_SCHEMA'. + + private: + Indicates if the private key should be included in the dictionary + returned. + + + tuf.FormatError, if 'key_value' does not conform to + 'tuf.formats.KEYVAL_SCHEMA'. + + + None. + + + A 'tuf.formats.KEY_SCHEMA' dictionary. + """ + + # Does 'keytype' have the correct format? + # This check will ensure 'keytype' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEYTYPE_SCHEMA.check_match(keytype) + + # Does 'key_value' have the correct format? + tuf.formats.KEYVAL_SCHEMA.check_match(key_value) + + if private is True and key_value['private']: + return {'keytype': keytype, 'keyval': key_value} + else: + public_key_value = {'public': key_value['public'], 'private': ''} + return {'keytype': keytype, 'keyval': public_key_value} + + + + + +def format_metadata_to_key(key_metadata): + """ + + Construct a TUF key dictionary (e.g., tuf.formats.RSAKEY_SCHEMA) + according to the keytype of 'key_metadata'. The dict returned by this + function has the exact format as the dict returned by one of the key + generations functions, like generate_ed25519_key(). The dict returned + has the form: + + {'keytype': keytype, + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyval': {'public': '...', + 'private': '...'}} + + For example, RSA key dictionaries in RSAKEY_SCHEMA format should be used by + modules storing a collection of keys, such as with keydb.py. RSA keys as + stored in metadata files use a different format, so this function should be + called if an RSA key is extracted from one of these metadata files and need + converting. The key generation functions create an entirely new key and + return it in the format appropriate for 'keydb.py'. + + >>> ed25519_key = generate_ed25519_key() + >>> key_val = ed25519_key['keyval'] + >>> keytype = ed25519_key['keytype'] + >>> ed25519_metadata = \ + format_keyval_to_metadata(keytype, key_val, private=True) + >>> ed25519_key_2 = format_metadata_to_key(ed25519_metadata) + >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) + True + >>> ed25519_key == ed25519_key_2 + True + + + key_metadata: + The TUF key dictionary as stored in Metadata files, conforming to + 'tuf.formats.KEY_SCHEMA'. It has the form: + + {'keytype': '...', + 'keyval': {'public': '...', + 'private': '...'}} + + + tuf.FormatError, if 'key_metadata' does not conform to + 'tuf.formats.KEY_SCHEMA'. + + + None. + + + In the case of an RSA key, a dictionary conformant to + 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'key_metadata' have the correct format? + # This check will ensure 'key_metadata' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEY_SCHEMA.check_match(key_metadata) + + # Construct the dictionary to be returned. + key_dict = {} + keytype = key_metadata['keytype'] + key_value = key_metadata['keyval'] + + # Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash + # The hash is in hexdigest form. + keyid = _get_keyid(keytype, key_value) + + # All the required key values gathered. Build 'key_dict'. + key_dict['keytype'] = keytype + key_dict['keyid'] = keyid + key_dict['keyval'] = key_value + + return key_dict + + + + + +def _get_keyid(keytype, key_value): + """Return the keyid for 'key_value'.""" + + # 'keyid' will be generated from an object conformant to KEY_SCHEMA, + # which is the format Metadata files (e.g., root.txt) store keys. + # 'format_keyval_to_metadata()' returns the object needed by _get_keyid(). + key_meta = format_keyval_to_metadata(keytype, key_value, private=False) + + # Convert the TUF key to JSON Canonical format, suitable for adding + # to digest objects. + key_update_data = tuf.formats.encode_canonical(key_meta) + + # Create a digest object and call update(), using the JSON + # canonical format of 'rskey_meta' as the update data. + digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) + digest_object.update(key_update_data) + + # 'keyid' becomes the hexadecimal representation of the hash. + keyid = digest_object.hexdigest() + + return keyid + + + + + +def _check_crypto_libraries(): + """ Ensure all the crypto libraries specified in tuf.conf are available. """ + + # The checks below all raise 'tuf.CryptoError' if the RSA and ED25519 + # crypto libraries specified in 'tuf.conf.py' are not supported or + # unavailable. The appropriate error message is added to the exception. + # The funcions of this module that depend on user-installed crypto libraries + # should call this private function to ensure the called routine does not fail + # with unpredictable exceptions in the event of a missing library. + # The supported and available lists checked are populated when 'tuf.keys.py' + # is imported. + if _RSA_CRYPTO_LIBRARY not in _SUPPORTED_RSA_CRYPTO_LIBRARIES: + message = 'The '+repr(_RSA_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.RSA_CRYPTO_LIBRARY" is not supported.\n'+ \ + 'Supported crypto libraries: '+repr(_SUPPORTED_RSA_CRYPTO_LIBRARIES)+'.' + raise tuf.CryptoError(message) + + if _ED25519_CRYPTO_LIBRARY not in _SUPPORTED_ED25519_CRYPTO_LIBRARIES: + message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+\ + ' in "tuf.conf.ED25519_CRYPTO_LIBRARY" is not supported.\n'+ \ + 'Supported crypto libraries: '+repr(_SUPPORTED_ED25519_CRYPTO_LIBRARIES)+'.' + raise tuf.CryptoError(message) + + if _RSA_CRYPTO_LIBRARY not in _available_crypto_libraries: + message = 'The '+repr(_RSA_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.RSA_CRYPTO_LIBRARY" could not be imported.' + raise tuf.CryptoError(message) + + if _ED25519_CRYPTO_LIBRARY not in _available_crypto_libraries: + message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+\ + ' in "tuf.conf.ED25519_CRYPTO_LIBRARY" could not be imported.' + raise tuf.CryptoError(message) + + + + + +def create_signature(key_dict, data): + """ + + Return a signature dictionary of the form: + {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'method': '...', + 'sig': '...'}. + + The signing process will use the private key in + key_dict['keyval']['private'] and 'data' to generate the signature. + + The following signature methods are supported: + + 'PyCrypto-PKCS#1 PSS' + RFC3447 - RSASSA-PSS + http://www.ietf.org/rfc/rfc3447. + + 'ed25519-python or 'ed25519-pynacl' + ed25519 - high-speed high security signatures + http://ed25519.cr.yp.to/ + + Which signature to generate is determined by the key type of 'key_dict' + and the available cryptography library specified in 'tuf.conf'. + + >>> ed25519_key = generate_ed25519_key() + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature = create_signature(ed25519_key, data) + >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) + True + >>> len(signature['sig']) + 128 + >>> rsa_key = generate_rsa_key(2048) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature = create_signature(rsa_key, data) + >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) + True + + + key_dict: + A dictionary containing the TUF keys. An example RSA key dict has the + form: + + {'keytype': 'rsa', + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are strings in PEM format. + + data: + Data object used by create_signature() to generate the signature. + + + tuf.FormatError, if 'key_dict' is improperly formatted. + + tuf.UnsupportedLibraryError, if an unsupported or unavailable library is + detected. + + TypeError, if 'key_dict' contains an invalid keytype. + + + The cryptography library specified in 'tuf.conf' called to perform the + actual signing routine. + + + A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. + """ + + # Does 'key_dict' have the correct format? + # This check will ensure 'key_dict' has the appropriate number of objects + # and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + # The key type of 'key_dict' must be either 'rsa' or 'ed25519'. + tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) + + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified + # in 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. + _check_crypto_libraries() + + # Signing the 'data' object requires a private key. + # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module), + # 'ed25519-pynacl' (i.e., 'nacl'), and 'ed25519-python (i.e., optimized pure + # python implementation of ed25519) are the only signing methods currently + # supported. + signature = {} + keytype = key_dict['keytype'] + public = key_dict['keyval']['public'] + private = key_dict['keyval']['private'] + keyid = key_dict['keyid'] + method = None + sig = None + + # Call the appropriate cryptography libraries for the supported key types, + # otherwise raise an exception. + if keytype == 'rsa': + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + sig, method = tuf.pycrypto_keys.create_rsa_signature(private, data) + else: + message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ + repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + elif keytype == 'ed25519': + public = binascii.unhexlify(public) + private = binascii.unhexlify(private) + if _ED25519_CRYPTO_LIBRARY == 'pynacl' \ + and 'pynacl' in _available_crypto_libraries: + sig, method = tuf.ed25519_keys.create_signature(public, private, + data, use_pynacl=True) + + # Fall back to using the optimized pure python implementation of ed25519. + else: + sig, method = tuf.ed25519_keys.create_signature(public, private, + data, use_pynacl=False) + else: + raise TypeError('Invalid key type.') + + # Build the signature dictionary to be returned. + # The hexadecimal representation of 'sig' is stored in the signature. + signature['keyid'] = keyid + signature['method'] = method + signature['sig'] = binascii.hexlify(sig) + + return signature + + + + + +def verify_signature(key_dict, signature, data): + """ + + Determine whether the private key belonging to 'key_dict' produced + 'signature'. verify_signature() will use the public key found in + 'key_dict', the 'method' and 'sig' objects contained in 'signature', + and 'data' to complete the verification. + + >>> ed25519_key = generate_ed25519_key() + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature = create_signature(ed25519_key, data) + >>> verify_signature(ed25519_key, signature, data) + True + >>> verify_signature(ed25519_key, signature, 'bad_data') + False + >>> rsa_key = generate_rsa_key() + >>> signature = create_signature(rsa_key, data) + >>> verify_signature(rsa_key, signature, data) + True + >>> verify_signature(rsa_key, signature, 'bad_data') + False + + + key_dict: + A dictionary containing the TUF keys and other identifying information. + If 'key_dict' is an RSA key, it has the form: + + {'keytype': 'rsa', + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are strings in PEM format. + + signature: + The signature dictionary produced by one of the key generation functions. + 'signature' has the form: + + {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'method': 'method', + 'sig': sig}. + + Conformant to 'tuf.formats.SIGNATURE_SCHEMA'. + + data: + Data object used by tuf.rsa_key.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + + tuf.FormatError, raised if either 'key_dict' or 'signature' are improperly + formatted. + + tuf.UnsupportedLibraryError, if an unsupported or unavailable library is + detected. + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported. + + + The cryptography library specified in 'tuf.conf' called to do the actual + verification. + + + Boolean. True if the signature is valid, False otherwise. + """ + + # Does 'key_dict' have the correct format? + # This check will ensure 'key_dict' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) + + # Does 'signature' have the correct format? + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + # Using the public key belonging to 'key_dict' + # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' + # was produced by key_dict's corresponding private key + # key_dict['keyval']['private']. + method = signature['method'] + sig = signature['sig'] + sig = binascii.unhexlify(sig) + public = key_dict['keyval']['public'] + keytype = key_dict['keytype'] + valid_signature = False + + # Call the appropriate cryptography libraries for the supported key types, + # otherwise raise an exception. + if keytype == 'rsa': + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + valid_signature = tuf.pycrypto_keys.verify_rsa_signature(sig, method, + public, data) + else: + message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ + repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + elif keytype == 'ed25519': + public = binascii.unhexlify(public) + if _RSA_CRYPTO_LIBRARY == 'pynacl' and \ + 'pynacl' in _available_crypto_libraries: + valid_signature = tuf.ed25519_keys.verify_signature(public, + method, sig, data, + use_pynacl=True) + # Fall back to the optimized pure python implementation of ed25519. + else: + valid_signature = tuf.ed25519_keys.verify_signature(public, + method, sig, data, + use_pynacl=False) + else: + raise TypeError('Unsupported key type.') + + return valid_signature + + + + + +def import_rsakey_from_encrypted_pem(encrypted_pem, password): + """ + + Generate public and private RSA keys, with modulus length 'bits'. In + addition, a keyid identifier for the RSA key is generated. The object + returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and has the + form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are strings in PEM format. + + Although the PyCrypto crytography library called sets a 1024-bit minimum + key size, generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + >>> rsa_key = generate_rsa_key() + >>> private = rsa_key['keyval']['private'] + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> rsa_key2 = import_key_from_encrypted_pem(encrypted_pem, passphrase) + >>> rsa_key == rsa_key2 + True + + + encrypted_pem: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + password: + + + tuf.FormatError, if 'bits' is improperly or invalid (i.e., not an integer + and not at least 2048). + + tuf.UnsupportedLibraryError, if any of the cryptography libraries specified + in 'tuf.conf.py' are unsupported or unavailable. + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'encrypted_pem' have the correct format? + # This check will ensure 'encrypted_pem' conforms to + # 'tuf.formats.PEMRSA_SCHEMA'. + tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified in + # 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. + _check_crypto_libraries() + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + public = None + private = None + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + public, private = \ + tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, + password) + else: + message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + # Generate the keyid of the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': public, + 'private': ''} + keyid = _get_keyid(keytype, key_value) + + # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA + # private key prior to adding 'key_value' to 'rsakey_dict'. + key_value['private'] = private + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def format_rsakey_from_pem(pem): + """ + + Generate public and private RSA keys, with modulus length 'bits'. In + addition, a keyid identifier for the RSA key is generated. The object + returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and has the + form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': ''}} + + The public and private keys are strings in PEM format. + + Although the PyCrypto crytography library called sets a 1024-bit minimum + key size, generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + >>> + >>> + >>> + + + pem: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + + tuf.FormatError, if 'bits' is improperly or invalid (i.e., not an integer + and not at least 2048). + + tuf.UnsupportedLibraryError, if any of the cryptography libraries specified + in 'tuf.conf.py' are unsupported or unavailable. + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'pem' have the correct format? + # This check will ensure 'pem' conforms to + # 'tuf.formats.PEMRSA_SCHEMA'. + tuf.formats.PEMRSA_SCHEMA.check_match(pem) + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + public = pem + + # Generate the keyid of the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': public, + 'private': ''} + keyid = _get_keyid(keytype, key_value) + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def create_rsa_encrypted_pem(private_key, passphrase): + """ + + Return a string in PEM format, where the private part of the RSA key is + encrypted. The private part of the RSA key is encrypted by the Triple + Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the + mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 + is used to strengthen 'passphrase'. + + https://en.wikipedia.org/wiki/Triple_DES + https://en.wikipedia.org/wiki/PBKDF2 + + >>> rsa_key = generate_rsa_key() + >>> private = rsa_key['keyval']['private'] + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> tuf.formats.PEMRSA_SCHEMA.matches(encrypted_pem) + True + + + private_key: + The private key string in PEM format. + + passphrase: + The passphrase, or password, to encrypt the private part of the RSA + key. 'passphrase' is not used directly as the encryption key, a stronger + encryption key is derived from it. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.CryptoError, if an RSA key in encrypted PEM format cannot be created. + + TypeError, 'private_key' is unset. + + + PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual + generation of the PEM-formatted output. + + + A string in PEM format, where the private RSA key is encrypted. + Conforms to 'tuf.formats.PEMRSA_SCHEMA'. + """ + + # Does 'private_key' have the correct format? + # This check will ensure 'private_key' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(private_key) + + # Does 'passphrase' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) + + encrypted_pem = None + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + encrypted_pem = \ + tuf.pycrypto_keys.create_rsa_encrypted_pem(private_key, passphrase) + else: + message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + return encrypted_pem + + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running 'keys.py' as a standalone module. + # python keys.py + import doctest + doctest.testmod() diff --git a/tuf/libtuf.py b/tuf/libtuf.py new file mode 100755 index 00000000..cfc14edd --- /dev/null +++ b/tuf/libtuf.py @@ -0,0 +1,2947 @@ +""" + + libtuf.py + + + Vladimir Diaz + + + October 19, 2013 + + + See LICENSE for licensing information. + + +""" + +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import os +import errno +import sys +import time +import getpass +import logging +import tempfile +import shutil +import json +import gzip + +import tuf +import tuf.formats +import tuf.util +import tuf.keydb +import tuf.roledb +import tuf.keys +import tuf.sig +import tuf.log + + +# See 'log.py' to learn how logging is handled in TUF. +logger = logging.getLogger('tuf.libtuf') + +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. 2048-bit keys +# are the recommended minimum and are good from the present through 2030. +DEFAULT_RSA_KEY_BITS = 3072 + +# The metadata filenames of the top-level roles. +ROOT_FILENAME = 'root.txt' +TARGETS_FILENAME = 'targets.txt' +RELEASE_FILENAME = 'release.txt' +TIMESTAMP_FILENAME = 'timestamp.txt' + +# The targets and metadata directory names. +METADATA_DIRECTORY_NAME = 'metadata.staged' +TARGETS_DIRECTORY_NAME = 'targets' + +# The supported file extensions of TUF metadata files. +METADATA_EXTENSIONS = ['.txt', '.txt.gz'] + +# The recognized compression extensions. +SUPPORTED_COMPRESSION_EXTENSIONS = ['.gz'] + +# Expiration date delta, in seconds, of the top-level roles. A metadata +# expiration date is set by taking the current time and adding the expiration +# seconds listed below. + +# Initial 'root.txt' expiration time of 1 year. +ROOT_EXPIRATION = 31556900 + +# Initial 'targets.txt' expiration time of 3 months. +TARGETS_EXPIRATION = 7889230 + +# Initial 'release.txt' expiration time of 1 week. +RELEASE_EXPIRATION = 604800 + +# Initial 'timestamp.txt' expiration time of 1 day. +TIMESTAMP_EXPIRATION = 86400 + +# The suffix added to metadata filenames of partially written metadata. +# Partial metadata may contain insufficient number of signatures and require +# multiple repository maintainers to independently sign them. +#PARTIAL_METADATA_SUFFIX = '.partial' + + +class Repository(object): + """ + + + + repository_directory: + + metadata_directory: + + targets_directory: + + + tuf.FormatError, if the arguments are improperly formatted. + + + + + Repository object. + """ + + def __init__(self, repository_directory, metadata_directory, targets_directory): + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + tuf.formats.PATH_SCHEMA.check_match(targets_directory) + + self._repository_directory = repository_directory + self._metadata_directory = metadata_directory + self._targets_directory = targets_directory + + # Set the top-level role objects. + self.root = Root() + self.release = Release() + self.timestamp = Timestamp() + self.targets = Targets(self._targets_directory, 'targets') + + + + def status(self): + """ + + + + None. + + + + + + + None. + """ + + root_roleinfo = tuf.roledb.get_roleinfo('root') + targets_roleinfo = tuf.roledb.get_roleinfo('targets') + release_roleinfo = tuf.roledb.get_roleinfo('release') + timestamp_roleinfo = tuf.roledb.get_roleinfo('timestamp') + temp_repository_directory = None + + try: + temp_repository_directory = tempfile.mkdtemp() + metadata_directory = os.path.join(temp_repository_directory, + METADATA_DIRECTORY_NAME) + os.mkdir(metadata_directory) + + filenames = get_metadata_filenames(metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + release_filename = filenames[RELEASE_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] + + # Delegated roles. + delegated_roles = tuf.roledb.get_delegated_rolenames('targets') + insufficient_keys = [] + insufficient_signatures = [] + for delegated_role in delegated_roles: + try: + _check_role_keys(delegated_role) + except tuf.InsufficientKeysError, e: + insufficient_keys.append(delegated_role) + continue + + roleinfo = tuf.roledb.get_roleinfo(delegated_role) + try: + write_delegated_metadata_file(temp_repository_directory, + self._targets_directory, + delegated_role, + roleinfo['version'], + roleinfo['expires'], + roleinfo['signing_keyids'], + roleinfo['paths'], + roleinfo['delegations'], + roleinfo['signatures'], + roleinfo['compressions'], + write_partial=False) + except tuf.Error, e: + insufficient_signatures.append(delegated_role) + if len(insufficient_keys): + message = 'Delegated roles with insufficient keys: '+ \ + repr(insufficient_keys) + print(message) + return + + if len(insufficient_signatures): + message = 'Delegated roles with insufficient signatures: '+ \ + repr(insufficient_signatures) + print(message) + return + + # Root role. + try: + _check_role_keys(self.root.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + root_metadata = generate_root_metadata(root_roleinfo['version'], + root_roleinfo['expires']) + signed_root = sign_metadata(root_metadata, root_roleinfo['signing_keyids'], + root_filename) + signed_root['signatures'].extend(root_roleinfo['signatures']) + root_status = tuf.sig.get_signature_status(signed_root, 'root') + message = repr(self.root.rolename)+' role contains '+ \ + repr(len(root_status['good_sigs']))+' / '+ \ + repr(root_status['threshold'])+' signatures.' + print(message) + + if tuf.sig.verify(signed_root, 'root'): + for compression in root_roleinfo['compressions']: + write_metadata_file(signed_root, root_filename, compression) + else: + return + + + # Targets role. + try: + _check_role_keys(self.targets.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + targets_metadata = generate_targets_metadata(self._targets_directory, + targets_roleinfo['paths'], + targets_roleinfo['version'], + targets_roleinfo['expires'], + targets_roleinfo['delegations']) + signed_targets = sign_metadata(targets_metadata, + targets_roleinfo['signing_keyids'], + targets_filename) + signed_targets['signatures'].extend(targets_roleinfo['signatures']) + targets_status = tuf.sig.get_signature_status(signed_targets, 'targets') + message = repr(self.targets.rolename)+' role contains '+ \ + repr(len(targets_status['good_sigs']))+' / '+ \ + repr(targets_status['threshold'])+' signatures.' + print(message) + + if tuf.sig.verify(signed_targets, 'targets'): + for compression in targets_roleinfo['compressions']: + write_metadata_file(signed_targets, targets_filename, compression) + else: + return + + + # Release role. + try: + _check_role_keys(self.release.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + release_metadata = generate_release_metadata(metadata_directory, + release_roleinfo['version'], + release_roleinfo['expires']) + signed_release = sign_metadata(release_metadata, + release_roleinfo['signing_keyids'], + release_filename) + signed_release['signatures'].extend(release_roleinfo['signatures']) + release_status = tuf.sig.get_signature_status(signed_release, 'release') + + message = repr(self.release.rolename)+' role contains '+ \ + repr(len(release_status['good_sigs']))+' / '+ \ + repr(release_status['threshold'])+' signatures.' + print(message) + if tuf.sig.verify(signed_release, 'release'): + for compression in release_roleinfo['compressions']: + write_metadata_file(signed_release, release_filename, compression) + else: + return + + # Timestamp role. + try: + _check_role_keys(self.timestamp.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + timestamp_metadata = generate_timestamp_metadata(release_filename, + timestamp_roleinfo['version'], + timestamp_roleinfo['expires'], + release_roleinfo['compressions']) + + signed_timestamp= sign_metadata(timestamp_metadata, + timestamp_roleinfo['signing_keyids'], + release_filename) + signed_timestamp['signatures'].extend(timestamp_roleinfo['signatures']) + timestamp_status = tuf.sig.get_signature_status(signed_timestamp, + 'timestamp') + + message = repr(self.timestamp.rolename)+' role contains '+ \ + repr(len(timestamp_status['good_sigs']))+' / '+ \ + repr(timestamp_status['threshold'])+' signatures.' + print(message) + if tuf.sig.verify(signed_timestamp, 'timestamp'): + for compressions in timestamp_roleinfo['compressions']: + write_metadata_file(signed_timestamp, timestamp_filename, compression) + else: + return + + finally: + shutil.rmtree(temp_repository_directory, ignore_errors=True) + + + + def write(self, write_partial=False): + """ + + Write all the JSON Metadata objects to their corresponding files. + + + None. + + + tuf.RepositoryError, if any of the top-level roles do not have a minimum + threshold of signatures. + + + Creates metadata files in the repository's metadata directory. + + + None. + """ + + # Does 'partial' have the correct format? + # Raise 'tuf.FormatError' if 'partial' is improperly formatted. + tuf.formats.TOGGLE_SCHEMA.check_match(write_partial) + + # At this point the tuf.keydb and tuf.roledb stores must be fully + # populated, otherwise write() throwns a 'tuf.Repository' exception if + # any of the top-level roles are missing signatures, keys, etc. + filenames = get_metadata_filenames(self._metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + release_filename = filenames[RELEASE_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] + + # Write the metadata files of all the delegated roles. + delegated_roles = tuf.roledb.get_delegated_rolenames('targets') + for delegated_role in delegated_roles: + roleinfo = tuf.roledb.get_roleinfo(delegated_role) + + write_delegated_metadata_file(self._repository_directory, + self._targets_directory, + delegated_role, + roleinfo['version'], roleinfo['expires'], + roleinfo['signing_keyids'], + roleinfo['paths'], + roleinfo['delegations'], + roleinfo['signatures'], + roleinfo['compressions'], + write_partial) + + # Generate the 'root.txt' metadata file. + roleinfo = tuf.roledb.get_roleinfo('root') + + root_metadata = generate_root_metadata(roleinfo['version'], + roleinfo['expires']) + signed_root = sign_metadata(root_metadata, roleinfo['signing_keyids'], + root_filename) + signed_root['signatures'].extend(roleinfo['signatures']) + if tuf.sig.verify(signed_root, 'root') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_root) + for compression in roleinfo['compressions']: + write_metadata_file(signed_root, root_filename, compression) + + else: + message = 'Not enough signatures for '+repr(root_filename) + raise tuf.Error(message) + + + # Generate the 'targets.txt' metadata file. + roleinfo = tuf.roledb.get_roleinfo('targets') + targets_metadata = generate_targets_metadata(self._targets_directory, + roleinfo['paths'], + roleinfo['version'], + roleinfo['expires'], + roleinfo['delegations']) + signed_targets = sign_metadata(targets_metadata, roleinfo['signing_keyids'], + targets_filename) + signed_targets['signatures'].extend(roleinfo['signatures']) + + if tuf.sig.verify(signed_targets, 'targets') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_targets) + for compression in roleinfo['compressions']: + write_metadata_file(signed_targets, targets_filename, compression) + + else: + message = 'Not enough signatures for '+repr(targets_filename) + raise tuf.Error(message) + + + # Generate the 'release.txt' metadata file. + roleinfo = tuf.roledb.get_roleinfo('release') + release_compressions = roleinfo['compressions'] + release_metadata = generate_release_metadata(self._metadata_directory, + roleinfo['version'], + roleinfo['expires']) + signed_release = sign_metadata(release_metadata, roleinfo['signing_keyids'], + release_filename) + signed_release['signatures'].extend(roleinfo['signatures']) + + if tuf.sig.verify(signed_release, 'release') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_release) + for compression in roleinfo['compressions']: + write_metadata_file(signed_release, release_filename, compression) + + else: + message = 'Not enough signatures for '+repr(release_filename) + raise tuf.Error(message) + + + # Generate the 'timestamp.txt' metadata file. + roleinfo = tuf.roledb.get_roleinfo('timestamp') + timestamp_metadata = generate_timestamp_metadata(release_filename, + roleinfo['version'], + roleinfo['expires'], + release_compressions) + signed_timestamp = sign_metadata(timestamp_metadata, + roleinfo['signing_keyids'], + timestamp_filename) + signed_timestamp['signatures'].extend(roleinfo['signatures']) + + if tuf.sig.verify(signed_timestamp, 'timestamp') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_timestamp) + for compression in roleinfo['compressions']: + write_metadata_file(signed_timestamp, timestamp_filename, compression) + + else: + message = 'Not enough signatures for '+repr(timestamp_filename) + raise tuf.Error(message) + + _delete_obsolete_metadata(self._metadata_directory) + + + + def get_filepaths_in_directory(self, files_directory, recursive_walk=False, + followlinks=True): + """ + + Walk the given files_directory to build a list of target files in it. + + + files_directory: + The path to a directory of target files. + + recursive_walk: + To recursively walk the directory, set recursive_walk=True. + + followlinks: + To follow symbolic links, set followlinks=True. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.Error, if + Python IO exceptions. + + + None. + + + A list of absolute paths to target files in the given files_directory. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(files_directory) + tuf.formats.TOGGLE_SCHEMA.check_match(recursive_walk) + tuf.formats.TOGGLE_SCHEMA.check_match(followlinks) + + if not os.path.isdir(files_directory): + message = repr(files_directory)+' is not a directory.' + raise tuf.Error(message) + + targets = [] + + # FIXME: We need a way to tell Python 2, but not Python 3, to return + # filenames in Unicode; see #61 and: + # http://docs.python.org/2/howto/unicode.html#unicode-filenames + for dirpath, dirnames, filenames in os.walk(files_directory, + followlinks=followlinks): + for filename in filenames: + full_target_path = os.path.join(dirpath, filename) + targets.append(full_target_path) + + # Prune the subdirectories to walk right now if we do not wish to + # recursively walk files_directory. + if recursive_walk is False: + del dirnames[:] + + return targets + + + + + +class Metadata(object): + """ + + Write all the Metadata objects' JSON contents to the corresponding files. + + + + + + + + + """ + + def __init__(self): + self._rolename = None + + + + def add_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + + + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + try: + tuf.keydb.add_key(key) + except tuf.KeyAlreadyExistsError, e: + pass + + keyid = key['keyid'] + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if keyid not in roleinfo['keyids']: + roleinfo['keyids'].append(keyid) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + + + + def remove_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + keyid = key['keyid'] + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if keyid in roleinfo['keyids']: + roleinfo['keyids'].remove(keyid) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + + + + def load_signing_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + if not len(key['keyval']['private']): + message = 'The private key is unavailable.' + raise tuf.Error(message) + + try: + tuf.keydb.add_key(key) + except tuf.KeyAlreadyExistsError, e: + tuf.keydb.remove_key(key['keyid']) + tuf.keydb.add_key(key) + + # Update 'signing_keys' in roledb. + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if key['keyid'] not in roleinfo['signing_keyids']: + roleinfo['signing_keyids'].append(key['keyid']) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def unload_signing_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + # Update 'signing_keys' in roledb. + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if key['keyid'] in roleinfo['signing_keyids']: + roleinfo['signing_keyids'].remove(key['keyid']) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def add_signature(self, signature): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if 'signatures' not in roleinfo: + roleinfo['signatures'] = [] + + if signature not in roleinfo['signatures']: + roleinfo['signatures'].append(signature) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def remove_signature(self, signature): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if signature in roleinfo['signatures']: + roleinfo['signatures'].remove(signature) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + @property + def signatures(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + signatures = roleinfo['signatures'] + + return signatures + + + + @property + def keys(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + keyids = roleinfo['keyids'] + + return keyids + + + + @property + def rolename(self): + """ + """ + + return self._rolename + + + + @property + def version(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + version = roleinfo['version'] + + return version + + + + @version.setter + def version(self, version): + """ + + + >>> + >>> + >>> + + + threshold: + tuf.formats.THRESHOLD_SCHEMA + + + tuf.FormatError, if the argument is improperly formatted. + + + Modifies the threshold attribute of the Repository object. + + + None. + """ + + # Does 'version' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['version'] = version + + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + + + + @property + def threshold(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + threshold = roleinfo['threshold'] + + return threshold + + + + @threshold.setter + def threshold(self, threshold): + """ + + + >>> + >>> + >>> + + + threshold: + tuf.formats.THRESHOLD_SCHEMA + + + tuf.FormatError, if the argument is improperly formatted. + + + Modifies the threshold attribute of the Repository object. + + + None. + """ + + # Does 'threshold' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) + + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + roleinfo['threshold'] = threshold + + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + self._threshold = threshold + + + @property + def expiration(self): + """ + + + >>> + >>> + >>> + + + None. + + + None. + + + None. + + + The role's expiration datetime, conformant to tuf.formats.DATETIME_SCHEMA. + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + return roleinfo['expires'] + + + + @expiration.setter + def expiration(self, expiration_datetime_utc): + """ + + + >>> + >>> + >>> + + + expiration_datetime_utc: + tuf.formats.DATETIME_SCHEMA + + + tuf.FormatError, if the argument is improperly formatted. + + + Modifies the expiration attribute of the Repository object. + + + None. + """ + + # Does 'expiration_datetime_utc' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.DATETIME_SCHEMA.check_match(expiration_datetime_utc) + + expiration_datetime_utc = expiration_datetime_utc+' UTC' + try: + unix_timestamp = tuf.formats.parse_time(expiration_datetime_utc) + except (tuf.FormatError, ValueError), e: + message = 'Invalid datetime argument: '+repr(expiration_datetime_utc) + raise tuf.FormatError(message) + + if unix_timestamp < time.time(): + message = 'The expiration date must occur after the current date.' + raise tuf.FormatError(message) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['expires'] = expiration_datetime_utc + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + @property + def signing_keys(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + signing_keyids = roleinfo['signing_keyids'] + + return signing_keyids + + + + @property + def compressions(self): + """ + """ + + tuf.roledb.get_roleinfo(self.rolename) + compressions = roleinfo['compressions'] + + return compressions + + + + @compressions.setter + def compressions(self, compression_list): + """ + """ + + # Does 'compression_name' have the correct format? + # Raise 'tuf.FormatError' if it is improperly formatted. + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compression_list) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['compressions'].extend(compression_list) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + + +class Root(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + + super(Root, self).__init__() + + self._rolename = 'root' + + expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) + + roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, + 'signatures': [], 'version': 1, 'compressions': [''], + 'expires': expiration} + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass + + + + + +class Timestamp(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + + super(Timestamp, self).__init__() + + self._rolename = 'timestamp' + + expiration = tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) + + roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, + 'signatures': [], 'version': 1, 'compressions': [''], + 'expires': expiration} + + try: + tuf.roledb.add_role(self.rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass + + + + + +class Release(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + + super(Release, self).__init__() + + self._rolename = 'release' + + expiration = tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) + + roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, + 'signatures': [], 'version': 1, 'compressions': [''], + 'expires': expiration} + + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass + + + def write_partial(self): + pass + + + + + +class Targets(Metadata): + """ + + + >>> + >>> + >>> + + + targets_directory: + The targets directory of the Repository object. + + + tuf.FormatError, if the targets directory argument is improerly formatted. + + + Mofifies the roleinfo of the targets role in 'tuf.roledb'. + + + None. + """ + + def __init__(self, targets_directory, rolename, roleinfo=None): + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any are improperly formatted. + tuf.formats.PATH_SCHEMA.check_match(targets_directory) + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + + if roleinfo is not None: + tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) + + super(Targets, self).__init__() + self._targets_directory = targets_directory + self._rolename = rolename + self._target_files = [] + + expiration = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) + + if roleinfo is None: + roleinfo = {'keyids': [], + 'signing_keyids': [], + 'threshold': 1, + 'version': 1, + 'compressions': [''], + 'expires': expiration, + 'signatures': [], + 'paths': [], + 'path_hash_prefixes': [], + 'delegations': {'keys': {}, + 'roles': []}} + + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass + + + + @property + def target_files(self): + """ + + + >>> + >>> + >>> + + + targets_directory: + The targets directory of the Repository object. + + + tuf.FormatError, if the targets directory argument is improerly formatted. + + + Mofifies the roleinfo of the targets role in 'tuf.roledb'. + + + None. + """ + + target_files = tuf.roledb.get_roleinfo(self._rolename)['paths'] + + return target_files + + + + def add_target(self, filepath): + """ + + Add a filepath (relative to 'self.targets_directory') to the Targets + object. This function does not actually create 'filepath' on the file + system. 'filepath' must already exist on the file system. + + Support regular expresssions? + + >>> + >>> + >>> + + + filepath: + + + tuf.FormatError, if 'filepath' is improperly formatted. + + + Adds 'filepath' to this role's list of targets. This role's + 'tuf.roledb.py' is also updated. + + + None. + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + filepath = os.path.abspath(filepath) + + if not os.path.commonprefix([self._targets_directory, filepath]) == \ + self._targets_directory: + message = repr(filepath)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + # TODO: Ensure 'filepath' is an allowed target path according to the parent's + # delegation. + """ + for child_target in actual_child_targets: + for allowed_child_path in allowed_child_paths: + prefix = os.path.commonprefix([child_target, allowed_child_path]) + if prefix == allowed_child_path: + break + """ + + # Add 'filepath' (i.e., relative to the targets directory) to the role's + # list of targets. + if os.path.isfile(filepath): + + # Update the role's 'tuf.roledb.py' entry and 'self._target_files'. + targets_directory_length = len(self._targets_directory) + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + relative_path = filepath[targets_directory_length+1:] + if relative_path not in roleinfo['paths']: + roleinfo['paths'].append(relative_path) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + + else: + message = repr(filepath)+' is not a valid file.' + raise tuf.Error(message) + + + + def add_targets(self, list_of_targets): + """ + + Add a list of target filepaths (all relative to 'self.targets_directory'). + This function does not actually create files on the file system. The + list of target must already exist. + + >>> + >>> + >>> + + + list_of_targets: + + + + + + + None. + """ + + # Does 'list_of_targets' have the correct format? + # Raise 'tuf.FormatError' if it is improperly formatted. + tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + + # TODO: Ensure list of targets allowed paths according to the parent's + # delegation. + + # Update the tuf.roledb entry. + targets_directory_length = len(self._targets_directory) + absolute_list_of_targets = [] + relative_list_of_targets = [] + + for target in list_of_targets: + filepath = os.path.abspath(target) + + if not os.path.commonprefix([self._targets_directory, filepath]) == \ + self._targets_directory: + message = repr(filepath)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + if os.path.isfile(filepath): + absolute_list_of_targets.append(filepath) + relative_list_of_targets.append(filepath[targets_directory_length+1:]) + else: + message = repr(filepath)+' is not a valid file.' + raise tuf.Error(message) + + # Update the role's target_files and its 'tuf.roledb.py' entry. + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + roleinfo['paths'].extend(relative_list_of_targets) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def remove_target(self, filepath): + """ + + Takes a filepath relative to the targets directory. Regular expresssions + would be useful here. + + >>> + >>> + >>> + + + filepath: + Relative to the targets directory. + + + tuf.FormatError, if 'filepath' is improperly formatted. + + tuf.Error, if 'filepath' is not under the targets directory. + + + Modifies the target role's 'tuf.roledb.py' entry. + + + None. + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.RELPATH_SCHEMA.check_match(filepath) + + filepath = os.path.abspath(filepath) + targets_directory_length = len(self._targets_directory) + + # Ensure 'filepath' is under the targets directory. + if not os.path.commonprefix([self._targets_directory, filepath]) == \ + self._targets_directory: + message = repr(filepath)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + relative_filepath = filepath[targets_directory_length+1:] + + fileinfo = tuf.roledb.get_roleinfo(self.rolename) + if relative_filepath in fileinfo['paths']: + fileinfo['paths'].remove(relative_filepath) + + tuf.roledb.update_roleinfo(self.rolename, fileinfo) + + + + def delegate(self, rolename, public_keys, list_of_targets, + threshold=1, restricted_paths=None, path_hash_prefixes=None): + """ + + 'targets' is a list of target filepaths, and can be empty. + + >>> + >>> + >>> + + + rolename: + + public_keys: + + list_of_targets: + + expiration: + + restricted_paths: + + + tuf.FormatError, if any of the arguments are improperly formatted. + + + A new Target object is created for 'rolename' that is accessible to the + caller (i.e., targets.unclaimed.). The 'tuf.keydb.py' and + 'tuf.roledb.py' stores are updated with 'public_keys'. + + + None. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + tuf.formats.ANYKEYLIST_SCHEMA.check_match(public_keys) + tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) + if restricted_paths is not None: + tuf.formats.RELPATHS_SCHEMA.check_match(restricted_paths) + if path_hash_prefixes is not None: + tuf.formats.PATH_HASH_PREFIXES_SCHEMA.check_match(path_hash_prefixes) + + full_rolename = self._rolename+'/'+rolename + keyids = [] + keydict = {} + + # Add public keys to tuf.keydb + for key in public_keys: + + try: + tuf.keydb.add_key(key) + except tuf.KeyAlreadyExistsError, e: + pass + + keyid = key['keyid'] + key_metadata_format = tuf.keys.format_keyval_to_metadata(key['keytype'], + key['keyval']) + keydict.update({keyid: key_metadata_format}) + keyids.append(keyid) + + # Validate 'list_of_targets'. + relative_targetpaths = [] + targets_directory_length = len(self._targets_directory) + + for target in list_of_targets: + target = os.path.abspath(target) + if not os.path.commonprefix([self._targets_directory, target]) == \ + self._targets_directory: + message = repr(target)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + relative_targetpaths.append(target[targets_directory_length+1:]) + + # Validate 'restricted_paths'. + relative_restricted_paths = [] + + if restricted_paths is not None: + for target in restricted_paths: + target = os.path.abspath(target) + if not os.path.commonprefix([self._targets_directory, target]) == \ + self._targets_directory: + message = repr(target)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + relative_restricted_paths.append(target[targets_directory_length+1:]) + + # Add role to 'tuf.roledb.py'. + expiration = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) + roleinfo = {'name': full_rolename, + 'keyids': keyids, + 'signing_keyids': [], + 'threshold': threshold, + 'version': 1, + 'compressions': [''], + 'expires': expiration, + 'signatures': [], + 'paths': relative_targetpaths, + 'delegations': {'keys': {}, + 'roles': []}} + #tuf.roledb.add_role(full_rolename, roleinfo) + new_targets_object = Targets(self._targets_directory, full_rolename, + roleinfo ) + + # Update the 'delegations' field of the current role. + current_roleinfo = tuf.roledb.get_roleinfo(self.rolename) + current_roleinfo['delegations']['keys'].update(keydict) + + # A ROLE_SCHEMA object requires only 'keyids', 'threshold', and 'paths'. + roleinfo = {'name': full_rolename, + 'keyids': roleinfo['keyids'], + 'threshold': roleinfo['threshold'], + 'paths': roleinfo['paths']} + if restricted_paths is not None: + roleinfo['paths'] = relative_restricted_paths + if path_hash_prefixes is not None: + roleinfo['path_hash_prefixes'] = path_hash_prefixes + + current_roleinfo['delegations']['roles'].append(roleinfo) + tuf.roledb.update_roleinfo(self.rolename, current_roleinfo) + + # Update 'new_targets_object' attributes. + for key in public_keys: + new_targets_object.add_key(key) + + self.__setattr__(rolename, new_targets_object) + + + + def revoke(self, rolename): + """ + + + >>> + >>> + >>> + + + rolename: + Not the full rolename ('Django' in 'targets/unclaimed/Django') of the + role the parent role (this role) wants to revoke. + + + tuf.FormatError, if 'rolename' is improperly formatted. + + + + + None. + """ + + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + + # Remove from this Target's delegations dict. + full_rolename = self.rolename+'/'+rolename + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + for role in roleinfo['delegations']['roles']: + if role['name'] == full_rolename: + roleinfo['delegations']['roles'].remove(role) + + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + # Remove from 'tuf.roledb.py'. The delegated roles of 'rolename' are also + # removed. + tuf.roledb.remove_role(full_rolename) + + # Remove the rolename attribute from the current role. + self.__delattr__(rolename) + + + @property + def delegations(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + delegations = roleinfo['delegations'] + + return delegations + + + +def _prompt(message, result_type=str): + """ + Prompt the user for input by printing 'message', converting + the input to 'result_type', and returning the value to the + caller. + """ + + return result_type(raw_input(message)) + + + + + +def _get_password(prompt='Password: ', confirm=False): + """ + Return the password entered by the user. If 'confirm' + is True, the user is asked to enter the previously + entered password once again. If they match, the + password is returned to the caller. + """ + + while True: + # getpass() prompts the user for a password without echoing + # the user input. + password = getpass.getpass(prompt, sys.stderr) + if not confirm: + return password + password2 = getpass.getpass('Confirm: ', sys.stderr) + if password == password2: + return password + else: + print('Mismatch; try again.') + + + + + +def _check_directory(directory): + """ + + Ensure 'directory' is valid and it exists. This is not a security check, + but a way for the caller to determine the cause of an invalid directory + provided by the user. If the directory argument is valid, it is returned + normalized and as an absolute path. + + + directory: + The directory to check. + + + tuf.Error, if 'directory' could not be validated. + + tuf.FormatError, if 'directory' is not properly formatted. + + + None. + + + The normalized absolutized path of 'directory'. + """ + + # Does 'directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(directory) + + # Check if the directory exists. + if not os.path.isdir(directory): + raise tuf.Error(repr(directory)+' directory does not exist') + + directory = os.path.abspath(directory) + + return directory + + + + + +def _check_role_keys(rolename): + """ + rolename: + full rolename. + """ + + roleinfo = tuf.roledb.get_roleinfo(rolename) + total_keyids = len(roleinfo['keyids']) + threshold = roleinfo['threshold'] + total_signatures = len(roleinfo['signatures']) + total_signing_keys = len(roleinfo['signing_keyids']) + + if total_keyids < threshold: + message = repr(rolename)+' role contains '+repr(total_keyids)+' / '+ \ + repr(threshold)+' public keys.' + raise tuf.InsufficientKeysError(message) + + if total_signatures == 0 and total_signing_keys < threshold: + message = repr(rolename)+' role contains '+repr(total_signing_keys)+' / '+ \ + repr(threshold)+' signing keys.' + raise tuf.InsufficientKeysError(message) + + + + + +def _remove_invalid_signatures(signable): + """ + Remove invalid signatures from 'signable'. + 'signable' may contain signatures (invalid) from previous versions + of the metadata and loaded with load_repository(). 'signable' is modified. + """ + + for signature in signable['signatures']: + data = tuf.formats.encode_canonical(signable['signed']) + keyid = signature['keyid'] + key = None + + # Remove 'signature' from 'signable' if the listed keyid does not exist. + try: + key = tuf.keydb.get_key(keyid) + except tuf.UnknownKeyError, e: + signable['signatures'].remove(signature) + + # Remove signature from 'signable' if it is invalid. + if not tuf.keys.verify_signature(key, signature, data): + signable['signatures'].remove(signature) + + + + + +def _delete_obsolete_metadata(metadata_directory): + """ + """ + + targets_metadata = os.path.join(metadata_directory, 'targets') + + if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): + for directory_path, junk_directories, files in os.walk(targets_metadata): + + # 'files' here is a list of target file names. + for basename in files: + metadata_path = os.path.join(directory_path, basename) + metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) + for metadata_extension in METADATA_EXTENSIONS: + if metadata_name.endswith(metadata_extension): + metadata_name = metadata_name[:-len(metadata_extension)] + if not tuf.roledb.role_exists(metadata_name): + os.remove(metadata_path) + + + + + +def create_new_repository(repository_directory): + """ + + Create a new repository with barebones metadata and return a Repository + object. + + + repository_directory: + + + + + + + libtuf.Repository object. + """ + + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + + # Create the repository, metadata, and target directories. + repository_directory = os.path.abspath(repository_directory) + metadata_directory = None + targets_directory = None + + # Try to create 'repository_directory' if it does not exist. + try: + os.makedirs(repository_directory) + # 'OSError' raised if the leaf directory already exists or cannot be created. + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + # + metadata_directory = \ + os.path.join(repository_directory, METADATA_DIRECTORY_NAME) + targets_directory = \ + os.path.join(repository_directory, TARGETS_DIRECTORY_NAME) + + # Try to create the metadata directory that will hold all of the metadata + # files, such as 'root.txt' and 'release.txt'. + try: + message = 'Creating '+repr(metadata_directory) + logger.info(message) + os.mkdir(metadata_directory) + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + # Try to create the targets directory that will hold all of the target files. + try: + message = 'Creating '+repr(targets_directory) + logger.info(message) + os.mkdir(targets_directory) + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + repository = Repository(repository_directory, metadata_directory, + targets_directory) + + return repository + + + +def load_repository(repository_directory): + """ + + Return a repository object containing the contents of metadata files loaded + from the repository. + + + repository_directory: + + + tuf.FormatError, if 'repository_directory' or any of the metadata files + are improperly formatted. Also raised if, at a minimum, the Root role + cannot be found. + + + All the metadata files found in the repository are loaded and their contents + stored in a libtuf.Repository object. + + + libtuf.Repository object. + """ + + # Does 'repository_directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + + # Load top-level metadata. + repository_directory = os.path.abspath(repository_directory) + metadata_directory = os.path.join(repository_directory, + METADATA_DIRECTORY_NAME) + targets_directory = os.path.join(repository_directory, + TARGETS_DIRECTORY_NAME) + + repository = None + + filenames = get_metadata_filenames(metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + release_filename = filenames[RELEASE_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] + + root_metadata = None + targets_metadata = None + release_metadata = None + timestamp_metadata = None + + # ROOT.txt + if os.path.exists(root_filename): + + # Initialize the key and role metadata of the top-level roles. + signable = tuf.util.load_json_file(root_filename) + tuf.formats.check_signable_object_format(signable) + root_metadata = signable['signed'] + tuf.keydb.create_keydb_from_root_metadata(root_metadata) + tuf.roledb.create_roledb_from_root_metadata(root_metadata) + + roleinfo = tuf.roledb.get_roleinfo('root') + roleinfo['signatures'] = [] + for signature in signable['signatures']: + if signature not in roleinfo['signatures']: + roleinfo['signatures'].append(signature) + + if os.path.exists(root_filename+'.gz'): + roleinfo['compressions'].append('gz') + tuf.roledb.update_roleinfo('root', roleinfo) + + else: + message = 'Cannot load the required root file: '+repr(root_filename) + raise tuf.RepositoryError(message) + + repository = Repository(repository_directory, metadata_directory, + targets_directory) + + # TARGETS.txt + if os.path.exists(targets_filename): + signable = tuf.util.load_json_file(targets_filename) + tuf.formats.check_signable_object_format(signable) + targets_metadata = signable['signed'] + + for signature in signable['signatures']: + repository.targets.add_signature(signature) + + # Update 'targets.txt' in 'tuf.roledb.py' + roleinfo = tuf.roledb.get_roleinfo('targets') + roleinfo['paths'] = targets_metadata['targets'].keys() + roleinfo['version'] = targets_metadata['version'] + roleinfo['expires'] = targets_metadata['expires'] + roleinfo['delegations'] = targets_metadata['delegations'] + if os.path.exists(targets_filename+'.gz'): + roleinfo['compressions'].append('gz') + tuf.roledb.update_roleinfo('targets', roleinfo) + + # Add the keys specified in the delegations field of the Targets role. + # TODO: Delegated role's are only missing the threshold value, which the + # parent role sets. Remember to request threshold value from parent role. + for key_metadata in targets_metadata['delegations']['keys'].values(): + key_object = tuf.keys.format_metadata_to_key(key_metadata) + tuf.keydb.add_key(key_object) + + for role in targets_metadata['delegations']['roles']: + rolename = role['name'] + roleinfo = {'name': role['name'], + 'keyids': role['keyids'], + 'threshold': role['threshold'], + 'signing_keyids': [], + 'signatures': [], + 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.add_role(rolename, roleinfo) + + else: + pass + + + # RELEASE.txt + if os.path.exists(release_filename): + signable = tuf.util.load_json_file(release_filename) + tuf.formats.check_signable_object_format(signable) + release_metadata = signable['signed'] + for signature in signable['signatures']: + repository.release.add_signature(signature) + + roleinfo = tuf.roledb.get_roleinfo('release') + roleinfo['expires'] = release_metadata['expires'] + roleinfo['version'] = release_metadata['version'] + if os.path.exists(release_filename+'.gz'): + roleinfo['compressions'].append('gz') + tuf.roledb.update_roleinfo('release', roleinfo) + + else: + pass + + + # TIMESTAMP.txt + if os.path.exists(timestamp_filename): + signable = tuf.util.load_json_file(timestamp_filename) + timestamp_metadata = signable['signed'] + for signature in signable['signatures']: + repository.timestamp.add_signature(signature) + + roleinfo = tuf.roledb.get_roleinfo('timestamp') + roleinfo['expires'] = timestamp_metadata['expires'] + roleinfo['version'] = timestamp_metadata['version'] + if os.path.exists(timestamp_filename+'.gz'): + roleinfo['compressions'].append('gz') + tuf.roledb.update_roleinfo('timestamp', roleinfo) + + else: + pass + + # Load delegated targets metadata. + # Walk the 'targets/' directory and generate the file info for all + # the files listed there. This information is stored in the 'meta' + # field of the release metadata object. + targets_objects = {} + targets_objects['targets'] = repository.targets + targets_metadata_directory = os.path.join(metadata_directory, + TARGETS_DIRECTORY_NAME) + if os.path.exists(targets_metadata_directory) and \ + os.path.isdir(targets_metadata_directory): + for root, directories, files in os.walk(targets_metadata_directory): + # 'files' here is a list of target file names. + for basename in files: + metadata_path = os.path.join(root, basename) + metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) + extension_length = len(METADATA_EXTENSION) + metadata_name = metadata_name[:-extension_length] + + signable = None + try: + signable = tuf.util.load_json_file(metadata_path) + except (ValueError, IOError), e: + continue + + metadata_object = signable['signed'] + + roleinfo = tuf.roledb.get_roleinfo(metadata_name) + roleinfo['signatures'].extend(signable['signatures']) + roleinfo['version'] = metadata_object['version'] + roleinfo['expires'] = metadata_object['expires'] + roleinfo['paths'] = metadata_object['targets'].keys() + + if os.path.exists(timestamp_filename+'.gz'): + roleinfo['compressions'].append('gz') + tuf.roledb.update_roleinfo(metadata_name, roleinfo) + + new_targets_object = Targets(targets_directory, metadata_name, roleinfo) + targets_object = targets_objects[tuf.roledb.get_parent_rolename(metadata_name)] + targets_object.__setattr__(os.path.basename(metadata_name), + new_targets_object) + + # Add the keys specified in the delegations field of the Targets role. + for key_metadata in metadata_object['delegations']['keys'].values(): + key_object = tuf.keys.format_metadata_to_key(key_metadata) + try: + tuf.keydb.add_key(key_object) + except tuf.KeyAlreadyExistsError, e: + pass + + for role in metadata_object['delegations']['roles']: + rolename = role['name'] + roleinfo = {'name': role['name'], + 'keyids': role['keyids'], + 'threshold': role['threshold'], + 'signing_keyids': [], + 'signatures': [], + 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.update_roleinfo(rolename, roleinfo) + + return repository + + + + + +def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, + password=None): + """ + + + + filepath: + The public and private key files are saved to .pub, , + respectively. + + bits: + The number of bits of the generated RSA key. + + password: + + + tuf.FormatError, if the arguments are improperly formatted. + + + Writes key files to '' and '.pub'. + + + None. + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + # Does 'bits' have the correct format? + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # If the caller does not provide a password argument, prompt for one. + if password is None: + message = 'Enter a password for the RSA key: ' + password = _get_password(message, confirm=True) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + rsa_key = tuf.keys.generate_rsa_key(bits) + public = rsa_key['keyval']['public'] + private = rsa_key['keyval']['private'] + encrypted_pem = tuf.keys.create_rsa_encrypted_pem(private, password) + + # Write public key (i.e., 'public', which is in PEM format) to + # '.pub'. + tuf.util.ensure_parent_dir(filepath) + + with open(filepath+'.pub', 'w') as file_object: + file_object.write(public) + + # Write the private key in encrypted PEM format to ''. + with open(filepath, 'w') as file_object: + file_object.write(encrypted_pem) + + + + + +def import_rsa_privatekey_from_file(filepath, password=None): + """ + + + + filepath: + file, an RSA encrypted PEM file. + + password: + The passphrase to decrypt 'filepath'. + + + + + + + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + # If the caller does not provide a password argument, prompt for one. + if password is None: + message = 'Enter a password for the RSA key: ' + password = _get_password(message, confirm=True) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + encrypted_pem = None + + with open(filepath, 'rb') as file_object: + encrypted_pem = file_object.read() + + rsa_key = tuf.keys.import_rsakey_from_encrypted_pem(encrypted_pem, password) + + return rsa_key + + + + + +def import_rsa_publickey_from_file(filepath): + """ + + If the RSA PEM in 'filepath' contains a private key, it is discarded. + + + filepath: + .pub file, an RSA PEM file. + + + tuf.FormatError, if 'filepath' is improperly formatted. + + + + + An RSA key object conformant to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + with open(filepath, 'rb') as file_object: + rsa_pubkey_pem = file_object.read() + + rsakey_dict = tuf.keys.format_rsakey_from_pem(rsa_pubkey_pem) + + return rsakey_dict + + + + + +def expiration_datetime_utc(input_datetime_utc): + """ + + TODO: return 'input_datetime_utc' in ISO 8601 format. + + + input_datetime_utc: + + + tuf.FormatError, if 'input_datetime_utc' is invalid. + + + None. + + + """ + if not tuf.formats.DATETIME_SCHEMA.matches(input_datetime_utc): + message = 'The datetime argument must be in "YYYY-MM-DD HH:MM:SS" format.' + raise tuf.FormatError(message) + try: + unix_timestamp = tuf.formats.parse_time(input_datetime_utc+' UTC') + except (tuf.FormatError, ValueError), e: + raise tuf.FormatError('Invalid date entered.') + + if unix_timestamp < time.time(): + message = 'The expiration date must occur after the current date.' + raise tuf.FormatError(message) + + return input_datetime_utc+' UTC' + + + + +def get_metadata_filenames(metadata_directory=None): + """ + + Return a dictionary containing the filenames of the top-level roles. + If 'metadata_directory' is set to 'metadata', the dictionary + returned would contain: + + filenames = {'root': 'metadata/root.txt', + 'targets': 'metadata/targets.txt', + 'release': 'metadata/release.txt', + 'timestamp': 'metadata/timestamp.txt'} + + If the metadata directory is not set by the caller, the current + directory is used. + + + metadata_directory: + The directory containing the metadata files. + + + tuf.FormatError, if 'metadata_directory' is improperly formatted. + + + None. + + + A dictionary containing the expected filenames of the top-level + metadata files, such as 'root.txt' and 'release.txt'. + """ + + if metadata_directory is None: + metadata_directory = '.' + + # Does 'metadata_directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + + filenames = {} + filenames[ROOT_FILENAME] = os.path.join(metadata_directory, ROOT_FILENAME) + filenames[TARGETS_FILENAME] = os.path.join(metadata_directory, TARGETS_FILENAME) + filenames[RELEASE_FILENAME] = os.path.join(metadata_directory, RELEASE_FILENAME) + filenames[TIMESTAMP_FILENAME] = os.path.join(metadata_directory, TIMESTAMP_FILENAME) + + return filenames + + + + + +def get_metadata_file_info(filename): + """ + + Retrieve the file information for 'filename'. The object returned + conforms to 'tuf.formats.FILEINFO_SCHEMA'. The information + generated for 'filename' is stored in metadata files like 'targets.txt'. + The fileinfo object returned has the form: + fileinfo = {'length': 1024, + 'hashes': {'sha256': 1233dfba312, ...}, + 'custom': {...}} + + + filename: + The metadata file whose file information is needed. + + + tuf.FormatError, if 'filename' is improperly formatted. + + tuf.Error, if 'filename' doesn't exist. + + + The file is opened and information about the file is generated, + such as file size and its hash. + + + A dictionary conformant to 'tuf.formats.FILEINFO_SCHEMA'. This + dictionary contains the length, hashes, and custom data about + the 'filename' metadata file. + """ + + # Does 'filename' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filename) + + if not os.path.isfile(filename): + message = repr(filename)+' is not a file.' + raise tuf.Error(message) + + # Note: 'filehashes' is a dictionary of the form + # {'sha256': 1233dfba312, ...}. 'custom' is an optional + # dictionary that a client might define to include additional + # file information, such as the file's author, version/revision + # numbers, etc. + filesize, filehashes = tuf.util.get_file_details(filename) + custom = None + + return tuf.formats.make_fileinfo(filesize, filehashes, custom) + + + + + +def generate_root_metadata(version, expiration_date): + """ + + Create the root metadata. 'tuf.roledb.py' and 'tuf.keydb.py' are read and the + information returned by these modules are used to generate the root metadata + object. + + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently + trusted. + + expiration_date: + + + tuf.FormatError, if the generated root metadata object could not + be generated with the correct format. + + tuf.Error, if an error is encountered while generating the root + metadata object. + + + The contents of 'tuf.keydb.py' and 'tuf.roledb.py' are read. + + + A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + # The role and key dictionaries to be saved in the root metadata object. + roledict = {} + keydict = {} + + # Extract the role, threshold, and keyid information from the config. + # The necessary role metadata is generated from this information. + for rolename in ['root', 'targets', 'release', 'timestamp']: + + # If a top-level role is missing from 'tuf.roledb.py', raise an exception. + if not tuf.roledb.role_exists(rolename): + raise tuf.Error(repr(rolename)+' not in "tuf.roledb".') + + keyids = [] + # Generate keys for the keyids listed by the role being processed. + for keyid in tuf.roledb.get_role_keyids(rolename): + key = tuf.keydb.get_key(keyid) + + # If 'key' is an RSA key, it would conform to 'tuf.formats.RSAKEY_SCHEMA', + # and have the form: + # {'keytype': 'rsa', + # 'keyid': keyid, + # 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + # 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + keyid = key['keyid'] + if keyid not in keydict: + + # This appears to be a new keyid. Let's generate the key for it. + if key['keytype'] in ['rsa', 'ed25519']: + keytype = key['keytype'] + keyval = key['keyval'] + keydict[keyid] = \ + tuf.keys.format_keyval_to_metadata(keytype, keyval) + + # This is not a recognized key. Raise an exception. + else: + raise tuf.Error('Unsupported keytype: '+keyid) + + # Do we have a duplicate? + if keyid in keyids: + raise tuf.Error('Same keyid listed twice: '+keyid) + + # Add the loaded keyid for the role being processed. + keyids.append(keyid) + + # Generate and store the role data belonging to the processed role. + role_threshold = tuf.roledb.get_role_threshold(rolename) + role_metadata = tuf.formats.make_role_metadata(keyids, role_threshold) + roledict[rolename] = role_metadata + + # Generate the root metadata object. + root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, + keydict, roledict) + + # Note: make_signable() returns the following dictionary: + # {'signed' : role_metadata, 'signatures' : []} + return tuf.formats.make_signable(root_metadata) + + + + + +def generate_targets_metadata(targets_directory, target_files, version, + expiration_date, delegations=None): + """ + + Generate the targets metadata object. The targets must exist at the same + path they should on the repo. 'target_files' is a list of targets. We're + not worrying about custom metadata at the moment. It is allowed to not + provide keys. + + + targets_directory: + The directory (absolute path) containing the target files and directories. + + target_files: + The target files tracked by 'targets.txt'. 'target_files' is a list of + paths/directories of target files that are relative to the targets + directory (e.g., ['file1.txt', 'Django/module.py']). If the target files + are saved in + the root folder 'targets' on the repository, then 'targets' must be + included in the target paths. The repository does not have to name + this folder 'targets'. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently + trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + delegations: + + + + tuf.FormatError, if an error occurred trying to generate the targets + metadata object. + + tuf.Error, if any of the target files could not be read. + + + The target files are read and file information generated about them. + + + A targets 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Do the arguments have the correct format. + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(targets_directory) + tuf.formats.PATHS_SCHEMA.check_match(target_files) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + if delegations is not None: + tuf.formats.DELEGATIONS_SCHEMA.check_match(delegations) + + filedict = {} + targets_directory = _check_directory(targets_directory) + + # Generate the file info for all the target files listed in 'target_files'. + for target in target_files: + + # Strip 'targets/' from from 'target' and keep the rest (e.g., + # 'targets/more_targets/somefile.txt' -> 'more_targets/somefile.txt' + #relative_targetpath = os.path.sep.join(target.split(os.path.sep)[1:]) + relative_targetpath = target + target_path = os.path.join(targets_directory, target) + + if not os.path.exists(target_path): + message = repr(target_path)+' could not be read. Unable to generate '+\ + 'targets metadata.' + raise tuf.Error(message) + + filedict[relative_targetpath] = get_metadata_file_info(target_path) + + # Generate the targets metadata object. + targets_metadata = tuf.formats.TargetsFile.make_metadata(version, + expiration_date, + filedict, + delegations) + + return tuf.formats.make_signable(targets_metadata) + + + + + +def generate_release_metadata(metadata_directory, version, expiration_date): + """ + + Create the release metadata. The minimum metadata must exist + (i.e., 'root.txt' and 'targets.txt'). This will also look through + the 'targets/' directory in 'metadata_directory' and the resulting + release file will list all the delegated roles. + + + metadata_directory: + The directory containing the 'root.txt' and 'targets.txt' metadata + files. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently + trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + + tuf.FormatError, if 'metadata_directory' is improperly formatted. + + tuf.Error, if an error occurred trying to generate the release metadata + object. + + + The 'root.txt' and 'targets.txt' files are read. + + + The release 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Does 'metadata_directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + metadata_directory = _check_directory(metadata_directory) + + # Retrieve the full filepath of the root and targets metadata file. + root_filename = os.path.join(metadata_directory, ROOT_FILENAME) + targets_filename = os.path.join(metadata_directory, TARGETS_FILENAME) + + # Retrieve the file info of 'root.txt' and 'targets.txt'. This file + # information includes data such as file length, hashes of the file, etc. + filedict = {} + filedict[ROOT_FILENAME] = get_metadata_file_info(root_filename) + filedict[TARGETS_FILENAME] = get_metadata_file_info(targets_filename) + + # Add compressed versions of the 'targets.txt' and 'root.txt' metadata. + for extension in SUPPORTED_COMPRESSION_EXTENSIONS: + compressed_root_filename = root_filename+extension + compressed_targets_filename = targets_filename+extension + if os.path.exists(compressed_root_filename): + filedict[ROOT_FILENAME+extension] = \ + get_metadata_file_info(compressed_root_filename) + if os.path.exists(compressed_targets_filename): + filedict[TARGETS_FILENAME+extension] = \ + get_metadata_file_info(compressed_targets_filename) + + # Walk the 'targets/' directory and generate the file info for all + # the files listed there. This information is stored in the 'meta' + # field of the release metadata object. + targets_metadata = os.path.join(metadata_directory, 'targets') + if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): + for directory_path, junk, files in os.walk(targets_metadata): + + # 'files' here is a list of target file names. + for basename in files: + metadata_path = os.path.join(directory_path, basename) + metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) + for metadata_extension in METADATA_EXTENSIONS: + if metadata_name.endswith(metadata_extension): + rolename = metadata_name[:-len(metadata_extension)] + if tuf.roledb.role_exists(rolename): + filedict[metadata_name] = get_metadata_file_info(metadata_path) + + # Generate the release metadata object. + release_metadata = tuf.formats.ReleaseFile.make_metadata(version, + expiration_date, + filedict) + + return tuf.formats.make_signable(release_metadata) + + + + + +def generate_timestamp_metadata(release_filename, version, + expiration_date, compressions=()): + """ + + Generate the timestamp metadata object. The 'release.txt' file must exist. + + + release_filename: + The required filename of the release metadata file. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently + trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + compressions: + Compression extensions (e.g., 'gz'). If 'release.txt' is also saved in + compressed form, these compression extensions should be stored in + 'compressions' so the compressed timestamp files can be added to the + timestamp metadata object. + + + tuf.FormatError, if the generated timestamp metadata object could + not be formatted correctly. + + + None. + + + A timestamp 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is mismatch. + tuf.formats.PATH_SCHEMA.check_match(release_filename) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + # Retrieve the file info for the release metadata file. + # This file information contains hashes, file length, custom data, etc. + fileinfo = {} + fileinfo[RELEASE_FILENAME] = get_metadata_file_info(release_filename) + + # Save the file info of the compressed versions of 'timestamp.txt'. + for file_extension in compressions: + + compressed_filename = release_filename + '.' + file_extension + try: + compressed_fileinfo = get_metadata_file_info(compressed_filename) + + except: + logger.warn('Could not get fileinfo about '+str(compressed_filename)) + + else: + logger.info('Including fileinfo about '+str(compressed_filename)) + fileinfo[RELEASE_FILENAME+'.' + file_extension] = compressed_fileinfo + + # Generate the timestamp metadata object. + timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, + expiration_date, + fileinfo) + + return tuf.formats.make_signable(timestamp_metadata) + + + + + +def sign_metadata(metadata, keyids, filename): + """ + + Sign a metadata object. If any of the keyids have already signed the file, + the old signature will be replaced. The keys in 'keyids' must already be + loaded in the keystore. + + + metadata: + The metadata object to sign. For example, 'metadata' might correspond to + 'tuf.formats.ROOT_SCHEMA' or 'tuf.formats.TARGETS_SCHEMA'. + + keyids: + The keyids list of the signing keys. + + filename: + The intended filename of the signed metadata object. + For example, 'root.txt' or 'targets.txt'. This function + does NOT save the signed metadata to this filename. + + + tuf.FormatError, if a valid 'signable' object could not be generated. + + tuf.Error, if an invalid keytype was found in the keystore. + + + None. + + + A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Does 'keyids' and 'filename' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.KEYIDS_SCHEMA.check_match(keyids) + tuf.formats.PATH_SCHEMA.check_match(filename) + + # Make sure the metadata is in 'signable' format. That is, + # it contains a 'signatures' field containing the result + # of signing the 'signed' field of 'metadata' with each + # keyid of 'keyids'. + signable = tuf.formats.make_signable(metadata) + + # Sign the metadata with each keyid in 'keyids'. + for keyid in keyids: + + # Load the signing key. + key = tuf.keydb.get_key(keyid) + logger.info('Signing '+repr(filename)+' with '+key['keyid']) + + # Create a new signature list. If 'keyid' is encountered, + # do not add it to new list. + signatures = [] + for signature in signable['signatures']: + if not keyid == signature['keyid']: + signatures.append(signature) + signable['signatures'] = signatures + + # Generate the signature using the appropriate signing method. + if key['keytype'] == 'rsa': + if len(key['keyval']['private']): + signed = signable['signed'] + signature = tuf.sig.generate_rsa_signature(signed, key) + signable['signatures'].append(signature) + else: + logger.warn('Private key unset. Skipping: '+repr(keyid)) + + else: + raise tuf.Error('The keydb contains a key with an invalid key type.') + + # Raise 'tuf.FormatError' if the resulting 'signable' is not formatted + # correctly. + tuf.formats.check_signable_object_format(signable) + + return signable + + + + + +def write_metadata_file(metadata, filename, compression=''): + """ + + Create the file containing the metadata. + + + metadata: + The object that will be saved to 'filename'. + + filename: + The filename (absolute path) of the metadata to be + written (e.g., 'root.txt'). + + compression: + Specify an algorithm as a string to compress the file; otherwise, the + file will be left uncompressed. Available options are 'gz' (gzip). + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.Error, if 'filename' doesn't exist. + + Any other runtime (e.g. IO) exception. + + + The 'filename' file is created or overwritten if it exists. + + + The path to the written metadata file. + """ + + # Are the arguments properly formatted? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.SIGNABLE_SCHEMA.check_match(metadata) + tuf.formats.PATH_SCHEMA.check_match(filename) + tuf.formats.COMPRESSION_SCHEMA.check_match(compression) + + # Verify 'filename' directory. + _check_directory(os.path.dirname(filename)) + + # We choose a file-like object that depends on the compression algorithm. + file_object = None + + # We may modify the filename, depending on the compression algorithm, so we + # store it separately. + filename_with_compression = filename + + # Take care of compression. + if not len(compression): + logger.info('No compression for '+str(filename)) + file_object = open(filename_with_compression, 'w') + + elif compression == 'gz': + logger.info('gzip compression for '+str(filename)) + filename_with_compression += '.gz' + file_object = gzip.open(filename_with_compression, 'w') + + else: + raise tuf.FormatError('Unknown compression algorithm: '+str(compression)) + + try: + tuf.formats.PATH_SCHEMA.check_match(filename_with_compression) + logger.info('Writing to '+str(filename_with_compression)) + + # The metadata object is saved to 'file_object'. The keys + # of the objects are sorted and indentation is used. + json.dump(metadata, file_object, indent=1, sort_keys=True) + + file_object.write('\n') + except: + # Raise any runtime exception. + raise + + else: + # Otherwise, return the written filename. + return filename_with_compression + + finally: + # Always close the file. + file_object.close() + + + + + +def write_delegated_metadata_file(repository_directory, targets_directory, + rolename, version, expiration, keyids, + list_of_targets, delegations, signatures, + compressions, write_partial=False): + """ + + Build the targets metadata file using the signing keys in + 'delegated_keyids'. The generated metadata file is saved to + 'metadata_directory'. The target files located in 'targets_directory' will + be tracked by the built targets metadata. + + + repository_directory: + The repository directory (absolute path) containing all the metadata + and target files. + + rolename: + The delegated role's full rolename (e.g., 'targets/unclaimed/django'). + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently + trusted. + + expiration: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + keyids: + The list of keyids to be used as the signing keys for the delegated + metadata file. + + list_of_targets: + The directory (absolute path) containing all the delegated target + files. The filepaths are not required to live under the targets + directory. The caller is reponsible for ensuring the correct location + of target files. + + delegations: + 'tuf.formats.DELEGATIONS_SCHEMA'. + + signatures: + 'tuf.formats.SIGNATURES_SCHEMA'. + + + tuf.FormatError, if any of the arguments are improperly formatted. + + tuf.Error, if there was an error while building the targets file. + + + The targets metadata file is written to a file. + + + The path for the written targets metadata file. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(targets_directory) + + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration) + tuf.formats.KEYIDS_SCHEMA.check_match(keyids) + tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + tuf.formats.DELEGATIONS_SCHEMA.check_match(delegations) + tuf.formats.SIGNATURES_SCHEMA.check_match(signatures) + tuf.formats.TOGGLE_SCHEMA.check_match(write_partial) + + # Check if 'repository_directory' is valid. + repository_directory = _check_directory(repository_directory) + metadata_directory = os.path.join(repository_directory, + METADATA_DIRECTORY_NAME) + + # Create the metadata object. Delegated roles are of type + # 'tuf.formats.TARGETS_SCHEMA', same as the Targets role. + + metadata_object = generate_targets_metadata(targets_directory, + list_of_targets, version, + expiration, delegations) + + # Delegated metadata are written to their respective directories on the + # repository. For example, the role 'targets/unclaimed/django' is written + # to '{repository_directory}/metadata/targets/unlaimed/django.txt'. + metadata_filepath = os.path.join(metadata_directory, rolename+'.txt') + + # Ensure the parent directories of metadata_filepath exist, otherwise an IO + # exception is raised. + tuf.util.ensure_parent_dir(metadata_filepath) + + # Sign it. + signable = sign_metadata(metadata_object, keyids, metadata_filepath) + for signature in signatures: + signable['signatures'].append(signature) + if tuf.sig.verify(signable, rolename) or write_partial: + if not write_partial: + _remove_invalid_signatures(signable) + for compression in compressions: + write_metadata_file(signable, metadata_filepath, compression) + + else: + raise tuf.Error('Not enough signatures for: '+repr(metadata_filepath)) + + + + + +def create_tuf_client_directory(repository_directory, client_directory): + """ + + Create the file containing the metadata. + + + repository_directory: + + client_directory: + + + tuf.FormatError, if the arguments are improperly formatted. + + + + + None. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(client_directory) + + repository_directory = os.path.abspath(repository_directory) + metadata_directory = os.path.join(repository_directory, + 'metadata') + + # Generate the 'client' directory containing the metadata of the created + # repository. 'tuf.client.updater.py' expects the 'current' and 'previous' + # directories to exist under 'metadata'. + client_directory = os.path.abspath(client_directory) + client_metadata_directory = os.path.join(client_directory, + 'metadata') + + try: + os.makedirs(client_metadata_directory) + except OSError, e: + if e.errno == errno.EEXIST: + message = 'Cannot create a fresh client metadata directory: '+ \ + repr(client_metadata_directory)+'. Already exists.' + raise tuf.RepositoryError(message) + else: + raise + + # Move the metadata to the client's 'current' and 'previous' directories. + client_current = os.path.join(client_metadata_directory, 'current') + client_previous = os.path.join(client_metadata_directory, 'previous') + shutil.copytree(metadata_directory, client_current) + shutil.copytree(metadata_directory, client_previous) + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running libtuf.py as a standalone module. + # python libtuf.py. + import doctest + doctest.testmod() diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py new file mode 100755 index 00000000..0a68e5d4 --- /dev/null +++ b/tuf/pycrypto_keys.py @@ -0,0 +1,466 @@ +""" + + pycrypto_keys.py + + + Vladimir Diaz + + + October 7, 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to support public-key cryptography and RSA + keys through the PyCrypto library. The RSA-related functions provided: + generate_rsa_public_and_private() + create_rsa_signature() + verify_rsa_signature() + create_rsa_encrypted_pem() + create_rsa_public_and_private_from_encrypted_pem() + + PyCrypto (i.e., the 'Crypto' package) performs the actual cryptographic + operations and the functions listed above can be viewed as an easy-to-use + public interface. + + https://en.wikipedia.org/wiki/RSA_(algorithm) + https://github.com/dlitz/pycrypto + """ + +# Crypto.PublicKey (i.e., PyCrypto's public-key cryptography modules) supports +# algorithms like the Digital Signature Algorithm (DSA) and the ElGamal +# encryption system. 'Crypto.PublicKey.RSA' is needed here to generate, sign, +# and verify RSA keys. +import Crypto.PublicKey.RSA + +# PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS +# signatures (i.e., Crypto.Signature.PKCS1_PSS). +import Crypto.Hash.SHA256 + +# RSA's probabilistic signature scheme with appendix (RSASSA-PSS). +# PKCS#1 v1.5 is available for compatibility with existing applications, but +# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates +# a random salt to ensure the signature generated is probabilistic rather than +# deterministic, like PKCS#1 v1.5. +# http://en.wikipedia.org/wiki/RSA-PSS#Schemes +# https://tools.ietf.org/html/rfc3447#section-8.1 +import Crypto.Signature.PKCS1_PSS + +# Import the TUF package and TUF-defined exceptions in __init__.py. +import tuf + +# Digest objects needed to generate hashes. +import tuf.hash + +# Perform object format-checking. +import tuf.formats + +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. +_DEFAULT_RSA_KEY_BITS = 3072 + + +def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): + """ + + Generate public and private RSA keys with modulus length 'bits'. + The public and private keys returned conform to 'tuf.formats.PEMRSA_SCHEMA' + and have the form: + '-----BEGIN RSA PUBLIC KEY----- ...' + + or + + '-----BEGIN RSA PRIVATE KEY----- ...' + + The public and private keys are returned as strings in PEM format. + + Although PyCrypto sets a 1024-bit minimum key size, + generate_rsa_public_and_private() enforces a minimum key size of 2048 bits. + If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key + size recommended by TUF. + + >>> public, private = generate_rsa_public_and_private(2048) + >>> tuf.formats.PEMRSA_SCHEMA.matches(public) + True + >>> tuf.formats.PEMRSA_SCHEMA.matches(private) + True + + + bits: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + + tuf.FormatError, if 'bits' does not contain the correct format. + + ValueError, if an exception occurs in the RSA key generation routine. + 'bits' must be a multiple of 256. The 'ValueError' exception is raised by + the PyCrypto key generation function. + + + The RSA keys are generated by PyCrypto's Crypto.PublicKey.RSA.generate(). + + + A (public, private) tuple containing the RSA keys in PEM format. + """ + + # Does 'bits' have the correct format? + # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. + # 'bits' must be an integer object, with a minimum value of 2048. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + rsa_key_object = Crypto.PublicKey.RSA.generate(bits) + + # Extract the public & private halves of the RSA key and generate their + # PEM-formatted representations. Return the key pair as a (public, private) + # tuple, where each RSA is a string in PEM format. + private = rsa_key_object.exportKey(format='PEM') + rsa_pubkey = rsa_key_object.publickey() + public = rsa_pubkey.exportKey(format='PEM') + + return public, private + + + + + +def create_rsa_signature(private_key, data): + """ + + Generate an RSASSA-PSS signature. The signature, and the method (signature + algorithm) used, is returned as a (signature, method) tuple. + + The signing process will use 'private_key' and 'data' to generate the + signature. + + RFC3447 - RSASSA-PSS + http://www.ietf.org/rfc/rfc3447.txt + + >>> public, private = generate_rsa_public_and_private(2048) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = create_rsa_signature(private, data) + >>> tuf.formats.NAME_SCHEMA.matches(method) + True + >>> method == 'PyCrypto-PKCS#1 PSS' + True + >>> tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.matches(method) + True + + + private_key: + The private RSA key, a string in PEM format. + + data: + Data object used by create_rsa_signature() to generate the signature. + + + tuf.FormatError, if 'private_key' is improperly formatted. + + TypeError, if 'private_key' is unset. + + tuf.CryptoError, if the signature cannot be generated. + + + PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to generate the signature. + + + A (signature, method) tuple, where the signature is a string and the method + is 'PyCrypto-PKCS#1 PSS'. + """ + + # Does 'private_key' have the correct format? + # This check will ensure 'private_key' conforms to 'tuf.formats.PEMRSA_SCHEMA'. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(private_key) + + # Signing the 'data' object requires a private key. + # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the + # only method currently supported. + method = 'PyCrypto-PKCS#1 PSS' + signature = None + + # Verify the signature, but only if the private key has been set. The private + # key is a NULL string if unset. Although it may be clearer to explicit check + # that 'private_key' is not '', we can/should check for a value and not + # compare identities with the 'is' keyword. + if len(private_key): + # Calculate the SHA256 hash of 'data' and generate the hash's PKCS1-PSS + # signature. + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) + sha256_object = Crypto.Hash.SHA256.new(data) + pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) + signature = pkcs1_pss_signer.sign(sha256_object) + except (ValueError, IndexError, TypeError), e: + message = 'An RSA signature could not be generated.' + raise tuf.CryptoError(message) + else: + raise TypeError('The required private key is unset.') + + return signature, method + + + + + +def verify_rsa_signature(signature, signature_method, public_key, data): + """ + + Determine whether the corresponding private key of 'public_key' produced + 'signature'. verify_signature() will use the public key, signature method, + and 'data' to complete the verification. + + >>> public, private = generate_rsa_public_and_private(2048) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = create_rsa_signature(private, data) + >>> verify_rsa_signature(signature, method, public, data) + True + >>> verify_rsa_signature(signature, method, public, 'bad_data') + False + + + signature: + An RSASSA PSS signature as a string. This is the signature returned + by create_rsa_signature(). + + signature_method: + A string that indicates the signature algorithm used to generate + 'signature'. 'PyCrypto-PKCS#1 PSS' is currently supported. + + public_key: + The RSA public key, a string in PEM format. + + data: + Data object used by tuf.rsa_key.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported by tuf.rsa_key.create_signature(). + + tuf.FormatError. Raised if 'signature', 'signature_method', or 'public_key' + is improperly formatted. + + + Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification. + + + Boolean. True if the signature is valid, False otherwise. + """ + + # Does 'public_key' have the correct format? + # This check will ensure 'public_key' conforms to 'tuf.formats.PEMRSA_SCHEMA'. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(public_key) + + # Does 'signature_method' have the correct format? + tuf.formats.NAME_SCHEMA.check_match(signature_method) + + # Does 'signature' have the correct format? + tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.check_match(signature) + + # Verify whether the private key of 'public_key' produced the signature. + # Before returning the Boolean result, ensure 'PyCrypto-PKCS#1 PSS' was used + # as the signing method. + signature = signature + method = signature_method + public = public_key + valid_signature = False + + # Verify the signature with PyCrypto if the signature method is valid, else + # raise 'tuf.UnknownMethodError'. + if method == 'PyCrypto-PKCS#1 PSS': + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key) + pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) + sha256_object = Crypto.Hash.SHA256.new(data) + valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature) + except (ValueError, IndexError, TypeError), e: + message = 'The RSA signature could not be verified.' + raise tuf.CryptoError(message) + else: + raise tuf.UnknownMethodError(method) + + return valid_signature + + + + + +def create_rsa_encrypted_pem(private_key, passphrase): + """ + + Return a string in PEM format, where the private part of the RSA key is + encrypted. The private part of the RSA key is encrypted by the Triple + Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the + mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 + is used to strengthen 'passphrase'. + + https://en.wikipedia.org/wiki/Triple_DES + https://en.wikipedia.org/wiki/PBKDF2 + + >>> public, private = generate_rsa_public_and_private(2048) + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> tuf.formats.PEMRSA_SCHEMA.matches(encrypted_pem) + True + + + private_key: + The private key string in PEM format. + + passphrase: + The passphrase, or password, to encrypt the private part of the RSA + key. 'passphrase' is not used directly as the encryption key, a stronger + encryption key is derived from it. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.CryptoError, if an RSA key in encrypted PEM format cannot be created. + + TypeError, 'private_key' is unset. + + + PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual + generation of the PEM-formatted output. + + + A string in PEM format, where the private RSA key is encrypted. + Conforms to 'tuf.formats.PEMRSA_SCHEMA'. + """ + + # Does 'private_key' have the correct format? + # This check will ensure 'private_key' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(private_key) + + # Does 'passphrase' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) + + # 'private_key' is in PEM format and unencrypted. The extracted key will be + # imported and converted to PyCrypto's RSA key object + # (i.e., Crypto.PublicKey.RSA). Use PyCrypto's exportKey method, with a + # passphrase specified, to create the string. PyCrypto uses PBKDF1+MD5 to + # strengthen 'passphrase', and 3DES with CBC mode for encryption. + # 'private_key' may still be a NULL string after the tuf.formats check. + if len(private_key): + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) + encrypted_pem = rsa_key_object.exportKey(format='PEM', passphrase=passphrase) + except (ValueError, IndexError, TypeError), e: + message = 'An encrypted RSA key in PEM format could not be generated.' + raise tuf.CryptoError(message) + else: + raise TypeError('The required private key is unset.') + + + return encrypted_pem + + + + + +def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase): + """ + + Generate public and private RSA keys from an encrypted PEM. + The public and private keys returned conform to 'tuf.formats.PEMRSA_SCHEMA' + and have the form: + '-----BEGIN RSA PUBLIC KEY----- ...' + + or + + '-----BEGIN RSA PRIVATE KEY----- ...' + + The public and private keys are returned as strings in PEM format. + + The private key part of 'encrypted_pem' is encrypted. PyCrypto's importKey + method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5 + to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption. + Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase + strengthened with PBKDF2+SHA256. See 'keystore.py'. + + >>> public, private = generate_rsa_public_and_private(2048) + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> returned_public, returned_private = \ + create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase) + >>> tuf.formats.PEMRSA_SCHEMA.matches(returned_public) + True + >>> tuf.formats.PEMRSA_SCHEMA.matches(returned_private) + True + >>> public == returned_public + True + >>> private == returned_private + True + + + encrypted_pem: + A byte string in PEM format, where the private key is encrypted. It has + the form: + + '-----BEGIN RSA PRIVATE KEY-----\n + Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...' + + passphrase: + The passphrase, or password, to decrypt the private part of the RSA + key. 'passphrase' is not directly used as the encryption key, instead + it is used to derive a stronger symmetric key. + + + tuf.FormatError, if the arguments are improperly formatted. + + + PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual + conversion from an encrypted RSA private key. + + + A (public, private) tuple containing the RSA keys in PEM format. + """ + + # Does 'encryped_pem' have the correct format? + # This check will ensure 'encrypted_pem' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem) + + # Does 'passphrase' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) + + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(encrypted_pem, passphrase) + except (ValueError, IndexError, TypeError), e: + message = 'An RSA key object could not be generated from the encrypted '+\ + 'PEM string.' + # Raise 'tuf.CryptoError' instead of PyCrypto's exception to avoid + # revealing sensitive error, such as a decryption error due to an + # invalid passphrase. + raise tuf.CryptoError(message) + + # Extract the public and private halves of the RSA key and generate their + # PEM-formatted representations. The dictionary returned contains the + # private and public RSA keys in PEM format, as strings. + private = rsa_key_object.exportKey(format='PEM') + rsa_pubkey = rsa_key_object.publickey() + public = rsa_pubkey.exportKey(format='PEM') + + return public, private + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running 'pycrypto_keys.py' as a standalone module. + # python -B pycrypto_keys.py + import doctest + doctest.testmod() diff --git a/tuf/repo/keystore.py b/tuf/repo/keystore.py index 8f5492e5..ce0c1f4d 100755 --- a/tuf/repo/keystore.py +++ b/tuf/repo/keystore.py @@ -34,7 +34,6 @@ algorithm. User passwords are strengthened with PBKDF2, currently set to 100,000 passphrase iterations. The previous evpy implementation used 1,000 iterations. - """ import os @@ -68,7 +67,7 @@ # the AES algorithm to perform cipher block operations on them. import Crypto.Util.Counter -import tuf.rsa_key +import tuf.keys import tuf.util import tuf.conf @@ -104,6 +103,9 @@ # https://en.wikipedia.org/wiki/PBKDF2 _PBKDF2_ITERATIONS = tuf.conf.PBKDF2_ITERATIONS +# +_SUPPORTED_KEY_TYPES = ['rsa', 'ed25519'] + # A user password is read and a derived key generated. The derived key returned # by the key derivation function (PBKDF2) is saved in '_derived_keys', along # with the salt and iterations used, which has the form: @@ -159,7 +161,6 @@ def add_rsakey(rsakey_dict, password, keyid=None): None. - """ # Does 'rsakey_dict' have the correct format? @@ -235,7 +236,6 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): A list containing the keyids of the loaded keys. - """ # Does 'directory_name' have the correct format? @@ -286,11 +286,11 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): # Create the key based on its key type. RSA keys currently # supported. - if keydata['keytype'] == 'rsa': + if keydata['keytype'] in _SUPPORTED_KEY_TYPES: # 'keydata' is stored in KEY_SCHEMA format. Call # create_from_metadata_format() to get the key in RSAKEY_SCHEMA # format, which is the format expected by 'add_rsakey()'. - rsa_key = tuf.rsa_key.create_from_metadata_format(keydata) + rsa_key = tuf.keys.create_from_metadata_format(keydata) # Ensure the keyid for 'rsa_key' is one of the keys specified in # 'keyids'. If not, do not load the key. @@ -343,7 +343,6 @@ def save_keystore_to_keyfiles(directory_name): None. - """ # Does 'directory_name' have the correct format? @@ -365,9 +364,11 @@ def save_keystore_to_keyfiles(directory_name): file_object = open(basefilename, 'w') # Determine the appropriate format to save the key based on its key type. - if key['keytype'] == 'rsa': + if key['keytype'] in _SUPPORTED_KEY_TYPES: + keytype = key['keytype'] + keyval = key['keyval'] key_metadata_format = \ - tuf.rsa_key.create_in_metadata_format(key['keyval'], private=True) + tuf.keys.create_in_metadata_format(keytype, keyval, private=True) else: logger.warn('The keystore has a key with an unrecognized key type.') continue @@ -402,7 +403,6 @@ def clear_keystore(): None. - """ _keystore.clear() @@ -442,7 +442,6 @@ def change_password(keyid, old_password, new_password): None. - """ # Does 'keyid' have the correct format? @@ -506,7 +505,6 @@ def get_key(keyid): The key belonging to 'keyid' (e.g., RSA key). - """ # Does 'keyid' have the correct format? @@ -530,7 +528,6 @@ def _generate_derived_key(password, salt=None, iterations=None): Derivation Function (PBKDF2). PyCrypto's PBKDF2 implementation is currently used. 'salt' may be specified so that a previous derived key may be regenerated. - """ if salt is None: @@ -584,7 +581,6 @@ def _encrypt(key_data, derived_key_information): 'iterations': '...'} 'tuf.CryptoError' raised if the encryption fails. - """ # Generate a random initialization vector (IV). The 'iv' is treated as the @@ -650,7 +646,6 @@ def _decrypt(file_contents, password): The corresponding decryption routine for _encrypt(). 'tuf.CryptoError' raised if the decryption fails. - """ # Extract the salt, iterations, hmac, initialization vector, and ciphertext diff --git a/tuf/repo/signercli.py b/tuf/repo/signercli.py index 5c36377b..8d5c4b03 100755 --- a/tuf/repo/signercli.py +++ b/tuf/repo/signercli.py @@ -46,7 +46,6 @@ See the parse_options() function for the full list of supported options. - """ import os @@ -92,7 +91,6 @@ def _get_password(prompt='Password: ', confirm=False): is True, the user is asked to enter the previously entered password once again. If they match, the password is returned to the caller. - """ while True: @@ -131,7 +129,6 @@ def _get_metadata_directory(): returned to the caller. 'tuf.FormatError' is raised if the directory is not properly formatted, and 'tuf.Error' if it does not exist. - """ metadata_directory = _prompt('\nEnter the metadata directory: ', str) @@ -151,7 +148,6 @@ def _list_keyids(keystore_directory, metadata_directory): It is assumed the directory arguments exist and have been validated by the caller. The keyids are listed without the '.key' extension, along with their associated roles. - """ # Determine the 'root.txt' filename. This metadata file is needed @@ -233,7 +229,6 @@ def _get_keyids(keystore_directory): key files are stored in encrypted form, the user is asked to enter the password that was used to encrypt the key file. - """ # The keyids list containing the keys loaded. @@ -288,7 +283,6 @@ def _get_all_config_keyids(config_filepath, keystore_directory): loaded_keyids = {'root': [1233d3d, 598djdks, ..], 'release': [sdfsd323, sdsd9090s, ..] ...} - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -338,7 +332,6 @@ def _get_role_config_keyids(config_filepath, keystore_directory, role): tuf.Error, if the required keys could not be loaded. - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -409,7 +402,6 @@ def _get_metadata_version(metadata_filename): 'metadata_filename' does not exist, return a version value of 1. Raise 'tuf.RepositoryError' if 'metadata_filename' cannot be read or validated. - """ # If 'metadata_filename' does not exist on the repository, this means @@ -442,7 +434,6 @@ def _get_metadata_expiration(): tuf.RepositoryError, if the entered expiration date is invalid. - """ message = '\nCurrent time: '+tuf.formats.format_time(time.time())+'.\n'+\ @@ -487,7 +478,6 @@ def change_password(keystore_directory): None. - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -563,7 +553,6 @@ def generate_rsa_key(keystore_directory): None. - """ # Save a reference to the generate_and_save_rsa_key() function. @@ -612,7 +601,6 @@ def list_signing_keys(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -654,7 +642,6 @@ def dump_key(keystore_directory): None. - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -704,8 +691,10 @@ def dump_key(keystore_directory): # Retrieve the key metadata according to the keytype. if key['keytype'] == 'rsa': - key_metadata = tuf.rsa_key.create_in_metadata_format(key['keyval'], - private=show_private) + keytype = key['keytype'] + keyval = key['keyval'] + key_metadata = tuf.keys.create_in_metadata_format(keytype, keyval, + private=show_private) else: message = 'The keystore contains an invalid key type.' raise tuf.RepositoryError(message) @@ -737,7 +726,6 @@ def make_root_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -804,7 +792,6 @@ def make_targets_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -886,7 +873,6 @@ def make_release_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -951,7 +937,6 @@ def make_timestamp_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -1017,7 +1002,6 @@ def sign_metadata_file(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -1084,7 +1068,6 @@ def make_delegation(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -1154,7 +1137,6 @@ def _load_parent_role(metadata_directory, keystore_directory, targets_roles): list of known targets roles and asked to enter the parent role to load. Ensure the parent role is loaded properly and return a string containing the parent role's full rolename and a list of keyids belonging to the parent. - """ # 'load_key' is a reference to the 'load_keystore_from_keyfiles function'. @@ -1210,7 +1192,6 @@ def _get_delegated_role(keystore_directory, metadata_directory): a list of keyids available in the keystore and asked to enter the keyid belonging to the delegated role. Return a string containing the delegated role's full rolename and its keyids. - """ # Retrieve the delegated rolename from the user (e.g., 'role1'). @@ -1240,7 +1221,6 @@ def _make_delegated_metadata(metadata_directory, delegated_targets, role. Determine the target files from the paths in 'delegated_targets' and the other information needed to generate the targets metadata file for delegated_role'. Return the delegated paths to the caller. - """ repository_directory, junk = os.path.split(metadata_directory) @@ -1336,7 +1316,6 @@ def _update_parent_metadata(metadata_directory, delegated_role, metadata file is updated with the key and role information belonging to the newly added delegated role. Finally, the metadata file is signed and written to the metadata directory. - """ # According to the specification, the 'paths' and 'path_hash_prefixes' @@ -1376,8 +1355,9 @@ def _update_parent_metadata(metadata_directory, delegated_role, # Retrieve the key belonging to 'delegated_keyid' from the keystore. role_key = tuf.repo.keystore.get_key(delegated_keyid) if role_key['keytype'] == 'rsa': + keytype = role_key['keytype'] keyval = role_key['keyval'] - keys[delegated_keyid] = tuf.rsa_key.create_in_metadata_format(keyval) + keys[delegated_keyid] = tuf.keys.create_in_metadata_format(keytype, keyval) else: message = 'Invalid keytype encountered: '+delegated_keyid+'\n' raise tuf.RepositoryError(message) @@ -1450,7 +1430,6 @@ def process_option(options): None. - """ # Determine which option was chosen and call its corresponding @@ -1506,7 +1485,6 @@ def parse_options(): The options object returned by the parser's parse_args() method. - """ usage = 'usage: %prog [option] ' diff --git a/tuf/repo/signerlib.py b/tuf/repo/signerlib.py index 3f44ea5e..5df17363 100755 --- a/tuf/repo/signerlib.py +++ b/tuf/repo/signerlib.py @@ -16,7 +16,6 @@ These functions contain code that can extract or create needed repository data, such as the extraction of role and keyid information from a config file, and the generation of actual metadata content. - """ import gzip @@ -27,7 +26,7 @@ import tuf import tuf.formats import tuf.hash -import tuf.rsa_key +import tuf.keys import tuf.repo.keystore import tuf.sig import tuf.util @@ -81,7 +80,6 @@ def read_config_file(filename): A dictionary containing the data loaded from the configuration file. - """ # Does 'filename' have the correct format? @@ -151,7 +149,6 @@ def get_metadata_file_info(filename): A dictionary conformant to 'tuf.formats.FILEINFO_SCHEMA'. This dictionary contains the length, hashes, and custom data about the 'filename' metadata file. - """ # Does 'filename' have the correct format? @@ -204,7 +201,6 @@ def get_metadata_filenames(metadata_directory=None): A dictionary containing the expected filenames of the top-level metadata files, such as 'root.txt' and 'release.txt'. - """ if metadata_directory is None: @@ -255,7 +251,6 @@ def generate_root_metadata(config_filepath, version): A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Does 'config_filepath' have the correct format? @@ -290,8 +285,10 @@ def generate_root_metadata(config_filepath, version): keyid = key['keyid'] # This appears to be a new keyid. Let's generate the key for it. if keyid not in keydict: - if key['keytype'] == 'rsa': - keydict[keyid] = tuf.rsa_key.create_in_metadata_format(key['keyval']) + if key['keytype'] in ['rsa', 'ed25519']: + keytype = key['keytype'] + keyval = key['keyval'] + keydict[keyid] = tuf.keys.create_in_metadata_format(keytype, keyval) # This is not a recognized key. Raise an exception. else: raise tuf.Error('Unsupported keytype: '+keyid) @@ -364,7 +361,6 @@ def generate_targets_metadata(repository_directory, target_files, version, A targets 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Do the arguments have the correct format. @@ -433,7 +429,6 @@ def generate_release_metadata(metadata_directory, version, expiration_date): The release 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Does 'metadata_directory' have the correct format? @@ -510,7 +505,6 @@ def generate_timestamp_metadata(release_filename, version, A timestamp 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Do the arguments have the correct format? @@ -575,7 +569,6 @@ def write_metadata_file(metadata, filename, compression=None): The path to the written metadata file. - """ # Are the arguments properly formatted? @@ -645,7 +638,6 @@ def read_metadata_file(filename): The metadata object. - """ return tuf.util.load_json_file(filename) @@ -684,7 +676,6 @@ def sign_metadata(metadata, keyids, filename): A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Does 'keyids' and 'filename' have the correct format? @@ -767,7 +758,6 @@ def generate_and_save_rsa_key(keystore_directory, password, 'keyid': keyid, 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - """ # Are the arguments correctly formatted? @@ -778,7 +768,7 @@ def generate_and_save_rsa_key(keystore_directory, password, keystore_directory = check_directory(keystore_directory) # tuf.FormatError or tuf.CryptoError raised. - rsakey = tuf.rsa_key.generate(bits) + rsakey = tuf.keys.generate_rsa_key(bits) logger.info('Generated a new key: '+rsakey['keyid']) @@ -820,7 +810,6 @@ def check_directory(directory): The normalized absolutized path of 'directory'. - """ # Does 'directory' have the correct format? @@ -868,7 +857,6 @@ def get_target_keyids(metadata_directory): A dictionary containing the role information extracted from the metadata. Ex: {'targets':[keyid1, ...], 'targets/role1':[keyid], ...} - """ # Does 'metadata_directory' have the correct format? @@ -964,7 +952,6 @@ def build_config_file(config_file_directory, timeout, role_info): The normalized absolutized path of the saved configuration file. - """ # Do the arguments have the correct format? @@ -1054,7 +1041,6 @@ def build_root_file(config_filepath, root_keyids, metadata_directory, version): The path for the written root metadata file. - """ # Do the arguments have the correct format? @@ -1116,7 +1102,6 @@ def build_targets_file(target_paths, targets_keyids, metadata_directory, The path for the written targets metadata file. - """ # Do the arguments have the correct format? @@ -1207,7 +1192,6 @@ def build_release_file(release_keyids, metadata_directory, The path for the written release metadata file. - """ # Do the arguments have the correct format? @@ -1282,7 +1266,6 @@ def build_timestamp_file(timestamp_keyids, metadata_directory, The path for the written timestamp metadata file. - """ # Do the arguments have the correct format? @@ -1370,7 +1353,6 @@ def build_delegated_role_file(delegated_targets_directory, delegated_keyids, The path for the written targets metadata file. - """ # Do the arguments have the correct format? @@ -1432,7 +1414,6 @@ def find_delegated_role(roles, delegated_role): None, if the role with the given name does not exist, or its unique index in the list of roles. - """ # Check argument types. @@ -1487,7 +1468,6 @@ def accept_any_file(full_target_path): True. - """ return True @@ -1524,7 +1504,6 @@ def get_targets(files_directory, recursive_walk=False, followlinks=True, A list of absolute paths to target files in the given files_directory. - """ targets = [] @@ -1546,8 +1525,3 @@ def get_targets(files_directory, recursive_walk=False, followlinks=True, del dirnames[:] return targets - - - - - diff --git a/tuf/roledb.py b/tuf/roledb.py index 4f114dfc..7cba394c 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -23,13 +23,20 @@ The role database is a dictionary conformant to 'tuf.formats.ROLEDICT_SCHEMA' and has the form: + {'rolename': {'keyids': ['34345df32093bd12...'], 'threshold': 1 - 'paths': ['path/to/role.txt']}} - + 'signatures': ['abcd3452...'], + 'paths': ['path/to/role.txt'], + 'path_hash_prefixes': ['ab34df13'], + 'delegations': {'keys': {}, 'roles': {}}} + + The 'name', 'paths', 'path_hash_prefixes', and 'delegations' dict keys are + optional. """ import logging +import copy import tuf import tuf.formats @@ -66,7 +73,6 @@ def create_roledb_from_root_metadata(root_metadata): None. - """ # Does 'root_metadata' have the correct object format? @@ -81,6 +87,16 @@ def create_roledb_from_root_metadata(root_metadata): # Iterate through the roles found in 'root_metadata' # and add them to '_roledb_dict'. Duplicates are avoided. for rolename, roleinfo in root_metadata['roles'].items(): + if rolename == 'root': + roleinfo['version'] = root_metadata['version'] + roleinfo['expires'] = root_metadata['expires'] + + roleinfo['signatures'] = [] + roleinfo['signing_keyids'] = [] + roleinfo['compressions'] = [''] + if rolename.startswith('targets'): + roleinfo['delegations'] = {'keys': {}, 'roles': []} + try: add_role(rolename, roleinfo) # tuf.Error raised if the parent role of 'rolename' does not exist. @@ -104,10 +120,17 @@ def add_role(rolename, roleinfo, require_parent=True): roleinfo: An object representing the role associated with 'rolename', conformant to - ROLE_SCHEMA. 'roleinfo' has the form: + ROLEDB_SCHEMA. 'roleinfo' has the form: {'keyids': ['34345df32093bd12...'], - 'threshold': 1} + 'threshold': 1, + 'signatures': ['ab23dfc32'] + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...], + 'delegations': {'keys': } + The 'paths', 'path_hash_prefixes', and 'delegations' dict keys are + optional. + The 'target' role has an additional 'paths' key. Its value is a list of strings representing the path of the target file(s). @@ -128,7 +151,6 @@ def add_role(rolename, roleinfo, require_parent=True): None. - """ # Does 'rolename' have the correct object format? @@ -137,7 +159,7 @@ def add_role(rolename, roleinfo, require_parent=True): tuf.formats.ROLENAME_SCHEMA.check_match(rolename) # Does 'roleinfo' have the correct object format? - tuf.formats.ROLE_SCHEMA.check_match(roleinfo) + tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) # Does 'require_parent' have the correct format? tuf.formats.TOGGLE_SCHEMA.check_match(require_parent) @@ -157,12 +179,71 @@ def add_role(rolename, roleinfo, require_parent=True): if parent_role not in _roledb_dict: raise tuf.Error('Parent role does not exist: '+parent_role) - _roledb_dict[rolename] = roleinfo + _roledb_dict[rolename] = copy.deepcopy(roleinfo) +def update_roleinfo(rolename, roleinfo): + """ + + + + rolename: + An object representing the role's name, conformant to 'ROLENAME_SCHEMA' + (e.g., 'root', 'release', 'timestamp'). + + roleinfo: + An object representing the role associated with 'rolename', conformant to + ROLEDB_SCHEMA. 'roleinfo' has the form: + {'name': 'role_name', + 'keyids': ['34345df32093bd12...'], + 'threshold': 1, + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...]} + + The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional. + + The 'target' role has an additional 'paths' key. Its value is a list of + strings representing the path of the target file(s). + + + tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct + object format. + + tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. + + tuf.InvalidNameError, if 'rolename' is improperly formatted. + + + The role database is modified. + + + None. + """ + + # Does 'rolename' have the correct object format? + # This check will ensure 'rolename' has the appropriate number of objects + # and object types, and that all dict keys are properly named. + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + + # Does 'roleinfo' have the correct object format? + tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) + + # Raises tuf.InvalidNameError. + _validate_rolename(rolename) + + if rolename not in _roledb_dict: + raise tuf.UnknownRoleError('Role does not exist: '+rolename) + + _roledb_dict[rolename] = copy.deepcopy(roleinfo) + + + + + + def get_parent_rolename(rolename): """ @@ -187,7 +268,6 @@ def get_parent_rolename(rolename): A string representing the name of the parent role. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -228,7 +308,6 @@ def get_all_parent_roles(rolename): A list containing all the parent roles. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -279,7 +358,6 @@ def role_exists(rolename): Boolean. True if 'rolename' is found in the role database, False otherwise. - """ # Raise tuf.FormatError, tuf.InvalidNameError. @@ -318,7 +396,6 @@ def remove_role(rolename): None. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -358,7 +435,6 @@ def remove_delegated_roles(rolename): None. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -390,7 +466,6 @@ def get_rolenames(): A list of rolenames. - """ return _roledb_dict.keys() @@ -399,6 +474,47 @@ def get_rolenames(): +def get_roleinfo(rolename): + """ + + Return the roleinfo of 'rolename'. + + {'keyids': ['34345df32093bd12...'], + 'threshold': 1, + 'signatures': ['ab453bdf...', ...], + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...], + 'delegations': {'keys': {}, 'roles': []}} + + The 'signatures', 'paths', 'path_hash_prefixes', and 'delegations' dict keys + are optional. + + + rolename: + An object representing the role's name, conformant to 'ROLENAME_SCHEMA' + (e.g., 'root', 'release', 'timestamp'). + + + tuf.FormatError, if 'rolename' is improperly formatted. + + tuf.UnknownRoleError, if 'rolename' does not exist. + + + None. + + + The roleinfo of 'rolename'. + """ + + # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. + _check_rolename(rolename) + + return copy.deepcopy(_roledb_dict[rolename]) + + + + + def get_role_keyids(rolename): """ @@ -426,7 +542,6 @@ def get_role_keyids(rolename): A list of keyids. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -462,7 +577,6 @@ def get_role_threshold(rolename): A threshold integer value. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -498,7 +612,6 @@ def get_role_paths(rolename): A list of paths. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -540,8 +653,7 @@ def get_delegated_rolenames(rolename): A list of rolenames. Note that the rolenames are *NOT* sorted by order of - delegation! - + delegation. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -578,7 +690,6 @@ def clear_roledb(): None. - """ _roledb_dict.clear() @@ -593,7 +704,6 @@ def _check_rolename(rolename): 'tuf.formats.ROLENAME_SCHEMA', tuf.UnknownRoleError if 'rolename' is not found in the role database, or tuf.InvalidNameError if 'rolename' is not formatted correctly. - """ # Does 'rolename' have the correct object format? @@ -616,7 +726,6 @@ def _validate_rolename(rolename): Raise tuf.InvalidNameError if 'rolename' is not formatted correctly. It is assumed 'rolename' has been checked against 'ROLENAME_SCHEMA' prior to calling this function. - """ if rolename == '': diff --git a/tuf/rsa_key.py b/tuf/rsa_key.py deleted file mode 100755 index 8d737391..00000000 --- a/tuf/rsa_key.py +++ /dev/null @@ -1,664 +0,0 @@ -""" - - rsa_key.py - - - Vladimir Diaz - - - March 9, 2012. Based on a previous version of this module by Geremy Condra. - - - See LICENSE for licensing information. - - - The goal of this module is to support public-key cryptography using the RSA - algorithm. The RSA-related functions provided include generate(), - create_signature(), and verify_signature(). The create_encrypted_pem() and - create_from_encrypted_pem() functions are optional, and may be used save a - generated RSA key to a file. The 'PyCrypto' package used by 'rsa_key.py' - generates the actual RSA keys and the functions listed above can be viewed - as an easy-to-use public interface. Additional functions contained here - include create_in_metadata_format() and create_from_metadata_format(). These - last two functions produce or use RSA keys compatible with the key structures - listed in TUF Metadata files. The generate() function returns a dictionary - containing all the information needed of RSA keys, such as public and private= - keys, keyIDs, and an idenfier. create_signature() and verify_signature() are - supplemental functions used for generating RSA signatures and verifying them. - https://en.wikipedia.org/wiki/RSA_(algorithm) - - Key IDs are used as identifiers for keys (e.g., RSA key). They are the - hexadecimal representation of the hash of key object (specifically, the key - object containing only the public key). Review 'rsa_key.py' and the - '_get_keyid()' function to see precisely how keyids are generated. One may - get the keyid of a key object by simply accessing the dictionary's 'keyid' - key (i.e., rsakey['keyid']). - - """ - - -# Required for hexadecimal conversions. Signatures are hexlified. -import binascii - -# Crypto.PublicKey (i.e., PyCrypto public-key cryptography) provides algorithms -# such as Digital Signature Algorithm (DSA) and the ElGamal encryption system. -# 'Crypto.PublicKey.RSA' is needed here to generate, sign, and verify RSA keys. -import Crypto.PublicKey.RSA - -# PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS -# signatures (i.e., Crypto.Signature.PKCS1_PSS). -import Crypto.Hash.SHA256 - -# RSA's probabilistic signature scheme with appendix (RSASSA-PSS). -# PKCS#1 v1.5 is provided for compatability with existing applications, but -# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates -# a random salt to ensure the signature generated is probabilistic rather than -# deterministic, like PKCS#1 v1.5. -# http://en.wikipedia.org/wiki/RSA-PSS#Schemes -# https://tools.ietf.org/html/rfc3447#section-8.1 -import Crypto.Signature.PKCS1_PSS - -import tuf - -# Digest objects needed to generate hashes. -import tuf.hash - -# Perform object format-checking. -import tuf.formats - - -_KEY_ID_HASH_ALGORITHM = 'sha256' - -# Recommended RSA key sizes: -# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 -# According to the document above, revised May 6, 2003, RSA keys of -# size 3072 provide security through 2031 and beyond. -_DEFAULT_RSA_KEY_BITS = 3072 - - -def generate(bits=_DEFAULT_RSA_KEY_BITS): - """ - - Generate public and private RSA keys, with modulus length 'bits'. - In addition, a keyid used as an identifier for RSA keys is generated. - The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. - - Although the crytography library called sets a 1024-bit minimum key size, - generate() enforces a minimum key size of 2048 bits. If 'bits' is - unspecified, a 3072-bit RSA key is generated, which is the key size - recommended by TUF. - - - bits: - The key size, or key length, of the RSA key. 'bits' must be 2048, or - greater, and a multiple of 256. - - - ValueError, if an exception occurs after calling the RSA key generation - routine. 'bits' must be a multiple of 256. The 'ValueError' exception is - raised by the key generation function of the cryptography library called. - - tuf.FormatError, if 'bits' does not contain the correct format. - - - The RSA keys are generated by calling PyCrypto's - Crypto.PublicKey.RSA.generate(). - - - A dictionary containing the RSA keys and other identifying information. - - """ - - - # Does 'bits' have the correct format? - # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. - # 'bits' must be an integer object, with a minimum value of 2048. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) - - # Begin building the RSA key dictionary. - rsakey_dict = {} - keytype = 'rsa' - - # Generate the public and private RSA keys. The PyCrypto module performs - # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 - # or not a multiple of 256, although a 2048-bit minimum is enforced by - # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). - rsa_key_object = Crypto.PublicKey.RSA.generate(bits) - - # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. The dictionary returned contains the - # private and public RSA keys in PEM format, as strings. - private_key_pem = rsa_key_object.exportKey(format='PEM') - rsa_pubkey = rsa_key_object.publickey() - public_key_pem = rsa_pubkey.exportKey(format='PEM') - - # Generate the keyid for the RSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': public_key_pem, - 'private': ''} - keyid = _get_keyid(key_value) - - # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA - # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = private_key_pem - - rsakey_dict['keytype'] = keytype - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value - - return rsakey_dict - - - - - -def create_in_metadata_format(key_value, private=False): - """ - - Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. - If 'private' is True, include the private key. The dictionary - returned has the form: - {'keytype': 'rsa', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - or if 'private' is False: - - {'keytype': 'rsa', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': ''}} - - The private and public keys are in PEM format. - - RSA keys are stored in Metadata files (e.g., root.txt) in the format - returned by this function. - - - key_value: - A dictionary containing a private and public RSA key. - 'key_value' is of the form: - - {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}, - conformat to 'tuf.formats.KEYVAL_SCHEMA'. - - private: - Indicates if the private key should be included in the - returned dictionary. - - - tuf.FormatError, if 'key_value' does not conform to - 'tuf.formats.KEYVAL_SCHEMA'. - - - None. - - - An 'KEY_SCHEMA' dictionary. - - """ - - - # Does 'key_value' have the correct format? - # This check will ensure 'key_value' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.KEYVAL_SCHEMA.check_match(key_value) - - if private is True and key_value['private']: - return {'keytype': 'rsa', 'keyval': key_value} - else: - public_key_value = {'public': key_value['public'], 'private': ''} - return {'keytype': 'rsa', 'keyval': public_key_value} - - - - - -def create_from_metadata_format(key_metadata): - """ - - Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA) - from 'key_metadata'. The dict returned by this function has the exact - format as the dict returned by generate(). It is of the form: - - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. - - RSA key dictionaries in RSAKEY_SCHEMA format should be used by - modules storing a collection of keys, such as a keydb and keystore. - RSA keys as stored in metadata files use a different format, so this - function should be called if an RSA key is extracted from one of these - metadata files and needs converting. Generate() creates an entirely - new key and returns it in the format appropriate for 'keydb.py' and - 'keystore.py'. - - - key_metadata: - The RSA key dictionary as stored in Metadata files, conforming to - 'tuf.formats.KEY_SCHEMA'. It has the form: - - {'keytype': '...', - 'keyval': {'public': '...', - 'private': '...'}} - - - tuf.FormatError, if 'key_metadata' does not conform to - 'tuf.formats.KEY_SCHEMA'. - - - None. - - - A dictionary containing the RSA keys and other identifying information. - - """ - - - # Does 'key_metadata' have the correct format? - # This check will ensure 'key_metadata' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.KEY_SCHEMA.check_match(key_metadata) - - # Construct the dictionary to be returned. - rsakey_dict = {} - keytype = 'rsa' - key_value = key_metadata['keyval'] - - # Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash - # The hash is in hexdigest form. - keyid = _get_keyid(key_value) - - # We now have all the required key values. Build 'rsakey_dict'. - rsakey_dict['keytype'] = keytype - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value - - return rsakey_dict - - - - - -def _get_keyid(key_value): - """Return the keyid for 'key_value'.""" - - # 'keyid' will be generated from an object conformant to KEY_SCHEMA, - # which is the format Metadata files (e.g., root.txt) store keys. - # 'create_in_metadata_format()' returns the object needed by _get_keyid(). - rsakey_meta = create_in_metadata_format(key_value, private=False) - - # Convert the RSA key to JSON Canonical format suitable for adding - # to digest objects. - rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta) - - # Create a digest object and call update(), using the JSON - # canonical format of 'rskey_meta' as the update data. - digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) - digest_object.update(rsakey_update_data) - - # 'keyid' becomes the hexadecimal representation of the hash. - keyid = digest_object.hexdigest() - - return keyid - - - - - -def create_signature(rsakey_dict, data): - """ - - Return a signature dictionary of the form: - {'keyid': keyid, - 'method': 'PyCrypto-PKCS#1 PPS', - 'sig': sig}. - - The signing process will use the private key - rsakey_dict['keyval']['private'] and 'data' to generate the signature. - - RFC3447 - RSASSA-PSS - http://www.ietf.org/rfc/rfc3447.txt - - - rsakey_dict: - A dictionary containing the RSA keys and other identifying information. - 'rsakey_dict' has the form: - - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. - - data: - Data object used by create_signature() to generate the signature. - - - TypeError, if a private key is not defined for 'rsakey_dict'. - - tuf.FormatError, if an incorrect format is found for the - 'rsakey_dict' object. - - - PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to perform the actual - signing. - - - A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. - - """ - - - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) - - # Signing the 'data' object requires a private key. - # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the - # only method currently supported. - signature = {} - private_key = rsakey_dict['keyval']['private'] - keyid = rsakey_dict['keyid'] - method = 'PyCrypto-PKCS#1 PSS' - sig = None - - # Verify the signature, but only if the private key has been set. The private - # key is a NULL string if unset. Although it may be clearer to explicit check - # that 'private_key' is not '', we can/should check for a value and not - # compare identities with the 'is' keyword. - if len(private_key): - # Calculate the SHA256 hash of 'data' and generate the hash's PKCS1-PSS - # signature. - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) - sha256_object = Crypto.Hash.SHA256.new(data) - pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) - sig = pkcs1_pss_signer.sign(sha256_object) - except (ValueError, IndexError, TypeError), e: - message = 'An RSA signature could not be generated.' - raise tuf.CryptoError(message) - else: - raise TypeError('The required private key is not defined for "rsakey_dict".') - - # Build the signature dictionary to be returned. - # The hexadecimal representation of 'sig' is stored in the signature. - signature['keyid'] = keyid - signature['method'] = method - signature['sig'] = binascii.hexlify(sig) - - return signature - - - - - -def verify_signature(rsakey_dict, signature, data): - """ - - Determine whether the private key belonging to 'rsakey_dict' produced - 'signature'. verify_signature() will use the public key found in - 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', - and 'data' to complete the verification. Type-checking performed on both - 'rsakey_dict' and 'signature'. - - - rsakey_dict: - A dictionary containing the RSA keys and other identifying information. - 'rsakey_dict' has the form: - - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. - - signature: - The signature dictionary produced by tuf.rsa_key.create_signature(). - 'signature' has the form: - {'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to - 'tuf.formats.SIGNATURE_SCHEMA'. - - data: - Data object used by tuf.rsa_key.create_signature() to generate - 'signature'. 'data' is needed here to verify the signature. - - - tuf.UnknownMethodError. Raised if the signing method used by - 'signature' is not one supported by tuf.rsa_key.create_signature(). - - tuf.FormatError. Raised if either 'rsakey_dict' - or 'signature' do not match their respective tuf.formats schema. - 'rsakey_dict' must conform to 'tuf.formats.RSAKEY_SCHEMA'. - 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. - - - Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification. - - - Boolean. True if the signature is valid, False otherwise. - - """ - - - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) - - # Does 'signature' have the correct format? - tuf.formats.SIGNATURE_SCHEMA.check_match(signature) - - # Using the public key belonging to 'rsakey_dict' - # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' - # was produced by rsakey_dict's corresponding private key - # rsakey_dict['keyval']['private']. Before returning the Boolean result, - # ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method. - method = signature['method'] - sig = signature['sig'] - public_key = rsakey_dict['keyval']['public'] - valid_signature = False - - if method == 'PyCrypto-PKCS#1 PSS': - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key) - pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) - sha256_object = Crypto.Hash.SHA256.new(data) - - # The metadata stores signatures in hex. Unhexlify and verify the - # signature. - signature = binascii.unhexlify(sig) - valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature) - except (ValueError, IndexError, TypeError), e: - message = 'The RSA signature could not be verified.' - raise tuf.CryptoError(message) - else: - raise tuf.UnknownMethodError(method) - - return valid_signature - - - - - -def create_encrypted_pem(rsakey_dict, passphrase): - """ - - Return a string in PEM format, where the private part of the RSA key is - encrypted. The private part of the RSA key is encrypted by the Triple - Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the - mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 - is used to strengthen 'passphrase'. - - https://en.wikipedia.org/wiki/Triple_DES - https://en.wikipedia.org/wiki/PBKDF2 - - - rsakey_dict: - A dictionary containing the RSA keys and other identifying information. - 'rsakey_dict' has the form: - - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. - - passphrase: - The passphrase, or password, to encrypt the private part of the RSA - key. 'passphrase' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - TypeError, if a private key is not defined for 'rsakey_dict'. - - tuf.FormatError, if an incorrect format is found for 'rsakey_dict'. - - - PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual - generation of the PEM-formatted output. - - - A string in PEM format, where the private RSA key is encrypted. - - """ - - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) - - # Does 'signature' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) - - # Extract the private key from 'rsakey_dict', which is stored in PEM format - # and unencrypted. The extracted key will be imported and converted to - # PyCrypto's RSA key object (i.e., Crypto.PublicKey.RSA).Use PyCrypto's - # exportKey method, with a passphrase specified, to create the string. - # PyCrypto uses PBKDF1+MD5 to strengthen 'passphrase', and 3DES with CBC mode - # for encryption. - private_key = rsakey_dict['keyval']['private'] - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) - rsakey_pem_encrypted = rsa_key_object.exportKey(format='PEM', - passphrase=passphrase) - except (ValueError, IndexError, TypeError), e: - message = 'An encrypted RSA key in PEM format could not be generated.' - raise tuf.CryptoError(message) - - return rsakey_pem_encrypted - - - - - -def create_from_encrypted_pem(encrypted_pem, passphrase): - """ - - Return an RSA key in 'tuf.formats.RSAKEY_SCHEMA' format, which has the - form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The RSAKEY_SCHEMA object is generated from a byte string in PEM format, - where the private part of the RSA key is encrypted. PyCrypto's importKey - method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5 - to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption. - Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase - strengthened with PBKDF2+SHA256. See 'keystore.py'. - - - encrypted_pem: - A byte string in PEM format, where the private key is encrypted. It has - the form: - - '-----BEGIN RSA PRIVATE KEY-----\n - Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...' - - passphrase: - The passphrase, or password, to decrypt the private part of the RSA - key. 'passphrase' is not directly used as the encryption key, instead - it is used to derive a stronger symmetric key. - - - TypeError, if a private key is not defined for 'rsakey_dict'. - - tuf.FormatError, if an incorrect format is found for the - 'rsakey_dict' object. - - - PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual - conversion from an encrypted RSA private key. - - - A dictionary in 'tuf.formats.RSAKEY_SCHEMA' format. - - """ - - # Does 'encryped_pem' have the correct format? - # This check will ensure 'encrypted_pem' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem) - - # Does 'passphrase' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) - - keytype = 'rsa' - rsakey_dict = {} - - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(encrypted_pem, passphrase) - except (ValueError, IndexError, TypeError), e: - message = 'An RSA key object could not be generated from the encrypted '+\ - 'PEM string.' - # Raise 'tuf.CryptoError' instead of PyCrypto's exception to avoid - # revealing sensitive error, such as a decryption error due to an - # invalid passphrase. - raise tuf.CryptoError(message) - - # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. The dictionary returned contains the - # private and public RSA keys in PEM format, as strings. - private_key_pem = rsa_key_object.exportKey(format='PEM') - rsa_pubkey = rsa_key_object.publickey() - public_key_pem = rsa_pubkey.exportKey(format='PEM') - - # Generate the keyid for the RSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': public_key_pem, - 'private': ''} - keyid = _get_keyid(key_value) - - # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA - # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = private_key_pem - - rsakey_dict['keytype'] = keytype - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value - - return rsakey_dict diff --git a/tuf/schema.py b/tuf/schema.py index acd50b63..278e1899 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -38,7 +38,6 @@ criteria. See 'tuf.formats.py' and the rest of this module for extensive examples. Anything related to the checking of TUF objects and their formats can be found in 'formats.py'. - """ @@ -55,7 +54,6 @@ class Schema: that are encodable in JSON. 'Schema' is the base class for the other classes defined in this module. All derived classes should implement check_match(). - """ def matches(self, object): @@ -64,7 +62,6 @@ def matches(self, object): Return True if 'object' matches this schema, False if it doesn't. If the caller wishes to signal an error on a failed match, check_match() should be called, which will raise a 'tuf.FormatError' exception. - """ try: @@ -82,7 +79,6 @@ def check_match(self, object): implement check_match(). If 'object' matches the schema, check_match() should simply return. If 'object' does not match the schema, 'tuf.FormatError' should be raised. - """ raise NotImplementedError() @@ -110,7 +106,6 @@ class Any(Schema): True >>> schema.matches([1, 'list']) True - """ def __init__(self): @@ -143,7 +138,6 @@ class String(Schema): True >>> schema.matches('Not hi') False - """ def __init__(self, string): @@ -187,7 +181,6 @@ class AnyString(Schema): True >>> schema.matches({}) False - """ def __init__(self): @@ -202,6 +195,48 @@ def check_match(self, object): +class LengthString(Schema): + """ + + Matches any string of a specified length. The argument object + must be a string. At instantiation, the string length is set + and any future comparisons are checked against this internal + string value length. + + Supported methods include + matches(): returns a Boolean result. + check_match(): raises 'tuf.FormatError' on a mismatch. + + + + >>> schema = LengthString(5) + >>> schema.matches('Hello') + True + >>> schema.matches('Hi') + False + """ + + def __init__(self, length): + if isinstance(length, bool) or not isinstance(length, (int, long)): + # We need to check for bool as a special case, since bool + # is for historical reasons a subtype of int. + raise tuf.FormatError('Got '+repr(length)+' instead of an integer.') + + self._string_length = length + + + def check_match(self, object): + if not isinstance(object, basestring): + raise tuf.FormatError('Expected a string but got '+repr(object)) + + if len(object) != self._string_length: + raise tuf.FormatError('Expected a string of length '+ + repr(self._string_length)) + + + + + class OneOf(Schema): """ @@ -229,7 +264,6 @@ class OneOf(Schema): True >>> schema.matches(['Hi']) False - """ def __init__(self, alternatives): @@ -275,7 +309,6 @@ class AllOf(Schema): False >>> schema.matches('a') True - """ def __init__(self, required_schemas): @@ -314,7 +347,6 @@ class Boolean(Schema): True >>> schema.matches(11) False - """ def __init__(self): @@ -367,7 +399,6 @@ class ListOf(Schema): True >>> schema.matches([3]*11) False - """ def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'): @@ -380,7 +411,6 @@ def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'): min_count: The minimum number of sub-schema in 'schema'. max_count: The maximum number of sub-schema in 'schema'. list_name: A string identifier for the ListOf object. - """ if not isinstance(schema, Schema): @@ -443,7 +473,6 @@ class Integer(Schema): True >>> Integer(lo=10, hi=30).matches(5) False - """ def __init__(self, lo= -sys.maxint, hi=sys.maxint): @@ -454,7 +483,6 @@ def __init__(self, lo= -sys.maxint, hi=sys.maxint): lo: The minimum value the int object argument can be. hi: The maximum value the int object argument can be. - """ self._lo = lo @@ -468,7 +496,7 @@ def check_match(self, object): raise tuf.FormatError('Got '+repr(object)+' instead of an integer.') elif not (self._lo <= object <= self._hi): - int_range = '['+repr(self._lo)+','+repr(self._hi)+'].' + int_range = '['+repr(self._lo)+', '+repr(self._hi)+'].' raise tuf.FormatError(repr(object)+' not in range '+int_range) @@ -502,7 +530,6 @@ class DictOf(Schema): False >>> schema.matches({'a': ['x', 'y'], 'e' : ['', ''], 'd' : ['a', 'b']}) False - """ def __init__(self, key_schema, value_schema): @@ -513,7 +540,6 @@ def __init__(self, key_schema, value_schema): key_schema: The dictionary's key. value_schema: The dictionary's value. - """ if not isinstance(key_schema, Schema): @@ -564,7 +590,6 @@ class Optional(Schema): False >>> schema.matches({'k1': 'X'}) True - """ def __init__(self, schema): @@ -604,7 +629,6 @@ class Object(Schema): False >>> schema.matches({'a':'ZYYY'}) False - """ def __init__(self, object_name='object', **required): @@ -616,7 +640,6 @@ def __init__(self, object_name='object', **required): object_name: A string identifier for the object argument. A variable number of keyword arguments is accepted. - """ # Ensure valid arguments. @@ -713,7 +736,6 @@ class Struct(Schema): False >>> schema.matches(['X', 3, 'A']) False - """ def __init__(self, sub_schemas, optional_schemas=[], allow_more=False, @@ -727,7 +749,6 @@ def __init__(self, sub_schemas, optional_schemas=[], allow_more=False, optional_schemas: The optional list of schemas. allow_more: Specifies that an optional list of types is allowed. struct_name: A string identifier for the Struct object. - """ # Ensure each item of the list contains the expected object type. @@ -792,7 +813,6 @@ class RegularExpression(Schema): False >>> schema.matches([33, 'Hello']) False - """ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): @@ -805,7 +825,6 @@ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): modifiers: Flags to use when compiling the pattern. re_object: A compiled regular expression object. re_name: Identifier for the regular expression object. - """ if not isinstance(pattern, basestring): diff --git a/tuf/sig.py b/tuf/sig.py index ce5b9f56..3ae7d9f6 100755 --- a/tuf/sig.py +++ b/tuf/sig.py @@ -33,7 +33,6 @@ that will determine if a role still has a sufficient number of valid keys. If a caller needs to update the signatures of a 'signable' object, there is also a function for that. - """ import tuf @@ -76,7 +75,6 @@ def get_signature_status(signable, role=None): A dictionary representing the status of the signatures in 'signable'. Conformant to tuf.formats.SIGNATURESTATUS_SCHEMA. - """ # Does 'signable' have the correct format? @@ -125,7 +123,7 @@ def get_signature_status(signable, role=None): # Identify key using an unknown key signing method. try: - valid_sig = tuf.rsa_key.verify_signature(key, signature, data) + valid_sig = tuf.keys.verify_signature(key, signature, data) except tuf.UnknownMethodError: unknown_method_sigs.append(keyid) continue @@ -201,7 +199,6 @@ def verify(signable, role): Boolean. True if the number of good signatures >= the role's threshold, False otherwise. - """ # Retrieve the signature status. tuf.sig.get_signature_status() raises @@ -243,10 +240,8 @@ def may_need_new_keys(signature_status): Boolean. - """ - # Does 'signature_status' have the correct format? # This check will ensure 'signature_status' has the appropriate number # of objects and object types, and that all dict keys are properly named. @@ -281,11 +276,11 @@ def generate_rsa_signature(signed, rsakey_dict): signed: - The data used by 'tuf.rsa_key.create_signature()' to generate signatures. + The data used by 'tuf.keys.create_signature()' to generate signatures. It is stored in the 'signed' field of 'signable'. rsakey_dict: - The RSA key, a tuf.formats.RSAKEY_SCHEMA dictionary. + The RSA key, a 'tuf.formats.RSAKEY_SCHEMA' dictionary. Used here to produce 'keyid', 'method', and 'sig'. @@ -300,7 +295,6 @@ def generate_rsa_signature(signed, rsakey_dict): Signature dictionary conformant to tuf.formats.SIGNATURE_SCHEMA. Has the form: {'keyid': keyid, 'method': 'evp', 'sig': sig} - """ # We need 'signed' in canonical JSON format to generate @@ -309,6 +303,6 @@ def generate_rsa_signature(signed, rsakey_dict): # Generate the RSA signature. # Raises tuf.FormatError and TypeError. - signature = tuf.rsa_key.create_signature(rsakey_dict, signed) + signature = tuf.keys.create_signature(rsakey_dict, signed) return signature diff --git a/tuf/tests/repository_setup.py b/tuf/tests/repository_setup.py index 066ca61e..778621d8 100644 --- a/tuf/tests/repository_setup.py +++ b/tuf/tests/repository_setup.py @@ -14,7 +14,6 @@ To provide a quick repository structure to be used in conjunction with test modules like test_updater.py for instance. - """ import os @@ -24,7 +23,6 @@ import tempfile import tuf.formats -import tuf.rsa_key as rsa_key import tuf.repo.keystore as keystore import tuf.repo.signerlib as signerlib import tuf.repo.signercli as signercli @@ -276,7 +274,6 @@ def create_repositories(): A dictionary of all repositories, with the following keys: (main_repository, client_repository, server_repository) - """ # Ensure the keyids for the required roles are loaded. Role keyids are diff --git a/tuf/tests/unittest_toolbox.py b/tuf/tests/unittest_toolbox.py index ff23af49..785fffcb 100644 --- a/tuf/tests/unittest_toolbox.py +++ b/tuf/tests/unittest_toolbox.py @@ -15,7 +15,6 @@ Provides an array of various methods for unit testing. Use it instead of actual unittest module. This module builds on unittest module. Specifically, Modified_TestCase is a derived class from unittest.TestCase. - """ import os @@ -27,7 +26,7 @@ import string import ConfigParser -import tuf.rsa_key as rsa_key +import tuf.keys import tuf.repo.keystore as keystore # Modify the number of iterations (from the higher default count) so the unit @@ -100,7 +99,6 @@ def setUp(): random_string(length=7): Generate a 'length' long string of random characters. - """ # List of all top level roles. @@ -222,6 +220,7 @@ def make_temp_config_file(self, suffix='', directory=None, config_dict={}, expir dictionary in it using ConfigParser. It then returns the temp file path, dictionary tuple. """ + config = ConfigParser.RawConfigParser() if not config_dict: # Using the fact that empty sequences are false. @@ -288,7 +287,6 @@ def make_temp_directory_with_data_files(self, _current_dir=None, Returns: ('/tmp/tmp_dir_Test_random/', [targets/tmp_random1.txt, targets/tmp_random2.txt, targets/more_targets/tmp_random3.txt]) - """ if not _current_dir: @@ -385,7 +383,7 @@ def generate_rsakey(): 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} """ - rsakey = rsa_key.generate() + rsakey = tuf.keys.generate_rsa_key() keyid = rsakey['keyid'] Modified_TestCase.rsa_keyids.append(keyid) password = Modified_TestCase.random_string() diff --git a/tuf/time_ed25519.py b/tuf/time_ed25519.py index ded684a1..89e8e726 100755 --- a/tuf/time_ed25519.py +++ b/tuf/time_ed25519.py @@ -3,30 +3,36 @@ import timeit import tuf -from tuf.ed25519_key import * +from tuf.ed25519_keys import * use_pynacl = False if '--pynacl' in sys.argv: use_pynacl = True -print('Time generate()') -print(timeit.timeit('generate(use_pynacl)', - setup='from __main__ import generate, use_pynacl', +print('Time generate_public_and_private()') +print(timeit.timeit('generate_public_and_private(use_pynacl)', + setup='from __main__ import generate_public_and_private, \ + use_pynacl', number=1)) print('\nTime create_signature()') -print(timeit.timeit('create_signature(ed25519_key, data, use_pynacl)', - setup='from __main__ import generate, create_signature, \ +print(timeit.timeit('create_signature(public, private, data, use_pynacl)', + setup='from __main__ import generate_public_and_private, \ + create_signature, \ use_pynacl; \ - ed25519_key = generate(use_pynacl);\ + public, private = \ + generate_public_and_private(use_pynacl); \ data = "The quick brown fox jumps over the lazy dog"', number=1)) print('\nTime verify_signature()') -print(timeit.timeit('verify_signature(ed25519_key, signature, data, use_pynacl)', - setup='from __main__ import generate, create_signature, \ - verify_signature, use_pynacl;\ - ed25519_key = generate(use_pynacl);\ +print(timeit.timeit('verify_signature(public, method, signature, data, use_pynacl)', + setup='from __main__ import generate_public_and_private, \ + create_signature, \ + verify_signature, use_pynacl; \ + public, private = \ + generate_public_and_private(use_pynacl); \ data = "The quick brown fox jumps over the lazy dog";\ - signature = create_signature(ed25519_key, data, use_pynacl)', + signature, method = \ + create_signature(public, private, data, use_pynacl)', number=1)) diff --git a/tuf/util.py b/tuf/util.py index 1460e838..249b9274 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -547,7 +547,7 @@ def load_json_file(filepath): Deserialize a JSON object from a file containing the object. - data: + filepath: Absolute path of JSON file.