mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-26 00:41:25 -06:00
292d686c0b
Even though Pytest supports xunit style setups, unittest and nose tests, this support is limited and may be dropped in the future releases. Worst of all is that the mixing of various test frameworks results in weird conflicts and of course, is not widely tested. This is a part of work to remove the mixing of test idioms in the IPA's test suite: 1) replace xunit style 2) employ the fixtures' interdependencies Related: https://pagure.io/freeipa/issue/7989 Signed-off-by: Stanislav Levin <slev@altlinux.org> Reviewed-By: Christian Heimes <cheimes@redhat.com>
453 lines
15 KiB
Python
453 lines
15 KiB
Python
#
|
|
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
|
|
#
|
|
|
|
"""
|
|
Test certificate requests with IP addresses in SAN, and error
|
|
scenarios.
|
|
|
|
Tests use the default profile (caIPAserviceCert) and the main CA.
|
|
The operator is ``admin``.
|
|
|
|
Various DNS records are created, modified and deleted during this
|
|
test.
|
|
|
|
"""
|
|
|
|
import ipaddress
|
|
import pytest
|
|
|
|
from cryptography import x509
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography.x509.oid import NameOID
|
|
|
|
from ipalib import api, errors
|
|
from ipatests.test_util import yield_fixture
|
|
from ipatests.test_xmlrpc.tracker.host_plugin import HostTracker
|
|
from ipatests.test_xmlrpc.tracker.user_plugin import UserTracker
|
|
from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test
|
|
|
|
host_fqdn = f'iptest.{api.env.domain}'
|
|
host_princ = f'host/{host_fqdn}'
|
|
host_ptr = f'{host_fqdn}.'
|
|
|
|
other_fqdn = f'other.{api.env.domain}'
|
|
other_ptr = f'{other_fqdn}.'
|
|
|
|
ipv4_address = '169.254.0.42'
|
|
ipv4_revzone_s = '0.254.169.in-addr.arpa.'
|
|
ipv4_revrec_s = '42'
|
|
|
|
ipv6_address = 'fe80::8f18:bdab:4299:95fa'
|
|
ipv6_revzone_s = '0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.'
|
|
ipv6_revrec_s = 'a.f.5.9.9.9.2.4.b.a.d.b.8.1.f.8'
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def host(request, xmlrpc_setup):
|
|
tr = HostTracker('iptest')
|
|
return tr.make_fixture(request)
|
|
|
|
|
|
def _zone_setup(host, zone):
|
|
try:
|
|
host.run_command('dnszone_add', zone)
|
|
except errors.DuplicateEntry:
|
|
delete = False
|
|
else:
|
|
delete = True
|
|
|
|
yield zone
|
|
|
|
if delete:
|
|
host.run_command('dnszone_del', zone)
|
|
|
|
|
|
def _record_setup(host, zone, record, **kwargs):
|
|
try:
|
|
host.run_command('dnsrecord_add', zone, record, **kwargs)
|
|
except (errors.DuplicateEntry, errors.EmptyModlist):
|
|
delete = False
|
|
else:
|
|
delete = True
|
|
|
|
yield
|
|
|
|
if delete:
|
|
host.run_command('dnsrecord_del', zone, record, **kwargs)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def ipv4_revzone(host):
|
|
yield from _zone_setup(host, ipv4_revzone_s)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def ipv6_revzone(host):
|
|
yield from _zone_setup(host, ipv6_revzone_s)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def ipv4_ptr(host, ipv4_revzone):
|
|
yield from _record_setup(
|
|
host, ipv4_revzone, ipv4_revrec_s, ptrrecord=host_ptr)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def ipv6_ptr(host, ipv6_revzone):
|
|
yield from _record_setup(
|
|
host, ipv6_revzone, ipv6_revrec_s, ptrrecord=host_ptr)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def ipv4_a(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, 'iptest', arecord=ipv4_address)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def ipv6_aaaa(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, 'iptest', aaaarecord=ipv6_address)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def other_forward_records(host):
|
|
"""
|
|
Create A and AAAA records (to the "correct" IP address) for
|
|
the name "other.{domain}.".
|
|
|
|
"""
|
|
yield from _record_setup(
|
|
host, api.env.domain, 'other',
|
|
arecord=ipv4_address, aaaarecord=ipv6_address)
|
|
|
|
|
|
@yield_fixture(scope='function')
|
|
def ipv4_ptr_other(host, ipv4_revzone):
|
|
yield from _record_setup(
|
|
host, ipv4_revzone, ipv4_revrec_s, ptrrecord=other_ptr)
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def cname1(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, 'cname1', cnamerecord='iptest')
|
|
|
|
|
|
@yield_fixture(scope='class')
|
|
def cname2(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, 'cname2', cnamerecord='cname1')
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
def private_key():
|
|
return rsa.generate_private_key(
|
|
public_exponent=65537,
|
|
key_size=2048,
|
|
backend=default_backend()
|
|
)
|
|
|
|
|
|
def csr(altnames, cn=host_fqdn):
|
|
"""
|
|
Return a fixture that generates a CSR with the given altnames.
|
|
The altname values MUST be of type x509.DNSName, x509.IPAddress, etc.
|
|
|
|
The subject DN is always to CN={host_fqdn}.
|
|
|
|
"""
|
|
def inner(private_key):
|
|
builder = x509.CertificateSigningRequestBuilder()
|
|
builder = builder.subject_name(
|
|
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]))
|
|
if len(altnames) > 0:
|
|
builder = builder.add_extension(
|
|
x509.SubjectAlternativeName(altnames), False)
|
|
csr = builder.sign(private_key, hashes.SHA256(), default_backend())
|
|
return csr.public_bytes(serialization.Encoding.PEM).decode('ascii')
|
|
|
|
return pytest.fixture(scope='module')(inner)
|
|
|
|
|
|
csr_ipv4 = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
])
|
|
csr_ipv6 = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv6_address)),
|
|
])
|
|
csr_ipv4_ipv6 = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
x509.IPAddress(ipaddress.ip_address(ipv6_address)),
|
|
])
|
|
csr_extra_ipv4 = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
x509.IPAddress(ipaddress.ip_address('172.16.254.254')),
|
|
])
|
|
csr_no_dnsname = csr([
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
])
|
|
csr_alice = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
], cn='alice')
|
|
csr_iptest_other = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.DNSName(other_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
])
|
|
csr_cname1 = csr([
|
|
x509.DNSName(f'cname1.{api.env.domain}'),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
])
|
|
csr_cname2 = csr([
|
|
x509.DNSName(f'cname2.{api.env.domain}'),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
])
|
|
|
|
|
|
@pytest.fixture
|
|
def user_alice(request):
|
|
user = UserTracker('alice', 'Alice', 'Able')
|
|
return user.make_fixture(request)
|
|
|
|
|
|
# Patterns for ValidationError messages
|
|
PAT_FWD = "unreachable from DNS names"
|
|
PAT_REV = "does not have PTR record"
|
|
PAT_LOOP = "does not match A/AAAA records"
|
|
PAT_USER = "forbidden for user principals"
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressSANIssuance(XMLRPC_test):
|
|
"""
|
|
These are the tests that can be executed using the "correct" DNS
|
|
records. Tests for failure scenarios that require "incorrect"
|
|
records are found in other classes.
|
|
|
|
"""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
|
|
def test_issuance_ipv4(self, host, ipv4_a, ipv4_ptr, csr_ipv4):
|
|
host.run_command('cert_request', csr_ipv4, principal=host_princ)
|
|
|
|
def test_issuance_ipv6(self, host, ipv6_aaaa, ipv6_ptr, csr_ipv6):
|
|
host.run_command('cert_request', csr_ipv6, principal=host_princ)
|
|
|
|
def test_issuance_ipv4_ipv6(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv4_ptr, ipv6_ptr, csr_ipv4_ipv6):
|
|
host.run_command('cert_request', csr_ipv4_ipv6, principal=host_princ)
|
|
|
|
def test_failure_extra_ip(self, host, ipv4_a, ipv4_ptr, csr_extra_ipv4):
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command(
|
|
'cert_request', csr_extra_ipv4, principal=host_princ)
|
|
|
|
def test_failure_no_dnsname(self, host, ipv4_a, ipv4_ptr, csr_no_dnsname):
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command(
|
|
'cert_request', csr_no_dnsname, principal=host_princ)
|
|
|
|
def test_failure_user_princ(
|
|
self, host, ipv4_a, ipv4_ptr, csr_alice, user_alice):
|
|
user_alice.ensure_exists()
|
|
with pytest.raises(errors.ValidationError, match=PAT_USER):
|
|
host.run_command(
|
|
'cert_request', csr_alice, principal=user_alice.uid)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressSANMissingARecord(XMLRPC_test):
|
|
"""When there is no A record for the DNS name."""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
|
|
def test_issuance_ipv4(
|
|
self, host, ipv6_aaaa, ipv6_ptr, ipv4_ptr, csr_ipv4):
|
|
"""Issuing with IPv4 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command('cert_request', csr_ipv4, principal=host_princ)
|
|
|
|
def test_issuance_ipv6(
|
|
self, host, ipv6_aaaa, ipv6_ptr, ipv4_ptr, csr_ipv6):
|
|
"""Issuing with IPv6 address succeeds."""
|
|
host.run_command('cert_request', csr_ipv6, principal=host_princ)
|
|
|
|
def test_issuance_ipv4_ipv6(
|
|
self, host, ipv6_aaaa, ipv4_ptr, ipv6_ptr, csr_ipv4_ipv6):
|
|
"""Issuing with IPv4 *and* IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command(
|
|
'cert_request', csr_ipv4_ipv6, principal=host_princ)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressSANMissingAAAARecord(XMLRPC_test):
|
|
"""When there is no AAAA record for the DNS name."""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
|
|
def test_issuance_ipv4(
|
|
self, host, ipv4_a, ipv6_ptr, ipv4_ptr, csr_ipv4):
|
|
"""Issuing with IPv4 address suceeds."""
|
|
host.run_command('cert_request', csr_ipv4, principal=host_princ)
|
|
|
|
def test_issuance_ipv6(
|
|
self, host, ipv4_a, ipv6_ptr, ipv4_ptr, csr_ipv6):
|
|
"""Issuing with IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command('cert_request', csr_ipv6, principal=host_princ)
|
|
|
|
def test_issuance_ipv4_ipv6(
|
|
self, host, ipv4_a, ipv4_ptr, ipv6_ptr, csr_ipv4_ipv6):
|
|
"""Issuing with IPv4 *and* IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command(
|
|
'cert_request', csr_ipv4_ipv6, principal=host_princ)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressSANMissingIPv4Ptr(XMLRPC_test):
|
|
"""When there is no IPv4 PTR record for the address."""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
|
|
def test_issuance_ipv4(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv6_ptr, csr_ipv4):
|
|
"""Issuing with IPv4 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_REV):
|
|
host.run_command('cert_request', csr_ipv4, principal=host_princ)
|
|
|
|
def test_issuance_ipv6(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv6_ptr, csr_ipv6):
|
|
"""Issuing with IPv6 address succeeds."""
|
|
host.run_command('cert_request', csr_ipv6, principal=host_princ)
|
|
|
|
def test_issuance_ipv4_ipv6(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv6_ptr, csr_ipv4_ipv6):
|
|
"""Issuing with IPv4 *and* IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_REV):
|
|
host.run_command(
|
|
'cert_request', csr_ipv4_ipv6, principal=host_princ)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressSANMissingIPv6Ptr(XMLRPC_test):
|
|
"""When there is no IPv6 PTR record for the address."""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
|
|
def test_issuance_ipv4(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv4_ptr, csr_ipv4):
|
|
"""Issuing with IPv4 address succeeds."""
|
|
host.run_command('cert_request', csr_ipv4, principal=host_princ)
|
|
|
|
def test_issuance_ipv6(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv4_ptr, csr_ipv6):
|
|
"""Issuing with IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_REV):
|
|
host.run_command('cert_request', csr_ipv6, principal=host_princ)
|
|
|
|
def test_issuance_ipv4_ipv6(
|
|
self, host, ipv4_a, ipv6_aaaa, ipv4_ptr, csr_ipv4_ipv6):
|
|
"""Issuing with IPv4 *and* IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_REV):
|
|
host.run_command(
|
|
'cert_request', csr_ipv4_ipv6, principal=host_princ)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressSANOtherForwardRecords(XMLRPC_test):
|
|
"""
|
|
A sanity check that we really are only looking at the
|
|
forward records of interest. We leave out records for
|
|
the DNS name of interest, but create A and AAAA records
|
|
for a different name in the same zone, which point to the
|
|
IP address of interest. Issuance must fail.
|
|
|
|
"""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
|
|
def test_issuance_ipv4(
|
|
self, host, other_forward_records, ipv4_ptr, ipv6_ptr, csr_ipv4):
|
|
"""Issuing with IPv4 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command('cert_request', csr_ipv4, principal=host_princ)
|
|
|
|
def test_issuance_ipv6(
|
|
self, host, other_forward_records, ipv4_ptr, ipv6_ptr, csr_ipv6):
|
|
"""Issuing with IPv6 address fails."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command('cert_request', csr_ipv6, principal=host_princ)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressPTRLoopback(XMLRPC_test):
|
|
"""
|
|
A PTR record must point back to the name from which the IP
|
|
address was reached. Even when the PTR points to a name in the
|
|
SAN, unless the aforementioned condition is satisfied, issuance
|
|
must not proceed.
|
|
|
|
ORDER IS IMPORTANT for this test (because the ipv4_ptr fixture
|
|
has *class* scope).
|
|
|
|
"""
|
|
def test_host_exists(self, host):
|
|
host.ensure_exists()
|
|
host.run_command(
|
|
'host_add_principal', host.fqdn, f'host/other.{api.env.domain}')
|
|
|
|
def test_failure(self, host, ipv4_a, ipv4_ptr_other, csr_iptest_other):
|
|
"""The A and PTR records are not symmetric."""
|
|
with pytest.raises(errors.ValidationError, match=PAT_LOOP):
|
|
host.run_command(
|
|
'cert_request', csr_iptest_other, principal=host_princ)
|
|
|
|
def test_success(self, host, ipv4_a, ipv4_ptr, csr_iptest_other):
|
|
"""
|
|
The A and PTR records are symmetric. This test ensures that the
|
|
presence of an extra DNSName does not interfere with the IP address
|
|
validation.
|
|
|
|
"""
|
|
host.run_command(
|
|
'cert_request', csr_iptest_other, principal=host_princ)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestIPAddressCNAME(XMLRPC_test):
|
|
"""
|
|
A single level of CNAME indirection is supported. PTR must be
|
|
symmetric with the *canonical* name.
|
|
|
|
Relevant principal aliases or managedby relationships are
|
|
required by the DNSName validation regime.
|
|
|
|
"""
|
|
def test_host_exists(self, host, cname1, cname2, ipv4_a, ipv4_ptr):
|
|
# for convenience, this test also establishes the DNS
|
|
# record fixtures, which have class scope
|
|
host.ensure_exists()
|
|
host.run_command(
|
|
'host_add_principal', host.fqdn, f'host/cname1.{api.env.domain}')
|
|
host.run_command(
|
|
'host_add_principal', host.fqdn, f'host/cname2.{api.env.domain}')
|
|
|
|
def test_one_level(self, host, csr_cname1):
|
|
host.run_command('cert_request', csr_cname1, principal=host_princ)
|
|
|
|
def test_two_levels(self, host, csr_cname2):
|
|
with pytest.raises(errors.ValidationError, match=PAT_FWD):
|
|
host.run_command('cert_request', csr_cname2, principal=host_princ)
|