diff --git a/tests/base.py b/tests/base.py index 45a0b6b131..d61cdca886 100644 --- a/tests/base.py +++ b/tests/base.py @@ -2668,10 +2668,11 @@ class ZuulTestCase(BaseTestCase): fn = os.path.join(key_root, '.version') with open(fn, 'w') as f: f.write('1') + # secrets key private_key_file = os.path.join( key_root, 'secrets', 'project', source, project, '0.pem') private_key_dir = os.path.dirname(private_key_file) - self.log.debug("Installing test keys for project %s at %s" % ( + self.log.debug("Installing test secrets keys for project %s at %s" % ( project, private_key_file)) if not os.path.isdir(private_key_dir): os.makedirs(private_key_dir) @@ -2679,6 +2680,18 @@ class ZuulTestCase(BaseTestCase): with open(private_key_file, 'w') as o: o.write(i.read()) + # ssh key + private_key_file = os.path.join( + key_root, 'ssh', 'project', source, project, '0.pem') + private_key_dir = os.path.dirname(private_key_file) + self.log.debug("Installing test ssh keys for project %s at %s" % ( + project, private_key_file)) + if not os.path.isdir(private_key_dir): + os.makedirs(private_key_dir) + with open(os.path.join(FIXTURE_DIR, 'ssh.pem')) as i: + with open(private_key_file, 'w') as o: + o.write(i.read()) + def setupZK(self): self.zk_chroot_fixture = self.useFixture( ChrootedKazooFixture(self.id())) @@ -2727,8 +2740,11 @@ class ZuulTestCase(BaseTestCase): if self.create_project_keys: return - with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i: - test_key = i.read() + test_keys = [] + key_fns = ['private.pem', 'ssh.pem'] + for fn in key_fns: + with open(os.path.join(FIXTURE_DIR, fn)) as i: + test_keys.append(i.read()) key_root = os.path.join(self.state_root, 'keys') for root, dirname, files in os.walk(key_root): @@ -2736,7 +2752,7 @@ class ZuulTestCase(BaseTestCase): if fn == '.version': continue with open(os.path.join(root, fn)) as f: - self.assertEqual(test_key, f.read()) + self.assertTrue(f.read() in test_keys) def assertFinalState(self): self.log.debug("Assert final state") diff --git a/tests/fixtures/ssh.pem b/tests/fixtures/ssh.pem new file mode 100644 index 0000000000..1b193b2ab8 --- /dev/null +++ b/tests/fixtures/ssh.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA2coHon7sFMbXqeRDUzydrFvp5qpHIbZBfaN8sJyaSiYFYkUz +kMW1Kcn1mRVUps2kBo2qzvCjDsOsNGGdbPnDxwrT+ssN3I+7stWVrcc6Sh9gFFvt +7O+F8a1FSpO/L3CkOBhYd/r0TLZ/UK6fBY/7E9lmnhoXpZxpvBzoxGHYB/0ohZG0 +6o1cWBcmC6wvvaoYeX7udUuWm6Wlx32DN++IErz1iwrayNflrOyd41fgn35RD3sd +ay9ezlQJrautnyjgJfPXWMbzXgPN0tcpRBCiFmVRRcI2qVPHeXWRS3yhNHPYpOTX +CA4Woc0iY08HAicg8yjIGnT8GisRAfvpqRZM0wIDAQABAoIBAQCanpRNCU8ScRkr +xKMHtUE73QVyffGCPaLBUBB2Urg3bEbmPbseTT8RLBDxXfN7eQO6o1lhEfaxxLm9 +dpANjkUwSr+0jfSJYoIftQNPHOKFPUE5Mwr37BVsP1eyWrKhO5dbO+2TQNewnuBE +p7S+fjoDHZV9KYkgSqvGob+frNdy0zjF7LbRLKbnGiVudMq0zNZ/E77XwKXDW4+U +2P6JTR+0jing7gRSFmCgVePBuo1aJO+F+Tr8wHqvArcYgDjn5jFW7xCQR53onKFS +FZVMTVERAAu1dqE5Ucsamy/N67Yu7jGRB/Vwa5WYbvjl23UjbOJiRt/EG+sf4doJ +/FywJ4gBAoGBAPs5o1ZAWFZEXsRbzR+ao4Vou6CaBdioR/h7xhS3xs4GAewmQfKK +cl8lqSd4a6rIwrnEwcvMOnJ0mP+if7ZoRrkK0RYR5A1qoEShTGz9xDyM5deg8nqK +VhvwkLZg20O1wtkr7mXun0pPs6s6lcjtuBZ4hPX8dTphfHLw5MsF8HiDAoGBAN3t +tKNXnPI/uyEzEoMOHs826bKt2aawGfagAUXRFdaQLPXEbuiYZT8YvwnUv2gUbmu+ +WeLBI3Oo+YJSs8r6JUVnuOXm+S45fj5I1Su2ykxecZWFG1GDa4LLlp/iYUEtgDmU +HMng6PRxD9zPha7EqirKsvOCYWO5qscGZzFoUYlxAoGAYJ6BQCnND5iJ7fD0ieQa +YbOu/YxfFT1bOKi5vLwVXKUY1i68jEBMzmUYklKQ7gT6RyHx+qRYEi7frOldPtUJ +5h7P3TISSEqqytpSH1TVxQfXWb/PoetURLiXn1zO11KvVoC71j4YyyauDfuhIb6z +XwkI8eYfW82kZDxbce2d12sCgYEAvy8qMJUnhaHViZI/3lrpu8Uoql8OY4TNuSK6 +NfUbhQ4LTWX9za6LekHNQaDfi8AeJ/+B29BaxCbLW7P3Y2L/fL0QEi5ad7Hbybhg +vBnqSMQLwa07jYtTsQfGKNKSyd1y2yd3bYqt5PcJnUXBen+9wMOCSjkFwS2Pq4ke +mPevVmECgYEAu5O2qgo25gEorjpu1q6qRcPQ3hSi5i6yKK9JywTxCCZdRjfyqPFs +M/2T6BQLxfffdcgIrstvnK2tEqugA292bKp2WAs6nXx6/qMM9CO+dq6zlYenLPwo +m6pb4KKm38dLDoMvtn2cqkpcR5Mr8sAEAEHNBD3quLSJZhFLXpo3X58= +-----END RSA PRIVATE KEY----- diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py index e37484df6b..d871a2d80b 100644 --- a/tests/unit/test_v3.py +++ b/tests/unit/test_v3.py @@ -21,6 +21,8 @@ import gc import time from unittest import skip +import paramiko + import zuul.configloader from zuul.lib import encryption from tests.base import ( @@ -2685,24 +2687,40 @@ class TestProjectKeys(ZuulTestCase): tenant_config_file = 'config/in-repo/main.yaml' def test_key_generation(self): + test_keys = [] + key_fns = ['private.pem', 'ssh.pem'] + for fn in key_fns: + with open(os.path.join(FIXTURE_DIR, fn)) as i: + test_keys.append(i.read()) + key_root = os.path.join(self.state_root, 'keys') - private_key_file = os.path.join( + secrets_key_file = os.path.join( key_root, 'secrets/project/gerrit/org/project/0.pem') # Make sure that a proper key was created on startup - with open(private_key_file, "rb") as f: - private_key, public_key = \ + with open(secrets_key_file, "rb") as f: + private_secrets_key, public_secrets_key = \ encryption.deserialize_rsa_keypair(f.read()) - with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i: - fixture_private_key = i.read() - # Make sure that we didn't just end up with the static fixture # key - self.assertNotEqual(fixture_private_key, private_key) + self.assertTrue(private_secrets_key not in test_keys) # Make sure it's the right length - self.assertEqual(4096, private_key.key_size) + self.assertEqual(4096, private_secrets_key.key_size) + + ssh_key_file = os.path.join( + key_root, + 'ssh/project/gerrit/org/project/0.pem') + # Make sure that a proper key was created on startup + ssh_key = paramiko.RSAKey.from_private_key_file(ssh_key_file) + + # Make sure that we didn't just end up with the static fixture + # key + self.assertTrue(private_secrets_key not in test_keys) + + # Make sure it's the right length + self.assertEqual(2048, ssh_key.get_bits()) class RoleTestCase(ZuulTestCase): diff --git a/zuul/configloader.py b/zuul/configloader.py index bd467d4a18..87e76c9eaf 100644 --- a/zuul/configloader.py +++ b/zuul/configloader.py @@ -1348,15 +1348,22 @@ class TenantParser(object): project.private_secrets_key_file = \ self.keystorage.getProjectSecretsKeyFile( connection_name, project.name) + project.private_ssh_key_file = \ + self.keystorage.getProjectSSHKeyFile( + connection_name, project.name) self._generateKeys(project) self._loadKeys(project) + (project.private_ssh_key, project.public_ssh_key) = \ + self.keystorage.getProjectSSHKeys(connection_name, project.name) + def _generateKeys(self, project): - if os.path.isfile(project.private_secrets_key_file): + filename = project.private_secrets_key_file + if os.path.isfile(filename): return - key_dir = os.path.dirname(project.private_secrets_key_file) + key_dir = os.path.dirname(filename) if not os.path.isdir(key_dir): os.makedirs(key_dir, 0o700) @@ -1370,16 +1377,15 @@ class TenantParser(object): # because the public key can be constructed from it. self.log.info( "Saving RSA keypair for project %s to %s" % ( - project.name, project.private_secrets_key_file) + project.name, filename) ) # Ensure private key is read/write for zuul user only. - with open(os.open(project.private_secrets_key_file, + with open(os.open(filename, os.O_CREAT | os.O_WRONLY, 0o600), 'wb') as f: f.write(pem_private_key) - @staticmethod - def _loadKeys(project): + def _loadKeys(self, project): # Check the key files specified are there if not os.path.isfile(project.private_secrets_key_file): raise Exception( diff --git a/zuul/lib/keystorage.py b/zuul/lib/keystorage.py index d4ccecec29..852d8d68ac 100644 --- a/zuul/lib/keystorage.py +++ b/zuul/lib/keystorage.py @@ -16,6 +16,10 @@ import tempfile import logging import os +import paramiko + +RSA_KEY_SIZE = 2048 + class Migration(object): log = logging.getLogger("zuul.KeyStorage") @@ -119,6 +123,7 @@ class MigrationV1(Migration): class KeyStorage(object): + log = logging.getLogger("zuul.KeyStorage") current_version = MigrationV1 def __init__(self, root): @@ -133,3 +138,41 @@ class KeyStorage(object): version = '0' return os.path.join(self.root, 'secrets', 'project', connection, project, version + '.pem') + + def getProjectSSHKeyFile(self, connection, project, version=None): + """Return the path to the private ssh key for the project""" + # We don't actually support multiple versions yet + if version is None: + version = '0' + return os.path.join(self.root, 'ssh', 'project', + connection, project, version + '.pem') + + def getProjectSSHKeys(self, connection, project): + """Return the private and public SSH keys for the project + + A new key will be created if necessary. + + :returns: A tuple containing the PEM encoded private key and + base64 encoded public key. + + """ + + private_key_file = self.getProjectSSHKeyFile(connection, project) + if not os.path.exists(private_key_file): + self.log.info( + "Generating SSH public key for project %s", project + ) + self._createSSHKey(private_key_file) + key = paramiko.RSAKey.from_private_key_file(private_key_file) + with open(private_key_file, 'r') as f: + private_key = f.read() + public_key = key.get_base64() + return (private_key, public_key) + + def _createSSHKey(self, fn): + key_dir = os.path.dirname(fn) + if not os.path.isdir(key_dir): + os.makedirs(key_dir, 0o700) + + pk = paramiko.RSAKey.generate(bits=RSA_KEY_SIZE) + pk.write_private_key_file(fn)