x509kit 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {x509kit-0.3.1/src/x509kit.egg-info → x509kit-0.3.2}/PKG-INFO +1 -1
- {x509kit-0.3.1 → x509kit-0.3.2}/pyproject.toml +1 -1
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit/__init__.py +15 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit/x509.py +291 -2
- {x509kit-0.3.1 → x509kit-0.3.2/src/x509kit.egg-info}/PKG-INFO +1 -1
- {x509kit-0.3.1 → x509kit-0.3.2}/test_all.py +172 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/CHANGELOG.md +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/LICENSE +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/MANIFEST.in +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/README.md +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/SECURITY.md +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/check_dispatcher.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/check_docs.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/check_no_hidden.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/compat_test.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/01_root_ca.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/02_issue_from_csr.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/03_self_signed_server.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/04_mtls_server.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/05_crl_revocation.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/06_ocsp_responder.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/07_pkcs12_export.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/08_parse_real_cert.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/09_encrypted_key.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/10_verify_chain.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/11_name_canonicalization.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/12_extensions.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/examples/README.md +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/flask/test_live_ca.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/flask/test_mtls.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/fuzz_x509.py +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/setup.cfg +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit/py.typed +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/SOURCES.txt +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/dependency_links.txt +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/requires.txt +0 -0
- {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/top_level.txt +0 -0
|
@@ -25,6 +25,10 @@ x509kit — X.509 / PKCS#10 / PKCS#12 / RSA / Ed25519 / ECDSA / CRL / OCSP / NC.
|
|
|
25
25
|
k.sign_pss(msg, "sha256") / k.verify_pss(msg, sig, "sha256")
|
|
26
26
|
k.size_bits / k.is_private / k.weak
|
|
27
27
|
|
|
28
|
+
# Шорткат на всех трёх типах ключей:
|
|
29
|
+
k.to_pkcs12(cert, chain=[ca], password="...", name="alice")
|
|
30
|
+
^ возвращает PKCS12, можно .save("alice.p12")
|
|
31
|
+
|
|
28
32
|
Ed25519Keys.generate() / Ed25519Keys(seed=bytes(32))
|
|
29
33
|
Ed25519Keys.from_pem(pem, private=True, password="...")
|
|
30
34
|
k.sign(msg) / k.verify(msg, sig) / k.pub
|
|
@@ -55,6 +59,10 @@ x509kit — X.509 / PKCS#10 / PKCS#12 / RSA / Ed25519 / ECDSA / CRL / OCSP / NC.
|
|
|
55
59
|
c.weak_hash / c.has_unknown_critical
|
|
56
60
|
c.verify_signature(issuer) / c.is_self_signed()
|
|
57
61
|
c.to_pem() / c.to_der()
|
|
62
|
+
c.info() — вывод в стиле openssl x509 -text
|
|
63
|
+
Cert.from_url("https://example.com") — скачать и распарсить сертификат
|
|
64
|
+
^ accepts "example.com" или "https://host:port"
|
|
65
|
+
^ возвращает Cert
|
|
58
66
|
|
|
59
67
|
CACert.create_self_signed(subject, keys, days=3650, path_len=None)
|
|
60
68
|
CACert.from_cert(cert, signing_key=None)
|
|
@@ -171,6 +179,11 @@ x509kit — X.509 / PKCS#10 / PKCS#12 / RSA / Ed25519 / ECDSA / CRL / OCSP / NC.
|
|
|
171
179
|
ASN1 — низкоуровневый DER-парсер (parse, as_int, as_oid, ...)
|
|
172
180
|
^ защита: _MAX_DER_SIZE = 50 MB, _MAX_DEPTH = 32
|
|
173
181
|
canonicalize_name(name) / names_equal(a, b) — RFC 4518
|
|
182
|
+
|
|
183
|
+
Name.from_string("CN=x, O=y, C=XX") — RFC 4514 parser
|
|
184
|
+
^ порядок "от конкретного к общему"
|
|
185
|
+
^ поддерживает escape: "CN=with\\, comma, O=Org"
|
|
186
|
+
Name.from_dict({...}) — из dict
|
|
174
187
|
san_extension(dns_names=[], ips=[], emails=[], uris=[])
|
|
175
188
|
validate_hostname(cert, "example.com") — RFC 6125
|
|
176
189
|
load_pem_certs / load_pem_csrs / load_pem_bundle / load_pem_crls
|
|
@@ -180,6 +193,8 @@ x509kit — X.509 / PKCS#10 / PKCS#12 / RSA / Ed25519 / ECDSA / CRL / OCSP / NC.
|
|
|
180
193
|
|
|
181
194
|
CertBundle([c1, c2, ...])
|
|
182
195
|
.from_pem(data) / .from_file(path) / .to_pem() / .save(path)
|
|
196
|
+
.to_pkcs12(key, password="...", name="...")
|
|
197
|
+
^ certs[0] — leaf, certs[1:] — chain; возвращает PKCS12
|
|
183
198
|
|
|
184
199
|
EKU_OIDS — {serverAuth, clientAuth, codeSigning, emailProtection,
|
|
185
200
|
timeStamping, OCSPSigning, anyExtendedKeyUsage, ...}
|
|
@@ -44,7 +44,7 @@ import socket
|
|
|
44
44
|
import unicodedata
|
|
45
45
|
import warnings as _warnings
|
|
46
46
|
|
|
47
|
-
__version__ = "0.3.
|
|
47
|
+
__version__ = "0.3.2"
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
# ======================================================================
|
|
@@ -361,6 +361,21 @@ KNOWN_EXTENSION_OIDS = {
|
|
|
361
361
|
OID_AIA,
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
|
|
365
|
+
EXT_NAMES = {
|
|
366
|
+
"2.5.29.17": "X509v3 Subject Alternative Name",
|
|
367
|
+
"2.5.29.19": "X509v3 Basic Constraints",
|
|
368
|
+
"2.5.29.15": "X509v3 Key Usage",
|
|
369
|
+
"2.5.29.37": "X509v3 Extended Key Usage",
|
|
370
|
+
"2.5.29.30": "X509v3 Name Constraints",
|
|
371
|
+
"2.5.29.14": "X509v3 Subject Key Identifier",
|
|
372
|
+
"2.5.29.35": "X509v3 Authority Key Identifier",
|
|
373
|
+
"2.5.29.31": "X509v3 CRL Distribution Points",
|
|
374
|
+
"2.5.29.20": "X509v3 CRL Number",
|
|
375
|
+
"2.5.29.21": "X509v3 CRL Reason Code",
|
|
376
|
+
"1.3.6.1.5.5.7.1.1": "Authority Information Access",
|
|
377
|
+
}
|
|
378
|
+
|
|
364
379
|
# RFC 5280 §5.3.1 reason codes
|
|
365
380
|
CRL_REASONS = {
|
|
366
381
|
"unspecified": 0,
|
|
@@ -475,6 +490,20 @@ def _escape_dn(v: str) -> str:
|
|
|
475
490
|
return "".join(out)
|
|
476
491
|
|
|
477
492
|
|
|
493
|
+
def _unescape_dn(s: str) -> str:
|
|
494
|
+
"""RFC 4514: убрать escape-последовательности в значении DN."""
|
|
495
|
+
out = []
|
|
496
|
+
i = 0
|
|
497
|
+
while i < len(s):
|
|
498
|
+
if s[i] == "\\" and i + 1 < len(s):
|
|
499
|
+
out.append(s[i + 1])
|
|
500
|
+
i += 2
|
|
501
|
+
continue
|
|
502
|
+
out.append(s[i])
|
|
503
|
+
i += 1
|
|
504
|
+
return "".join(out)
|
|
505
|
+
|
|
506
|
+
|
|
478
507
|
def _encode_rdn(short_or_oid: str, value: str) -> bytes:
|
|
479
508
|
oid = ATTR_OIDS.get(short_or_oid, short_or_oid)
|
|
480
509
|
if not isinstance(value, str):
|
|
@@ -568,6 +597,72 @@ class Name:
|
|
|
568
597
|
|
|
569
598
|
def __hash__(self):
|
|
570
599
|
return hash(self.raw)
|
|
600
|
+
|
|
601
|
+
@classmethod
|
|
602
|
+
def from_string(cls, s):
|
|
603
|
+
r"""RFC 4514: разобрать строку DN в Name.
|
|
604
|
+
|
|
605
|
+
Порядок в строке — от конкретного к общему:
|
|
606
|
+
"CN=example.com, O=MyOrg, C=XX"
|
|
607
|
+
|
|
608
|
+
Эквивалентно Name.from_dict({"C": "XX", "O": "MyOrg", "CN": "example.com"}).
|
|
609
|
+
|
|
610
|
+
Поддерживает escape-последовательности:
|
|
611
|
+
CN=with\, comma, O=Org
|
|
612
|
+
CN=with\backslash
|
|
613
|
+
"""
|
|
614
|
+
if not s or not s.strip():
|
|
615
|
+
raise X509ParseError("Name.from_string: empty string")
|
|
616
|
+
|
|
617
|
+
# Разбить по неэкранированным запятым
|
|
618
|
+
parts, cur, i = [], [], 0
|
|
619
|
+
while i < len(s):
|
|
620
|
+
c = s[i]
|
|
621
|
+
if c == "\\" and i + 1 < len(s):
|
|
622
|
+
cur.append(c)
|
|
623
|
+
cur.append(s[i + 1])
|
|
624
|
+
i += 2
|
|
625
|
+
continue
|
|
626
|
+
if c == ",":
|
|
627
|
+
parts.append("".join(cur))
|
|
628
|
+
cur = []
|
|
629
|
+
i += 1
|
|
630
|
+
continue
|
|
631
|
+
cur.append(c)
|
|
632
|
+
i += 1
|
|
633
|
+
if cur:
|
|
634
|
+
parts.append("".join(cur))
|
|
635
|
+
|
|
636
|
+
rdns = []
|
|
637
|
+
for part in parts:
|
|
638
|
+
part = part.strip()
|
|
639
|
+
if not part:
|
|
640
|
+
continue
|
|
641
|
+
key_b, val_b, in_val, j = [], [], False, 0
|
|
642
|
+
while j < len(part):
|
|
643
|
+
c = part[j]
|
|
644
|
+
if c == "\\" and j + 1 < len(part):
|
|
645
|
+
(val_b if in_val else key_b).append(c)
|
|
646
|
+
(val_b if in_val else key_b).append(part[j + 1])
|
|
647
|
+
j += 2
|
|
648
|
+
continue
|
|
649
|
+
if c == "=" and not in_val:
|
|
650
|
+
in_val = True
|
|
651
|
+
j += 1
|
|
652
|
+
continue
|
|
653
|
+
(val_b if in_val else key_b).append(c)
|
|
654
|
+
j += 1
|
|
655
|
+
if not in_val:
|
|
656
|
+
raise X509ParseError(f"Name.from_string: missing '=' in {part!r}")
|
|
657
|
+
k = "".join(key_b).strip()
|
|
658
|
+
v = _unescape_dn("".join(val_b).strip())
|
|
659
|
+
oid = ATTR_OIDS.get(k, k)
|
|
660
|
+
rdns.append((oid, v))
|
|
661
|
+
|
|
662
|
+
rdn_list = [_encode_rdn(oid, val) for oid, val in reversed(rdns)]
|
|
663
|
+
node, _ = ASN1.parse(der_seq(*rdn_list))
|
|
664
|
+
return cls(node)
|
|
665
|
+
|
|
571
666
|
class AlgorithmIdentifier:
|
|
572
667
|
__slots__ = ("oid", "params", "raw")
|
|
573
668
|
|
|
@@ -732,7 +827,6 @@ def _parse_pss_params(params_node):
|
|
|
732
827
|
salt_len = child.children[0].as_int()
|
|
733
828
|
return (hash_name, salt_len)
|
|
734
829
|
|
|
735
|
-
|
|
736
830
|
class RSAKeys:
|
|
737
831
|
__slots__ = ("n", "e", "d", "p", "q", "dp", "dq", "qinv")
|
|
738
832
|
|
|
@@ -950,6 +1044,27 @@ class RSAKeys:
|
|
|
950
1044
|
self.dp = 0; self.dq = 0; self.qinv = 0
|
|
951
1045
|
|
|
952
1046
|
|
|
1047
|
+
def to_pkcs12(self, cert, chain=None, password="", name=None,
|
|
1048
|
+
iterations=2048, cipher="aes-256-cbc"):
|
|
1049
|
+
"""Упаковать этот ключ + сертификат (+цепочку) в PKCS#12.
|
|
1050
|
+
|
|
1051
|
+
Возвращает PKCS12. Для сохранения:
|
|
1052
|
+
key.to_pkcs12(cert, chain=[ca]).save("client.p12")
|
|
1053
|
+
"""
|
|
1054
|
+
b = PKCS12Builder()
|
|
1055
|
+
b.set_key(self)
|
|
1056
|
+
b.set_cert(cert)
|
|
1057
|
+
if chain:
|
|
1058
|
+
b.add_chain(*chain)
|
|
1059
|
+
b.set_password(password)
|
|
1060
|
+
if name:
|
|
1061
|
+
b.set_name(name)
|
|
1062
|
+
b.set_iterations(iterations)
|
|
1063
|
+
if cipher != "aes-256-cbc":
|
|
1064
|
+
b.set_cipher(cipher)
|
|
1065
|
+
return b.build()
|
|
1066
|
+
|
|
1067
|
+
|
|
953
1068
|
# ======================================================================
|
|
954
1069
|
# Ed25519Keys (RFC 8032)
|
|
955
1070
|
# ======================================================================
|
|
@@ -1196,6 +1311,27 @@ class Ed25519Keys:
|
|
|
1196
1311
|
self.seed = None
|
|
1197
1312
|
|
|
1198
1313
|
|
|
1314
|
+
def to_pkcs12(self, cert, chain=None, password="", name=None,
|
|
1315
|
+
iterations=2048, cipher="aes-256-cbc"):
|
|
1316
|
+
"""Упаковать этот ключ + сертификат (+цепочку) в PKCS#12.
|
|
1317
|
+
|
|
1318
|
+
Возвращает PKCS12. Для сохранения:
|
|
1319
|
+
key.to_pkcs12(cert, chain=[ca]).save("client.p12")
|
|
1320
|
+
"""
|
|
1321
|
+
b = PKCS12Builder()
|
|
1322
|
+
b.set_key(self)
|
|
1323
|
+
b.set_cert(cert)
|
|
1324
|
+
if chain:
|
|
1325
|
+
b.add_chain(*chain)
|
|
1326
|
+
b.set_password(password)
|
|
1327
|
+
if name:
|
|
1328
|
+
b.set_name(name)
|
|
1329
|
+
b.set_iterations(iterations)
|
|
1330
|
+
if cipher != "aes-256-cbc":
|
|
1331
|
+
b.set_cipher(cipher)
|
|
1332
|
+
return b.build()
|
|
1333
|
+
|
|
1334
|
+
|
|
1199
1335
|
# ======================================================================
|
|
1200
1336
|
# ECDSA — P-256, P-384, P-521, secp256k1
|
|
1201
1337
|
# ======================================================================
|
|
@@ -1586,6 +1722,27 @@ class ECDSAKeys:
|
|
|
1586
1722
|
self.d = None
|
|
1587
1723
|
|
|
1588
1724
|
|
|
1725
|
+
def to_pkcs12(self, cert, chain=None, password="", name=None,
|
|
1726
|
+
iterations=2048, cipher="aes-256-cbc"):
|
|
1727
|
+
"""Упаковать этот ключ + сертификат (+цепочку) в PKCS#12.
|
|
1728
|
+
|
|
1729
|
+
Возвращает PKCS12. Для сохранения:
|
|
1730
|
+
key.to_pkcs12(cert, chain=[ca]).save("client.p12")
|
|
1731
|
+
"""
|
|
1732
|
+
b = PKCS12Builder()
|
|
1733
|
+
b.set_key(self)
|
|
1734
|
+
b.set_cert(cert)
|
|
1735
|
+
if chain:
|
|
1736
|
+
b.add_chain(*chain)
|
|
1737
|
+
b.set_password(password)
|
|
1738
|
+
if name:
|
|
1739
|
+
b.set_name(name)
|
|
1740
|
+
b.set_iterations(iterations)
|
|
1741
|
+
if cipher != "aes-256-cbc":
|
|
1742
|
+
b.set_cipher(cipher)
|
|
1743
|
+
return b.build()
|
|
1744
|
+
|
|
1745
|
+
|
|
1589
1746
|
# ======================================================================
|
|
1590
1747
|
# SPKI
|
|
1591
1748
|
# ======================================================================
|
|
@@ -2661,6 +2818,114 @@ class Cert:
|
|
|
2661
2818
|
|
|
2662
2819
|
def __hash__(self):
|
|
2663
2820
|
return hash(self.der)
|
|
2821
|
+
|
|
2822
|
+
|
|
2823
|
+
@classmethod
|
|
2824
|
+
def from_url(cls, url, port=None, timeout=5, verify=False):
|
|
2825
|
+
"""Загрузить сертификат с HTTPS-сервера.
|
|
2826
|
+
|
|
2827
|
+
url — "https://example.com" или просто "example.com"
|
|
2828
|
+
verify=False (default) — принять любой сертификат (для инспекции)
|
|
2829
|
+
verify=True — упасть, если сертификат невалиден
|
|
2830
|
+
|
|
2831
|
+
Возвращает Cert.
|
|
2832
|
+
"""
|
|
2833
|
+
import ssl as _ssl
|
|
2834
|
+
from urllib.parse import urlparse
|
|
2835
|
+
|
|
2836
|
+
if "://" in url:
|
|
2837
|
+
parsed = urlparse(url)
|
|
2838
|
+
host = parsed.hostname
|
|
2839
|
+
if port is None:
|
|
2840
|
+
port = parsed.port or 443
|
|
2841
|
+
else:
|
|
2842
|
+
host = url
|
|
2843
|
+
if port is None:
|
|
2844
|
+
port = 443
|
|
2845
|
+
if not host:
|
|
2846
|
+
raise X509Error(f"Cert.from_url: cannot parse host from {url!r}")
|
|
2847
|
+
|
|
2848
|
+
if verify:
|
|
2849
|
+
ctx = _ssl.create_default_context()
|
|
2850
|
+
else:
|
|
2851
|
+
ctx = _ssl._create_unverified_context()
|
|
2852
|
+
|
|
2853
|
+
with socket.create_connection((host, port), timeout=timeout) as sock:
|
|
2854
|
+
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
|
2855
|
+
der = ssock.getpeercert(binary_form=True)
|
|
2856
|
+
|
|
2857
|
+
return cls(der)
|
|
2858
|
+
|
|
2859
|
+
def info(self):
|
|
2860
|
+
"""Текстовое представление в стиле `openssl x509 -text`."""
|
|
2861
|
+
def _fmt_time(dt):
|
|
2862
|
+
return dt.strftime("%b %d %H:%M:%S %Y GMT")
|
|
2863
|
+
|
|
2864
|
+
lines = []
|
|
2865
|
+
lines.append("Certificate:")
|
|
2866
|
+
lines.append(" Data:")
|
|
2867
|
+
lines.append(f" Version: {self.version} (0x{self.version - 1:x})")
|
|
2868
|
+
lines.append(f" Serial Number: {self.serial} (0x{self.serial:x})")
|
|
2869
|
+
lines.append(f" Signature Algorithm: {self.signature_algorithm_name}")
|
|
2870
|
+
lines.append(f" Issuer: {self.issuer}")
|
|
2871
|
+
lines.append( " Validity")
|
|
2872
|
+
lines.append(f" Not Before: {_fmt_time(self.not_before)}")
|
|
2873
|
+
lines.append(f" Not After : {_fmt_time(self.not_after)}")
|
|
2874
|
+
lines.append(f" Subject: {self.subject}")
|
|
2875
|
+
|
|
2876
|
+
key = self.public_key
|
|
2877
|
+
lines.append(" Subject Public Key Info:")
|
|
2878
|
+
if isinstance(key, RSAKeys):
|
|
2879
|
+
lines.append(" Public Key Algorithm: rsaEncryption")
|
|
2880
|
+
lines.append(f" Public-Key: ({key.size_bits} bit)")
|
|
2881
|
+
h = key.n.to_bytes((key.n.bit_length() + 7) // 8, "big").hex()
|
|
2882
|
+
lines.append(" Modulus:")
|
|
2883
|
+
for i in range(0, len(h), 64):
|
|
2884
|
+
lines.append(f" {h[i:i+64]}")
|
|
2885
|
+
lines.append(f" Exponent: {key.e} (0x{key.e:x})")
|
|
2886
|
+
elif isinstance(key, Ed25519Keys):
|
|
2887
|
+
lines.append(" Public Key Algorithm: ED25519")
|
|
2888
|
+
lines.append(" ED25519 Public-Key:")
|
|
2889
|
+
h = key.pub.hex()
|
|
2890
|
+
for i in range(0, len(h), 64):
|
|
2891
|
+
lines.append(f" {h[i:i+64]}")
|
|
2892
|
+
elif isinstance(key, ECDSAKeys):
|
|
2893
|
+
lines.append(" Public Key Algorithm: id-ecPublicKey")
|
|
2894
|
+
lines.append(f" ASN1 OID: {key.curve_oid}")
|
|
2895
|
+
size = key.curve["size"]
|
|
2896
|
+
x, y = key.point
|
|
2897
|
+
xh = x.to_bytes(size, "big").hex()
|
|
2898
|
+
yh = y.to_bytes(size, "big").hex()
|
|
2899
|
+
lines.append(" pub:")
|
|
2900
|
+
for i in range(0, len(xh), 64):
|
|
2901
|
+
lines.append(f" {xh[i:i+64]}")
|
|
2902
|
+
for i in range(0, len(yh), 64):
|
|
2903
|
+
lines.append(f" {yh[i:i+64]}")
|
|
2904
|
+
else:
|
|
2905
|
+
lines.append(f" Public Key Algorithm: {self.key_algorithm_oid}")
|
|
2906
|
+
|
|
2907
|
+
if self.extensions:
|
|
2908
|
+
lines.append(" X509v3 extensions:")
|
|
2909
|
+
for ext in self.extensions:
|
|
2910
|
+
name = EXT_NAMES.get(ext.oid, ext.oid)
|
|
2911
|
+
crit = " critical" if ext.critical else ""
|
|
2912
|
+
lines.append(f" {name}:{crit}")
|
|
2913
|
+
v = ext.value
|
|
2914
|
+
if v:
|
|
2915
|
+
hexed = v.hex()
|
|
2916
|
+
if len(hexed) > 64:
|
|
2917
|
+
lines.append(f" {hexed[:64]}...")
|
|
2918
|
+
else:
|
|
2919
|
+
lines.append(f" {hexed}")
|
|
2920
|
+
|
|
2921
|
+
lines.append(f" Signature Algorithm: {self.signature_algorithm_name}")
|
|
2922
|
+
sig_hex = self.signature.hex()
|
|
2923
|
+
for i in range(0, len(sig_hex), 64):
|
|
2924
|
+
lines.append(f" {sig_hex[i:i+64]}")
|
|
2925
|
+
|
|
2926
|
+
return "\n".join(lines)
|
|
2927
|
+
|
|
2928
|
+
|
|
2664
2929
|
def _sign_and_build_alg(key, data, hash_name="sha256", sig_scheme="pkcs1v15"):
|
|
2665
2930
|
"""Подписать `data` и вернуть (AlgorithmIdentifier DER, signature bytes)."""
|
|
2666
2931
|
if not isinstance(key, (RSAKeys, Ed25519Keys, ECDSAKeys)):
|
|
@@ -3289,6 +3554,30 @@ class CertBundle:
|
|
|
3289
3554
|
def __repr__(self): return f"<CertBundle {len(self.certs)} certs>"
|
|
3290
3555
|
|
|
3291
3556
|
|
|
3557
|
+
def to_pkcs12(self, key, password="", name=None, iterations=2048,
|
|
3558
|
+
cipher="aes-256-cbc"):
|
|
3559
|
+
"""Собрать PKCS#12 из цепочки: certs[0] — leaf, остальные — chain.
|
|
3560
|
+
|
|
3561
|
+
key — приватный ключ leaf'а.
|
|
3562
|
+
"""
|
|
3563
|
+
if not self.certs:
|
|
3564
|
+
raise X509Error("CertBundle.to_pkcs12: empty bundle")
|
|
3565
|
+
leaf = self.certs[0]
|
|
3566
|
+
chain = self.certs[1:]
|
|
3567
|
+
b = PKCS12Builder()
|
|
3568
|
+
b.set_key(key)
|
|
3569
|
+
b.set_cert(leaf)
|
|
3570
|
+
if chain:
|
|
3571
|
+
b.add_chain(*chain)
|
|
3572
|
+
b.set_password(password)
|
|
3573
|
+
if name:
|
|
3574
|
+
b.set_name(name)
|
|
3575
|
+
b.set_iterations(iterations)
|
|
3576
|
+
if cipher != "aes-256-cbc":
|
|
3577
|
+
b.set_cipher(cipher)
|
|
3578
|
+
return b.build()
|
|
3579
|
+
|
|
3580
|
+
|
|
3292
3581
|
# ======================================================================
|
|
3293
3582
|
# CRLBuilder — выпуск CRL
|
|
3294
3583
|
# ======================================================================
|
|
@@ -36,6 +36,7 @@ import sys
|
|
|
36
36
|
import traceback
|
|
37
37
|
|
|
38
38
|
from x509kit import (
|
|
39
|
+
PKCS12,
|
|
39
40
|
RSAKeys, Ed25519Keys, ECDSAKeys,
|
|
40
41
|
Cert, CACert, CSR, CertBundle,
|
|
41
42
|
CRL, CRLBuilder, CRL_REASONS,
|
|
@@ -986,6 +987,173 @@ def test_pem_roundtrip():
|
|
|
986
987
|
# Main
|
|
987
988
|
# ======================================================================
|
|
988
989
|
|
|
990
|
+
|
|
991
|
+
# ======================================================================
|
|
992
|
+
# 16. Новые фичи: from_string, from_url, info, to_pkcs12
|
|
993
|
+
# ======================================================================
|
|
994
|
+
|
|
995
|
+
def test_name_from_string():
|
|
996
|
+
hr("16. Name.from_string (RFC 4514)")
|
|
997
|
+
|
|
998
|
+
n1 = Name.from_string("CN=example.com, O=MyOrg, C=XX")
|
|
999
|
+
n2 = Name.from_dict({"C": "XX", "O": "MyOrg", "CN": "example.com"})
|
|
1000
|
+
T.expect("round-trip == from_dict", n1.raw == n2.raw,
|
|
1001
|
+
f"\n from_string: {n1}\n from_dict: {n2}")
|
|
1002
|
+
T.expect("str содержит CN", "CN=example.com" in str(n1))
|
|
1003
|
+
|
|
1004
|
+
n3 = Name.from_string(str(n2))
|
|
1005
|
+
T.expect("round-trip через str", n3.raw == n2.raw)
|
|
1006
|
+
|
|
1007
|
+
n4 = Name.from_string(r"CN=with\, comma, O=Org")
|
|
1008
|
+
T.expect("escape запятой", n4.get("CN") == "with, comma",
|
|
1009
|
+
f"got: {n4.get('CN')!r}")
|
|
1010
|
+
|
|
1011
|
+
n5 = Name.from_string(r"CN=with\\backslash")
|
|
1012
|
+
T.expect("escape backslash", n5.get("CN") == r"with\backslash",
|
|
1013
|
+
f"got: {n5.get('CN')!r}")
|
|
1014
|
+
|
|
1015
|
+
try:
|
|
1016
|
+
Name.from_string("")
|
|
1017
|
+
T.fail("пустая строка → X509Error", "не бросило")
|
|
1018
|
+
except X509Error:
|
|
1019
|
+
T.ok("пустая строка → X509Error")
|
|
1020
|
+
|
|
1021
|
+
try:
|
|
1022
|
+
Name.from_string("no-equals-sign")
|
|
1023
|
+
T.fail("нет '=' → X509Error", "не бросило")
|
|
1024
|
+
except X509Error:
|
|
1025
|
+
T.ok("нет '=' → X509Error")
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def test_cert_from_url():
|
|
1029
|
+
hr("17. Cert.from_url")
|
|
1030
|
+
|
|
1031
|
+
T.expect("Cert.from_url определён", hasattr(Cert, "from_url"))
|
|
1032
|
+
|
|
1033
|
+
try:
|
|
1034
|
+
cert = Cert.from_url("example.com", timeout=5)
|
|
1035
|
+
T.expect("возвращает Cert", isinstance(cert, Cert))
|
|
1036
|
+
T.expect("subject есть", cert.subject.get("CN") is not None)
|
|
1037
|
+
T.expect("SAN есть", len(cert.san) > 0)
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
T.skip("Cert.from_url — нет сети", str(e)[:60])
|
|
1040
|
+
|
|
1041
|
+
try:
|
|
1042
|
+
Cert.from_url("", timeout=2)
|
|
1043
|
+
T.fail("пустой URL → X509Error", "не бросило")
|
|
1044
|
+
except X509Error:
|
|
1045
|
+
T.ok("пустой URL → X509Error")
|
|
1046
|
+
except Exception as e:
|
|
1047
|
+
T.ok(f"пустой URL → {type(e).__name__}")
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def test_cert_info():
|
|
1051
|
+
hr("18. Cert.info() — openssl x509 -text style")
|
|
1052
|
+
|
|
1053
|
+
key = Ed25519Keys.generate()
|
|
1054
|
+
cert = (CertificateBuilder()
|
|
1055
|
+
.add_subject({"C": "XX", "O": "Info", "CN": "info.example"})
|
|
1056
|
+
.add_public_key(key)
|
|
1057
|
+
.add_serial_random().add_validity(days=30)
|
|
1058
|
+
.add_san_dns("info.example")
|
|
1059
|
+
.self_signed().sign(key))
|
|
1060
|
+
|
|
1061
|
+
info = cert.info()
|
|
1062
|
+
|
|
1063
|
+
checks = [
|
|
1064
|
+
("начинается с 'Certificate:'", info.startswith("Certificate:")),
|
|
1065
|
+
("есть 'Version: 3'", "Version: 3" in info),
|
|
1066
|
+
("есть 'Serial Number:'", "Serial Number:" in info),
|
|
1067
|
+
("есть 'Signature Algorithm:'", "Signature Algorithm:" in info),
|
|
1068
|
+
("есть 'Issuer:'", "Issuer:" in info),
|
|
1069
|
+
("есть 'Validity'", "Validity" in info),
|
|
1070
|
+
("есть 'Not Before'", "Not Before:" in info),
|
|
1071
|
+
("есть 'Not After'", "Not After :" in info),
|
|
1072
|
+
("есть 'Subject:'", "Subject:" in info),
|
|
1073
|
+
("есть 'Subject Public Key Info:'",
|
|
1074
|
+
"Subject Public Key Info:" in info),
|
|
1075
|
+
("есть 'Public Key Algorithm: ED25519'",
|
|
1076
|
+
"Public Key Algorithm: ED25519" in info),
|
|
1077
|
+
("есть 'X509v3 extensions:'", "X509v3 extensions:" in info),
|
|
1078
|
+
("есть 'X509v3 Subject Alternative Name'",
|
|
1079
|
+
"X509v3 Subject Alternative Name" in info),
|
|
1080
|
+
("есть SAN содержимое", "info.example" in info),
|
|
1081
|
+
]
|
|
1082
|
+
for label, ok in checks:
|
|
1083
|
+
T.expect(label, ok)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def test_pkcs12_shortcuts():
|
|
1087
|
+
hr("19. PKCS#12 шорткаты")
|
|
1088
|
+
|
|
1089
|
+
ca_key = Ed25519Keys.generate()
|
|
1090
|
+
ca = (CertificateBuilder(is_ca=True)
|
|
1091
|
+
.add_subject({"CN": "Shortcut CA"})
|
|
1092
|
+
.add_public_key(ca_key)
|
|
1093
|
+
.add_serial_random().add_validity(days=365)
|
|
1094
|
+
.add_basic_constraints(path_len=0)
|
|
1095
|
+
.add_key_usage("keyCertSign", "cRLSign")
|
|
1096
|
+
.self_signed().sign(ca_key))
|
|
1097
|
+
|
|
1098
|
+
leaf_key = Ed25519Keys.generate()
|
|
1099
|
+
leaf = (CertificateBuilder()
|
|
1100
|
+
.add_subject({"CN": "shortcut.example"})
|
|
1101
|
+
.add_issuer(ca).add_public_key(leaf_key)
|
|
1102
|
+
.add_serial_random().add_validity(days=30)
|
|
1103
|
+
.add_san_dns("shortcut.example")
|
|
1104
|
+
.sign(ca_key))
|
|
1105
|
+
|
|
1106
|
+
# key.to_pkcs12
|
|
1107
|
+
p12 = leaf_key.to_pkcs12(leaf, chain=[ca], password="hunter2",
|
|
1108
|
+
name="shortcut")
|
|
1109
|
+
T.expect("key.to_pkcs12 → PKCS12", isinstance(p12, PKCS12))
|
|
1110
|
+
T.expect("to_pkcs12: MAC valid", p12.verify_mac())
|
|
1111
|
+
T.expect("to_pkcs12: chain ok", p12.verify_chain(trust_anchor=ca))
|
|
1112
|
+
|
|
1113
|
+
reloaded = PKCS12.load(p12.to_der(), password="hunter2")
|
|
1114
|
+
T.expect("to_pkcs12 round-trip: key matches",
|
|
1115
|
+
reloaded.key.pub == leaf_key.pub)
|
|
1116
|
+
T.expect("to_pkcs12 round-trip: cert matches",
|
|
1117
|
+
reloaded.cert.der == leaf.der)
|
|
1118
|
+
|
|
1119
|
+
# CertBundle.to_pkcs12
|
|
1120
|
+
bundle = CertBundle([leaf, ca])
|
|
1121
|
+
p12b = bundle.to_pkcs12(leaf_key, password="hunter2", name="bundle")
|
|
1122
|
+
T.expect("bundle.to_pkcs12 → PKCS12", isinstance(p12b, PKCS12))
|
|
1123
|
+
T.expect("bundle.to_pkcs12: MAC valid", p12b.verify_mac())
|
|
1124
|
+
|
|
1125
|
+
reloaded_b = PKCS12.load(p12b.to_der(), password="hunter2")
|
|
1126
|
+
T.expect("bundle round-trip: cert matches",
|
|
1127
|
+
reloaded_b.cert.der == leaf.der)
|
|
1128
|
+
T.expect("bundle round-trip: chain len=1",
|
|
1129
|
+
len(reloaded_b.chain) == 1)
|
|
1130
|
+
|
|
1131
|
+
# Все три типа ключей
|
|
1132
|
+
for label, kf in [
|
|
1133
|
+
("RSA", lambda: RSAKeys.generate(2048)),
|
|
1134
|
+
("Ed25519", lambda: Ed25519Keys.generate()),
|
|
1135
|
+
("EC-P256", lambda: ECDSAKeys.generate("P-256")),
|
|
1136
|
+
]:
|
|
1137
|
+
try:
|
|
1138
|
+
k = kf()
|
|
1139
|
+
c = (CertificateBuilder()
|
|
1140
|
+
.add_subject({"CN": f"{label}.shortcut"})
|
|
1141
|
+
.add_public_key(k)
|
|
1142
|
+
.add_serial_random().add_validity(days=30)
|
|
1143
|
+
.self_signed().sign(k))
|
|
1144
|
+
p = k.to_pkcs12(c, password="pw", name=label)
|
|
1145
|
+
T.expect(f"{label}: to_pkcs12 + MAC", p.verify_mac())
|
|
1146
|
+
except Exception as e:
|
|
1147
|
+
T.fail(f"{label}: to_pkcs12", str(e)[:80])
|
|
1148
|
+
|
|
1149
|
+
# Пустой bundle
|
|
1150
|
+
try:
|
|
1151
|
+
CertBundle([]).to_pkcs12(leaf_key)
|
|
1152
|
+
T.fail("пустой bundle → X509Error", "не бросило")
|
|
1153
|
+
except X509Error:
|
|
1154
|
+
T.ok("пустой bundle → X509Error")
|
|
1155
|
+
|
|
1156
|
+
|
|
989
1157
|
def main():
|
|
990
1158
|
print("╔" + "═" * 70 + "╗")
|
|
991
1159
|
print("║ x509kit — полный прогон" + " " * 46 + "║")
|
|
@@ -1012,6 +1180,10 @@ def main():
|
|
|
1012
1180
|
test_aia()
|
|
1013
1181
|
test_canonicalization()
|
|
1014
1182
|
test_pem_roundtrip()
|
|
1183
|
+
test_name_from_string()
|
|
1184
|
+
test_cert_from_url()
|
|
1185
|
+
test_cert_info()
|
|
1186
|
+
test_pkcs12_shortcuts()
|
|
1015
1187
|
except Exception as e:
|
|
1016
1188
|
print()
|
|
1017
1189
|
print("\033[31m!!! Непойманное исключение:\033[0m", type(e).__name__, e)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|