mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 15:40:01 -06:00
68ada5f204
The 'cert_request' command accumulates DNS names from the CSR, before checking that all IP addresses in the CSR are reachable from those DNS names. Before adding a DNS name to the set, we check that that it corresponds to the FQDN of a known host/service principal (including principal aliases). When a DNS name maps to a "alternative" principal (i.e. not the one given via the 'principal' argument), this check was not being performed correctly. Specifically, we were looking for the 'krbprincipalname' field on the RPC response object directly, instead of its 'result' field. To resolve the issue, dereference the RPC response to its 'result' field before invoking the '_dns_name_matches_principal' subroutine. Fixes: https://pagure.io/freeipa/issue/8368 Reviewed-By: Rob Crittenden <rcritten@redhat.com>
506 lines
16 KiB
Python
506 lines
16 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_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_shortname = 'iptest'
|
|
host_fqdn = f'{host_shortname}.{api.env.domain}'
|
|
host_princ = f'host/{host_fqdn}'
|
|
host_ptr = f'{host_fqdn}.'
|
|
|
|
host2_shortname = 'iptest2'
|
|
host2_fqdn = f'{host2_shortname}.{api.env.domain}'
|
|
host2_princ = f'host/{host2_fqdn}'
|
|
host2_ptr = f'{host2_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'
|
|
|
|
host2_ipv4_address = '169.254.0.43'
|
|
host2_ipv4_revzone_s = '0.254.169.in-addr.arpa.'
|
|
host2_ipv4_revrec_s = '43'
|
|
|
|
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(host_shortname)
|
|
return tr.make_fixture(request)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def host2(request, xmlrpc_setup):
|
|
tr = HostTracker(host2_shortname)
|
|
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)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ipv4_revzone(host):
|
|
yield from _zone_setup(host, ipv4_revzone_s)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ipv6_revzone(host):
|
|
yield from _zone_setup(host, ipv6_revzone_s)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def host2_ipv4_ptr(host2, ipv4_revzone):
|
|
yield from _record_setup(
|
|
host2, ipv4_revzone, host2_ipv4_revrec_s, ptrrecord=host2_ptr)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ipv4_ptr(host, ipv4_revzone):
|
|
yield from _record_setup(
|
|
host, ipv4_revzone, ipv4_revrec_s, ptrrecord=host_ptr)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ipv6_ptr(host, ipv6_revzone):
|
|
yield from _record_setup(
|
|
host, ipv6_revzone, ipv6_revrec_s, ptrrecord=host_ptr)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def host2_ipv4_a(host2):
|
|
yield from _record_setup(
|
|
host2, api.env.domain, host2_shortname, arecord=host2_ipv4_address)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ipv4_a(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, host_shortname, arecord=ipv4_address)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def ipv6_aaaa(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, host_shortname, aaaarecord=ipv6_address)
|
|
|
|
|
|
@pytest.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)
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def ipv4_ptr_other(host, ipv4_revzone):
|
|
yield from _record_setup(
|
|
host, ipv4_revzone, ipv4_revrec_s, ptrrecord=other_ptr)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def cname1(host):
|
|
yield from _record_setup(
|
|
host, api.env.domain, 'cname1', cnamerecord='iptest')
|
|
|
|
|
|
@pytest.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)),
|
|
])
|
|
csr_two_dnsname_two_ip = csr([
|
|
x509.DNSName(host_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(ipv4_address)),
|
|
x509.DNSName(host2_fqdn),
|
|
x509.IPAddress(ipaddress.ip_address(host2_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)
|
|
|
|
|
|
@pytest.mark.tier1
|
|
class TestTwoHostsTwoIPAddresses(XMLRPC_test):
|
|
"""
|
|
Test certificate issuance with CSR containing two hosts
|
|
and two IP addresses (one for each host).
|
|
|
|
"""
|
|
def test_host_exists(
|
|
self, host, host2, ipv4_a, ipv4_ptr, host2_ipv4_a, host2_ipv4_ptr,
|
|
):
|
|
# for convenience, this test also establishes the DNS
|
|
# record fixtures, which have class scope
|
|
host.ensure_exists()
|
|
host2.ensure_exists()
|
|
|
|
def test_issuance(self, host, csr_two_dnsname_two_ip):
|
|
host.run_command(
|
|
'cert_request', csr_two_dnsname_two_ip, principal=host_princ)
|