From 353a4910897976471630639fc445a7c66f71b1a5 Mon Sep 17 00:00:00 2001 From: Michal Reznik Date: Oct 03 2017 06:47:52 +0000 Subject: test_external_ca: switch to python-cryptography Switch external CA generation from certutil to python-cryptography as this way of handling the certificates should be more readable, maintainable and extendable (e.g. extensions handling). Also as external CA is now a separate module we can import it and use elsewhere. https://pagure.io/freeipa/issue/7154 Reviewed-By: Stanislav Laznicka Reviewed-By: Christian Heimes --- diff --git a/ipatests/test_integration/create_external_ca.py b/ipatests/test_integration/create_external_ca.py new file mode 100644 index 0000000..dc4ef04 --- /dev/null +++ b/ipatests/test_integration/create_external_ca.py @@ -0,0 +1,155 @@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +import datetime +import six + + +class ExternalCA(object): + """ + Provide external CA for testing + """ + def create_ca(self, cn='example.test'): + """Create root CA. + + :returns: bytes -- Root CA in PEM format. + """ + self.ca_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + + self.ca_public_key = self.ca_key.public_key() + + subject = self.issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, six.text_type(cn)), + ]) + + builder = x509.CertificateBuilder() + builder = builder.subject_name(subject) + builder = builder.issuer_name(self.issuer) + builder = builder.public_key(self.ca_public_key) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.not_valid_before(datetime.datetime.utcnow()) + builder = builder.not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ) + + builder = builder.add_extension( + x509.KeyUsage( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=True, + ) + + builder = builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(self.ca_public_key), + critical=False, + ) + + builder = builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key( + self.ca_public_key + ), + critical=False, + ) + + cert = builder.sign(self.ca_key, hashes.SHA256(), default_backend()) + + return cert.public_bytes(serialization.Encoding.PEM) + + def sign_csr(self, ipa_csr): + """Sign certificate CSR. + + :param ipa_csr: CSR in PEM format. + :type ipa_csr: bytes. + :returns: bytes -- Signed CA in PEM format. + """ + csr_tbs = x509.load_pem_x509_csr(ipa_csr, default_backend()) + + csr_public_key = csr_tbs.public_key() + csr_subject = csr_tbs.subject + + builder = x509.CertificateBuilder() + builder = builder.public_key(csr_public_key) + builder = builder.subject_name(csr_subject) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.issuer_name(self.issuer) + builder = builder.not_valid_before(datetime.datetime.utcnow()) + builder = builder.not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365)) + + builder = builder.add_extension( + x509.KeyUsage( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + + builder = builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(csr_public_key), + critical=False, + ) + + builder = builder.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key( + self.ca_public_key + ), + critical=False, + ) + + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=1), + critical=True, + ) + + cert = builder.sign( + private_key=self.ca_key, + algorithm=hashes.SHA256(), + backend=default_backend(), + ) + + return cert.public_bytes(serialization.Encoding.PEM) diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py index 7fc89cc..967a8dd 100644 --- a/ipatests/test_integration/test_external_ca.py +++ b/ipatests/test_integration/test_external_ca.py @@ -1,8 +1,6 @@ -# Authors: -# Ana Krivokapic # -# Copyright (C) 2013 Red Hat -# see file 'COPYING' for use and warranty information +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,15 +14,12 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + import os -import base64 from ipatests.pytest_plugins.integration import tasks from ipatests.test_integration.base import IntegrationTest - - -EXTERNAL_CA_KEY_ID = base64.b64encode(os.urandom(64)) -IPA_CA_KEY_ID = base64.b64encode(os.urandom(64)) +from ipatests.test_integration.create_external_ca import ExternalCA class TestExternalCA(IntegrationTest): @@ -44,70 +39,31 @@ class TestExternalCA(IntegrationTest): '--external-ca' ]) - nss_db = os.path.join(self.master.config.test_dir, 'testdb') - external_cert_file = os.path.join(nss_db, 'ipa.crt') - external_ca_file = os.path.join(nss_db, 'ca.crt') - noisefile = os.path.join(self.master.config.test_dir, 'noise.txt') - pwdfile = os.path.join(self.master.config.test_dir, 'pwdfile.txt') + test_dir = self.master.config.test_dir - # Create noise and password files for NSS database - self.master.run_command('date | sha256sum > %s' % noisefile) - self.master.run_command('echo %s > %s' % - (self.master.config.admin_password, pwdfile)) + # Get IPA CSR as bytes + ipa_csr = self.master.get_file_contents('/root/ipa.csr') - # Create NSS database - self.master.run_command(['mkdir', nss_db]) - self.master.run_command([ - 'certutil', '-N', - '-d', nss_db, - '-f', pwdfile - ]) - - # Create external CA - self.master.run_command([ - 'certutil', '-S', - '-d', nss_db, - '-f', pwdfile, - '-n', 'external', - '-s', 'CN=External CA, O=%s' % self.master.domain.name, - '-x', - '-t', 'CTu,CTu,CTu', - '-g', '2048', - '-m', '0', - '-v', '60', - '-z', noisefile, - '-2', '-1', '-5', '--extSKID' - ], stdin_text='5\n9\nn\ny\n10\ny\n{}\nn\n5\n6\n7\n9\nn\n' - ''.format(EXTERNAL_CA_KEY_ID)) + external_ca = ExternalCA() + # Create root CA + root_ca = external_ca.create_ca() + # Sign CSR + ipa_ca = external_ca.sign_csr(ipa_csr) - # Sign IPA cert request using the external CA - self.master.run_command([ - 'certutil', '-C', - '-d', nss_db, - '-f', pwdfile, - '-c', 'external', - '-m', '1', - '-v', '60', - '-2', '-1', '-3', '--extSKID', - '-i', '/root/ipa.csr', - '-o', external_cert_file, - '-a' - ], stdin_text='0\n1\n5\n9\ny\ny\n\ny\ny\n{}\n-1\n\nn\n{}\nn\n' - ''.format(EXTERNAL_CA_KEY_ID, IPA_CA_KEY_ID)) + root_ca_fname = os.path.join(test_dir, 'root_ca.crt') + ipa_ca_fname = os.path.join(test_dir, 'ipa_ca.crt') - # Export external CA file - self.master.run_command( - 'certutil -L -d %s -n "external" -a > %s' % - (nss_db, external_ca_file) - ) + # Transport certificates (string > file) to master + self.master.put_file_contents(root_ca_fname, root_ca) + self.master.put_file_contents(ipa_ca_fname, ipa_ca) # Step 2 of ipa-server-install self.master.run_command([ 'ipa-server-install', '-a', self.master.config.admin_password, '-p', self.master.config.dirman_password, - '--external-cert-file', external_cert_file, - '--external-cert-file', external_ca_file + '--external-cert-file', ipa_ca_fname, + '--external-cert-file', root_ca_fname ]) # Make sure IPA server is working properly