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.
Files changed (37) hide show
  1. {x509kit-0.3.1/src/x509kit.egg-info → x509kit-0.3.2}/PKG-INFO +1 -1
  2. {x509kit-0.3.1 → x509kit-0.3.2}/pyproject.toml +1 -1
  3. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit/__init__.py +15 -0
  4. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit/x509.py +291 -2
  5. {x509kit-0.3.1 → x509kit-0.3.2/src/x509kit.egg-info}/PKG-INFO +1 -1
  6. {x509kit-0.3.1 → x509kit-0.3.2}/test_all.py +172 -0
  7. {x509kit-0.3.1 → x509kit-0.3.2}/CHANGELOG.md +0 -0
  8. {x509kit-0.3.1 → x509kit-0.3.2}/LICENSE +0 -0
  9. {x509kit-0.3.1 → x509kit-0.3.2}/MANIFEST.in +0 -0
  10. {x509kit-0.3.1 → x509kit-0.3.2}/README.md +0 -0
  11. {x509kit-0.3.1 → x509kit-0.3.2}/SECURITY.md +0 -0
  12. {x509kit-0.3.1 → x509kit-0.3.2}/check_dispatcher.py +0 -0
  13. {x509kit-0.3.1 → x509kit-0.3.2}/check_docs.py +0 -0
  14. {x509kit-0.3.1 → x509kit-0.3.2}/check_no_hidden.py +0 -0
  15. {x509kit-0.3.1 → x509kit-0.3.2}/compat_test.py +0 -0
  16. {x509kit-0.3.1 → x509kit-0.3.2}/examples/01_root_ca.py +0 -0
  17. {x509kit-0.3.1 → x509kit-0.3.2}/examples/02_issue_from_csr.py +0 -0
  18. {x509kit-0.3.1 → x509kit-0.3.2}/examples/03_self_signed_server.py +0 -0
  19. {x509kit-0.3.1 → x509kit-0.3.2}/examples/04_mtls_server.py +0 -0
  20. {x509kit-0.3.1 → x509kit-0.3.2}/examples/05_crl_revocation.py +0 -0
  21. {x509kit-0.3.1 → x509kit-0.3.2}/examples/06_ocsp_responder.py +0 -0
  22. {x509kit-0.3.1 → x509kit-0.3.2}/examples/07_pkcs12_export.py +0 -0
  23. {x509kit-0.3.1 → x509kit-0.3.2}/examples/08_parse_real_cert.py +0 -0
  24. {x509kit-0.3.1 → x509kit-0.3.2}/examples/09_encrypted_key.py +0 -0
  25. {x509kit-0.3.1 → x509kit-0.3.2}/examples/10_verify_chain.py +0 -0
  26. {x509kit-0.3.1 → x509kit-0.3.2}/examples/11_name_canonicalization.py +0 -0
  27. {x509kit-0.3.1 → x509kit-0.3.2}/examples/12_extensions.py +0 -0
  28. {x509kit-0.3.1 → x509kit-0.3.2}/examples/README.md +0 -0
  29. {x509kit-0.3.1 → x509kit-0.3.2}/flask/test_live_ca.py +0 -0
  30. {x509kit-0.3.1 → x509kit-0.3.2}/flask/test_mtls.py +0 -0
  31. {x509kit-0.3.1 → x509kit-0.3.2}/fuzz_x509.py +0 -0
  32. {x509kit-0.3.1 → x509kit-0.3.2}/setup.cfg +0 -0
  33. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit/py.typed +0 -0
  34. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/SOURCES.txt +0 -0
  35. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/dependency_links.txt +0 -0
  36. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/requires.txt +0 -0
  37. {x509kit-0.3.1 → x509kit-0.3.2}/src/x509kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x509kit
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Pure-Python X.509 / PKCS#10 / CRL / OCSP / PKCS#12 toolkit — stdlib only
5
5
  Author-email: ArtProsto <artemkamirn@mail.ru>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x509kit"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Pure-Python X.509 / PKCS#10 / CRL / OCSP / PKCS#12 toolkit — stdlib only"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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.1"
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
  # ======================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x509kit
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Pure-Python X.509 / PKCS#10 / CRL / OCSP / PKCS#12 toolkit — stdlib only
5
5
  Author-email: ArtProsto <artemkamirn@mail.ru>
6
6
  License: MIT
@@ -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