Verify external CA's basic constraint pathlen

IPA no verifies that intermediate certs of external CAs have a basic
constraint path len of at least 1 and increasing.

Fixes: https://pagure.io/freeipa/issue/7877
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Fraser Tweedale <ftweedal@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
Christian Heimes 2019-04-03 10:43:29 +02:00
parent 3509545897
commit 3c354e74f3
8 changed files with 52 additions and 16 deletions

View File

@ -541,6 +541,9 @@ class NSSDatabase:
def get_trust_chain(self, nickname): def get_trust_chain(self, nickname):
"""Return names of certs in a given cert's trust chain """Return names of certs in a given cert's trust chain
The list starts with root ca, then first intermediate CA, second
intermediate, and so on.
:param nickname: Name of the cert :param nickname: Name of the cert
:return: List of certificate names :return: List of certificate names
""" """
@ -912,7 +915,7 @@ class NSSDatabase:
except ValueError: except ValueError:
raise ValueError('invalid for server %s' % hostname) raise ValueError('invalid for server %s' % hostname)
def verify_ca_cert_validity(self, nickname): def verify_ca_cert_validity(self, nickname, minpathlen=None):
cert = self.get_cert(nickname) cert = self.get_cert(nickname)
if not cert.subject: if not cert.subject:
@ -926,6 +929,15 @@ class NSSDatabase:
if not bc.value.ca: if not bc.value.ca:
raise ValueError("not a CA certificate") raise ValueError("not a CA certificate")
if minpathlen is not None:
# path_length is None means no limitation
pl = bc.value.path_length
if pl is not None and pl < minpathlen:
raise ValueError(
"basic contraint pathlen {}, must be at least {}".format(
pl, minpathlen
)
)
try: try:
ski = cert.extensions.get_extension_for_class( ski = cert.extensions.get_extension_for_class(

View File

@ -893,9 +893,12 @@ def load_pkcs12(cert_files, key_password, key_nickname, ca_cert_files,
"The full certificate chain is not present in %s" % "The full certificate chain is not present in %s" %
(", ".join(cert_files))) (", ".join(cert_files)))
for nickname in trust_chain[1:]: # verify CA validity and pathlen. The trust_chain list is in reverse
# order. trust_chain[1] is the first intermediate CA cert and must
# have pathlen >= 0.
for minpathlen, nickname in enumerate(trust_chain[1:], start=0):
try: try:
nssdb.verify_ca_cert_validity(nickname) nssdb.verify_ca_cert_validity(nickname, minpathlen)
except ValueError as e: except ValueError as e:
raise ScriptError( raise ScriptError(
"CA certificate %s in %s is not valid: %s" % "CA certificate %s in %s is not valid: %s" %
@ -1038,9 +1041,12 @@ def load_external_cert(files, ca_subject):
"missing certificate with subject '%s'" % "missing certificate with subject '%s'" %
(", ".join(files), issuer)) (", ".join(files), issuer))
for nickname in trust_chain: # verify CA validity and pathlen. The trust_chain list is in reverse
# order. The first entry is the signed IPA-CA and must have a
# pathlen of >= 0.
for minpathlen, nickname in enumerate(trust_chain, start=0):
try: try:
nssdb.verify_ca_cert_validity(nickname) nssdb.verify_ca_cert_validity(nickname, minpathlen)
except ValueError as e: except ValueError as e:
cert, subject, issuer = cache[nickname] cert, subject, issuer = cache[nickname]
raise ScriptError( raise ScriptError(

View File

@ -38,7 +38,7 @@ class ExternalCA:
self.now = datetime.datetime.utcnow() self.now = datetime.datetime.utcnow()
self.delta = datetime.timedelta(days=days) self.delta = datetime.timedelta(days=days)
def create_ca(self, cn=ISSUER_CN): def create_ca(self, cn=ISSUER_CN, path_length=None):
"""Create root CA. """Create root CA.
:returns: bytes -- Root CA in PEM format. :returns: bytes -- Root CA in PEM format.
@ -79,7 +79,7 @@ class ExternalCA:
) )
builder = builder.add_extension( builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None), x509.BasicConstraints(ca=True, path_length=path_length),
critical=True, critical=True,
) )

View File

@ -1152,7 +1152,7 @@ jobs:
class: RunPytest class: RunPytest
args: args:
build_url: '{fedora-28/build_url}' build_url: '{fedora-28/build_url}'
test_suite: test_integration/test_external_ca.py::TestExternalCAInvalidCert test_suite: test_integration/test_external_ca.py::TestExternalCAInvalidCert test_integration/test_external_ca.py::TestExternalCAInvalidIntermediate
template: *ci-master-f28 template: *ci-master-f28
timeout: 3600 timeout: 3600
topology: *master_1repl topology: *master_1repl

View File

@ -1152,7 +1152,7 @@ jobs:
class: RunPytest class: RunPytest
args: args:
build_url: '{fedora-29/build_url}' build_url: '{fedora-29/build_url}'
test_suite: test_integration/test_external_ca.py::TestExternalCAInvalidCert test_suite: test_integration/test_external_ca.py::TestExternalCAInvalidCert test_integration/test_external_ca.py::TestExternalCAInvalidIntermediate
template: *ci-master-f29 template: *ci-master-f29
timeout: 3600 timeout: 3600
topology: *master_1repl topology: *master_1repl

View File

@ -1152,7 +1152,7 @@ jobs:
class: RunPytest class: RunPytest
args: args:
build_url: '{fedora-rawhide/build_url}' build_url: '{fedora-rawhide/build_url}'
test_suite: test_integration/test_external_ca.py::TestExternalCAInvalidCert test_suite: test_integration/test_external_ca.py::TestExternalCAInvalidCert test_integration/test_external_ca.py::TestExternalCAInvalidIntermediate
template: *ci-master-frawhide template: *ci-master-frawhide
timeout: 3600 timeout: 3600
topology: *master_1repl topology: *master_1repl

View File

@ -1632,7 +1632,8 @@ def add_dns_zone(master, zone, skip_overlap_check=False,
logger.debug('Zone %s already added.', zone) logger.debug('Zone %s already added.', zone)
def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name): def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name,
root_ca_path_length=None, ipa_ca_path_length=1):
""" """
Sign ipa csr and save signed CA together with root CA back to the host. Sign ipa csr and save signed CA together with root CA back to the host.
Returns root CA and IPA CA paths on the host. Returns root CA and IPA CA paths on the host.
@ -1645,9 +1646,9 @@ def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name):
external_ca = ExternalCA() external_ca = ExternalCA()
# Create root CA # Create root CA
root_ca = external_ca.create_ca() root_ca = external_ca.create_ca(path_length=root_ca_path_length)
# Sign CSR # Sign CSR
ipa_ca = external_ca.sign_csr(ipa_csr) ipa_ca = external_ca.sign_csr(ipa_csr, path_length=ipa_ca_path_length)
root_ca_fname = os.path.join(test_dir, root_ca_name) root_ca_fname = os.path.join(test_dir, root_ca_name)
ipa_ca_fname = os.path.join(test_dir, ipa_ca_name) ipa_ca_fname = os.path.join(test_dir, ipa_ca_name)
@ -1656,7 +1657,7 @@ def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name):
host.put_file_contents(root_ca_fname, root_ca) host.put_file_contents(root_ca_fname, root_ca)
host.put_file_contents(ipa_ca_fname, ipa_ca) host.put_file_contents(ipa_ca_fname, ipa_ca)
return (root_ca_fname, ipa_ca_fname) return root_ca_fname, ipa_ca_fname
def generate_ssh_keypair(): def generate_ssh_keypair():

View File

@ -80,7 +80,8 @@ def install_server_external_ca_step1(host, extra_args=()):
) )
def install_server_external_ca_step2(host, ipa_ca_cert, root_ca_cert): def install_server_external_ca_step2(host, ipa_ca_cert, root_ca_cert,
raiseonerr=True):
"""Step 2 to install the ipa server with external ca""" """Step 2 to install the ipa server with external ca"""
args = ['ipa-server-install', args = ['ipa-server-install',
'-a', host.config.admin_password, '-a', host.config.admin_password,
@ -88,7 +89,7 @@ def install_server_external_ca_step2(host, ipa_ca_cert, root_ca_cert):
'--external-cert-file', ipa_ca_cert, '--external-cert-file', ipa_ca_cert,
'--external-cert-file', root_ca_cert] '--external-cert-file', root_ca_cert]
cmd = host.run_command(args) cmd = host.run_command(args, raiseonerr=raiseonerr)
return cmd return cmd
@ -333,6 +334,22 @@ class TestExternalCAInvalidCert(IntegrationTest):
assert result.returncode == 1 assert result.returncode == 1
class TestExternalCAInvalidIntermediate(IntegrationTest):
"""Test case for https://pagure.io/freeipa/issue/7877"""
def test_invalid_intermediate(self):
install_server_external_ca_step1(self.master)
root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport(
self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA,
root_ca_path_length=0
)
result = install_server_external_ca_step2(
self.master, ipa_ca_fname, root_ca_fname, raiseonerr=False
)
assert result.returncode > 0
assert "basic contraint pathlen" in result.stderr_text
class TestExternalCAInstall(IntegrationTest): class TestExternalCAInstall(IntegrationTest):
"""install CA cert manually """ """install CA cert manually """