mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2026-11-02 08:59:57 -06:00
Correctly recognize OID 2.5.4.97, organizationIdentifier as a subject/issuer DN of the CA certificate
OID 2.5.4.97 added to the ATTR_NAME_BY_OID list, and cainstance during 2-step installation is re-fetching CA certificate and re-parsing OIDs based on the list. Example: cert issuer: <Name(C=CZ,2.5.4.97=LLCCZ-123456789,O=Corp,CN=ROOT)> DN(cert issuer): CN=ROOT,O=Corp,organizationIdentifier=LLCCZ-123456789,C=CZ Fixes: https://pagure.io/freeipa/issue/9866 Signed-off-by: Aleksandr Sharov <asharov@redhat.com> Reviewed-By: Rob Crittenden <rcritten@redhat.com> Reviewed-By: David Hanina <dhanina@redhat.com>
This commit is contained in:
1 parent
ec22295fed
commit
e0b9ad0ae0
3 files changed
+381
-2
No files matched your search
@@ -1462,4 +1462,5 @@ ATTR_NAME_BY_OID = {
|
||||
cryptography.x509.ObjectIdentifier('2.5.4.9'): 'STREET',
|
||||
cryptography.x509.ObjectIdentifier('2.5.4.17'): 'postalCode',
|
||||
cryptography.x509.ObjectIdentifier('0.9.2342.19200300.100.1.1'): 'UID',
|
||||
cryptography.x509.ObjectIdentifier('2.5.4.97'): 'organizationIdentifier',
|
||||
}
|
||||
@@ -2394,10 +2394,16 @@ def ensure_ipa_authority_entry():
|
||||
api.Backend.ra_lightweight_ca.override_port = 8443
|
||||
with api.Backend.ra_lightweight_ca as lwca:
|
||||
data = lwca.read_ca('host-authority')
|
||||
# Loading certificate to properly re-parse issuer and subject DNs in
|
||||
# case CA doesn't recognize some of the OIDs. DN class will re-access
|
||||
# the OIDs based on ATTR_NAME_BY_OID list.
|
||||
cert_data = lwca.read_ca_cert('host-authority')
|
||||
cert = x509.load_der_x509_certificate(cert_data)
|
||||
|
||||
attrs = dict(
|
||||
ipacaid=data['id'],
|
||||
ipacaissuerdn=data['issuerDN'],
|
||||
ipacasubjectdn=data['dn'],
|
||||
ipacaissuerdn=DN(cert.issuer),
|
||||
ipacasubjectdn=DN(cert.subject),
|
||||
)
|
||||
api.Backend.ra_lightweight_ca.override_port = None
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import time
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509.oid import ObjectIdentifier, NameOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
|
||||
from ipatests.pytest_ipa.integration import tasks
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
@@ -108,6 +110,142 @@ def check_ipaca_issuerDN(host, expected_dn):
|
||||
assert "Issuer DN: {}".format(expected_dn) in result.stdout_text
|
||||
|
||||
|
||||
def create_external_ca_with_subject(subject_attrs):
|
||||
"""
|
||||
Create an external CA with custom subject attributes including non-standard
|
||||
OIDs.
|
||||
|
||||
:param subject_attrs: List of x509.NameAttribute objects to include in
|
||||
subject
|
||||
:return: Tuple of (ExternalCA object, root CA certificate as PEM bytes)
|
||||
|
||||
Example:
|
||||
subj_attrs = [
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, 'My CA'),
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
|
||||
x509.NameAttribute(ObjectIdentifier('2.5.4.97'), 'VATEU-123456789')
|
||||
]
|
||||
external_ca, root_ca_pem = create_external_ca_with_subject(subj_attrs)
|
||||
signed_cert = external_ca.sign_csr(csr_data)
|
||||
"""
|
||||
external_ca = ExternalCA()
|
||||
external_ca.create_ca_key()
|
||||
|
||||
# Create the custom subject
|
||||
subject = x509.Name(subject_attrs)
|
||||
external_ca.issuer = subject
|
||||
|
||||
# Build the root CA certificate
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject)
|
||||
builder = builder.issuer_name(subject) # self-signed
|
||||
builder = builder.public_key(external_ca.ca_public_key)
|
||||
builder = builder.serial_number(x509.random_serial_number())
|
||||
builder = builder.not_valid_before(external_ca.now)
|
||||
builder = builder.not_valid_after(external_ca.now + external_ca.delta)
|
||||
|
||||
# Add required extensions for a CA certificate
|
||||
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(
|
||||
external_ca.ca_public_key
|
||||
),
|
||||
critical=False,
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(
|
||||
external_ca.ca_public_key
|
||||
),
|
||||
critical=False,
|
||||
)
|
||||
|
||||
# Sign the certificate
|
||||
root_ca_cert = builder.sign(
|
||||
external_ca.ca_key, hashes.SHA256(), default_backend()
|
||||
)
|
||||
root_ca_pem = root_ca_cert.public_bytes(serialization.Encoding.PEM)
|
||||
|
||||
return external_ca, root_ca_pem
|
||||
|
||||
|
||||
def find_cert_in_chain(cert_chain, subject_attrs=None, issuer_attrs=None):
|
||||
"""
|
||||
Retrieves a certificate from a provided chain that matches specified
|
||||
criteria. The search can be filtered using dictionaries of subject
|
||||
attributes, issuer attributes, or a combination of both.
|
||||
|
||||
:param cert_chain: List of certificates to search through
|
||||
:param subject_attrs: Dict of OID -> expected value for subject attributes
|
||||
:param issuer_attrs: Dict of OID -> expected value for issuer attributes
|
||||
:return: The matching certificate or None if not found
|
||||
|
||||
Example:
|
||||
from cryptography.x509.oid import NameOID, ObjectIdentifier
|
||||
org_id_oid = ObjectIdentifier("2.5.4.97")
|
||||
|
||||
# Find IPA CA cert with specific subject and issuer
|
||||
cert = find_cert_in_chain(
|
||||
ca_chain,
|
||||
subject_attrs={
|
||||
NameOID.COMMON_NAME: "Certificate Authority",
|
||||
NameOID.ORGANIZATION_NAME: "EXAMPLE.TEST"
|
||||
},
|
||||
issuer_attrs={
|
||||
org_id_oid: "VATEU-123456789"
|
||||
}
|
||||
)
|
||||
"""
|
||||
for cert in cert_chain:
|
||||
# Check subject attributes if provided
|
||||
if subject_attrs:
|
||||
subject_match = True
|
||||
for oid, expected_value in subject_attrs.items():
|
||||
attrs = [attr for attr in cert.subject if attr.oid == oid]
|
||||
if not any(attr.value == expected_value for attr in attrs):
|
||||
# This cert doesn't match, move to next cert
|
||||
subject_match = False
|
||||
break
|
||||
if not subject_match:
|
||||
continue
|
||||
|
||||
# Check issuer attributes if provided
|
||||
if issuer_attrs:
|
||||
issuer_match = True
|
||||
for oid, expected_value in issuer_attrs.items():
|
||||
attrs = [attr for attr in cert.issuer if attr.oid == oid]
|
||||
if not any(attr.value == expected_value for attr in attrs):
|
||||
# This cert doesn't match, move to next cert
|
||||
issuer_match = False
|
||||
break
|
||||
if not issuer_match:
|
||||
continue
|
||||
|
||||
# All specified attributes match, return this cert
|
||||
return cert
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def check_mscs_extension(ipa_csr, template):
|
||||
csr = x509.load_pem_x509_csr(ipa_csr, default_backend())
|
||||
extensions = [
|
||||
@@ -243,6 +381,133 @@ class TestExternalCAConstraints(IntegrationTest):
|
||||
self.master.run_command(['ipa', 'ping'])
|
||||
|
||||
|
||||
class TestExternalCAInstallWithOrgId(IntegrationTest):
|
||||
"""Test 2-step installation with external CA containing
|
||||
organizationIdentifier.
|
||||
|
||||
This test verifies that FreeIPA can successfully install with a 2-step
|
||||
external CA process when the external CA certificate contains the
|
||||
organizationIdentifier attribute (OID 2.5.4.97) in its issuer DN.
|
||||
|
||||
This tests the fix for DN parsing in ensure_ipa_authority_entry in
|
||||
cainstance.py where the issuer DN must be properly parsed against
|
||||
ATTR_NAME_BY_OID to recognize all OIDs including organizationIdentifier.
|
||||
"""
|
||||
num_replicas = 0
|
||||
num_clients = 0
|
||||
|
||||
def test_external_ca_install_with_organization_identifier(self):
|
||||
"""Test 2-step installation with organizationIdentifier (OID 2.5.4.97)
|
||||
|
||||
Verify that FreeIPA can successfully complete a 2-step installation
|
||||
with an external CA that contains organizationIdentifier (OID 2.5.4.97)
|
||||
in the issuer DN. The issuer DN should be properly parsed and stored
|
||||
in LDAP during the ensure_ipa_authority_entry process.
|
||||
"""
|
||||
|
||||
# Test parameters
|
||||
org_id_value = "VATEU-123456789"
|
||||
org_id_oid = ObjectIdentifier("2.5.4.97") # organizationIdentifier OID
|
||||
external_ca_cn = "External CA with OrgID"
|
||||
|
||||
# Step 1 of ipa-server-install
|
||||
result = install_server_external_ca_step1(self.master)
|
||||
assert result.returncode == 0
|
||||
|
||||
# Get the CSR generated by step 1
|
||||
ipa_csr = self.master.get_file_contents(paths.ROOT_IPA_CSR)
|
||||
|
||||
# Create an external CA with organizationIdentifier in the subject
|
||||
subject_attrs = [
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, external_ca_cn),
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Test Organization'),
|
||||
x509.NameAttribute(org_id_oid, org_id_value),
|
||||
]
|
||||
external_ca, root_ca = create_external_ca_with_subject(subject_attrs)
|
||||
|
||||
# Sign the IPA CSR with the external CA that has organizationIdentifier
|
||||
ipa_ca = external_ca.sign_csr(ipa_csr)
|
||||
|
||||
# Write certificates to files
|
||||
root_ca_fname = os.path.join(
|
||||
self.master.config.test_dir,
|
||||
'root_ca_with_orgid.crt'
|
||||
)
|
||||
ipa_ca_fname = os.path.join(
|
||||
self.master.config.test_dir,
|
||||
'ipa_ca_signed_with_orgid.crt'
|
||||
)
|
||||
|
||||
# Transport certificates 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
|
||||
# This should succeed despite organizationIdentifier in issuer DN
|
||||
result = install_server_external_ca_step2(
|
||||
self.master, ipa_ca_fname, root_ca_fname
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
# Make sure IPA server is working properly
|
||||
tasks.kinit_admin(self.master)
|
||||
result = self.master.run_command(['ipa', 'user-show', 'admin'])
|
||||
assert 'User login: admin' in result.stdout_text
|
||||
|
||||
# Verify IPA is functional
|
||||
result = self.master.run_command(['ipa', 'ping'])
|
||||
assert result.returncode == 0
|
||||
|
||||
# Verify the certificate chain contains the expected certificates
|
||||
# Load all certificates from /etc/ipa/ca.crt (the CA chain)
|
||||
ca_chain_content = self.master.get_file_contents(paths.IPA_CA_CRT)
|
||||
ca_chain = ipa_x509.load_certificate_list(ca_chain_content)
|
||||
|
||||
# 1. Find and verify the IPA CA certificate
|
||||
# It should have subject O=REALM, CN=Certificate Authority
|
||||
# and issuer with organizationIdentifier
|
||||
ipa_ca_cert = find_cert_in_chain(
|
||||
ca_chain,
|
||||
subject_attrs={
|
||||
NameOID.COMMON_NAME: "Certificate Authority",
|
||||
NameOID.ORGANIZATION_NAME: self.master.domain.realm
|
||||
},
|
||||
issuer_attrs={
|
||||
org_id_oid: org_id_value,
|
||||
NameOID.COMMON_NAME: external_ca_cn
|
||||
}
|
||||
)
|
||||
assert ipa_ca_cert is not None, \
|
||||
f"Did not find IPA CA certificate with subject " \
|
||||
f"O={self.master.domain.realm}, CN=Certificate Authority " \
|
||||
f"and issuer with organizationIdentifier={org_id_value}"
|
||||
|
||||
# 2. Find and verify the external root CA certificate
|
||||
# It should be self-signed with organizationIdentifier in subject
|
||||
external_ca_cert = find_cert_in_chain(
|
||||
ca_chain,
|
||||
subject_attrs={
|
||||
NameOID.COMMON_NAME: external_ca_cn,
|
||||
org_id_oid: org_id_value
|
||||
},
|
||||
issuer_attrs={
|
||||
NameOID.COMMON_NAME: external_ca_cn,
|
||||
org_id_oid: org_id_value
|
||||
}
|
||||
)
|
||||
assert external_ca_cert is not None, \
|
||||
f"Did not find external root CA certificate (CN={external_ca_cn})" \
|
||||
f" with organizationIdentifier={org_id_value} in subject"
|
||||
|
||||
# 3. Verify the issuer DN is correctly stored in LDAP
|
||||
# The issuer DN should contain organizationIdentifier
|
||||
# Note: The order in the DN string representation matters
|
||||
result = self.master.run_command(['ipa', 'ca-show', 'ipa'])
|
||||
assert f"organizationIdentifier={org_id_value}" in result.stdout_text, \
|
||||
"organizationIdentifier not found in IPA CA issuer DN"
|
||||
|
||||
|
||||
def verify_caentry(host, cert):
|
||||
"""
|
||||
Verify the content of cn=DOMAIN IPA CA,cn=certificates,cn=ipa,cn=etc,basedn
|
||||
@@ -475,6 +740,113 @@ class TestExternalCAInstall(IntegrationTest):
|
||||
self.master.run_command([paths.IPA_CACERT_MANAGE, 'install',
|
||||
root_ca_fname])
|
||||
|
||||
def test_renew_external_ca_with_organization_identifier(self):
|
||||
"""Test CA renewal with organizationIdentifier (OID 2.5.4.97)
|
||||
|
||||
Verify that FreeIPA can successfully renew CA with an external CA
|
||||
that contains organizationIdentifier (OID 2.5.4.97) in the issuer DN.
|
||||
The IPA CA will be signed by this external CA, and the issuer DN
|
||||
will contain the organizationIdentifier attribute.
|
||||
"""
|
||||
|
||||
# Test parameters
|
||||
org_id_value = "VATEU-123456789"
|
||||
org_id_oid = ObjectIdentifier("2.5.4.97") # organizationIdentifier OID
|
||||
external_ca_cn = "External CA with OrgID"
|
||||
|
||||
# Initiate CA renewal with external CA
|
||||
result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew',
|
||||
'--external-ca'])
|
||||
assert result.returncode == 0
|
||||
|
||||
# Get the CSR generated by the renewal process
|
||||
ipa_csr = self.master.get_file_contents(paths.IPA_CA_CSR)
|
||||
|
||||
# Create an external CA with organizationIdentifier in the subject
|
||||
subject_attrs = [
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, external_ca_cn),
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Test Organization'),
|
||||
x509.NameAttribute(org_id_oid, org_id_value),
|
||||
]
|
||||
external_ca, root_ca = create_external_ca_with_subject(subject_attrs)
|
||||
|
||||
# Sign the IPA CSR with the external CA that has organizationIdentifier
|
||||
ipa_ca = external_ca.sign_csr(ipa_csr)
|
||||
|
||||
# Write certificates to files
|
||||
root_ca_fname = os.path.join(
|
||||
self.master.config.test_dir,
|
||||
'root_ca_with_orgid.crt'
|
||||
)
|
||||
ipa_ca_fname = os.path.join(
|
||||
self.master.config.test_dir,
|
||||
'ipa_ca_signed_with_orgid.crt'
|
||||
)
|
||||
|
||||
# Transport certificates to master
|
||||
self.master.put_file_contents(root_ca_fname, root_ca)
|
||||
self.master.put_file_contents(ipa_ca_fname, ipa_ca)
|
||||
|
||||
# Complete the renewal with the signed certificates
|
||||
# This should succeed despite organizationIdentifier in issuer DN
|
||||
result = self.master.run_command([
|
||||
paths.IPA_CACERT_MANAGE, 'renew',
|
||||
'--external-cert-file', ipa_ca_fname,
|
||||
'--external-cert-file', root_ca_fname
|
||||
])
|
||||
assert result.returncode == 0
|
||||
|
||||
# Verify the CA was properly installed
|
||||
result = self.master.run_command([paths.IPA_CERTUPDATE])
|
||||
assert result.returncode == 0
|
||||
|
||||
# Verify IPA is still functional
|
||||
tasks.kinit_admin(self.master)
|
||||
result = self.master.run_command(['ipa', 'ping'])
|
||||
assert result.returncode == 0
|
||||
|
||||
# Verify the certificate chain contains the expected certificates
|
||||
# Load all certificates from /etc/ipa/ca.crt (the CA chain)
|
||||
ca_chain_content = self.master.get_file_contents(paths.IPA_CA_CRT)
|
||||
ca_chain = ipa_x509.load_certificate_list(ca_chain_content)
|
||||
|
||||
# 1. Find and verify the IPA CA certificate
|
||||
# It should have subject O=REALM, CN=Certificate Authority
|
||||
# and issuer with organizationIdentifier
|
||||
ipa_ca_cert = find_cert_in_chain(
|
||||
ca_chain,
|
||||
subject_attrs={
|
||||
NameOID.COMMON_NAME: "Certificate Authority",
|
||||
NameOID.ORGANIZATION_NAME: self.master.domain.realm
|
||||
},
|
||||
issuer_attrs={
|
||||
org_id_oid: org_id_value,
|
||||
NameOID.COMMON_NAME: external_ca_cn
|
||||
}
|
||||
)
|
||||
assert ipa_ca_cert is not None, \
|
||||
f"Did not find IPA CA certificate with subject " \
|
||||
f"O={self.master.domain.realm}, CN=Certificate Authority " \
|
||||
f"and issuer with organizationIdentifier={org_id_value}"
|
||||
|
||||
# 2. Find and verify the external root CA certificate
|
||||
# It should be self-signed with organizationIdentifier in subject
|
||||
external_ca_cert = find_cert_in_chain(
|
||||
ca_chain,
|
||||
subject_attrs={
|
||||
NameOID.COMMON_NAME: external_ca_cn,
|
||||
org_id_oid: org_id_value
|
||||
},
|
||||
issuer_attrs={
|
||||
NameOID.COMMON_NAME: external_ca_cn,
|
||||
org_id_oid: org_id_value
|
||||
}
|
||||
)
|
||||
assert external_ca_cert is not None, \
|
||||
f"Did not find external root CA certificate (CN={external_ca_cn})"\
|
||||
f" with organizationIdentifier={org_id_value} in subject"
|
||||
|
||||
|
||||
class TestMultipleExternalCA(IntegrationTest):
|
||||
"""Setup externally signed ca1
|
||||
|
||||
Reference in new issue
Block a user