Add full XCA curve and digest parity
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
"""End-to-end core workflows for XCA curve and signature-hash parity."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
from kca.core import algorithms, certificates, crls, exports, imports, keys, requests
|
||||
from kca.core.extensions import default_extension_spec
|
||||
from kca.core.names import entries_from_json, name_from_entries
|
||||
|
||||
|
||||
def _name(cn: str = "Algorithm parity CA") -> x509.Name:
|
||||
return name_from_entries(entries_from_json([{"name": "CN", "value": cn}]))
|
||||
|
||||
|
||||
def _ca_spec() -> dict:
|
||||
spec = default_extension_spec()
|
||||
spec["basic_constraints"] = {"critical": True, "ca": True, "path_length": None}
|
||||
spec["key_usage"] = {"critical": True, "usages": ["digital_signature", "key_cert_sign", "crl_sign"]}
|
||||
spec["subject_key_identifier"] = {"critical": False}
|
||||
spec["authority_key_identifier"] = {"critical": False}
|
||||
return spec
|
||||
|
||||
|
||||
class AlgorithmWorkflowTest(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
def assert_workflow(self, key: object, signature_hash: str | None, curve: str | None = None) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
public = keys.public_part(key)
|
||||
public_der = keys.public_der(public)
|
||||
|
||||
csr = requests.build_csr(
|
||||
subject=_name("Algorithm parity CSR"),
|
||||
private_key=key,
|
||||
extension_spec=_ca_spec(),
|
||||
signature_hash=signature_hash,
|
||||
attributes={requests.CHALLENGE_PASSWORD_OID: "challenge"},
|
||||
)
|
||||
self.assertTrue(requests.verify_csr(csr))
|
||||
request_info = requests.request_info(csr)
|
||||
self.assertEqual(request_info.public_key_der, public_der)
|
||||
self.assertEqual(requests.csr_spec(csr)["signature_hash"], signature_hash)
|
||||
self.assertEqual(requests.attribute_values(csr)[requests.CHALLENGE_PASSWORD_OID], "challenge")
|
||||
|
||||
cert = certificates.build_self_signed(
|
||||
subject=_name(),
|
||||
private_key=key,
|
||||
serial=1,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
signature_hash=signature_hash,
|
||||
)
|
||||
self.assertTrue(certificates.is_self_signed(cert))
|
||||
self.assertTrue(certificates.verify_directly_issued_by(cert, cert))
|
||||
cert_info = certificates.certificate_info(cert)
|
||||
self.assertEqual(cert_info.public_key_der, public_der)
|
||||
self.assertEqual(certificates.signature_hash_name(cert), signature_hash)
|
||||
ski = cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
|
||||
self.assertEqual(ski.digest, keys.public_key_identifier(public))
|
||||
aki = cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
|
||||
self.assertEqual(aki.key_identifier, ski.digest)
|
||||
|
||||
crl = crls.build_crl(
|
||||
issuer_cert=cert,
|
||||
issuer_private_key=key,
|
||||
revocations=[],
|
||||
last_update=now,
|
||||
next_update=now + timedelta(days=7),
|
||||
crl_number=1,
|
||||
signature_hash=signature_hash,
|
||||
)
|
||||
self.assertTrue(crls.verify_crl(crl, cert))
|
||||
self.assertEqual(crl.signature_algorithm_oid, cert.signature_algorithm_oid)
|
||||
|
||||
expected_curve = curve if curve is not None else keys.curve_name(key)
|
||||
reloaded_private = keys.private_key_from_der(keys.private_der(key))
|
||||
self.assertEqual(keys.public_der(reloaded_private), public_der)
|
||||
self.assertEqual(keys.curve_name(reloaded_private), expected_curve)
|
||||
reloaded_public = keys.public_key_from_der(public_der)
|
||||
self.assertEqual(keys.public_der(reloaded_public), public_der)
|
||||
self.assertEqual(keys.curve_name(reloaded_public), expected_curve)
|
||||
|
||||
def test_every_xca_curve(self) -> None:
|
||||
self.assertEqual(len(algorithms.CURVES), 79)
|
||||
for curve in algorithms.CURVES:
|
||||
with self.subTest(curve=curve.name):
|
||||
key = keys.generate_key("ec", curve=curve.name)
|
||||
info = keys.key_info(key)
|
||||
self.assertEqual(info.key_type, "EC")
|
||||
self.assertEqual(info.curve, curve.name)
|
||||
self.assertTrue(info.is_private)
|
||||
self.assert_workflow(key, "sha256", curve.name)
|
||||
|
||||
def test_mixed_native_and_compat_issuers(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
for ca_curve, leaf_curve in (("secp112r1", "secp256r1"), ("secp256r1", "sect163k1")):
|
||||
with self.subTest(ca_curve=ca_curve, leaf_curve=leaf_curve):
|
||||
ca_key = keys.generate_key("ec", curve=ca_curve)
|
||||
leaf_key = keys.generate_key("ec", curve=leaf_curve)
|
||||
ca = certificates.build_self_signed(
|
||||
subject=_name("Mixed CA"),
|
||||
private_key=ca_key,
|
||||
serial=10,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
)
|
||||
leaf_spec = default_extension_spec()
|
||||
leaf_spec["subject_key_identifier"] = {"critical": False}
|
||||
leaf_spec["authority_key_identifier"] = {"critical": False}
|
||||
leaf = certificates.build_signed(
|
||||
subject=_name("Mixed leaf"),
|
||||
public_key=keys.public_part(leaf_key),
|
||||
issuer_cert=ca,
|
||||
issuer_private_key=ca_key,
|
||||
serial=11,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=10),
|
||||
extension_spec=leaf_spec,
|
||||
)
|
||||
self.assertTrue(certificates.verify_directly_issued_by(leaf, ca))
|
||||
self.assertFalse(certificates.verify_directly_issued_by(leaf, leaf))
|
||||
self.assertEqual(certificates.certificate_info(leaf).public_key_der, keys.public_der(leaf_key))
|
||||
ca_ski = ca.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value
|
||||
leaf_aki = leaf.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value
|
||||
self.assertEqual(leaf_aki.key_identifier, ca_ski.digest)
|
||||
|
||||
def test_ed25519_issuer_with_compat_subject(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
ca_key = keys.generate_key("ed25519")
|
||||
leaf_key = keys.generate_key("ec", curve="sect163k1")
|
||||
ca = certificates.build_self_signed(
|
||||
subject=_name("Ed25519 CA"),
|
||||
private_key=ca_key,
|
||||
serial=20,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
signature_hash=None,
|
||||
)
|
||||
leaf = certificates.build_signed(
|
||||
subject=_name("Ed25519 signed compat leaf"),
|
||||
public_key=keys.public_part(leaf_key),
|
||||
issuer_cert=ca,
|
||||
issuer_private_key=ca_key,
|
||||
serial=21,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=10),
|
||||
extension_spec=default_extension_spec(),
|
||||
signature_hash=None,
|
||||
)
|
||||
self.assertTrue(certificates.verify_directly_issued_by(leaf, ca))
|
||||
self.assertEqual(certificates.certificate_info(leaf).public_key_der, keys.public_der(leaf_key))
|
||||
self.assertIsNone(certificates.signature_hash_name(leaf))
|
||||
|
||||
def test_issuer_name_is_checked_in_openssl_fallback(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
ca_key = keys.generate_key("ec", curve="secp112r1")
|
||||
first = certificates.build_self_signed(
|
||||
subject=_name("First issuer"),
|
||||
private_key=ca_key,
|
||||
serial=30,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
)
|
||||
other_with_same_key = certificates.build_self_signed(
|
||||
subject=_name("Other issuer with the same key"),
|
||||
private_key=ca_key,
|
||||
serial=31,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
)
|
||||
leaf = certificates.build_signed(
|
||||
subject=_name("Leaf"),
|
||||
public_key=keys.public_part(ca_key),
|
||||
issuer_cert=first,
|
||||
issuer_private_key=ca_key,
|
||||
serial=32,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=10),
|
||||
extension_spec=default_extension_spec(),
|
||||
)
|
||||
self.assertTrue(certificates.verify_directly_issued_by(leaf, first))
|
||||
self.assertFalse(certificates.verify_directly_issued_by(leaf, other_with_same_key))
|
||||
|
||||
def test_xca_signature_hash_matrix(self) -> None:
|
||||
cases = [
|
||||
("rsa", {"size": 1024}, algorithms.possible_hashes("rsa")),
|
||||
("ec", {"curve": "secp256r1"}, algorithms.possible_hashes("ec")),
|
||||
("dsa", {"size": 1024}, algorithms.possible_hashes("dsa")),
|
||||
]
|
||||
for key_type, kwargs, hashes in cases:
|
||||
key = keys.generate_key(key_type, **kwargs)
|
||||
for signature_hash in hashes:
|
||||
with self.subTest(key_type=key_type, signature_hash=signature_hash):
|
||||
self.assert_workflow(key, signature_hash)
|
||||
|
||||
def test_ed25519_has_no_separate_hash(self) -> None:
|
||||
key = keys.generate_key("ed25519")
|
||||
self.assert_workflow(key, None)
|
||||
with self.assertRaises(ValueError):
|
||||
requests.build_csr(subject=_name(), private_key=key, signature_hash="sha256")
|
||||
|
||||
def test_invalid_key_hash_combinations(self) -> None:
|
||||
ec_key = keys.generate_key("ec", curve="secp256r1")
|
||||
dsa_key = keys.generate_key("dsa", size=1024)
|
||||
with self.assertRaises(ValueError):
|
||||
requests.build_csr(subject=_name(), private_key=ec_key, signature_hash="md5")
|
||||
with self.assertRaises(ValueError):
|
||||
requests.build_csr(subject=_name(), private_key=dsa_key, signature_hash="sha3-256")
|
||||
|
||||
def test_representative_encrypted_compat_roundtrips(self) -> None:
|
||||
for curve in ("secp112r1", "prime192v2", "sect163k1", "brainpoolP160t1", "wap-wsg-idm-ecid-wtls6"):
|
||||
with self.subTest(curve=curve):
|
||||
key = keys.generate_key("ec", curve=curve)
|
||||
for traditional in (False, True):
|
||||
for password in (None, "secret"):
|
||||
encoded = keys.private_pem(key, password=password, traditional=traditional)
|
||||
loaded, private = keys.load_key_bytes(encoded, password=password)
|
||||
self.assertTrue(private)
|
||||
self.assertEqual(keys.public_der(loaded), keys.public_der(key))
|
||||
encrypted_der = keys.private_der_bytes(key, password="secret")
|
||||
loaded = keys.private_key_from_der(encrypted_der, password="secret")
|
||||
self.assertEqual(keys.public_der(loaded), keys.public_der(key))
|
||||
public, private = keys.load_key_bytes(keys.public_pem(key).encode("ascii"))
|
||||
self.assertFalse(private)
|
||||
self.assertEqual(keys.public_der(public), keys.public_der(key))
|
||||
|
||||
def test_pkcs12_compat_export_import(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
key = keys.generate_key("ec", curve="secp112r1")
|
||||
cert = certificates.build_self_signed(
|
||||
subject=_name("PKCS12 compat"),
|
||||
private_key=key,
|
||||
serial=40,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
)
|
||||
encoded = exports.export_certificate(
|
||||
cert,
|
||||
"pkcs12",
|
||||
name="compat",
|
||||
private_key=key,
|
||||
password="p12-secret",
|
||||
).data
|
||||
items = imports.parse_pkcs12(encoded, password="p12-secret")
|
||||
by_kind = {}
|
||||
for item in items:
|
||||
by_kind.setdefault(item.kind, []).append(item)
|
||||
self.assertEqual(len(by_kind["certificate"]), 1)
|
||||
self.assertEqual(len(by_kind["key"]), 1)
|
||||
self.assertEqual(
|
||||
certificates.certificate_der(by_kind["certificate"][0].obj),
|
||||
certificates.certificate_der(cert),
|
||||
)
|
||||
self.assertTrue(by_kind["key"][0].is_private)
|
||||
self.assertEqual(keys.public_der(by_kind["key"][0].obj), keys.public_der(key))
|
||||
|
||||
def test_non_ec_keys_survive_openssl_fallback_import(self) -> None:
|
||||
key = keys.generate_key("rsa", size=1024)
|
||||
der = keys.private_der(key)
|
||||
original = serialization.load_der_private_key
|
||||
calls = 0
|
||||
|
||||
def legacy_only(data: bytes, password: object = None) -> object:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
raise ValueError("simulated legacy-only encoding")
|
||||
return original(data, password=password)
|
||||
|
||||
with patch("kca.core.keys.serialization.load_der_private_key", new=legacy_only):
|
||||
loaded = keys.private_key_from_der(der)
|
||||
self.assertIsInstance(loaded, rsa.RSAPrivateKey)
|
||||
self.assertEqual(keys.public_der(loaded), keys.public_der(key))
|
||||
self.assertEqual(calls, 2)
|
||||
|
||||
def test_blank_pkcs12_passwords_remain_unencrypted(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
compat_key = keys.generate_key("ec", curve="secp112r1")
|
||||
compat_ca = certificates.build_self_signed(
|
||||
subject=_name("Unencrypted compat CA"),
|
||||
private_key=compat_key,
|
||||
serial=50,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=30),
|
||||
extension_spec=_ca_spec(),
|
||||
)
|
||||
leaf_key = keys.generate_key("ec", curve="secp256r1")
|
||||
leaf = certificates.build_signed(
|
||||
subject=_name("Mixed-chain leaf"),
|
||||
public_key=keys.public_part(leaf_key),
|
||||
issuer_cert=compat_ca,
|
||||
issuer_private_key=compat_key,
|
||||
serial=51,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=10),
|
||||
extension_spec=default_extension_spec(),
|
||||
)
|
||||
pbes2_oid = bytes.fromhex("06092a864886f70d01050d")
|
||||
for password in (None, ""):
|
||||
with self.subTest(password=password):
|
||||
encoded = exports.export_certificate(
|
||||
leaf,
|
||||
"pkcs12-chain",
|
||||
name="unencrypted",
|
||||
chain=[leaf, compat_ca],
|
||||
private_key=leaf_key,
|
||||
password=password,
|
||||
).data
|
||||
self.assertNotIn(pbes2_oid, encoded)
|
||||
items = imports.parse_pkcs12(encoded, password=password)
|
||||
imported_keys = [item for item in items if item.kind == "key"]
|
||||
self.assertEqual(len(imported_keys), 1)
|
||||
self.assertEqual(keys.public_der(imported_keys[0].obj), keys.public_der(leaf_key))
|
||||
|
||||
def test_option_metadata_is_complete(self) -> None:
|
||||
options = keys.key_type_options()
|
||||
self.assertEqual(len(options), 4)
|
||||
by_type = {item["type"]: item for item in options}
|
||||
self.assertEqual(len(by_type["ec"]["curves"]), 79)
|
||||
for key_type, option in by_type.items():
|
||||
self.assertEqual(option["hashes"], list(algorithms.possible_hashes(key_type)))
|
||||
self.assertEqual(len(keys.curve_options()), 79)
|
||||
self.assertEqual(len(keys.hash_options()), 11)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""XCA algorithm catalogue and signature metadata tests."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from kca.core import algorithms, signatures
|
||||
|
||||
|
||||
EXPECTED_CURVES = {
|
||||
"secp192r1", "secp224r1", "secp256r1", "secp384r1", "secp521r1",
|
||||
"prime192v2", "prime192v3", "prime239v1", "prime239v2", "prime239v3",
|
||||
"c2pnb163v1", "c2pnb163v2", "c2pnb163v3", "c2pnb176v1",
|
||||
"c2tnb191v1", "c2tnb191v2", "c2tnb191v3", "c2pnb208w1",
|
||||
"c2tnb239v1", "c2tnb239v2", "c2tnb239v3", "c2pnb272w1",
|
||||
"c2pnb304w1", "c2tnb359v1", "c2pnb368w1", "c2tnb431r1",
|
||||
"sect113r1", "sect113r2", "sect131r1", "sect131r2", "sect163k1",
|
||||
"sect163r1", "sect163r2", "sect193r1", "sect193r2", "sect233k1",
|
||||
"sect233r1", "sect239k1", "sect283k1", "sect283r1", "sect409k1",
|
||||
"sect409r1", "sect571k1", "sect571r1",
|
||||
"secp112r1", "secp112r2", "secp128r1", "secp128r2", "secp160k1",
|
||||
"secp160r1", "secp160r2", "secp192k1", "secp224k1", "secp256k1",
|
||||
"wap-wsg-idm-ecid-wtls1", "wap-wsg-idm-ecid-wtls3",
|
||||
"wap-wsg-idm-ecid-wtls4", "wap-wsg-idm-ecid-wtls5",
|
||||
"wap-wsg-idm-ecid-wtls6", "wap-wsg-idm-ecid-wtls7",
|
||||
"wap-wsg-idm-ecid-wtls8", "wap-wsg-idm-ecid-wtls9",
|
||||
"wap-wsg-idm-ecid-wtls10", "wap-wsg-idm-ecid-wtls11",
|
||||
"wap-wsg-idm-ecid-wtls12",
|
||||
"brainpoolP160r1", "brainpoolP160t1", "brainpoolP192r1",
|
||||
"brainpoolP192t1", "brainpoolP224r1", "brainpoolP224t1",
|
||||
"brainpoolP256r1", "brainpoolP256t1", "brainpoolP320r1",
|
||||
"brainpoolP320t1", "brainpoolP384r1", "brainpoolP384t1",
|
||||
"brainpoolP512r1", "brainpoolP512t1",
|
||||
}
|
||||
|
||||
|
||||
class AlgorithmCatalogueTest(unittest.TestCase):
|
||||
def test_exact_xca_curve_set(self) -> None:
|
||||
self.assertEqual({curve.name for curve in algorithms.CURVES}, EXPECTED_CURVES)
|
||||
self.assertEqual(len(algorithms.CURVES), 79)
|
||||
self.assertEqual(len({curve.openssl_name for curve in algorithms.CURVES}), 79)
|
||||
|
||||
def test_aliases_preserve_existing_kca_names(self) -> None:
|
||||
self.assertEqual(algorithms.canonical_curve_name("prime192v1"), "secp192r1")
|
||||
self.assertEqual(algorithms.canonical_curve_name("prime256v1"), "secp256r1")
|
||||
self.assertEqual(algorithms.curve_info("BRAINPOOLP256R1").name, "brainpoolP256r1")
|
||||
with self.assertRaises(ValueError):
|
||||
algorithms.curve_info("SM2")
|
||||
|
||||
def test_xca_hash_matrix(self) -> None:
|
||||
self.assertEqual(
|
||||
algorithms.possible_hashes("RSA"),
|
||||
("md5", "ripemd160", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3-224", "sha3-256", "sha3-384", "sha3-512"),
|
||||
)
|
||||
self.assertEqual(
|
||||
algorithms.possible_hashes("ec"),
|
||||
("sha1", "sha224", "sha256", "sha384", "sha512", "sha3-224", "sha3-256", "sha3-384", "sha3-512"),
|
||||
)
|
||||
self.assertEqual(algorithms.possible_hashes("dsa"), ("sha1", "sha224", "sha256"))
|
||||
self.assertEqual(algorithms.possible_hashes("ed25519"), ())
|
||||
self.assertEqual(algorithms.default_signature_hash("ec", "md5"), "sha256")
|
||||
self.assertIsNone(algorithms.default_signature_hash("ed25519", "md5"))
|
||||
with self.assertRaises(ValueError):
|
||||
algorithms.validate_signature_hash("ec", "md5")
|
||||
with self.assertRaises(ValueError):
|
||||
algorithms.validate_signature_hash("dsa", "sha3-256")
|
||||
with self.assertRaises(ValueError):
|
||||
algorithms.validate_signature_hash("ed25519", "sha256")
|
||||
|
||||
def test_signature_oid_names_and_hashes(self) -> None:
|
||||
class OID:
|
||||
def __init__(self, dotted: str) -> None:
|
||||
self.dotted_string = dotted
|
||||
self._name = "Unknown OID"
|
||||
|
||||
cases = {
|
||||
"1.2.840.113549.1.1.4": ("md5WithRSAEncryption", "md5"),
|
||||
"1.3.36.3.3.1.2": ("ripemd160WithRSA", "ripemd160"),
|
||||
"2.16.840.1.101.3.4.3.14": ("RSA-SHA3-256", "sha3-256"),
|
||||
"2.16.840.1.101.3.4.3.10": ("ecdsa_with_SHA3-256", "sha3-256"),
|
||||
"1.2.840.10040.4.3": ("dsa_with_SHA1", "sha1"),
|
||||
}
|
||||
for dotted, (name, hash_name) in cases.items():
|
||||
with self.subTest(oid=dotted):
|
||||
oid = OID(dotted)
|
||||
self.assertEqual(signatures.signature_algorithm_name(oid), name)
|
||||
self.assertEqual(signatures.signature_hash_name(oid), hash_name)
|
||||
self.assertEqual(signatures.signature_algorithm_name(OID("1.3.101.112")), "Ed25519")
|
||||
self.assertIsNone(signatures.signature_hash_name(OID("1.3.101.112")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,449 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
from cryptography.x509.oid import NameOID
|
||||
from kca.core import openssl_compat as compat
|
||||
|
||||
UNSUPPORTED_CURVES = (
|
||||
"secp112r1",
|
||||
"prime192v2",
|
||||
"sect163k1",
|
||||
"brainpoolP160t1",
|
||||
"wap-wsg-idm-ecid-wtls6",
|
||||
)
|
||||
|
||||
|
||||
def _require_curve(name: str) -> None:
|
||||
available = compat.available_ec_curves()
|
||||
canonical = "prime256v1" if name == "secp256r1" else name
|
||||
if canonical not in available:
|
||||
raise unittest.SkipTest(f"system OpenSSL does not provide EC curve {name}")
|
||||
|
||||
|
||||
def _self_signed_certificate(
|
||||
private_key: rsa.RSAPrivateKey,
|
||||
common_name: str,
|
||||
serial: int,
|
||||
) -> x509.Certificate:
|
||||
now = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
|
||||
return (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(name)
|
||||
.issuer_name(name)
|
||||
.public_key(private_key.public_key())
|
||||
.serial_number(serial)
|
||||
.not_valid_before(now)
|
||||
.not_valid_after(now + timedelta(days=3650))
|
||||
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
||||
.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False,
|
||||
)
|
||||
.sign(private_key, hashes.SHA256())
|
||||
)
|
||||
|
||||
|
||||
def _build_native_templates() -> dict[str, object]:
|
||||
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
certificate = _self_signed_certificate(private_key, "OpenSSL compatibility", 1000)
|
||||
extra_ca = _self_signed_certificate(private_key, "OpenSSL compatibility extra CA", 1001)
|
||||
now = datetime(2024, 2, 1, tzinfo=timezone.utc)
|
||||
csr = (
|
||||
x509.CertificateSigningRequestBuilder()
|
||||
.subject_name(certificate.subject)
|
||||
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
||||
.sign(private_key, hashes.SHA256())
|
||||
)
|
||||
revoked = (
|
||||
x509.RevokedCertificateBuilder()
|
||||
.serial_number(42)
|
||||
.revocation_date(now)
|
||||
.build()
|
||||
)
|
||||
crl = (
|
||||
x509.CertificateRevocationListBuilder()
|
||||
.issuer_name(certificate.subject)
|
||||
.last_update(now)
|
||||
.next_update(now + timedelta(days=30))
|
||||
.add_revoked_certificate(revoked)
|
||||
.add_extension(x509.CRLNumber(7), critical=False)
|
||||
.sign(private_key, hashes.SHA256())
|
||||
)
|
||||
return {
|
||||
"key_object": private_key,
|
||||
"private_der": private_key.private_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
),
|
||||
"public_der": private_key.public_key().public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
),
|
||||
"cert_object": certificate,
|
||||
"cert_der": certificate.public_bytes(serialization.Encoding.DER),
|
||||
"csr_der": csr.public_bytes(serialization.Encoding.DER),
|
||||
"crl_der": crl.public_bytes(serialization.Encoding.DER),
|
||||
"extra_ca_object": extra_ca,
|
||||
"extra_ca_der": extra_ca.public_bytes(serialization.Encoding.DER),
|
||||
}
|
||||
|
||||
|
||||
def _resign_all(
|
||||
templates: dict[str, object], key: compat.CompatKey, digest: str
|
||||
) -> tuple[bytes, bytes, bytes]:
|
||||
cert = compat.resign_certificate(
|
||||
templates["cert_der"], key.public_der, key.private_der, digest
|
||||
)
|
||||
csr = compat.resign_csr(
|
||||
templates["csr_der"], key.public_der, key.private_der, digest
|
||||
)
|
||||
crl = compat.resign_crl(templates["crl_der"], key.private_der, digest)
|
||||
return cert, csr, crl
|
||||
|
||||
|
||||
class OpenSSLCompatTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.native_templates = _build_native_templates()
|
||||
|
||||
def test_backend_and_curve_discovery(self) -> None:
|
||||
self.assertTrue(compat.backend_available())
|
||||
self.assertTrue(compat.openssl_version().startswith("OpenSSL 3."))
|
||||
self.assertTrue(compat.library_path())
|
||||
curves = compat.available_ec_curves()
|
||||
self.assertTrue(curves)
|
||||
self.assertEqual(len(curves), len(set(curves)))
|
||||
self.assertIn("prime256v1", curves)
|
||||
|
||||
def test_ec_key_size_is_curve_field_degree(self) -> None:
|
||||
for curve, key_size in (("c2pnb176v1", 176), ("secp160k1", 160)):
|
||||
with self.subTest(curve=curve):
|
||||
_require_curve(curve)
|
||||
self.assertEqual(compat.generate_ec(curve).key_size, key_size)
|
||||
|
||||
def test_native_ec_interoperates_with_cryptography(self) -> None:
|
||||
_require_curve("prime256v1")
|
||||
key = compat.generate_ec("prime256v1")
|
||||
self.assertTrue(key.is_private)
|
||||
self.assertEqual(key.curve, "prime256v1")
|
||||
self.assertEqual(key.key_size, 256)
|
||||
|
||||
# Native keys still cross the API boundary only as DER, never as pointers.
|
||||
native_private = serialization.load_der_private_key(key.private_der, password=None)
|
||||
native_public = serialization.load_der_public_key(key.public_der)
|
||||
self.assertEqual(
|
||||
native_private.private_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
),
|
||||
key.private_der,
|
||||
)
|
||||
self.assertEqual(
|
||||
native_public.public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
),
|
||||
key.public_der,
|
||||
)
|
||||
|
||||
def test_arbitrary_curve_and_key_round_trips(self) -> None:
|
||||
for curve in UNSUPPORTED_CURVES:
|
||||
with self.subTest(curve=curve):
|
||||
_require_curve(curve)
|
||||
key = compat.generate_ec(curve)
|
||||
self.assertEqual(key.curve, curve)
|
||||
self.assertTrue(key.is_private)
|
||||
self.assertGreater(key.key_size, 0)
|
||||
self.assertIsNotNone(key.private_der)
|
||||
|
||||
public = key.public_key()
|
||||
self.assertFalse(public.is_private)
|
||||
self.assertIsNone(public.private_der)
|
||||
self.assertIs(public.public_key(), public)
|
||||
self.assertEqual(public.public_der, key.public_der)
|
||||
self.assertEqual(compat.public_key_from_der(key.public_der).public_der, key.public_der)
|
||||
self.assertEqual(compat.load_key_bytes(key.public_der), public)
|
||||
self.assertEqual(compat.private_key_from_der(key.private_der), key)
|
||||
self.assertEqual(compat.load_key_bytes(key.private_der), key)
|
||||
|
||||
public_pem = compat.public_pem(key)
|
||||
self.assertIsInstance(public_pem, str)
|
||||
self.assertTrue(public_pem.startswith("-----BEGIN PUBLIC KEY-----"))
|
||||
self.assertEqual(compat.load_key_bytes(public_pem.encode("ascii")), public)
|
||||
|
||||
pkcs8_pem = compat.private_pem(key)
|
||||
self.assertTrue(pkcs8_pem.startswith(b"-----BEGIN PRIVATE KEY-----"))
|
||||
self.assertEqual(compat.load_key_bytes(pkcs8_pem), key)
|
||||
traditional_pem = compat.private_pem(key, traditional=True)
|
||||
self.assertTrue(traditional_pem.startswith(b"-----BEGIN EC PRIVATE KEY-----"))
|
||||
self.assertEqual(compat.load_key_bytes(traditional_pem), key)
|
||||
|
||||
encrypted_pem = compat.private_pem(key, password="correct horse")
|
||||
self.assertTrue(encrypted_pem.startswith(b"-----BEGIN ENCRYPTED PRIVATE KEY-----"))
|
||||
self.assertEqual(compat.load_key_bytes(encrypted_pem, password="correct horse"), key)
|
||||
encrypted_traditional = compat.private_pem(
|
||||
key, password="correct horse", traditional=True
|
||||
)
|
||||
self.assertTrue(encrypted_traditional.startswith(b"-----BEGIN EC PRIVATE KEY-----"))
|
||||
self.assertIn(b"DEK-Info:", encrypted_traditional)
|
||||
self.assertEqual(
|
||||
compat.load_key_bytes(encrypted_traditional, password="correct horse"), key
|
||||
)
|
||||
|
||||
self.assertEqual(compat.private_der_bytes(key), key.private_der)
|
||||
encrypted_der = compat.private_der_bytes(key, password="correct horse")
|
||||
self.assertNotEqual(encrypted_der, key.private_der)
|
||||
self.assertEqual(
|
||||
compat.private_key_from_der(encrypted_der, password="correct horse"), key
|
||||
)
|
||||
self.assertEqual(compat.load_key_bytes(encrypted_der, password="correct horse"), key)
|
||||
|
||||
def test_wrong_password_errors_do_not_disclose_password(self) -> None:
|
||||
_require_curve("secp112r1")
|
||||
key = compat.generate_ec("secp112r1")
|
||||
password = "not-for-error-output"
|
||||
encrypted = compat.private_der_bytes(key, password=password)
|
||||
attempted_password = "definitely-wrong-secret"
|
||||
with self.assertRaises(ValueError) as caught:
|
||||
compat.private_key_from_der(encrypted, password=attempted_password)
|
||||
self.assertNotIn(password, str(caught.exception))
|
||||
self.assertNotIn(attempted_password, str(caught.exception))
|
||||
with self.assertRaises(ValueError):
|
||||
compat.private_pem(key.public_key())
|
||||
self.assertEqual(compat.private_pem(key, password=""), compat.private_pem(key))
|
||||
self.assertEqual(compat.private_der_bytes(key, password=""), key.private_der)
|
||||
|
||||
def test_native_rsa_traditional_and_pkcs1_der(self) -> None:
|
||||
templates = self.native_templates
|
||||
private_key = templates["key_object"]
|
||||
traditional_der = private_key.private_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
serialization.NoEncryption(),
|
||||
)
|
||||
pkcs1_public_der = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.PKCS1,
|
||||
)
|
||||
for encoded in (templates["private_der"], traditional_der):
|
||||
loaded = compat.load_key_bytes(encoded)
|
||||
self.assertTrue(loaded.is_private)
|
||||
self.assertIsNone(loaded.curve)
|
||||
self.assertEqual(loaded.key_size, 2048)
|
||||
self.assertEqual(loaded.public_der, templates["public_der"])
|
||||
public = compat.public_key_from_der(pkcs1_public_der)
|
||||
self.assertEqual(public.public_der, templates["public_der"])
|
||||
pkcs1_public_pem = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.PKCS1,
|
||||
)
|
||||
self.assertEqual(compat.load_key_bytes(pkcs1_public_pem).public_der, templates["public_der"])
|
||||
|
||||
encrypted_by_cryptography = private_key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.BestAvailableEncryption(b"import-password"),
|
||||
)
|
||||
self.assertEqual(
|
||||
compat.load_key_bytes(encrypted_by_cryptography, "import-password").public_der,
|
||||
templates["public_der"],
|
||||
)
|
||||
|
||||
def test_spki_key_identifier_known_answer(self) -> None:
|
||||
# RFC 8032 test 1 Ed25519 public key in SubjectPublicKeyInfo. The expected
|
||||
# value is SHA-1 of only the 32 bytes in the BIT STRING, independently of
|
||||
# cryptography's SubjectKeyIdentifier implementation.
|
||||
spki = bytes.fromhex(
|
||||
"302a300506032b6570032100"
|
||||
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
|
||||
)
|
||||
self.assertEqual(
|
||||
compat.spki_key_identifier(spki).hex(),
|
||||
"5b27aa5589179770e47575b162a1ded97b8bfc6d",
|
||||
)
|
||||
self.assertNotEqual(compat.spki_key_identifier(spki), hashlib.sha1(spki).digest())
|
||||
|
||||
def test_unsupported_curve_cert_csr_crl_sha256(self) -> None:
|
||||
for curve in UNSUPPORTED_CURVES:
|
||||
with self.subTest(curve=curve):
|
||||
_require_curve(curve)
|
||||
key = compat.generate_ec(curve)
|
||||
cert, csr, crl = _resign_all(self.native_templates, key, "sha256")
|
||||
|
||||
self.assertEqual(compat.certificate_public_der(cert), key.public_der)
|
||||
self.assertEqual(compat.csr_public_der(csr), key.public_der)
|
||||
self.assertTrue(compat.verify_certificate(cert, cert))
|
||||
self.assertTrue(compat.verify_csr(csr))
|
||||
self.assertTrue(compat.verify_crl(crl, cert))
|
||||
# No call to cryptography.Certificate.public_key() or CSR.public_key()
|
||||
# is needed (or valid) for these unsupported curves.
|
||||
|
||||
def test_unsupported_subject_with_native_rsa_issuer(self) -> None:
|
||||
_require_curve("sect163k1")
|
||||
subject_key = compat.generate_ec("sect163k1")
|
||||
cert = compat.resign_certificate(
|
||||
self.native_templates["cert_der"],
|
||||
subject_key.public_der,
|
||||
self.native_templates["private_der"],
|
||||
"sha256",
|
||||
)
|
||||
self.assertEqual(compat.certificate_public_der(cert), subject_key.public_der)
|
||||
self.assertTrue(compat.verify_certificate(cert, self.native_templates["cert_der"]))
|
||||
self.assertFalse(compat.verify_certificate(cert, cert))
|
||||
|
||||
def test_rsa_legacy_digest_cert_csr_crl(self) -> None:
|
||||
for digest in ("sha256", "sha1", "md5", "ripemd160"):
|
||||
with self.subTest(digest=digest):
|
||||
key = compat.private_key_from_der(self.native_templates["private_der"])
|
||||
cert, csr, crl = _resign_all(self.native_templates, key, digest)
|
||||
self.assertTrue(compat.verify_certificate(cert, cert))
|
||||
self.assertTrue(compat.verify_csr(csr))
|
||||
self.assertTrue(compat.verify_crl(crl, cert))
|
||||
|
||||
# The signature algorithm must actually change, not merely verify
|
||||
# under a cached template signature.
|
||||
parsed_cert = x509.load_der_x509_certificate(cert)
|
||||
parsed_csr = x509.load_der_x509_csr(csr)
|
||||
parsed_crl = x509.load_der_x509_crl(crl)
|
||||
expected_oid = {
|
||||
"sha256": "1.2.840.113549.1.1.11",
|
||||
"sha1": "1.2.840.113549.1.1.5",
|
||||
"md5": "1.2.840.113549.1.1.4",
|
||||
"ripemd160": "1.3.36.3.3.1.2",
|
||||
}[digest]
|
||||
self.assertEqual(parsed_cert.signature_algorithm_oid.dotted_string, expected_oid)
|
||||
self.assertEqual(parsed_csr.signature_algorithm_oid.dotted_string, expected_oid)
|
||||
self.assertEqual(parsed_crl.signature_algorithm_oid.dotted_string, expected_oid)
|
||||
|
||||
def test_legacy_and_sha3_ec_cert_csr_crl(self) -> None:
|
||||
for digest in ("sha1", "sha3-256"):
|
||||
with self.subTest(digest=digest):
|
||||
_require_curve("prime256v1")
|
||||
key = compat.generate_ec("prime256v1")
|
||||
cert, csr, crl = _resign_all(self.native_templates, key, digest)
|
||||
self.assertTrue(compat.verify_certificate(cert, cert))
|
||||
self.assertTrue(compat.verify_csr(csr))
|
||||
self.assertTrue(compat.verify_crl(crl, cert))
|
||||
self.assertEqual(
|
||||
x509.load_der_x509_certificate(cert).signature_hash_algorithm.name, digest
|
||||
)
|
||||
self.assertEqual(x509.load_der_x509_csr(csr).signature_hash_algorithm.name, digest)
|
||||
self.assertEqual(x509.load_der_x509_crl(crl).signature_hash_algorithm.name, digest)
|
||||
|
||||
def test_bad_signatures_return_false(self) -> None:
|
||||
key = compat.private_key_from_der(self.native_templates["private_der"])
|
||||
cert, csr, crl = _resign_all(self.native_templates, key, "sha256")
|
||||
|
||||
other_native = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
other_cert = _self_signed_certificate(other_native, "Wrong issuer", 2000)
|
||||
other_der = other_cert.public_bytes(serialization.Encoding.DER)
|
||||
self.assertFalse(compat.verify_certificate(cert, other_der))
|
||||
self.assertFalse(compat.verify_crl(crl, other_der))
|
||||
|
||||
damaged_csr = bytearray(csr)
|
||||
damaged_csr[-1] ^= 1
|
||||
self.assertFalse(compat.verify_csr(bytes(damaged_csr)))
|
||||
|
||||
def test_malformed_and_unsupported_inputs_raise_value_error(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
compat.public_key_from_der(b"not DER")
|
||||
with self.assertRaises(ValueError):
|
||||
compat.private_key_from_der(b"not DER")
|
||||
with self.assertRaises(ValueError):
|
||||
compat.certificate_public_der(b"not DER")
|
||||
with self.assertRaises(ValueError):
|
||||
compat.csr_public_der(b"not DER")
|
||||
with self.assertRaises(ValueError):
|
||||
compat.spki_key_identifier(b"not DER")
|
||||
with self.assertRaises(ValueError):
|
||||
compat.verify_certificate(b"not DER", self.native_templates["cert_der"])
|
||||
with self.assertRaises(ValueError):
|
||||
compat.verify_csr(b"not DER")
|
||||
with self.assertRaises(ValueError):
|
||||
compat.verify_crl(b"not DER", self.native_templates["cert_der"])
|
||||
with self.assertRaisesRegex(ValueError, "digest"):
|
||||
compat.resign_crl(
|
||||
self.native_templates["crl_der"],
|
||||
self.native_templates["private_der"],
|
||||
"not-a-digest",
|
||||
)
|
||||
|
||||
def test_pkcs12_modern_serialize_and_load(self) -> None:
|
||||
encoded = compat.serialize_pkcs12(
|
||||
"compat friendly name",
|
||||
self.native_templates["private_der"],
|
||||
self.native_templates["cert_der"],
|
||||
[self.native_templates["extra_ca_der"]],
|
||||
"p12-password",
|
||||
)
|
||||
# id-pbes2, aes256-CBC, and id-sha256: modern PBKDF2 encryption and MAC,
|
||||
# not legacy RC2/3DES PBE with SHA-1.
|
||||
self.assertIn(bytes.fromhex("06092a864886f70d01050d"), encoded)
|
||||
self.assertIn(bytes.fromhex("060960864801650304012a"), encoded)
|
||||
self.assertIn(bytes.fromhex("0609608648016503040201"), encoded)
|
||||
|
||||
private_der, cert_der, ca_ders = compat.load_pkcs12(encoded, "p12-password")
|
||||
self.assertIsNotNone(private_der)
|
||||
self.assertEqual(
|
||||
compat.private_key_from_der(private_der).public_der,
|
||||
self.native_templates["public_der"],
|
||||
)
|
||||
self.assertEqual(cert_der, self.native_templates["cert_der"])
|
||||
self.assertEqual(ca_ders, [self.native_templates["extra_ca_der"]])
|
||||
|
||||
native_key, native_cert, native_cas = pkcs12.load_key_and_certificates(
|
||||
encoded, b"p12-password"
|
||||
)
|
||||
self.assertIsNotNone(native_key)
|
||||
self.assertIsNotNone(native_cert)
|
||||
self.assertIsNotNone(native_cas)
|
||||
self.assertEqual(len(native_cas), 1)
|
||||
|
||||
with self.assertRaises(ValueError) as caught:
|
||||
compat.load_pkcs12(encoded, "wrong-password")
|
||||
self.assertNotIn("wrong-password", str(caught.exception))
|
||||
|
||||
def test_pkcs12_loads_cryptography_container(self) -> None:
|
||||
encoded = pkcs12.serialize_key_and_certificates(
|
||||
b"native friendly name",
|
||||
self.native_templates["key_object"],
|
||||
self.native_templates["cert_object"],
|
||||
[self.native_templates["extra_ca_object"]],
|
||||
serialization.BestAvailableEncryption(b"native-p12-password"),
|
||||
)
|
||||
private_der, cert_der, ca_ders = compat.load_pkcs12(encoded, "native-p12-password")
|
||||
self.assertIsNotNone(private_der)
|
||||
self.assertEqual(
|
||||
compat.private_key_from_der(private_der).public_der,
|
||||
self.native_templates["public_der"],
|
||||
)
|
||||
self.assertEqual(cert_der, self.native_templates["cert_der"])
|
||||
self.assertEqual(ca_ders, [self.native_templates["extra_ca_der"]])
|
||||
|
||||
def test_pkcs12_supports_unsupported_ec_curve(self) -> None:
|
||||
_require_curve("secp112r1")
|
||||
key = compat.generate_ec("secp112r1")
|
||||
cert = compat.resign_certificate(
|
||||
self.native_templates["cert_der"], key.public_der, key.private_der, "sha256"
|
||||
)
|
||||
encoded = compat.serialize_pkcs12("ec compat", key.private_der, cert, [], "ec-p12")
|
||||
private_der, loaded_cert, ca_ders = compat.load_pkcs12(encoded, "ec-p12")
|
||||
self.assertIsNotNone(private_der)
|
||||
self.assertEqual(compat.private_key_from_der(private_der).public_der, key.public_der)
|
||||
self.assertEqual(loaded_cert, cert)
|
||||
self.assertEqual(ca_ders, [])
|
||||
self.assertTrue(compat.verify_certificate(loaded_cert, loaded_cert))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -130,6 +130,91 @@ class XcaDbTest(unittest.TestCase):
|
||||
self.assertEqual(items[3].name, "Synthetic CRL")
|
||||
self.assertEqual(items[4].obj["name"], "Synthetic template")
|
||||
|
||||
def test_import_openssl_only_curve_database(self) -> None:
|
||||
key = keys.generate_key("ec", curve="secp112r1")
|
||||
now = datetime.now(timezone.utc)
|
||||
spec = default_extension_spec()
|
||||
spec["basic_constraints"] = {"critical": True, "ca": True, "path_length": None}
|
||||
cert = certificates.build_self_signed(
|
||||
subject=_name("Compat XCA CA"),
|
||||
private_key=key,
|
||||
serial=43,
|
||||
not_before=now,
|
||||
not_after=now + timedelta(days=365),
|
||||
extension_spec=spec,
|
||||
)
|
||||
csr = requests.build_csr(subject=_name("Compat XCA CSR"), private_key=key, extension_spec=spec)
|
||||
crl = crls.build_crl(
|
||||
issuer_cert=cert,
|
||||
issuer_private_key=key,
|
||||
revocations=[],
|
||||
last_update=now,
|
||||
next_update=now + timedelta(days=30),
|
||||
crl_number=4,
|
||||
)
|
||||
encrypted = keys.private_der_bytes(key, password="dbpass")
|
||||
date = now.strftime("%Y%m%d%H%M%SZ")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".xdb", delete=False) as handle:
|
||||
path = handle.name
|
||||
try:
|
||||
db = sqlite3.connect(path)
|
||||
db.executescript(
|
||||
"""
|
||||
CREATE TABLE settings (key_ CHAR(20) UNIQUE, value TEXT);
|
||||
CREATE TABLE items (id INTEGER PRIMARY KEY, name VARCHAR(128), type INTEGER, source INTEGER, date CHAR(15), comment VARCHAR(2048), stamp INTEGER DEFAULT 0, del SMALLINT DEFAULT 0);
|
||||
CREATE TABLE public_keys (item INTEGER, type CHAR(4), hash INTEGER, len INTEGER, "public" TEXT);
|
||||
CREATE TABLE private_keys (item INTEGER, ownPass INTEGER, private TEXT);
|
||||
CREATE TABLE x509super (item INTEGER, subj_hash INTEGER, pkey INTEGER, key_hash INTEGER);
|
||||
CREATE TABLE requests (item INTEGER, hash INTEGER, signed INTEGER, request TEXT);
|
||||
CREATE TABLE certs (item INTEGER, hash INTEGER, iss_hash INTEGER, serial VARCHAR(64), issuer INTEGER, ca INTEGER, cert TEXT);
|
||||
CREATE TABLE authority (item INTEGER, template INTEGER, crlExpire CHAR(15), crlNo INTEGER, crlDays INTEGER);
|
||||
CREATE TABLE crls (item INTEGER, hash INTEGER, num INTEGER, iss_hash INTEGER, issuer INTEGER, crl TEXT);
|
||||
CREATE TABLE revocations (caId INTEGER, serial VARCHAR(64), date CHAR(15), invaldate CHAR(15), crlNo INTEGER, reasonBit INTEGER);
|
||||
CREATE TABLE templates (item INTEGER, version INTEGER, template TEXT);
|
||||
CREATE TABLE takeys (item INTEGER UNIQUE, value TEXT);
|
||||
"""
|
||||
)
|
||||
db.execute("INSERT INTO settings VALUES ('schema', '8')")
|
||||
for item_id, name, item_type in ((1, "Compat key", 1), (2, "Compat CA", 3), (3, "Compat CSR", 2), (4, "Compat CRL", 4)):
|
||||
db.execute("INSERT INTO items VALUES (?, ?, ?, 2, ?, '', 0, 0)", (item_id, name, item_type, date))
|
||||
db.execute(
|
||||
"INSERT INTO public_keys VALUES (1, 'EC', 0, 112, ?)",
|
||||
(base64.b64encode(keys.public_der(key)).decode(),),
|
||||
)
|
||||
db.execute("INSERT INTO private_keys VALUES (1, 0, ?)", (base64.b64encode(encrypted).decode(),))
|
||||
db.execute("INSERT INTO x509super VALUES (2, 0, 1, 0)")
|
||||
db.execute("INSERT INTO x509super VALUES (3, 0, 1, 0)")
|
||||
db.execute(
|
||||
"INSERT INTO certs VALUES (2, 0, 0, '2b', NULL, 1, ?)",
|
||||
(base64.b64encode(certificates.certificate_der(cert)).decode(),),
|
||||
)
|
||||
db.execute("INSERT INTO authority VALUES (2, NULL, NULL, 0, 30)")
|
||||
db.execute(
|
||||
"INSERT INTO requests VALUES (3, 0, 0, ?)",
|
||||
(base64.b64encode(requests.csr_der(csr)).decode(),),
|
||||
)
|
||||
db.execute(
|
||||
"INSERT INTO crls VALUES (4, 0, 1, 0, 2, ?)",
|
||||
(base64.b64encode(crls.crl_der(crl)).decode(),),
|
||||
)
|
||||
db.commit()
|
||||
db.close()
|
||||
with open(path, "rb") as handle:
|
||||
items = import_xca_database(handle.read(), "compat.xdb", "dbpass")
|
||||
finally:
|
||||
import os
|
||||
|
||||
os.unlink(path)
|
||||
|
||||
self.assertEqual([item.kind for item in items], ["key", "certificate", "request", "crl"])
|
||||
self.assertTrue(items[0].is_private)
|
||||
self.assertEqual(keys.curve_name(items[0].obj), "secp112r1")
|
||||
self.assertEqual(keys.public_der(items[0].obj), keys.public_der(key))
|
||||
self.assertEqual(certificates.certificate_info(items[1].obj).public_key_der, keys.public_der(key))
|
||||
self.assertTrue(requests.verify_csr(items[2].obj))
|
||||
self.assertTrue(crls.verify_crl(items[3].obj, items[1].obj))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user