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,157 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from logging import Logger
5
+ from pathlib import Path
6
+ from typing import Protocol, cast, runtime_checkable
7
+
8
+ from cryptography import x509
9
+ from cryptography.hazmat._oid import NameOID
10
+ from cryptography.hazmat.backends import default_backend
11
+ from cryptography.hazmat.bindings._rust import ObjectIdentifier
12
+ from cryptography.hazmat.primitives import serialization
13
+ from cryptography.hazmat.primitives.asymmetric import rsa
14
+
15
+ from tiny_ca.const import ALLOWED_CERT_EXTENSIONS
16
+ from tiny_ca.exc import ErrorLoadCert, IsNotFile, NotExistCertFile, WrongType
17
+ from tiny_ca.models.certtificate import CertificateInfo
18
+ from tiny_ca.settings import DEFAULT_LOGGER
19
+
20
+ from .file_loader import CAFileLoader
21
+
22
+
23
+ class AsyncCAFileLoader:
24
+ """
25
+ Loads a CA certificate and private key from PEM files on the local filesystem.
26
+
27
+ Responsibility: file reading and PEM deserialisation *only*.
28
+ Does not generate certificates, manage sessions, or perform any
29
+ cryptographic operations beyond deserialisation.
30
+
31
+ On construction the loader:
32
+ 1. Validates that both paths point to existing, regular files with
33
+ permitted extensions (see ``ALLOWED_CERT_EXTENSIONS``).
34
+ 2. Deserialises the CA certificate and private key from PEM.
35
+ 3. Extracts ``CertificateInfo`` from the CA certificate's Subject.
36
+
37
+ After successful construction all three ``ICALoader`` properties are
38
+ available and will not change for the lifetime of the instance.
39
+
40
+ Parameters
41
+ ----------
42
+ ca_cert_path : str | Path
43
+ Path to the PEM-encoded CA certificate file.
44
+ ca_key_path : str | Path
45
+ Path to the PEM-encoded CA private key file.
46
+ ca_key_password : str | bytes | None
47
+ Optional password protecting the private key. A ``str`` value is
48
+ encoded to ``bytes`` using UTF-8 before being passed to the
49
+ cryptography library. ``None`` means the key is unencrypted.
50
+ logger : Logger | None
51
+ Logger instance for diagnostic messages. Falls back to
52
+ ``DEFAULT_LOGGER`` when ``None``.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ ca_cert_path: str | Path,
58
+ ca_key_path: str | Path,
59
+ ca_key_password: str | bytes | None = None,
60
+ logger: Logger | None = None,
61
+ ) -> None:
62
+ self._logger = logger or DEFAULT_LOGGER
63
+ self._ca_cert_path = CAFileLoader._validate_file(Path(ca_cert_path))
64
+ self._ca_key_path = CAFileLoader._validate_file(Path(ca_key_path))
65
+
66
+ self._password_bytes: bytes | None = None
67
+ if ca_key_password is not None:
68
+ self._password_bytes = (
69
+ ca_key_password.encode("utf-8")
70
+ if isinstance(ca_key_password, str)
71
+ else ca_key_password
72
+ )
73
+
74
+ self._ca_cert: x509.Certificate | None = None
75
+ self._ca_key: rsa.RSAPrivateKey | None = None
76
+ self._base_info: CertificateInfo | None = None
77
+
78
+ @classmethod
79
+ async def create(
80
+ cls,
81
+ ca_cert_path: str | Path,
82
+ ca_key_path: str | Path,
83
+ ca_key_password: str | bytes | None = None,
84
+ logger: Logger | None = None,
85
+ ) -> AsyncCAFileLoader:
86
+
87
+ instance = cls(ca_cert_path, ca_key_path, ca_key_password, logger)
88
+ await instance.load()
89
+ return instance
90
+
91
+ # ------------------------------------------------------------------
92
+ # ICALoader properties
93
+ # ------------------------------------------------------------------
94
+
95
+ @property
96
+ def ca_cert(self) -> x509.Certificate:
97
+ if self._ca_cert is None:
98
+ raise RuntimeError("Call 'await loader.load()' before accessing ca_cert.")
99
+ return self._ca_cert
100
+
101
+ @property
102
+ def ca_key(self) -> rsa.RSAPrivateKey:
103
+ if self._ca_key is None:
104
+ raise RuntimeError("Call 'await loader.load()' before accessing ca_key.")
105
+ return self._ca_key
106
+
107
+ @property
108
+ def base_info(self) -> CertificateInfo:
109
+ if self._base_info is None:
110
+ raise RuntimeError("Call 'await loader.load()' before accessing base_info.")
111
+ return self._base_info
112
+
113
+ async def load(self) -> None:
114
+ loop = asyncio.get_event_loop()
115
+ ca_cert, ca_key = await loop.run_in_executor(
116
+ None,
117
+ self._load_sync,
118
+ )
119
+ self._ca_cert = ca_cert
120
+ self._ca_key = ca_key
121
+ self._base_info = self._extract_info()
122
+
123
+ def _load_sync(self) -> tuple[x509.Certificate, rsa.RSAPrivateKey]:
124
+
125
+ try:
126
+ with open(self._ca_cert_path, "rb") as fh:
127
+ ca_cert = x509.load_pem_x509_certificate(fh.read(), default_backend())
128
+ except Exception as exc:
129
+ raise ErrorLoadCert(path_to_file=self._ca_cert_path, exc=str(exc)) from exc
130
+
131
+ try:
132
+ with open(self._ca_key_path, "rb") as fh:
133
+ ca_key = serialization.load_pem_private_key(
134
+ fh.read(), password=self._password_bytes, backend=default_backend()
135
+ )
136
+ except Exception as exc:
137
+ raise ErrorLoadCert(path_to_file=self._ca_key_path, exc=str(exc)) from exc
138
+
139
+ self._logger.info("CA loaded successfully from %s", self._ca_cert_path)
140
+ if not isinstance(ca_key, rsa.RSAPrivateKey):
141
+ raise TypeError("CA key must be RSA")
142
+ return ca_cert, ca_key
143
+
144
+ def _extract_info(self) -> CertificateInfo:
145
+ # assert self._ca_cert is not None
146
+
147
+ def _attr(oid: ObjectIdentifier) -> str | None:
148
+ attrs = self._ca_cert.subject.get_attributes_for_oid(oid) # type: ignore[union-attr]
149
+ return cast(str, attrs[0].value) if attrs else None
150
+
151
+ return CertificateInfo(
152
+ organization=_attr(NameOID.ORGANIZATION_NAME),
153
+ organizational_unit=_attr(NameOID.ORGANIZATIONAL_UNIT_NAME),
154
+ country=_attr(NameOID.COUNTRY_NAME),
155
+ state=_attr(NameOID.STATE_OR_PROVINCE_NAME),
156
+ locality=_attr(NameOID.LOCALITY_NAME),
157
+ )
@@ -0,0 +1,290 @@
1
+ """
2
+ CA material loading: Protocol definition and PEM-file-backed implementation.
3
+
4
+ This module provides two public symbols:
5
+
6
+ - ``ICALoader`` — a ``@runtime_checkable`` Protocol that defines the minimum
7
+ interface any CA-material provider must satisfy. Consumers
8
+ (e.g. ``CertificateFactory``) depend only on this Protocol,
9
+ never on a concrete loader class (DIP).
10
+
11
+ - ``CAFileLoader`` — reads a CA certificate and private key from PEM files on
12
+ the local filesystem and exposes them through ``ICALoader``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from logging import Logger
18
+ from pathlib import Path
19
+ from typing import Protocol, cast, runtime_checkable
20
+
21
+ from cryptography import x509
22
+ from cryptography.hazmat._oid import NameOID
23
+ from cryptography.hazmat.backends import default_backend
24
+ from cryptography.hazmat.bindings._rust import ObjectIdentifier
25
+ from cryptography.hazmat.primitives import serialization
26
+ from cryptography.hazmat.primitives.asymmetric import rsa
27
+
28
+ from tiny_ca.const import ALLOWED_CERT_EXTENSIONS
29
+ from tiny_ca.exc import ErrorLoadCert, IsNotFile, NotExistCertFile, WrongType
30
+ from tiny_ca.models.certtificate import CertificateInfo
31
+ from tiny_ca.settings import DEFAULT_LOGGER
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # ISP / DIP: minimal interface for CA-material providers
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @runtime_checkable
39
+ class ICALoader(Protocol):
40
+ """
41
+ Protocol that defines the minimum contract for CA-material providers.
42
+
43
+ Any object that exposes the three properties below satisfies this Protocol
44
+ and can be injected into ``CertificateFactory`` without any inheritance.
45
+ This makes it trivial to substitute the real filesystem loader with an
46
+ in-memory stub, an HSM-backed loader, or a mock in unit tests.
47
+
48
+ Properties
49
+ ----------
50
+ ca_cert : x509.Certificate
51
+ The loaded CA certificate object.
52
+ ca_key : rsa.RSAPrivateKey
53
+ The loaded CA private key used for signing.
54
+ base_info : CertificateInfo
55
+ Structured metadata extracted from the CA certificate's Subject field
56
+ (organization, country, state, locality, organizational unit).
57
+ """
58
+
59
+ @property
60
+ def ca_cert(self) -> x509.Certificate: ...
61
+
62
+ @property
63
+ def ca_key(self) -> rsa.RSAPrivateKey: ...
64
+
65
+ @property
66
+ def base_info(self) -> CertificateInfo: ...
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # SRP: PEM-file-backed CA loader
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class CAFileLoader:
75
+ """
76
+ Loads a CA certificate and private key from PEM files on the local filesystem.
77
+
78
+ Responsibility: file reading and PEM deserialisation *only*.
79
+ Does not generate certificates, manage sessions, or perform any
80
+ cryptographic operations beyond deserialisation.
81
+
82
+ On construction the loader:
83
+ 1. Validates that both paths point to existing, regular files with
84
+ permitted extensions (see ``ALLOWED_CERT_EXTENSIONS``).
85
+ 2. Deserialises the CA certificate and private key from PEM.
86
+ 3. Extracts ``CertificateInfo`` from the CA certificate's Subject.
87
+
88
+ After successful construction all three ``ICALoader`` properties are
89
+ available and will not change for the lifetime of the instance.
90
+
91
+ Parameters
92
+ ----------
93
+ ca_cert_path : str | Path
94
+ Path to the PEM-encoded CA certificate file.
95
+ ca_key_path : str | Path
96
+ Path to the PEM-encoded CA private key file.
97
+ ca_key_password : str | bytes | None
98
+ Optional password protecting the private key. A ``str`` value is
99
+ encoded to ``bytes`` using UTF-8 before being passed to the
100
+ cryptography library. ``None`` means the key is unencrypted.
101
+ logger : Logger | None
102
+ Logger instance for diagnostic messages. Falls back to
103
+ ``DEFAULT_LOGGER`` when ``None``.
104
+ """
105
+
106
+ # ------------------------------------------------------------------
107
+ # ICALoader properties
108
+ # ------------------------------------------------------------------
109
+
110
+ @property
111
+ def ca_cert(self) -> x509.Certificate:
112
+ """
113
+ The deserialized CA certificate.
114
+
115
+ Returns
116
+ -------
117
+ x509.Certificate
118
+ The CA certificate loaded from *ca_cert_path*.
119
+ """
120
+ return self._ca_cert
121
+
122
+ @property
123
+ def ca_key(self) -> rsa.RSAPrivateKey:
124
+ """
125
+ The deserialized CA private key.
126
+
127
+ Returns
128
+ -------
129
+ rsa.RSAPrivateKey
130
+ The private key loaded from *ca_key_path*.
131
+ """
132
+ return self._ca_key
133
+
134
+ @property
135
+ def base_info(self) -> CertificateInfo:
136
+ """
137
+ Structured metadata extracted from the CA certificate Subject.
138
+
139
+ Returns
140
+ -------
141
+ CertificateInfo
142
+ Contains *organization*, *organizational_unit*, *country*,
143
+ *state*, and *locality* fields; any absent attribute is ``None``.
144
+ """
145
+ return self._base_info
146
+
147
+ # ------------------------------------------------------------------
148
+ # Construction
149
+ # ------------------------------------------------------------------
150
+
151
+ def __init__(
152
+ self,
153
+ ca_cert_path: str | Path,
154
+ ca_key_path: str | Path,
155
+ ca_key_password: str | bytes | None = None,
156
+ logger: Logger | None = None,
157
+ ) -> None:
158
+ self._logger = logger or DEFAULT_LOGGER
159
+
160
+ self._ca_cert_path = self._validate_file(Path(ca_cert_path))
161
+ self._ca_key_path = self._validate_file(Path(ca_key_path))
162
+
163
+ # Normalise password to bytes so the cryptography library always
164
+ # receives the expected type.
165
+ password_bytes: bytes | None = None
166
+ if ca_key_password is not None:
167
+ password_bytes = (
168
+ ca_key_password.encode("utf-8")
169
+ if isinstance(ca_key_password, str)
170
+ else ca_key_password
171
+ )
172
+
173
+ self._ca_cert, self._ca_key = self._load(password_bytes)
174
+ self._base_info = self._extract_info()
175
+
176
+ # ------------------------------------------------------------------
177
+ # Private helpers
178
+ # ------------------------------------------------------------------
179
+
180
+ @staticmethod
181
+ def _validate_file(
182
+ path: Path,
183
+ allowed: tuple[str, ...] = ALLOWED_CERT_EXTENSIONS,
184
+ ) -> Path:
185
+ """
186
+ Verify that *path* points to an existing regular file with a
187
+ permitted extension.
188
+
189
+ Parameters
190
+ ----------
191
+ path : Path
192
+ Filesystem path to validate.
193
+ allowed : tuple[str, ...]
194
+ Whitelist of accepted file extensions (including the leading dot,
195
+ e.g. ``".pem"``). Defaults to ``ALLOWED_CERT_EXTENSIONS``.
196
+
197
+ Returns
198
+ -------
199
+ Path
200
+ The validated *path*, unchanged.
201
+
202
+ Raises
203
+ ------
204
+ NotExistCertFile
205
+ If *path* does not exist on the filesystem.
206
+ IsNotFile
207
+ If *path* exists but is a directory or other non-regular file.
208
+ WrongType
209
+ If the file extension is not listed in *allowed*.
210
+ """
211
+ if not path.exists():
212
+ raise NotExistCertFile(path_to_file=path)
213
+ if not path.is_file():
214
+ raise IsNotFile(path_to_file=path)
215
+ if path.suffix not in allowed:
216
+ raise WrongType(wrong_type=path.suffix, allowed_types=allowed)
217
+ return path
218
+
219
+ def _load(
220
+ self,
221
+ password: bytes | None,
222
+ ) -> tuple[x509.Certificate, rsa.RSAPrivateKey]:
223
+ """
224
+ Read and deserialise the CA certificate and private key from PEM files.
225
+
226
+ The certificate and key are loaded independently; a separate
227
+ ``try/except`` block wraps each operation so that the failure message
228
+ identifies exactly which file could not be read.
229
+
230
+ Parameters
231
+ ----------
232
+ password : bytes | None
233
+ Password for the private key, or ``None`` for unencrypted keys.
234
+
235
+ Returns
236
+ -------
237
+ tuple[x509.Certificate, rsa.RSAPrivateKey]
238
+ ``(ca_cert, ca_key)`` ready for use.
239
+
240
+ Raises
241
+ ------
242
+ ErrorLoadCert
243
+ Wraps any underlying exception thrown during file I/O or PEM
244
+ deserialisation, annotated with the offending file path.
245
+ """
246
+ try:
247
+ with open(self._ca_cert_path, "rb") as fh:
248
+ ca_cert = x509.load_pem_x509_certificate(fh.read(), default_backend())
249
+ except Exception as exc:
250
+ raise ErrorLoadCert(path_to_file=self._ca_cert_path, exc=str(exc)) from exc
251
+
252
+ try:
253
+ with open(self._ca_key_path, "rb") as fh:
254
+ ca_key = serialization.load_pem_private_key(
255
+ fh.read(), password=password, backend=default_backend()
256
+ )
257
+ except Exception as exc:
258
+ raise ErrorLoadCert(path_to_file=self._ca_key_path, exc=str(exc)) from exc
259
+
260
+ self._logger.info("CA loaded successfully from %s", self._ca_cert_path)
261
+ if not isinstance(ca_key, rsa.RSAPrivateKey):
262
+ raise TypeError("CA key must be RSA")
263
+ return ca_cert, ca_key
264
+
265
+ def _extract_info(self) -> CertificateInfo:
266
+ """
267
+ Parse the CA certificate's Subject and return a structured info object.
268
+
269
+ Iterates over well-known OIDs (organization, organizational unit,
270
+ country, state, locality) and reads the first matching attribute value
271
+ for each. Missing attributes are stored as ``None``.
272
+
273
+ Returns
274
+ -------
275
+ CertificateInfo
276
+ Dataclass populated with Subject attribute values extracted from
277
+ ``self._ca_cert``.
278
+ """
279
+
280
+ def _attr(oid: ObjectIdentifier) -> str | None:
281
+ attrs = self._ca_cert.subject.get_attributes_for_oid(oid)
282
+ return cast(str, attrs[0].value) if attrs else None
283
+
284
+ return CertificateInfo(
285
+ organization=_attr(NameOID.ORGANIZATION_NAME),
286
+ organizational_unit=_attr(NameOID.ORGANIZATIONAL_UNIT_NAME),
287
+ country=_attr(NameOID.COUNTRY_NAME),
288
+ state=_attr(NameOID.STATE_OR_PROVINCE_NAME),
289
+ locality=_attr(NameOID.LOCALITY_NAME),
290
+ )
@@ -0,0 +1,199 @@
1
+ """
2
+ Stateless utilities for computing and inspecting X.509 certificate validity
3
+ periods.
4
+
5
+ All methods are ``@staticmethod``; the class carries no instance state and
6
+ exists purely as a logical namespace. It can be used from any module without
7
+ instantiation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ from datetime import UTC, datetime, timedelta
14
+ from typing import cast
15
+
16
+ from cryptography import x509
17
+
18
+ from tiny_ca.exc import InvalidRangeTimeCertificate
19
+
20
+
21
+ class CertLifetime:
22
+ """
23
+ Stateless helper that computes and inspects X.509 certificate validity windows.
24
+
25
+ All operations are pure functions (no side effects, no shared state) and are
26
+ therefore safe to call from multiple threads simultaneously.
27
+
28
+ Use this class to:
29
+ - Compute a ``(not_before, not_after)`` pair for a new certificate.
30
+ - Extract the ``not_valid_after`` / ``not_valid_before`` timestamps from an
31
+ existing certificate as timezone-aware UTC ``datetime`` objects.
32
+ """
33
+
34
+ @staticmethod
35
+ def compute(
36
+ valid_from: datetime | None = None,
37
+ days_valid: int = 365,
38
+ ) -> tuple[datetime, datetime]:
39
+ """
40
+ Calculate the ``(not_before, not_after)`` validity interval for a new
41
+ certificate.
42
+
43
+ If *valid_from* is ``None`` the current UTC time is used as the start
44
+ of the interval. The end of the interval is *valid_from* plus
45
+ *days_valid* calendar days.
46
+
47
+ The result is validated to ensure the computed end date has not already
48
+ passed (which would produce an immediately-invalid certificate).
49
+
50
+ Parameters
51
+ ----------
52
+ valid_from : datetime | None
53
+ Start of the validity period as a timezone-aware ``datetime``.
54
+ Pass ``None`` to use ``datetime.now(timezone.utc)`` automatically.
55
+ days_valid : int
56
+ Number of calendar days the certificate should remain valid.
57
+ Default: ``365`` (one year).
58
+
59
+ Returns
60
+ -------
61
+ tuple[datetime, datetime]
62
+ ``(not_before, not_after)`` both expressed in UTC with
63
+ ``tzinfo=timezone.utc``.
64
+
65
+ Raises
66
+ ------
67
+ InvalidRangeTimeCertificate
68
+ If the computed *not_after* is earlier than the current UTC time,
69
+ meaning the certificate would be expired immediately upon issuance.
70
+
71
+ Examples
72
+ --------
73
+ >>> start, end = CertLifetime.compute(days_valid=90)
74
+ >>> assert (end - start).days == 90
75
+ """
76
+ now = datetime.now(UTC)
77
+ start = valid_from or now
78
+ end = start + timedelta(days=days_valid)
79
+ if end < now:
80
+ raise InvalidRangeTimeCertificate(valid_from=start, valid_to=end, now=now)
81
+ return start, end
82
+
83
+ @staticmethod
84
+ def valid_to(cert: x509.Certificate) -> datetime:
85
+ """
86
+ Return the expiry timestamp of *cert* as a timezone-aware UTC datetime.
87
+
88
+ Wraps ``cert.not_valid_after_utc`` and ensures the returned value
89
+ always carries ``tzinfo=timezone.utc`` for safe comparison with other
90
+ aware datetimes.
91
+
92
+ Parameters
93
+ ----------
94
+ cert : x509.Certificate
95
+ The certificate whose expiry date should be read.
96
+
97
+ Returns
98
+ -------
99
+ datetime
100
+ ``cert.not_valid_after_utc`` with ``tzinfo`` explicitly set to
101
+ ``timezone.utc``.
102
+ """
103
+ return cert.not_valid_after_utc.replace(tzinfo=UTC)
104
+
105
+ @staticmethod
106
+ def valid_from(cert: x509.Certificate) -> datetime:
107
+ """
108
+ Return the activation timestamp of *cert* as a timezone-aware UTC datetime.
109
+
110
+ Wraps ``cert.not_valid_before_utc`` and ensures the returned value
111
+ always carries ``tzinfo=timezone.utc`` for safe comparison with other
112
+ aware datetimes.
113
+
114
+ Parameters
115
+ ----------
116
+ cert : x509.Certificate
117
+ The certificate whose activation date should be read.
118
+
119
+ Returns
120
+ -------
121
+ datetime
122
+ ``cert.not_valid_before_utc`` with ``tzinfo`` explicitly set to
123
+ ``timezone.utc``.
124
+ """
125
+ return cert.not_valid_after_utc.replace(tzinfo=UTC)
126
+
127
+ @staticmethod
128
+ async def compute_async(
129
+ valid_from: datetime | None = None,
130
+ days_valid: int = 365,
131
+ ) -> tuple[datetime, datetime]:
132
+ """
133
+ Async-версія :meth:`compute`.
134
+
135
+ Виконує обчислення в пулі потоків, щоб не блокувати event loop.
136
+
137
+ Parameters
138
+ ----------
139
+ valid_from : datetime | None
140
+ Початок вікна дійсності (UTC). ``None`` → поточний UTC-час.
141
+ days_valid : int
142
+ Тривалість у календарних днях. За замовчуванням: ``365``.
143
+
144
+ Returns
145
+ -------
146
+ tuple[datetime, datetime]
147
+ ``(not_before, not_after)`` в UTC.
148
+
149
+ Raises
150
+ ------
151
+ InvalidRangeTimeCertificate
152
+ Якщо обчислена дата закінчення вже в минулому.
153
+
154
+ Examples
155
+ --------
156
+ #>>> start, end = await CertLifetime.compute_async(days_valid=90)
157
+ #>>> assert (end - start).days == 90
158
+ """
159
+ loop = asyncio.get_event_loop()
160
+ return await loop.run_in_executor(
161
+ None,
162
+ lambda: CertLifetime.compute(valid_from, days_valid),
163
+ )
164
+
165
+ @staticmethod
166
+ async def valid_to_async(cert: x509.Certificate) -> datetime:
167
+ """
168
+ Async-версія :meth:`valid_to`.
169
+
170
+ Parameters
171
+ ----------
172
+ cert : x509.Certificate
173
+ Сертифікат, дату закінчення якого потрібно прочитати.
174
+
175
+ Returns
176
+ -------
177
+ datetime
178
+ ``cert.not_valid_after_utc`` з ``tzinfo=UTC``.
179
+ """
180
+ loop = asyncio.get_event_loop()
181
+ return await loop.run_in_executor(None, CertLifetime.valid_to, cert)
182
+
183
+ @staticmethod
184
+ async def valid_from_async(cert: x509.Certificate) -> datetime:
185
+ """
186
+ Async-версія :meth:`valid_from`.
187
+
188
+ Parameters
189
+ ----------
190
+ cert : x509.Certificate
191
+ Сертифікат, дату початку дії якого потрібно прочитати.
192
+
193
+ Returns
194
+ -------
195
+ datetime
196
+ ``cert.not_valid_before_utc`` з ``tzinfo=UTC``.
197
+ """
198
+ loop = asyncio.get_event_loop()
199
+ return await loop.run_in_executor(None, CertLifetime.valid_from, cert)