Isolate encryption-related methods

Create an interface to the cryptography library so that internally
Zuul uses simple facade methods.  Unit test that interface, and
that it is compatible with OpenSSL.

Change-Id: I57da1081c8d43b0b44af5967d075908459c91687
This commit is contained in:
James E. Blair 2017-03-17 10:59:37 -07:00
parent c49e5e713f
commit bf1a4f2192
8 changed files with 231 additions and 80 deletions

View File

@ -4,6 +4,7 @@
mysql-client [test]
mysql-server [test]
libjpeg-dev [test]
openssl [test]
zookeeperd [platform:dpkg]
build-essential [platform:dpkg]
gcc [platform:rpm]

View File

@ -15,10 +15,7 @@
import sys
import os
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from zuul.lib import encryption
FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
'fixtures')
@ -27,24 +24,10 @@ FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
def main():
private_key_file = os.path.join(FIXTURE_DIR, 'private.pem')
with open(private_key_file, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
private_key, public_key = \
encryption.deserialize_rsa_keypair(f.read())
# Extract public key from private
public_key = private_key.public_key()
# https://cryptography.io/en/stable/hazmat/primitives/asymmetric/rsa/#encryption
ciphertext = public_key.encrypt(
sys.argv[1],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
ciphertext = encryption.encrypt_pkcs1(sys.argv[1], public_key)
print(ciphertext.encode('base64'))
if __name__ == '__main__':

View File

@ -0,0 +1,69 @@
# Copyright 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import subprocess
import tempfile
from zuul.lib import encryption
from tests.base import BaseTestCase
class TestEncryption(BaseTestCase):
def setUp(self):
super(TestEncryption, self).setUp()
self.private, self.public = encryption.generate_rsa_keypair()
def test_serialization(self):
"Verify key serialization"
pem_private = encryption.serialize_rsa_private_key(self.private)
private2, public2 = encryption.deserialize_rsa_keypair(pem_private)
# cryptography public / private key objects don't implement
# equality testing, so we make sure they have the same numbers.
self.assertEqual(self.private.private_numbers(),
private2.private_numbers())
self.assertEqual(self.public.public_numbers(),
public2.public_numbers())
def test_pkcs1(self):
"Verify encryption and decryption"
orig_plaintext = "some text to encrypt"
ciphertext = encryption.encrypt_pkcs1(orig_plaintext, self.public)
plaintext = encryption.decrypt_pkcs1(ciphertext, self.private)
self.assertEqual(orig_plaintext, plaintext)
def test_openssl_pkcs1(self):
"Verify that we can decrypt something encrypted with OpenSSL"
orig_plaintext = "some text to encrypt"
pem_public = encryption.serialize_rsa_public_key(self.public)
public_file = tempfile.NamedTemporaryFile(delete=False)
try:
public_file.write(pem_public)
public_file.close()
p = subprocess.Popen(['openssl', 'rsautl', '-encrypt',
'-oaep', '-pubin', '-inkey',
public_file.name],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
(stdout, stderr) = p.communicate(orig_plaintext)
ciphertext = stdout
finally:
os.unlink(public_file.name)
plaintext = encryption.decrypt_pkcs1(ciphertext, self.private)
self.assertEqual(orig_plaintext, plaintext)

View File

@ -19,11 +19,10 @@ import random
import fixtures
import testtools
import yaml
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from zuul import model
from zuul import configloader
from zuul.lib import encryption
from tests.base import BaseTestCase, FIXTURE_DIR
@ -35,11 +34,8 @@ class TestJob(BaseTestCase):
self.project = model.Project('project', None)
private_key_file = os.path.join(FIXTURE_DIR, 'private.pem')
with open(private_key_file, "rb") as f:
self.project.private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
self.project.private_key, self.project.public_key = \
encryption.deserialize_rsa_keypair(f.read())
self.context = model.SourceContext(self.project, 'master',
'test', True)
self.start_mark = yaml.Mark('name', 0, 0, 0, '', 0)

View File

@ -17,11 +17,10 @@
import os
import textwrap
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import testtools
import zuul.configloader
from zuul.lib import encryption
from tests.base import AnsibleZuulTestCase, ZuulTestCase, FIXTURE_DIR
@ -328,11 +327,8 @@ class TestProjectKeys(ZuulTestCase):
private_key_file = os.path.join(key_root, 'gerrit/org/project.pem')
# Make sure that a proper key was created on startup
with open(private_key_file, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
private_key, public_key = \
encryption.deserialize_rsa_keypair(f.read())
with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
fixture_private_key = i.read()

View File

@ -21,15 +21,11 @@ import textwrap
import voluptuous as vs
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from zuul import model
import zuul.manager.dependent
import zuul.manager.independent
from zuul import change_matcher
from zuul.lib import encryption
# Several forms accept either a single item or a list, this makes
@ -147,16 +143,7 @@ class EncryptedPKCS1(yaml.YAMLObject):
return cls(node.value)
def decrypt(self, private_key):
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#decryption
plaintext = private_key.decrypt(
self.ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
return plaintext
return encryption.decrypt_pkcs1(self.ciphertext, private_key)
class NodeSetParser(object):
@ -793,26 +780,15 @@ class TenantParser(object):
TenantParser.log.info(
"Generating RSA keypair for project %s" % (project.name,)
)
private_key, public_key = encryption.generate_rsa_keypair()
pem_private_key = encryption.serialize_rsa_private_key(private_key)
# Generate private RSA key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
# Serialize private key
pem_private_key = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
# Dump keys to filesystem. We only save the private key
# because the public key can be constructed from it.
TenantParser.log.info(
"Saving RSA keypair for project %s to %s" % (
project.name, project.private_key_file)
)
# Dump keys to filesystem
with open(project.private_key_file, 'wb') as f:
f.write(pem_private_key)
@ -824,16 +800,10 @@ class TenantParser(object):
'Private key file {0} not found'.format(
project.private_key_file))
# Load private key
# Load keypair
with open(project.private_key_file, "rb") as f:
project.private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
# Extract public key from private
project.public_key = project.private_key.public_key()
(project.private_key, project.public_key) = \
encryption.deserialize_rsa_keypair(f.read())
@staticmethod
def _loadTenantConfigRepos(project_key_dir, connections, conf_tenant):

138
zuul/lib/encryption.py Normal file
View File

@ -0,0 +1,138 @@
# Copyright 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#generation
def generate_rsa_keypair():
"""Generate an RSA keypair.
:returns: A tuple (private_key, public_key)
"""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
public_key = private_key.public_key()
return (private_key, public_key)
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#key-serialization
def serialize_rsa_private_key(private_key):
"""Serialize an RSA private key
This returns a PEM-encoded serialized form of an RSA private key
suitable for storing on disk. It is not password-protected.
:arg private_key: A private key object as returned by
:func:generate_rsa_keypair()
:returns: A PEM-encoded string representation of the private key.
"""
return private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
def serialize_rsa_public_key(public_key):
"""Serialize an RSA public key
This returns a PEM-encoded serialized form of an RSA public key
suitable for distribution.
:arg public_key: A pubilc key object as returned by
:func:generate_rsa_keypair()
:returns: A PEM-encoded string representation of the public key.
"""
return public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#key-loading
def deserialize_rsa_keypair(data):
"""Deserialize an RSA private key
This deserializes an RSA private key and returns the keypair
(private and public) for use in decryption.
:arg data: A PEM-encoded serialized private key
:returns: A tuple (private_key, public_key)
"""
private_key = serialization.load_pem_private_key(
data,
password=None,
backend=default_backend()
)
public_key = private_key.public_key()
return (private_key, public_key)
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#decryption
def decrypt_pkcs1(ciphertext, private_key):
"""Decrypt PKCS1 (RSAES-OAEP) encoded ciphertext
:arg ciphertext: A string previously encrypted with PKCS1
(RSAES-OAEP).
:arg private_key: A private key object as returned by
:func:generate_rsa_keypair()
:returns: The decrypted form of the ciphertext as a string.
"""
return private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#encryption
def encrypt_pkcs1(plaintext, public_key):
"""Encrypt data with PKCS1 (RSAES-OAEP)
:arg plaintext: A string to encrypt with PKCS1 (RSAES-OAEP).
:arg public_key: A public key object as returned by
:func:generate_rsa_keypair()
:returns: The encrypted form of the plaintext.
"""
return public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)

View File

@ -22,7 +22,8 @@ import time
from paste import httpserver
import webob
from webob import dec
from cryptography.hazmat.primitives import serialization
from zuul.lib import encryption
"""Zuul main web app.
@ -111,11 +112,8 @@ class WebApp(threading.Thread):
if not project:
raise webob.exc.HTTPNotFound()
# Serialize public key
pem_public_key = project.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
pem_public_key = encryption.serialize_rsa_public_key(
project.public_key)
response = webob.Response(body=pem_public_key,
content_type='text/plain')