Fix: chain exports use certificate equality instead of object identity

The chain export formatters in kca-core used  identity checks
(e.g. , ) to determine whether to
exclude the leaf certificate from the chain. This caused duplicates
when the chain was built from separately-parsed certificate objects
that were cryptographically identical (same DER/fingerprint) but
different Python instances.

Changed  →  for value equality across all affected formats:
  - _pem_chain()        : PEM chain export
  - PKCS#7 chain        : serialization_certificates
  - PKCS#12 chain       : serialize_key_and_certificates
  - OpenVPN             : issuer detection
  - JWK chain           : x5c certificate list

Also added a regression test (test_chain_exports_exclude_equivalent_leaf)
that reproduces the exact issue: a leaf certificate re-parsed from its
DER form is treated as a distinct object, so the old -based checks
kept both copies in the output. The new -based checks correctly
deduplicate, retain the issuer, and handle OpenVPN's CA selection.
This commit is contained in:
2026-10-26 12:06:16 +08:00
parent 82d448d8d4
commit c263f967dc
2 changed files with 58 additions and 5 deletions
@@ -2,6 +2,7 @@
from __future__ import annotations
import base64
import json
import time
import unittest
from datetime import datetime, timedelta, timezone
@@ -478,6 +479,56 @@ class ExportImportTest(unittest.TestCase):
items = imports.parse_file(result.data, "export.pfx", "secret")
self.assertGreaterEqual(len(items), 2)
def test_chain_exports_exclude_equivalent_leaf(self) -> None:
now = datetime.now(timezone.utc)
leaf = certificates.build_signed(
subject=_name("Export Leaf"),
public_key=self.key.public_key(),
issuer_cert=self.cert,
issuer_private_key=self.key,
serial=7,
not_before=now,
not_after=now + timedelta(days=30),
extension_spec=default_extension_spec(),
)
leaf_copy = certificates.parse_certificate(certificates.certificate_der(leaf))
self.assertIsNot(leaf_copy, leaf)
self.assertEqual(leaf_copy, leaf)
chain = [leaf_copy, self.cert]
for fmt in ("pem-chain", "pkcs7-chain", "pkcs12-chain"):
result = exports.export_certificate(
leaf,
fmt,
name="export",
chain=chain,
private_key=self.key,
password="secret",
)
items = imports.parse_file(result.data, result.filename, "secret")
exported_certs = [item.obj for item in items if item.kind == "certificate"]
self.assertEqual(len(exported_certs), 2, fmt)
self.assertEqual(sum(item == leaf for item in exported_certs), 1, fmt)
self.assertEqual(sum(item == self.cert for item in exported_certs), 1, fmt)
result = exports.export_certificate(leaf, "jwk-chain", name="export", chain=chain)
x5c = json.loads(result.data)["x5c"]
exported_der = [base64.b64decode(item) for item in x5c]
self.assertEqual(
exported_der,
[certificates.certificate_der(leaf), certificates.certificate_der(self.cert)],
)
result = exports.export_certificate(
leaf,
"openvpn",
name="export",
chain=[self.cert, leaf_copy],
private_key=self.key,
)
ca_pem = result.data.decode("ascii").split("<ca>\n", 1)[1].split("</ca>", 1)[0]
self.assertEqual(ca_pem, certificates.certificate_pem(self.cert))
class CryptoTest(unittest.TestCase):
def test_envelope(self) -> None: