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: I57da1081c8d43b0b44af5967d075908459c91687changes/87/447087/7
parent
c49e5e713f
commit
bf1a4f2192
|
@ -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]
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue