tiny-ca 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,658 @@
1
+ """
2
+ factory.py
3
+
4
+ X.509 certificate factory: issues end-entity certificates, builds self-signed
5
+ CA certificates, generates CRLs, and validates certificate chains.
6
+
7
+ This module contains a single public class, ``CertificateFactory``, whose only
8
+ responsibility is cryptographic construction. It never touches the filesystem,
9
+ the database, or any business-logic layer.
10
+
11
+ SOLID notes
12
+ -----------
13
+ SRP : ``CertificateFactory`` handles cryptographic generation only.
14
+ File persistence → ``BaseStorage``. DB registration → ``BaseDB``.
15
+ OCP : New X.509 extension types are added in ``_build_extensions`` without
16
+ modifying any other method.
17
+ LSP : Any ``ICALoader`` implementation can be swapped in transparently.
18
+ ISP : ``SerialWithEncoding`` and ``CertLifetime`` are separate utility modules;
19
+ only the symbols needed here are imported.
20
+ DIP : CA material is provided through the ``ICALoader`` Protocol injected at
21
+ construction time; the factory never instantiates a loader itself.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import AsyncGenerator, Generator
27
+ import datetime
28
+ import ipaddress
29
+ from logging import Logger
30
+ import os
31
+ from typing import Any, cast
32
+
33
+ from cryptography import x509
34
+ from cryptography.hazmat.backends import default_backend
35
+ from cryptography.hazmat.primitives import hashes
36
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
37
+ from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
38
+ from sqlalchemy import Row
39
+
40
+ from ..const import CertType
41
+ from ..db.models import CertificateRecord
42
+ from ..exc import ValidationCertError
43
+ from ..settings import DEFAULT_LOGGER
44
+ from ..utils.serial_generator import SerialWithEncoding
45
+ from .utils import CertLifetime, ICALoader
46
+
47
+
48
+ class CertificateFactory:
49
+ """
50
+ Cryptographic factory for X.509 certificates, CSRs, and CRLs.
51
+
52
+ ``CertificateFactory`` is the single source of all certificate-generation
53
+ logic in the library. It accepts an ``ICALoader`` at construction time and
54
+ uses the CA certificate and key it provides to sign all issued artefacts.
55
+
56
+ Responsibilities
57
+ ----------------
58
+ - Generate self-signed root CA certificates (``build_self_signed_ca``).
59
+ - Issue end-entity certificates signed by the loaded CA (``issue_certificate``).
60
+ - Build and sign Certificate Revocation Lists (``build_crl``).
61
+ - Validate an existing certificate against the loaded CA (``validate_cert``).
62
+
63
+ Out of scope
64
+ ------------
65
+ - Writing any files to disk.
66
+ - Recording certificates in a database.
67
+ - Business-level rules (duplicate CN detection, rotation policies, etc.).
68
+
69
+ Parameters
70
+ ----------
71
+ ca_loader : ICALoader
72
+ Provider of the CA certificate, private key, and base Subject info.
73
+ Must satisfy the ``ICALoader`` Protocol (see ``file_loader.py``).
74
+ logger : Logger | None
75
+ Logger for operational messages. Falls back to ``DEFAULT_LOGGER``
76
+ when ``None``.
77
+
78
+ Raises
79
+ ------
80
+ TypeError
81
+ If *ca_loader* does not implement the ``ICALoader`` Protocol.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ ca_loader: ICALoader,
87
+ logger: Logger | None = None,
88
+ ) -> None:
89
+ if not isinstance(ca_loader, ICALoader):
90
+ raise TypeError(
91
+ f"ca_loader must implement ICALoader, got {type(ca_loader)}"
92
+ )
93
+ self._ca = ca_loader
94
+ self._logger = logger or DEFAULT_LOGGER
95
+
96
+ # ------------------------------------------------------------------
97
+ # Public API
98
+ # ------------------------------------------------------------------
99
+
100
+ @staticmethod
101
+ def build_self_signed_ca(
102
+ common_name: str = "Internal CA",
103
+ organization: str = "My Company",
104
+ country: str = "UA",
105
+ key_size: int = 2048,
106
+ days_valid: int = 3650,
107
+ valid_from: datetime.datetime | None = None,
108
+ logger: Logger | None = None,
109
+ ) -> tuple[x509.Certificate, rsa.RSAPrivateKey]:
110
+ """
111
+ Generate a self-signed root CA certificate and its private key.
112
+
113
+ This is a ``@staticmethod`` — it requires no loaded CA because the
114
+ resulting certificate is its own issuer. It is typically called once
115
+ during bootstrap to establish the trust anchor for the PKI.
116
+
117
+ The generated certificate includes:
118
+ - ``BasicConstraints(ca=True)`` — marks it as a CA certificate.
119
+ - ``KeyUsage`` with ``key_cert_sign`` and ``crl_sign`` set to ``True``.
120
+ - ``SubjectKeyIdentifier`` derived from the public key.
121
+
122
+ Parameters
123
+ ----------
124
+ common_name : str
125
+ Common Name (CN) for the CA Subject / Issuer fields.
126
+ Default: ``"Internal CA"``.
127
+ organization : str
128
+ Organization (O) field. Default: ``"My Company"``.
129
+ country : str
130
+ Two-letter ISO 3166-1 alpha-2 country code (C field).
131
+ Default: ``"UA"``.
132
+ key_size : int
133
+ RSA key length in bits. Use ``2048`` for standard security or
134
+ ``4096`` for long-lived roots. Default: ``2048``.
135
+ days_valid : int
136
+ Validity period in calendar days. Default: ``3650`` (≈10 years).
137
+ valid_from : datetime.datetime | None
138
+ Start of the validity period. ``None`` uses the current UTC time.
139
+ logger : Logger | None
140
+ Optional logger. Falls back to ``DEFAULT_LOGGER``.
141
+
142
+ Returns
143
+ -------
144
+ tuple[x509.Certificate, rsa.RSAPrivateKey]
145
+ ``(certificate, private_key)`` — both must be persisted by the caller.
146
+
147
+ Raises
148
+ ------
149
+ InvalidRangeTimeCertificate
150
+ If the computed expiry date is already in the past.
151
+ """
152
+ _log = logger or DEFAULT_LOGGER
153
+ _log.info(
154
+ "Generating CA certificate: CN=%s, key_size=%d", common_name, key_size
155
+ )
156
+
157
+ private_key = rsa.generate_private_key(
158
+ public_exponent=65537,
159
+ key_size=key_size,
160
+ backend=default_backend(),
161
+ )
162
+ subject = issuer = x509.Name(
163
+ [
164
+ x509.NameAttribute(NameOID.COUNTRY_NAME, country),
165
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
166
+ x509.NameAttribute(NameOID.COMMON_NAME, common_name),
167
+ ]
168
+ )
169
+ valid_from_dt, valid_to_dt = CertLifetime.compute(valid_from, days_valid)
170
+ serial_number = SerialWithEncoding.generate(
171
+ name=common_name.lower().replace(os.sep, "_"),
172
+ serial_type=CertType.CA,
173
+ )
174
+
175
+ certificate = (
176
+ x509.CertificateBuilder()
177
+ .subject_name(subject)
178
+ .issuer_name(issuer)
179
+ .public_key(private_key.public_key())
180
+ .serial_number(serial_number)
181
+ .not_valid_before(valid_from_dt)
182
+ .not_valid_after(valid_to_dt)
183
+ .add_extension(
184
+ x509.BasicConstraints(ca=True, path_length=None), critical=True
185
+ )
186
+ .add_extension(
187
+ x509.KeyUsage(
188
+ digital_signature=False,
189
+ key_encipherment=False,
190
+ content_commitment=False,
191
+ data_encipherment=False,
192
+ key_agreement=False,
193
+ key_cert_sign=True,
194
+ crl_sign=True,
195
+ encipher_only=False,
196
+ decipher_only=False,
197
+ ),
198
+ critical=True,
199
+ )
200
+ .add_extension(
201
+ x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
202
+ critical=False,
203
+ )
204
+ .sign(private_key, hashes.SHA256(), backend=default_backend())
205
+ )
206
+
207
+ _log.info(
208
+ "CA certificate created: subject=%s, valid_until=%s",
209
+ subject.rfc4514_string(),
210
+ valid_to_dt.isoformat(),
211
+ )
212
+ return certificate, private_key
213
+
214
+ def issue_certificate(
215
+ self,
216
+ common_name: str,
217
+ serial_type: CertType = CertType.SERVICE,
218
+ key_size: int = 2048,
219
+ days_valid: int = 365,
220
+ valid_from: datetime.datetime | None = None,
221
+ email: str | None = None,
222
+ is_server_cert: bool = False,
223
+ is_client_cert: bool = False,
224
+ san_dns: list[str] | None = None,
225
+ san_ip: list[str] | None = None,
226
+ ) -> tuple[x509.Certificate, rsa.RSAPrivateKey, x509.CertificateSigningRequest]:
227
+ """
228
+ Issue a signed end-entity certificate for the given subject parameters.
229
+
230
+ Workflow:
231
+ 1. Generate a fresh RSA key pair.
232
+ 2. Build the Subject ``x509.Name`` from CA base info + *common_name* / *email*.
233
+ 3. Create a CSR signed with the new private key.
234
+ 4. Assemble X.509 extensions (KeyUsage, EKU, SAN, SKI, AKI).
235
+ 5. Sign the certificate with the CA key from ``self._ca``.
236
+
237
+ The Subject inherits *country* and *organization* from the CA's own
238
+ certificate so that all issued certificates share a consistent issuer
239
+ hierarchy.
240
+
241
+ Parameters
242
+ ----------
243
+ common_name : str
244
+ Common Name (CN) for the new certificate's Subject.
245
+ serial_type : CertType
246
+ Certificate category used when encoding the serial number.
247
+ Default: ``CertType.SERVICE``.
248
+ key_size : int
249
+ RSA key length in bits. Default: ``2048``.
250
+ days_valid : int
251
+ Validity period in calendar days. Default: ``365``.
252
+ valid_from : datetime.datetime | None
253
+ Start of the validity period. ``None`` uses the current UTC time.
254
+ email : str | None
255
+ Optional email address added as an ``emailAddress`` Subject attribute.
256
+ is_server_cert : bool
257
+ When ``True``, adds ``ServerAuth`` to the Extended Key Usage
258
+ extension and includes *common_name* as a DNS SAN (RFC 2818
259
+ compliance).
260
+ is_client_cert : bool
261
+ When ``True``, adds ``ClientAuth`` to the Extended Key Usage
262
+ extension.
263
+ san_dns : list[str] | None
264
+ Additional DNS names for the Subject Alternative Name extension.
265
+ san_ip : list[str] | None
266
+ IP addresses (as strings) for the Subject Alternative Name extension.
267
+
268
+ Returns
269
+ -------
270
+ tuple[x509.Certificate, rsa.RSAPrivateKey, x509.CertificateSigningRequest]
271
+ ``(certificate, private_key, csr)`` — the certificate and key must
272
+ be persisted by the caller; the CSR is returned for audit purposes.
273
+
274
+ Raises
275
+ ------
276
+ InvalidRangeTimeCertificate
277
+ If the computed expiry date is already in the past.
278
+ """
279
+ self._logger.info(
280
+ "Issuing certificate: CN=%s, type=%s, server=%s, client=%s",
281
+ common_name,
282
+ serial_type,
283
+ is_server_cert,
284
+ is_client_cert,
285
+ )
286
+
287
+ private_key = rsa.generate_private_key(
288
+ public_exponent=65537,
289
+ key_size=key_size,
290
+ backend=default_backend(),
291
+ )
292
+
293
+ subject = self._build_subject(common_name, email)
294
+ csr = self._build_csr(subject, private_key)
295
+ extensions = self._build_extensions(
296
+ csr, is_server_cert, is_client_cert, san_dns, san_ip
297
+ )
298
+
299
+ valid_from_dt, valid_to_dt = CertLifetime.compute(valid_from, days_valid)
300
+ serial_number = SerialWithEncoding.generate(
301
+ name=common_name.lower().replace(os.sep, "_"),
302
+ serial_type=serial_type,
303
+ )
304
+
305
+ builder = (
306
+ x509.CertificateBuilder()
307
+ .subject_name(subject)
308
+ .issuer_name(self._ca.ca_cert.subject)
309
+ .public_key(csr.public_key())
310
+ .serial_number(serial_number)
311
+ .not_valid_before(valid_from_dt)
312
+ .not_valid_after(valid_to_dt)
313
+ )
314
+ for ext, critical in extensions:
315
+ builder = builder.add_extension(ext, critical=critical)
316
+
317
+ certificate = builder.sign(self._ca.ca_key, hashes.SHA256(), default_backend())
318
+
319
+ self._logger.info(
320
+ "Certificate issued: CN=%s, serial=%s, valid_until=%s",
321
+ common_name,
322
+ certificate.serial_number,
323
+ valid_to_dt.isoformat(),
324
+ )
325
+ return certificate, private_key, csr
326
+
327
+ def build_crl(
328
+ self,
329
+ revoked_certs: Generator[CertificateRecord, None, None],
330
+ days_valid: int = 1,
331
+ ) -> x509.CertificateRevocationList:
332
+ """
333
+ Build and sign a Certificate Revocation List from the provided records.
334
+
335
+ Iterates over *revoked_certs*, adds each entry to the CRL builder, then
336
+ signs the list with the CA private key. The resulting CRL is valid from
337
+ the current UTC time until ``now + days_valid`` days.
338
+
339
+ Parameters
340
+ ----------
341
+ revoked_certs : Generator[CertificateRecord, None, None]
342
+ Iterable of revoked certificate records as returned by
343
+ ``BaseDB.get_revoked_certificates``. Each record must expose
344
+ ``serial_number`` (castable to ``int``) and ``revocation_date``
345
+ (a ``datetime`` object).
346
+ days_valid : int
347
+ Number of days until the CRL expires and must be regenerated.
348
+ Typical values are ``1`` (daily rotation) to ``7`` (weekly).
349
+ Default: ``1``.
350
+
351
+ Returns
352
+ -------
353
+ x509.CertificateRevocationList
354
+ The signed CRL object. The caller is responsible for persisting it
355
+ to storage via ``BaseStorage``.
356
+ """
357
+ now = datetime.datetime.now(datetime.UTC)
358
+ self._logger.info(
359
+ "Building CRL: valid until %s",
360
+ (now + datetime.timedelta(days=days_valid)).isoformat(),
361
+ )
362
+
363
+ builder = (
364
+ x509.CertificateRevocationListBuilder()
365
+ .issuer_name(self._ca.ca_cert.subject)
366
+ .last_update(now)
367
+ .next_update(now + datetime.timedelta(days=days_valid))
368
+ )
369
+ for record in revoked_certs:
370
+ revoked = (
371
+ x509.RevokedCertificateBuilder()
372
+ .serial_number(int(record.serial_number))
373
+ .revocation_date(record.revocation_date) # type: ignore
374
+ .build()
375
+ )
376
+ builder = builder.add_revoked_certificate(revoked)
377
+
378
+ crl = builder.sign(self._ca.ca_key, hashes.SHA256(), default_backend())
379
+ self._logger.debug("CRL signed successfully")
380
+ return crl
381
+
382
+ async def abuild_crl(
383
+ self,
384
+ revoked_certs: AsyncGenerator[Row[tuple[str, datetime.datetime, str]], None],
385
+ days_valid: int = 1,
386
+ ) -> x509.CertificateRevocationList:
387
+
388
+ now = datetime.datetime.now(datetime.UTC)
389
+ self._logger.info(
390
+ "Building CRL: valid until %s",
391
+ (now + datetime.timedelta(days=days_valid)).isoformat(),
392
+ )
393
+
394
+ builder = (
395
+ x509.CertificateRevocationListBuilder()
396
+ .issuer_name(self._ca.ca_cert.subject)
397
+ .last_update(now)
398
+ .next_update(now + datetime.timedelta(days=days_valid))
399
+ )
400
+
401
+ async for record in revoked_certs:
402
+ revoked = (
403
+ x509.RevokedCertificateBuilder()
404
+ .serial_number(int(record[0]))
405
+ .revocation_date(record[1]) # type: ignore
406
+ .build()
407
+ )
408
+ builder = builder.add_revoked_certificate(revoked)
409
+ crl = builder.sign(self._ca.ca_key, hashes.SHA256(), default_backend())
410
+ self._logger.debug("CRL signed successfully")
411
+ return crl
412
+
413
+ def validate_cert(self, cert: x509.Certificate) -> None:
414
+ """
415
+ Verify that *cert* was issued by this CA, is within its validity window,
416
+ and carries a cryptographically correct signature.
417
+
418
+ Three checks are performed in order:
419
+ 1. **Issuer match** — ``cert.issuer`` must equal the CA's Subject.
420
+ 2. **Validity window** — current UTC time must be between
421
+ ``cert.not_valid_before_utc`` and ``cert.not_valid_after_utc``.
422
+ 3. **Signature** — the CA public key is used to verify the certificate
423
+ signature using PKCS#1 v1.5 with the algorithm declared in the cert.
424
+
425
+ Parameters
426
+ ----------
427
+ cert : x509.Certificate
428
+ The certificate to validate.
429
+
430
+ Returns
431
+ -------
432
+ None
433
+ Returns silently when all checks pass.
434
+
435
+ Raises
436
+ ------
437
+ ValidationCertError
438
+ If any of the three checks fails. The message describes which
439
+ check failed and includes the relevant values (timestamps, issuer).
440
+ """
441
+ self._logger.debug("Validating certificate serial=%s", cert.serial_number)
442
+
443
+ if cert.issuer != self._ca.ca_cert.subject:
444
+ raise ValidationCertError(
445
+ "Certificate issuer does not match the CA subject."
446
+ )
447
+
448
+ now = datetime.datetime.now(datetime.UTC)
449
+ not_before = cert.not_valid_before_utc
450
+ not_after = cert.not_valid_after_utc
451
+ if not_before > now or not_after < now:
452
+ raise ValidationCertError(
453
+ f"Certificate is outside its validity window: "
454
+ f"not_before={not_before.isoformat()}, "
455
+ f"not_after={not_after.isoformat()}, "
456
+ f"now={now.isoformat()}"
457
+ )
458
+
459
+ try:
460
+ self._ca.ca_cert.public_key().verify( # type: ignore
461
+ cert.signature,
462
+ cert.tbs_certificate_bytes,
463
+ padding.PKCS1v15(), # type: ignore
464
+ cert.signature_hash_algorithm,
465
+ )
466
+ except Exception as exc:
467
+ raise ValidationCertError(f"Signature verification failed: {exc}") from exc
468
+
469
+ self._logger.debug(
470
+ "Certificate serial=%s passed all validation checks", cert.serial_number
471
+ )
472
+
473
+ # ------------------------------------------------------------------
474
+ # Private helpers
475
+ # ------------------------------------------------------------------
476
+
477
+ def _build_subject(self, common_name: str, email: str | None) -> x509.Name:
478
+ """
479
+ Construct the Subject ``x509.Name`` for a new end-entity certificate.
480
+
481
+ Attributes are populated in the following order (absent CA attributes
482
+ are skipped):
483
+ 1. Country (C) — inherited from the CA's base info.
484
+ 2. Organization (O) — inherited from the CA's base info.
485
+ 3. Common Name (CN) — always present, taken from *common_name*.
486
+ 4. Email Address — only if *email* is not ``None``.
487
+
488
+ Parameters
489
+ ----------
490
+ common_name : str
491
+ CN value for the new certificate's Subject.
492
+ email : str | None
493
+ Optional email address to include as an ``emailAddress`` attribute.
494
+
495
+ Returns
496
+ -------
497
+ x509.Name
498
+ Fully constructed Subject name ready for use in a certificate builder.
499
+ """
500
+ attrs: list[x509.NameAttribute[Any]] = []
501
+ info = self._ca.base_info
502
+
503
+ if info.country:
504
+ attrs.append(x509.NameAttribute(NameOID.COUNTRY_NAME, info.country))
505
+ if info.organization:
506
+ attrs.append(
507
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, info.organization)
508
+ )
509
+
510
+ attrs.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
511
+
512
+ if email:
513
+ attrs.append(x509.NameAttribute(NameOID.EMAIL_ADDRESS, email))
514
+
515
+ return x509.Name(attrs)
516
+
517
+ @staticmethod
518
+ def _build_csr(
519
+ subject: x509.Name,
520
+ private_key: rsa.RSAPrivateKey,
521
+ ) -> x509.CertificateSigningRequest:
522
+ """
523
+ Create a Certificate Signing Request for *subject* signed with *private_key*.
524
+
525
+ The CSR is self-signed (the applicant proves possession of the private
526
+ key) using SHA-256. It carries no extensions — extensions are added to
527
+ the final certificate by the CA, not the CSR.
528
+
529
+ Parameters
530
+ ----------
531
+ subject : x509.Name
532
+ The Subject name to embed in the CSR.
533
+ private_key : rsa.RSAPrivateKey
534
+ The private key corresponding to the public key that will appear
535
+ in the issued certificate.
536
+
537
+ Returns
538
+ -------
539
+ x509.CertificateSigningRequest
540
+ The signed CSR, retained for audit purposes and passed to the
541
+ certificate builder to copy the public key.
542
+ """
543
+ return (
544
+ x509.CertificateSigningRequestBuilder()
545
+ .subject_name(subject)
546
+ .sign(private_key, hashes.SHA256(), backend=default_backend())
547
+ )
548
+
549
+ def _build_extensions(
550
+ self,
551
+ csr: x509.CertificateSigningRequest,
552
+ is_server_cert: bool,
553
+ is_client_cert: bool,
554
+ san_dns: list[str] | None,
555
+ san_ip: list[str] | None,
556
+ ) -> list[tuple[x509.ExtensionType, bool]]:
557
+ """
558
+ Assemble the list of X.509 v3 extensions for a new end-entity certificate.
559
+
560
+ Always-present extensions
561
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
562
+ - ``BasicConstraints(ca=False)`` — critical; marks this as a leaf cert.
563
+ - ``KeyUsage(digital_signature=True, key_encipherment=True)`` — critical.
564
+
565
+ Conditionally-added extensions
566
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
567
+ - ``ExtendedKeyUsage`` — added when at least one of *is_server_cert*
568
+ or *is_client_cert* is ``True``; contains ``ServerAuth`` and/or
569
+ ``ClientAuth`` OIDs accordingly.
570
+ - ``SubjectAlternativeName`` — added when any SAN entries exist.
571
+ When *is_server_cert* is ``True`` the CN is automatically prepended
572
+ as a DNS SAN for RFC 2818 compliance. Additional DNS names and IP
573
+ addresses from *san_dns* / *san_ip* are appended.
574
+ - ``SubjectKeyIdentifier`` — always added; derived from the CSR public key.
575
+ - ``AuthorityKeyIdentifier`` — always added; derived from the CA's own
576
+ ``SubjectKeyIdentifier`` extension.
577
+
578
+ Parameters
579
+ ----------
580
+ csr : x509.CertificateSigningRequest
581
+ The CSR whose public key is used to derive ``SubjectKeyIdentifier``.
582
+ is_server_cert : bool
583
+ Include ``ServerAuth`` EKU and add CN as a DNS SAN.
584
+ is_client_cert : bool
585
+ Include ``ClientAuth`` EKU.
586
+ san_dns : list[str] | None
587
+ Extra DNS names for the SAN extension.
588
+ san_ip : list[str] | None
589
+ IP address strings (e.g. ``"192.168.1.1"``) for the SAN extension.
590
+
591
+ Returns
592
+ -------
593
+ list[tuple[x509.ExtensionType, bool]]
594
+ Ordered list of ``(extension_object, is_critical)`` pairs ready to
595
+ be passed to ``CertificateBuilder.add_extension``.
596
+ """
597
+ result: list[tuple[x509.ExtensionType, bool]] = [
598
+ (x509.BasicConstraints(ca=False, path_length=None), True),
599
+ (
600
+ x509.KeyUsage(
601
+ digital_signature=True,
602
+ key_encipherment=True,
603
+ content_commitment=False,
604
+ data_encipherment=False,
605
+ key_agreement=False,
606
+ key_cert_sign=False,
607
+ crl_sign=False,
608
+ encipher_only=False,
609
+ decipher_only=False,
610
+ ),
611
+ True,
612
+ ),
613
+ ]
614
+
615
+ # Extended Key Usage — only added when at least one usage flag is set.
616
+ eku: list[x509.ObjectIdentifier] = []
617
+ if is_server_cert:
618
+ eku.append(ExtendedKeyUsageOID.SERVER_AUTH)
619
+ if is_client_cert:
620
+ eku.append(ExtendedKeyUsageOID.CLIENT_AUTH)
621
+ if eku:
622
+ result.append((x509.ExtendedKeyUsage(eku), False))
623
+
624
+ # Subject Alternative Names.
625
+ # For server certificates the CN is included per RFC 2818 §3.1 which
626
+ # requires modern TLS clients to check SANs rather than the CN.
627
+ san: list[x509.GeneralName] = []
628
+ if is_server_cert:
629
+ try:
630
+ cn = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
631
+ san.append(
632
+ x509.DNSName(cn if isinstance(cn, str) else cn.decode("utf-8"))
633
+ )
634
+ except IndexError: # pragma: no cover
635
+ pass
636
+ if san_dns:
637
+ san.extend(x509.DNSName(d) for d in san_dns)
638
+ if san_ip:
639
+ san.extend(x509.IPAddress(ipaddress.ip_address(ip)) for ip in san_ip)
640
+ if san:
641
+ result.append((x509.SubjectAlternativeName(san), is_server_cert))
642
+
643
+ # Key identifiers — required by RFC 5280 §4.2.1.2 and §4.2.1.1.
644
+ result.append(
645
+ (x509.SubjectKeyIdentifier.from_public_key(csr.public_key()), False)
646
+ )
647
+ result.append(
648
+ (
649
+ x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
650
+ self._ca.ca_cert.extensions.get_extension_for_class(
651
+ x509.SubjectKeyIdentifier
652
+ ).value
653
+ ),
654
+ False,
655
+ )
656
+ )
657
+
658
+ return result
@@ -0,0 +1,10 @@
1
+ from .file_loader import CAFileLoader, ICALoader
2
+ from .life_time import CertLifetime
3
+ from .serial import SerialWithEncoding # type: ignore
4
+
5
+ __all__ = [
6
+ "CertLifetime",
7
+ "ICALoader",
8
+ "CAFileLoader",
9
+ "SerialWithEncoding",
10
+ ]