1# -*- coding: utf-8 -*-
2"""Certificate utilities for acme2certifier"""
3
4import base64
5import logging
6from typing import List, Tuple, Optional, Set
7from cryptography import x509
8from cryptography.hazmat.backends import default_backend
9from cryptography.hazmat.primitives import serialization, hashes
10from cryptography.hazmat.primitives.serialization.pkcs7 import (
11 load_pem_pkcs7_certificates,
12 load_der_pkcs7_certificates,
13)
14from cryptography.x509 import load_pem_x509_certificate, ocsp
15from cryptography.hazmat.primitives.asymmetric import (
16 dsa,
17 ec,
18 ed25519,
19 ed448,
20 padding,
21 rsa,
22)
23from .encoding import (
24 convert_string_to_byte,
25 convert_byte_to_string,
26 build_pem_file,
27 b64_url_recode,
28 b64_decode,
29)
30from .datetime_utils import date_to_uts_utc
31from .global_variables import CONFIGURATION_ERROR_DETAIL
32from pyasn1.codec.der import decoder
33from pyasn1.type import univ
34from pyasn1_modules import rfc5280
35
36_OID_SKI = "2.5.29.14"
37_OID_AKI = "2.5.29.35"
38# id-pe-acmeIdentifier (RFC 8737)
39ACME_TLS_ALPN_OID = x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31")
40ACME_TLS_ALPN_PROTOCOL = "acme-tls/1"
41
42
43def _cert_pem_to_der(logger: logging.Logger, certificate: str) -> bytes:
44 """Convert certificate input to DER bytes without parsing extensions."""
45 pem_data = convert_string_to_byte(
46 build_pem_file(logger, None, b64_url_recode(logger, certificate), True)
47 )
48 # Prefer cryptography load for PEM framing only; public_bytes touches extensions
49 # on some versions, so strip PEM manually.
50 lines = convert_byte_to_string(pem_data).strip().splitlines()
51 b64 = "".join(line for line in lines if not line.startswith("-----"))
52 return base64.b64decode(b64)
53
54
55def _cert_extension_raw_get(
56 logger: logging.Logger, certificate: str, oid: str
57) -> Optional[bytes]:
58 """Return extnValue (OCTET STRING contents) for oid, or None."""
59 logger.debug("_cert_extension_raw_get(%s)", oid)
60 der = _cert_pem_to_der(logger, certificate)
61 cert_asn1, _ = decoder.decode(der, asn1Spec=rfc5280.Certificate())
62 extensions = cert_asn1["tbsCertificate"]["extensions"]
63 if extensions is None or not extensions.hasValue():
64 return None
65 for ext in extensions:
66 if str(ext["extnID"]) == oid:
67 return bytes(ext["extnValue"])
68 return None
69
70
71def _cert_aki_asn1_get(logger: logging.Logger, certificate: str) -> Optional[str]:
72 """Get AKI keyIdentifier as hex via ASN.1 (tolerates illegal BasicConstraints)."""
73 logger.debug("_cert_aki_asn1_get()")
74 extn_value = _cert_extension_raw_get(logger, certificate, _OID_AKI)
75 if not extn_value:
76 logger.warning("No AKI found in certificate")
77 return None
78 aki, _ = decoder.decode(extn_value, asn1Spec=rfc5280.AuthorityKeyIdentifier())
79 if not aki["keyIdentifier"].isValue:
80 logger.warning("AKI extension present but keyIdentifier missing")
81 return None
82 aki_hex = bytes(aki["keyIdentifier"]).hex()
83 logger.debug("_cert_aki_asn1_get() ended with: %s", aki_hex)
84 return aki_hex
85
86
87def cert_aki_get(logger: logging.Logger, certificate: str) -> str:
88 """get subject key identifier from certificate"""
89 logger.debug("Helper.cert_ski_get()")
90
91 cert = cert_load(logger, certificate, recode=True)
92 try:
93 aki = cert.extensions.get_extension_for_oid(x509.OID_AUTHORITY_KEY_IDENTIFIER)
94 aki_value = aki.value.key_identifier.hex()
95 except Exception as _err:
96 logger.error(
97 "Error while getting AKI from certificate: %s. Fallback to ASN.1 method",
98 _err,
99 )
100 aki_value = _cert_aki_asn1_get(logger, certificate)
101
102 logger.debug("cert_aki_get() ended with: %s", aki_value)
103 return aki_value
104
105
106def _cert_ski_asn1_get(logger: logging.Logger, certificate: str) -> Optional[str]:
107 """Get SKI as hex via ASN.1 (tolerates illegal BasicConstraints)."""
108 logger.debug("_cert_ski_asn1_get()")
109 extn_value = _cert_extension_raw_get(logger, certificate, _OID_SKI)
110 if not extn_value:
111 logger.warning("No SKI found in certificate")
112 return None
113 ski, _ = decoder.decode(extn_value, asn1Spec=univ.OctetString())
114 ski_hex = bytes(ski).hex()
115 logger.debug("_cert_ski_asn1_get() ended with: %s", ski_hex)
116 return ski_hex
117
118
119def cert_load(
120 logger: logging.Logger, certificate: str, recode: bool
121) -> x509.Certificate:
122 """load certificate object from pem _Format"""
123 logger.debug("Helper.cert_load(%s)", recode)
124
125 if recode:
126 pem_data = convert_string_to_byte(
127 build_pem_file(logger, None, b64_url_recode(logger, certificate), True)
128 )
129 else:
130 pem_data = convert_string_to_byte(certificate)
131 cert = x509.load_pem_x509_certificate(pem_data, default_backend())
132
133 return cert
134
135
136def cert_dates_get(logger: logging.Logger, certificate: str) -> Tuple[int, int]:
137 """get date number form certificate"""
138 logger.debug("Helper.cert_dates_get()")
139
140 issue_date = 0
141 expiration_date = 0
142 try:
143 cert = cert_load(logger, certificate, recode=True)
144 issue_date = date_to_uts_utc(
145 cert.not_valid_before_utc, _tformat="%Y-%m-%d %H:%M:%S"
146 )
147 expiration_date = date_to_uts_utc(
148 cert.not_valid_after_utc, _tformat="%Y-%m-%d %H:%M:%S"
149 )
150 except Exception as err:
151 logger.debug(
152 "Error while getting dates from certificate. Fallback to deprecated method: %s",
153 err,
154 )
155 try:
156 issue_date = date_to_uts_utc(
157 cert.not_valid_before, _tformat="%Y-%m-%d %H:%M:%S"
158 )
159 expiration_date = date_to_uts_utc(
160 cert.not_valid_after, _tformat="%Y-%m-%d %H:%M:%S"
161 )
162 except Exception:
163 logger.error("Error while getting dates from certificate: %s", err)
164 issue_date = 0
165 expiration_date = 0
166
167 logger.debug("cert_dates_get() ended with: %s/%s", issue_date, expiration_date)
168 return (issue_date, expiration_date)
169
170
171def cert_cn_get(logger: logging.Logger, certificate: str) -> str:
172 """get cn from certificate"""
173 logger.debug("Helper.cert_cn_get()")
174
175 cert = cert_load(logger, certificate, recode=True)
176 # get subject and look for common name
177 subject = cert.subject
178 result = None
179 for attr in subject:
180 if attr.oid == x509.NameOID.COMMON_NAME:
181 result = attr.value
182 break
183 logger.debug("Helper.cert_cn_get() ended with: %s", result)
184 return result
185
186
187def cert_der2pem(der_cert: bytes) -> str:
188 """convert certificate der to pem"""
189 cert = x509.load_der_x509_certificate(der_cert)
190 pem_cert = cert.public_bytes(serialization.Encoding.PEM)
191 return pem_cert
192
193
194def cert_issuer_get(logger: logging.Logger, certificate: str) -> str:
195 """get certificate issuer from certificate"""
196 logger.debug("Helper.cert_issuer_get()")
197
198 cert = cert_load(logger, certificate, recode=True)
199 result = cert.issuer.rfc4514_string()
200 logger.debug("Helper.cert_issuer_get() ended with: %s", result)
201 return result
202
203
204def cert_pem2der(pem_cert: str) -> bytes:
205 """convert certificate pem to der"""
206 cert = x509.load_pem_x509_certificate(pem_cert.encode(), default_backend())
207 der_cert = cert.public_bytes(serialization.Encoding.DER)
208 return der_cert
209
210
211def cert_pubkey_get(logger: logging.Logger, certificate=str) -> str:
212 """get public key from certificate"""
213 logger.debug("Helper.cert_pubkey_get()")
214 cert = cert_load(logger, certificate, recode=False)
215 public_key = cert.public_key()
216 pubkey_str = public_key.public_bytes(
217 encoding=serialization.Encoding.PEM,
218 format=serialization.PublicFormat.SubjectPublicKeyInfo,
219 )
220 logger.debug("Helper.cert_pubkey_get() ended with: %s", pubkey_str)
221 return convert_byte_to_string(pubkey_str)
222
223
224def cert_san_get(
225 logger: logging.Logger, certificate: str, recode: bool = True
226) -> List[str]:
227 """get subject alternate names from certificate"""
228 logger.debug("Helper.cert_san_get(%s)", recode)
229
230 cert = cert_load(logger, certificate, recode=recode)
231 sans = []
232 try:
233 ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
234 sans_list = ext.value.get_values_for_type(x509.DNSName)
235 for san in sans_list:
236 sans.append(f"DNS:{san}")
237 sans_list = ext.value.get_values_for_type(x509.IPAddress)
238 for san in sans_list:
239 sans.append(f"IP:{san}")
240 sans_list = ext.value.get_values_for_type(x509.RFC822Name)
241 for san in sans_list:
242 sans.append(f"EMAIL:{san}")
243 except Exception as err:
244 logger.error("Error while getting SANs from certificate: %s", err)
245
246 logger.debug("Helper.cert_san_get() ended")
247 return sans
248
249
250def cert_bound_names_get(
251 logger: logging.Logger,
252 certificate: str,
253 recode: bool = True,
254 email_identifier_rewrite: bool = False,
255) -> Set[Tuple[str, str]]:
256 """Return normalized (type, value) pairs from certificate SANs and subject CN."""
257 from .csr import _cn_bound_type, _normalize_bound_name
258
259 logger.debug(
260 "Helper.cert_bound_names_get(email_identifier_rewrite=%s)",
261 email_identifier_rewrite,
262 )
263 names: Set[Tuple[str, str]] = set()
264 if not certificate:
265 logger.debug("Helper.cert_bound_names_get() ended with: %s", names)
266 return names
267
268 for san in cert_san_get(logger, certificate, recode=recode):
269 try:
270 san_type, san_value = san.split(":", 1)
271 except ValueError as err:
272 logger.error("Error while splitting SAN %s: %s", san, err)
273 continue
274 normalized = _normalize_bound_name(
275 san_type, san_value, email_identifier_rewrite=email_identifier_rewrite
276 )
277 if normalized:
278 names.add(normalized)
279
280 cn = cert_cn_get(logger, certificate)
281 if cn:
282 normalized = _normalize_bound_name(
283 _cn_bound_type(cn),
284 cn,
285 email_identifier_rewrite=email_identifier_rewrite,
286 )
287 if normalized:
288 names.add(normalized)
289
290 logger.debug("Helper.cert_bound_names_get() ended with: %s", names)
291 return names
292
293
294def cert_ski_get(logger: logging.Logger, certificate: str) -> str:
295 """get subject key identifier from certificate"""
296 logger.debug("Helper.cert_ski_get()")
297
298 cert = cert_load(logger, certificate, recode=True)
299 try:
300 ski = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_KEY_IDENTIFIER)
301 ski_value = ski.value.digest.hex()
302 except Exception as err:
303 logger.error("Error while getting the SKI: %s. Fallback to ASN.1 method", err)
304 ski_value = _cert_ski_asn1_get(logger, certificate)
305
306 logger.debug("Helper.cert_ski_get() ended with: %s", ski_value)
307 return ski_value
308
309
310def cert_extensions_get(logger: logging.Logger, certificate: str, recode: bool = True):
311 """get extenstions from certificate certificate"""
312 logger.debug("Helper.cert_extensions_get()")
313
314 cert = cert_load(logger, certificate, recode=recode)
315 extension_list = []
316 for extension in cert.extensions:
317 extension_list.append(
318 convert_byte_to_string(base64.b64encode(extension.value.public_bytes()))
319 )
320
321 logger.debug("Helper.cert_extensions_get() ended with: %s", extension_list)
322 return extension_list
323
324
325def cert_acme_tls_alpn_extension_ok(
326 logger: logging.Logger,
327 certificate: str,
328 sha256_digest_hex: str,
329 recode: bool = False,
330) -> bool:
331 """Return True when cert has a critical id-pe-acmeIdentifier with expected digest.
332
333 RFC 8737 §3: extension OID 1.3.6.1.5.5.7.1.31 MUST be critical and contain an
334 OCTET STRING of the SHA-256 digest of the key authorization.
335 """
336 logger.debug("Helper.cert_acme_tls_alpn_extension_ok()")
337 try:
338 cert = cert_load(logger, certificate, recode=recode)
339 extension = cert.extensions.get_extension_for_oid(ACME_TLS_ALPN_OID)
340 except x509.ExtensionNotFound:
341 logger.warning("tls-alpn-01: id-pe-acmeIdentifier extension not found")
342 return False
343 except Exception as err:
344 logger.warning(
345 "tls-alpn-01: failed to parse ACME identifier extension: %s", err
346 )
347 return False
348
349 if not extension.critical:
350 logger.warning("tls-alpn-01: id-pe-acmeIdentifier extension is not critical")
351 return False
352
353 try:
354 digest = bytes.fromhex(sha256_digest_hex)
355 except ValueError:
356 logger.warning("tls-alpn-01: invalid expected digest hex")
357 return False
358 if len(digest) != 32:
359 logger.warning("tls-alpn-01: expected digest must be 32 bytes")
360 return False
361
362 # DER OCTET STRING: tag 0x04, length 0x20, then digest
363 expected_der = b"\x04\x20" + digest
364 raw = getattr(extension.value, "value", None)
365 if raw is None:
366 try:
367 raw = extension.value.public_bytes()
368 except Exception as err:
369 logger.warning(
370 "tls-alpn-01: cannot read ACME identifier extension value: %s", err
371 )
372 return False
373
374 if raw not in (expected_der, digest):
375 logger.warning("tls-alpn-01: ACME identifier extension value mismatch")
376 return False
377
378 logger.debug("Helper.cert_acme_tls_alpn_extension_ok() ended with: True")
379 return True
380
381
382def cert_serial_get(logger: logging.Logger, certificate: str, hexformat: bool = False):
383 """get serial number form certificate"""
384 logger.debug("Helper.cert_serial_get()")
385 cert = cert_load(logger, certificate, recode=True)
386 if hexformat:
387 serial_number = f"{cert.serial_number:x}"
388 # add leading zero if needed
389 serial_number = serial_number.zfill(len(serial_number) + len(serial_number) % 2)
390 else:
391 serial_number = cert.serial_number
392 logger.debug("Helper.cert_serial_get() ended with: %s", serial_number)
393 return serial_number
394
395
396def pembundle_to_list(logger: logging.Logger, pem_bundle: str) -> List[str]:
397 """split pem bundle into a list of certificates"""
398 logger.debug("Helper.pembundle_to_list()")
399 cert_list = []
400 pem_data = ""
401 if "-----BEGIN CERTIFICATE-----" in pem_bundle:
402 for line in pem_bundle.splitlines():
403 line = line.strip()
404 if line.startswith("-----BEGIN CERTIFICATE-----") and pem_data:
405 cert_list.append(pem_data)
406 pem_data = ""
407 pem_data += line + "\n"
408 if pem_data:
409 cert_list.append(pem_data)
410 logger.debug("Helper.pembundle_to_list() returned %s certificates", cert_list)
411 return cert_list
412
413
414def _cert_sha256_fingerprint(cert: x509.Certificate) -> str:
415 """SHA-256 fingerprint as lowercase hex (no colons)."""
416 return cert.fingerprint(hashes.SHA256()).hex()
417
418
419def _pem_fingerprint(
420 logger: logging.Logger, pem_cert: str
421) -> Tuple[Optional[str], Optional[str]]:
422 """Return ``(error, fingerprint)`` for a single PEM certificate."""
423 try:
424 cert = cert_load(logger, pem_cert, recode=False)
425 except Exception as err_:
426 logger.error("Failed to parse certificate in chain: %s", err_)
427 return (
428 f"{CONFIGURATION_ERROR_DETAIL}: Failed to parse certificate chain",
429 None,
430 )
431 return None, _cert_sha256_fingerprint(cert)
432
433
434def cert_chain_skip(
435 logger: logging.Logger,
436 pem_bundle: Optional[str],
437 skip_list: Optional[List[str]],
438 link_check: bool = True,
439) -> Tuple[Optional[str], Optional[str]]:
440 """Drop certificates whose SHA-256 fingerprint is in *skip_list*."""
441 logger.debug("Helper.cert_chain_skip()")
442 if not pem_bundle:
443 logger.debug("Helper.cert_chain_skip() ended (empty bundle)")
444 return None, pem_bundle
445 if not skip_list:
446 logger.debug("Helper.cert_chain_skip() ended (skip list empty)")
447 return None, pem_bundle
448
449 pem_list = pembundle_to_list(logger, pem_bundle)
450 if not pem_list:
451 logger.error("cert_chain_skip_list is set but the bundle is not a PEM chain")
452 return (
453 f"{CONFIGURATION_ERROR_DETAIL}: Failed to parse certificate chain",
454 None,
455 )
456
457 skip_set = set(skip_list)
458 error, leaf_fp = _pem_fingerprint(logger, pem_list[0])
459 if error:
460 return error, None
461 if leaf_fp in skip_set:
462 logger.error("cert_chain_skip_list must not include the end-entity certificate")
463 return (
464 f"{CONFIGURATION_ERROR_DETAIL}: "
465 "cert_chain_skip_list includes the end-entity certificate",
466 None,
467 )
468
469 kept = [pem_list[0]]
470 for pem_cert in pem_list[1:]:
471 error, fingerprint = _pem_fingerprint(logger, pem_cert)
472 if error:
473 return error, None
474 if fingerprint not in skip_set:
475 kept.append(pem_cert)
476
477 if len(kept) != len(pem_list):
478 error, certs = _certs_from_pems(logger, kept)
479 if error:
480 return error, None
481 error = _chain_links_error(logger, certs, 0, link_check=link_check)
482 if error:
483 return error, None
484
485 result = "".join(kept)
486 logger.debug(
487 "Helper.cert_chain_skip() ended with %d of %d certificates kept",
488 len(kept),
489 len(pem_list),
490 )
491 return None, result
492
493
494def _cert_certifies(issuer: x509.Certificate, subject: x509.Certificate) -> bool:
495 """Return True when *issuer* signed *subject* (RFC 8555 chain link)."""
496 if subject.issuer != issuer.subject:
497 return False
498 public_key = issuer.public_key()
499 try:
500 if isinstance(public_key, rsa.RSAPublicKey):
501 hash_alg = subject.signature_hash_algorithm
502 if hash_alg is None:
503 return False
504 public_key.verify(
505 subject.signature,
506 subject.tbs_certificate_bytes,
507 padding.PKCS1v15(),
508 hash_alg,
509 )
510 return True
511 if isinstance(public_key, ec.EllipticCurvePublicKey):
512 hash_alg = subject.signature_hash_algorithm
513 if hash_alg is None:
514 return False
515 public_key.verify(
516 subject.signature,
517 subject.tbs_certificate_bytes,
518 ec.ECDSA(hash_alg),
519 )
520 return True
521 if isinstance(public_key, dsa.DSAPublicKey):
522 hash_alg = subject.signature_hash_algorithm
523 if hash_alg is None:
524 return False
525 public_key.verify(
526 subject.signature, subject.tbs_certificate_bytes, hash_alg
527 )
528 return True
529 if isinstance(public_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
530 public_key.verify(subject.signature, subject.tbs_certificate_bytes)
531 return True
532 except Exception:
533 return False
534 return False
535
536
537def _certs_from_pems(
538 logger: logging.Logger, pem_list: List[str]
539) -> Tuple[Optional[str], List[x509.Certificate]]:
540 """Load PEM strings with ``cert_load()``. Fail closed on parse errors."""
541 certs: List[x509.Certificate] = []
542 for pem_cert in pem_list:
543 try:
544 certs.append(cert_load(logger, pem_cert, recode=False))
545 except Exception as err_:
546 logger.error("Failed to parse certificate in chain: %s", err_)
547 return (
548 f"{CONFIGURATION_ERROR_DETAIL}: Failed to parse certificate chain",
549 [],
550 )
551 return None, certs
552
553
554def _parse_pem_bundle(
555 logger: logging.Logger, pem_bundle: str
556) -> Tuple[Optional[str], List[str], List[x509.Certificate]]:
557 """Split *pem_bundle* and load each PEM. Fail closed on an empty or invalid chain."""
558 pems = pembundle_to_list(logger, pem_bundle)
559 if not pems:
560 logger.error("cert_chain_append is set but the bundle is not a PEM chain")
561 return (
562 f"{CONFIGURATION_ERROR_DETAIL}: Failed to parse certificate chain",
563 [],
564 [],
565 )
566 error, certs = _certs_from_pems(logger, pems)
567 if error:
568 return error, [], []
569 return None, pems, certs
570
571
572def _append_entry_check(
573 logger: logging.Logger,
574 cert: x509.Certificate,
575 leaf_fp: str,
576 seen_fps: Set[str],
577) -> Tuple[Optional[str], Optional[str]]:
578 """Return ``(error, fingerprint)``. Fingerprint is set only when the cert may be appended."""
579 logger.debug("Helper._append_entry_check()")
580 fingerprint = _cert_sha256_fingerprint(cert)
581 if fingerprint == leaf_fp:
582 logger.error("cert_chain_append must not include the end-entity certificate")
583 return (
584 f"{CONFIGURATION_ERROR_DETAIL}: "
585 "cert_chain_append includes the end-entity certificate",
586 None,
587 )
588 if fingerprint in seen_fps:
589 logger.error("cert_chain_append includes a certificate already in the chain")
590 return (
591 f"{CONFIGURATION_ERROR_DETAIL}: "
592 "cert_chain_append includes a certificate already in the chain",
593 None,
594 )
595 logger.debug("Helper._append_entry_check() ended")
596 return None, fingerprint
597
598
599def _chain_links_error(
600 logger: logging.Logger,
601 certs: List[x509.Certificate],
602 start: int,
603 *,
604 link_check: bool = True,
605) -> Optional[str]:
606 """Fail if any certificate from *start* does not certify the previous one."""
607 logger.debug("Helper._chain_links_error()")
608 for idx in range(start, len(certs) - 1):
609 prev = certs[idx]
610 nxt = certs[idx + 1]
611 issuer_name_match = prev.issuer == nxt.subject
612 certifies = _cert_certifies(nxt, prev)
613 if not certifies:
614 if issuer_name_match:
615 reason = "issuer name matches but signature verification failed"
616 else:
617 reason = (
618 f"previous issuer {prev.issuer.rfc4514_string()} != "
619 f"following subject {nxt.subject.rfc4514_string()}"
620 )
621 if not link_check:
622 logger.warning(
623 "cert_chain_link_check is False; certificate "
624 "does not certify the previous one (%s)",
625 reason,
626 )
627 continue
628 logger.error(
629 "certificate %d does not certify the previous one (%s)",
630 idx + 1,
631 reason,
632 )
633 return (
634 f"{CONFIGURATION_ERROR_DETAIL}: "
635 "certificate does not certify the previous one "
636 f"({reason})"
637 )
638 logger.debug("Helper._chain_links_error() ended")
639 return None
640
641
642def _extend_chain(
643 logger: logging.Logger,
644 pems: List[str],
645 certs: List[x509.Certificate],
646 pem_list: List[str],
647 *,
648 link_check: bool = True,
649) -> Optional[str]:
650 """Parse *pem_list*, reject leaf/duplicates, append in place, check RFC 8555 links."""
651 logger.debug("Helper._extend_chain()")
652
653 join_at = len(certs) - 1
654 leaf_fp = _cert_sha256_fingerprint(certs[0])
655 seen_fps = {_cert_sha256_fingerprint(cert) for cert in certs}
656 error, appended = _certs_from_pems(logger, pem_list)
657 if error:
658 return error
659 for pem_cert, parsed in zip(pem_list, appended):
660 error, fingerprint = _append_entry_check(logger, parsed, leaf_fp, seen_fps)
661 if error:
662 return error
663 seen_fps.add(fingerprint)
664 pems.append(pem_cert)
665 certs.append(parsed)
666
667 logger.debug("Helper._extend_chain() ended with: %s", pems)
668 return _chain_links_error(logger, certs, join_at, link_check=link_check)
669
670
671def cert_chain_append(
672 logger: logging.Logger,
673 pem_bundle: Optional[str],
674 pem_list: Optional[List[str]],
675 *,
676 link_check: bool = True,
677) -> Tuple[Optional[str], Optional[str]]:
678 """Append PEM certificates to *pem_bundle* and check RFC 8555 chain links."""
679 logger.debug("Helper.cert_chain_append()")
680
681 if not pem_bundle:
682 logger.debug("Helper.cert_chain_append() ended (empty bundle)")
683 return None, pem_bundle
684 if not pem_list:
685 logger.debug("Helper.cert_chain_append() ended (append list empty)")
686 return None, pem_bundle
687
688 error, pems, certs = _parse_pem_bundle(logger, pem_bundle)
689 if error:
690 return error, None
691
692 error = _extend_chain(logger, pems, certs, pem_list, link_check=link_check)
693 if error:
694 return error, None
695
696 result = "".join(pems)
697 logger.debug(
698 "Helper.cert_chain_append() ended with %d certificates",
699 len(pems),
700 )
701 logger.debug("Helper.cert_chain_append() ended with: %s", result)
702 return None, result
703
704
705def certid_asn1_get(logger: logging.Logger, cert_pem: str, issuer_pem: str) -> str:
706 """get renewal information from certificate"""
707 logger.debug("Helper.certid_asn1_get()")
708
709 cert = load_pem_x509_certificate(convert_string_to_byte(cert_pem))
710 issuer = load_pem_x509_certificate(convert_string_to_byte(issuer_pem))
711
712 builder = ocsp.OCSPRequestBuilder()
713 builder = builder.add_certificate(cert, issuer, hashes.SHA256())
714 ocsprequest = builder.build()
715 ocsprequest_hex = ocsprequest.public_bytes(serialization.Encoding.DER).hex()
716
717 # this is ugly but i did not find a better way to do this
718 _header, certid_hex = ocsprequest_hex.split("0420", 1)
719
720 return certid_hex
721
722
723def certid_hex_get(logger: logging.Logger, renewal_info: str) -> Tuple[str, str]:
724 """get certid in hex from renewal_info field"""
725 logger.debug("Helper.certid_hex_get()")
726
727 renewal_info_b64 = b64_url_recode(logger, renewal_info)
728 renewal_info_hex = b64_decode(logger, renewal_info_b64).hex()
729
730 # this is ugly but i did not find a better way to do this
731 mda, certid_renewal = renewal_info_hex.split("0420", 1)
732 mda = mda[4:]
733
734 logger.debug("Helper.certid_hex_get() endet with %s", certid_renewal)
735 return mda, certid_renewal
736
737
738def certid_check(
739 logger: logging.Logger, renewal_info: str, certid_database: str
740) -> str:
741 """compare certid with renewal info"""
742 logger.debug("Helper.certid_check()")
743
744 renewal_info_b64 = b64_url_recode(logger, renewal_info)
745 renewal_info_hex = b64_decode(logger, renewal_info_b64).hex()
746
747 # this is ugly but i did not find a better way to do this
748 _header, certid_renewal = renewal_info_hex.split("0420", 1)
749 result = certid_renewal == certid_database
750
751 logger.debug("Helper.certid_check() ended with: %s", result)
752 return result
753
754
755def pkcs7_to_pem(logger, pkcs7_content: str, outform: str = "string") -> List[str]:
756 """convert pkcs7 to pem"""
757 logger.debug("CAhandler._pkcs7_to_pem()")
758
759 # Define loading strategies in order of preference
760 loading_strategies = [
761 # Strategy 1: Load as PEM directly
762 lambda content: load_pem_pkcs7_certificates(convert_string_to_byte(content)),
763 # Strategy 2: Replace CERTIFICATE with PKCS7 tag and load as PEM
764 lambda content: load_pem_pkcs7_certificates(
765 convert_string_to_byte(content.replace("CERTIFICATE", "PKCS7"))
766 ),
767 # Strategy 3: Load as DER
768 lambda content: load_der_pkcs7_certificates(content),
769 ]
770
771 pkcs7_obj = None
772 last_error = None
773
774 for i, strategy in enumerate(loading_strategies):
775 try:
776 pkcs7_obj = strategy(pkcs7_content)
777 if i == 1: # Log only for the tag replacement strategy
778 logger.error("PKCS7-TAG not found, updated content successfully")
779 break
780 except Exception as err:
781 last_error = err
782 if i == 0:
783 logger.error("PKCS7-TAG not found updating content...")
784 elif i == 1:
785 logger.debug("CAhandler._pkcs7_to_pem(): load pem failed. Try der...")
786
787 if pkcs7_obj is None:
788 logger.error("All PKCS7 loading strategies failed. Last error: %s", last_error)
789 raise last_error
790
791 # Convert certificates to PEM format
792 cert_pem_list = [
793 convert_byte_to_string(cert.public_bytes(serialization.Encoding.PEM))
794 for cert in pkcs7_obj
795 ]
796
797 # Define output format
798 output_formats = {
799 "string": lambda certs: "".join(certs),
800 "list": lambda certs: certs,
801 }
802
803 result = output_formats.get(outform, lambda _: None)(cert_pem_list)
804
805 logger.debug("Certificate._pkcs7_to_pem() ended")
806 return result