provide-foundation 0.0.0.dev0__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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,896 @@
|
|
1
|
+
"""X.509 certificate generation and management."""
|
2
|
+
|
3
|
+
from datetime import UTC, datetime, timedelta
|
4
|
+
from enum import StrEnum, auto
|
5
|
+
from functools import cached_property
|
6
|
+
import os
|
7
|
+
from pathlib import Path
|
8
|
+
import traceback
|
9
|
+
from typing import NotRequired, Self, TypeAlias, TypedDict, cast
|
10
|
+
|
11
|
+
from attrs import Factory, define, field
|
12
|
+
|
13
|
+
try:
|
14
|
+
from cryptography import x509
|
15
|
+
from cryptography.hazmat.backends import default_backend
|
16
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
17
|
+
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
|
18
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
19
|
+
from cryptography.x509 import Certificate as X509Certificate
|
20
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
21
|
+
|
22
|
+
_HAS_CRYPTO = True
|
23
|
+
except ImportError:
|
24
|
+
# Stub out cryptography types for type hints
|
25
|
+
x509 = None
|
26
|
+
default_backend = None
|
27
|
+
hashes = None
|
28
|
+
serialization = None
|
29
|
+
ec = None
|
30
|
+
padding = None
|
31
|
+
rsa = None
|
32
|
+
load_pem_private_key = None
|
33
|
+
X509Certificate = None
|
34
|
+
ExtendedKeyUsageOID = None
|
35
|
+
NameOID = None
|
36
|
+
_HAS_CRYPTO = False
|
37
|
+
|
38
|
+
from provide.foundation import logger
|
39
|
+
from provide.foundation.crypto.constants import (
|
40
|
+
DEFAULT_CERTIFICATE_CURVE,
|
41
|
+
DEFAULT_CERTIFICATE_KEY_TYPE,
|
42
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
43
|
+
DEFAULT_RSA_KEY_SIZE,
|
44
|
+
)
|
45
|
+
from provide.foundation.errors.config import ValidationError
|
46
|
+
|
47
|
+
|
48
|
+
def _require_crypto():
|
49
|
+
"""Ensure cryptography is available for crypto operations."""
|
50
|
+
if not _HAS_CRYPTO:
|
51
|
+
raise ImportError(
|
52
|
+
"Cryptography features require optional dependencies. Install with: "
|
53
|
+
"pip install 'provide-foundation[crypto]'"
|
54
|
+
)
|
55
|
+
|
56
|
+
|
57
|
+
class CertificateError(ValidationError):
|
58
|
+
"""Certificate-related errors."""
|
59
|
+
|
60
|
+
def __init__(self, message: str, hint: str | None = None) -> None:
|
61
|
+
super().__init__(
|
62
|
+
message=message,
|
63
|
+
field="certificate",
|
64
|
+
value=None,
|
65
|
+
rule=hint or "Certificate operation failed",
|
66
|
+
)
|
67
|
+
|
68
|
+
|
69
|
+
class KeyType(StrEnum):
|
70
|
+
RSA = auto()
|
71
|
+
ECDSA = auto()
|
72
|
+
|
73
|
+
|
74
|
+
class CurveType(StrEnum):
|
75
|
+
SECP256R1 = auto()
|
76
|
+
SECP384R1 = auto()
|
77
|
+
SECP521R1 = auto()
|
78
|
+
|
79
|
+
|
80
|
+
class CertificateConfig(TypedDict):
|
81
|
+
common_name: str
|
82
|
+
organization: str
|
83
|
+
alt_names: list[str]
|
84
|
+
key_type: KeyType
|
85
|
+
not_valid_before: datetime
|
86
|
+
not_valid_after: datetime
|
87
|
+
# Optional key generation parameters
|
88
|
+
key_size: NotRequired[int]
|
89
|
+
curve: NotRequired[CurveType]
|
90
|
+
|
91
|
+
|
92
|
+
if _HAS_CRYPTO:
|
93
|
+
KeyPair: TypeAlias = rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey
|
94
|
+
PublicKey: TypeAlias = rsa.RSAPublicKey | ec.EllipticCurvePublicKey
|
95
|
+
else:
|
96
|
+
KeyPair: TypeAlias = None
|
97
|
+
PublicKey: TypeAlias = None
|
98
|
+
|
99
|
+
|
100
|
+
@define(slots=True, frozen=True)
|
101
|
+
class CertificateBase:
|
102
|
+
"""Immutable base certificate data."""
|
103
|
+
|
104
|
+
subject: "x509.Name"
|
105
|
+
issuer: "x509.Name"
|
106
|
+
public_key: "PublicKey"
|
107
|
+
not_valid_before: datetime
|
108
|
+
not_valid_after: datetime
|
109
|
+
serial_number: int
|
110
|
+
|
111
|
+
@classmethod
|
112
|
+
def create(cls, config: CertificateConfig) -> tuple[Self, "KeyPair"]:
|
113
|
+
"""Create a new certificate base and private key."""
|
114
|
+
_require_crypto()
|
115
|
+
try:
|
116
|
+
logger.debug("📜📝🚀 CertificateBase.create: Starting base creation")
|
117
|
+
not_valid_before = config["not_valid_before"]
|
118
|
+
not_valid_after = config["not_valid_after"]
|
119
|
+
|
120
|
+
if not_valid_before.tzinfo is None:
|
121
|
+
not_valid_before = not_valid_before.replace(tzinfo=UTC)
|
122
|
+
if not_valid_after.tzinfo is None:
|
123
|
+
not_valid_after = not_valid_after.replace(tzinfo=UTC)
|
124
|
+
|
125
|
+
logger.debug(
|
126
|
+
f"📜⏳✅ CertificateBase.create: Using validity: "
|
127
|
+
f"{not_valid_before} to {not_valid_after}"
|
128
|
+
)
|
129
|
+
|
130
|
+
private_key: KeyPair
|
131
|
+
match config["key_type"]:
|
132
|
+
case KeyType.RSA:
|
133
|
+
key_size = config.get("key_size", DEFAULT_RSA_KEY_SIZE)
|
134
|
+
logger.debug(f"📜🔑🚀 Generating RSA key (size: {key_size})")
|
135
|
+
private_key = rsa.generate_private_key(
|
136
|
+
public_exponent=65537, key_size=key_size
|
137
|
+
)
|
138
|
+
case KeyType.ECDSA:
|
139
|
+
curve_choice = config.get("curve", CurveType.SECP384R1)
|
140
|
+
logger.debug(f"📜🔑🚀 Generating ECDSA key (curve: {curve_choice})")
|
141
|
+
curve = getattr(ec, curve_choice.name)()
|
142
|
+
private_key = ec.generate_private_key(curve)
|
143
|
+
case _:
|
144
|
+
raise ValueError(
|
145
|
+
f"Internal Error: Unsupported key type: {config['key_type']}"
|
146
|
+
)
|
147
|
+
|
148
|
+
subject = cls._create_name(config["common_name"], config["organization"])
|
149
|
+
issuer = cls._create_name(config["common_name"], config["organization"])
|
150
|
+
|
151
|
+
serial_number = x509.random_serial_number()
|
152
|
+
logger.debug(f"📜🔑✅ Generated serial number: {serial_number}")
|
153
|
+
|
154
|
+
base = cls(
|
155
|
+
subject=subject,
|
156
|
+
issuer=issuer,
|
157
|
+
public_key=private_key.public_key(),
|
158
|
+
not_valid_before=not_valid_before,
|
159
|
+
not_valid_after=not_valid_after,
|
160
|
+
serial_number=serial_number,
|
161
|
+
)
|
162
|
+
logger.debug("📜📝✅ CertificateBase.create: Base creation complete")
|
163
|
+
return base, private_key
|
164
|
+
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(
|
167
|
+
f"📜❌ CertificateBase.create: Failed: {e}",
|
168
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
169
|
+
)
|
170
|
+
raise CertificateError(f"Failed to generate certificate base: {e}") from e
|
171
|
+
|
172
|
+
@staticmethod
|
173
|
+
def _create_name(common_name: str, org: str) -> "x509.Name":
|
174
|
+
"""Helper method to construct an X.509 name."""
|
175
|
+
return x509.Name(
|
176
|
+
[
|
177
|
+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
178
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
|
179
|
+
]
|
180
|
+
)
|
181
|
+
|
182
|
+
|
183
|
+
@define(slots=True, eq=False, hash=False, repr=False)
|
184
|
+
class Certificate:
|
185
|
+
"""X.509 certificate management using attrs."""
|
186
|
+
|
187
|
+
cert_pem_or_uri: str | None = field(default=None, kw_only=True)
|
188
|
+
key_pem_or_uri: str | None = field(default=None, kw_only=True)
|
189
|
+
generate_keypair: bool = field(default=False, kw_only=True)
|
190
|
+
key_type: str = field(default=DEFAULT_CERTIFICATE_KEY_TYPE, kw_only=True)
|
191
|
+
key_size: int = field(default=DEFAULT_RSA_KEY_SIZE, kw_only=True)
|
192
|
+
ecdsa_curve: str = field(default=DEFAULT_CERTIFICATE_CURVE, kw_only=True)
|
193
|
+
common_name: str = field(default="localhost", kw_only=True)
|
194
|
+
alt_names: list[str] | None = field(
|
195
|
+
default=Factory(lambda: ["localhost"]), kw_only=True
|
196
|
+
)
|
197
|
+
organization_name: str = field(default="Default Organization", kw_only=True)
|
198
|
+
validity_days: int = field(default=DEFAULT_CERTIFICATE_VALIDITY_DAYS, kw_only=True)
|
199
|
+
|
200
|
+
_base: CertificateBase = field(init=False, repr=False)
|
201
|
+
_private_key: "KeyPair | None" = field(init=False, default=None, repr=False)
|
202
|
+
_cert: "X509Certificate" = field(init=False, repr=False)
|
203
|
+
_trust_chain: list["Certificate"] = field(init=False, factory=list, repr=False)
|
204
|
+
|
205
|
+
cert: str = field(init=False, default="", repr=True)
|
206
|
+
key: str | None = field(init=False, default=None, repr=False)
|
207
|
+
|
208
|
+
def __attrs_post_init__(self) -> None:
|
209
|
+
"""Handle loading or generation logic after attrs initialization."""
|
210
|
+
try:
|
211
|
+
if self.generate_keypair:
|
212
|
+
logger.debug(
|
213
|
+
"📜🔑🚀 Certificate.__attrs_post_init__: Generating new keypair"
|
214
|
+
)
|
215
|
+
|
216
|
+
now = datetime.now(UTC)
|
217
|
+
not_valid_before = now - timedelta(days=1)
|
218
|
+
not_valid_after = now + timedelta(days=self.validity_days)
|
219
|
+
|
220
|
+
normalized_key_type_str = self.key_type.lower()
|
221
|
+
match normalized_key_type_str:
|
222
|
+
case "rsa":
|
223
|
+
gen_key_type = KeyType.RSA
|
224
|
+
case "ecdsa":
|
225
|
+
gen_key_type = KeyType.ECDSA
|
226
|
+
case _:
|
227
|
+
raise ValueError(
|
228
|
+
f"Unsupported key_type string: '{self.key_type}'. "
|
229
|
+
"Must be 'rsa' or 'ecdsa'."
|
230
|
+
)
|
231
|
+
|
232
|
+
gen_curve: CurveType | None = None
|
233
|
+
gen_key_size = None
|
234
|
+
|
235
|
+
if gen_key_type == KeyType.ECDSA:
|
236
|
+
try:
|
237
|
+
gen_curve = CurveType[self.ecdsa_curve.upper()]
|
238
|
+
except KeyError as e_curve:
|
239
|
+
raise ValueError(
|
240
|
+
f"Unsupported ECDSA curve: {self.ecdsa_curve}"
|
241
|
+
) from e_curve
|
242
|
+
else: # RSA
|
243
|
+
gen_key_size = self.key_size
|
244
|
+
|
245
|
+
conf: CertificateConfig = {
|
246
|
+
"common_name": self.common_name,
|
247
|
+
"organization": self.organization_name,
|
248
|
+
"alt_names": self.alt_names or ["localhost"],
|
249
|
+
"key_type": gen_key_type,
|
250
|
+
"not_valid_before": not_valid_before,
|
251
|
+
"not_valid_after": not_valid_after,
|
252
|
+
}
|
253
|
+
if gen_curve is not None:
|
254
|
+
conf["curve"] = gen_curve
|
255
|
+
if gen_key_size is not None:
|
256
|
+
conf["key_size"] = gen_key_size
|
257
|
+
logger.debug(f"📜🔑🚀 Generation config: {conf}")
|
258
|
+
|
259
|
+
self._base, self._private_key = CertificateBase.create(conf)
|
260
|
+
|
261
|
+
self._cert = self._create_x509_certificate(
|
262
|
+
is_ca=False,
|
263
|
+
is_client_cert=True,
|
264
|
+
)
|
265
|
+
|
266
|
+
if self._cert is None:
|
267
|
+
raise CertificateError(
|
268
|
+
"Certificate object (_cert) is None after creation"
|
269
|
+
)
|
270
|
+
|
271
|
+
self.cert = self._cert.public_bytes(serialization.Encoding.PEM).decode(
|
272
|
+
"utf-8"
|
273
|
+
)
|
274
|
+
if self._private_key:
|
275
|
+
self.key = self._private_key.private_bytes(
|
276
|
+
encoding=serialization.Encoding.PEM,
|
277
|
+
format=serialization.PrivateFormat.PKCS8,
|
278
|
+
encryption_algorithm=serialization.NoEncryption(),
|
279
|
+
).decode("utf-8")
|
280
|
+
else:
|
281
|
+
self.key = None
|
282
|
+
|
283
|
+
logger.debug(
|
284
|
+
"📜🔑✅ Certificate.__attrs_post_init__: Generated cert and key"
|
285
|
+
)
|
286
|
+
|
287
|
+
else:
|
288
|
+
if not self.cert_pem_or_uri:
|
289
|
+
raise CertificateError(
|
290
|
+
"cert_pem_or_uri required when not generating"
|
291
|
+
)
|
292
|
+
|
293
|
+
logger.debug("📜🔑🚀 Loading certificate from provided data")
|
294
|
+
cert_data = self._load_from_uri_or_pem(self.cert_pem_or_uri)
|
295
|
+
self.cert = cert_data
|
296
|
+
|
297
|
+
logger.debug("📜🔑🔍 Loading X.509 certificate from PEM data")
|
298
|
+
self._cert = x509.load_pem_x509_certificate(cert_data.encode("utf-8"))
|
299
|
+
logger.debug("📜🔑✅ X.509 certificate object loaded from PEM")
|
300
|
+
|
301
|
+
if self.key_pem_or_uri:
|
302
|
+
logger.debug("📜🔑🚀 Loading private key")
|
303
|
+
key_data = self._load_from_uri_or_pem(self.key_pem_or_uri)
|
304
|
+
self.key = key_data
|
305
|
+
|
306
|
+
loaded_priv_key = load_pem_private_key(
|
307
|
+
key_data.encode("utf-8"), password=None
|
308
|
+
)
|
309
|
+
if not isinstance(
|
310
|
+
loaded_priv_key,
|
311
|
+
rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey,
|
312
|
+
):
|
313
|
+
raise CertificateError(
|
314
|
+
f"Loaded private key is of unsupported type: {type(loaded_priv_key)}. "
|
315
|
+
"Expected RSA or ECDSA private key."
|
316
|
+
)
|
317
|
+
self._private_key = loaded_priv_key
|
318
|
+
logger.debug("📜🔑✅ Private key object loaded and type validated")
|
319
|
+
else:
|
320
|
+
self.key = None
|
321
|
+
|
322
|
+
loaded_not_valid_before = self._cert.not_valid_before_utc
|
323
|
+
loaded_not_valid_after = self._cert.not_valid_after_utc
|
324
|
+
if loaded_not_valid_before.tzinfo is None:
|
325
|
+
loaded_not_valid_before = loaded_not_valid_before.replace(
|
326
|
+
tzinfo=UTC
|
327
|
+
)
|
328
|
+
if loaded_not_valid_after.tzinfo is None:
|
329
|
+
loaded_not_valid_after = loaded_not_valid_after.replace(tzinfo=UTC)
|
330
|
+
|
331
|
+
cert_public_key = self._cert.public_key()
|
332
|
+
if not isinstance(
|
333
|
+
cert_public_key, rsa.RSAPublicKey | ec.EllipticCurvePublicKey
|
334
|
+
):
|
335
|
+
raise CertificateError(
|
336
|
+
f"Certificate's public key is of unsupported type: {type(cert_public_key)}. "
|
337
|
+
"Expected RSA or ECDSA public key."
|
338
|
+
)
|
339
|
+
|
340
|
+
self._base = CertificateBase(
|
341
|
+
subject=self._cert.subject,
|
342
|
+
issuer=self._cert.issuer,
|
343
|
+
public_key=cert_public_key,
|
344
|
+
not_valid_before=loaded_not_valid_before,
|
345
|
+
not_valid_after=loaded_not_valid_after,
|
346
|
+
serial_number=self._cert.serial_number,
|
347
|
+
)
|
348
|
+
logger.debug("📜🔑✅ Reconstructed CertificateBase from loaded cert")
|
349
|
+
|
350
|
+
except Exception as e:
|
351
|
+
logger.error(
|
352
|
+
f"📜❌ Certificate.__attrs_post_init__: Failed. Error: {type(e).__name__}: {e}",
|
353
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
354
|
+
)
|
355
|
+
raise CertificateError(
|
356
|
+
f"Failed to initialize certificate. Original error: {type(e).__name__}"
|
357
|
+
) from e
|
358
|
+
|
359
|
+
def _create_x509_certificate(
|
360
|
+
self,
|
361
|
+
issuer_name_override: "x509.Name | None" = None,
|
362
|
+
signing_key_override: "KeyPair | None" = None,
|
363
|
+
is_ca: bool = False,
|
364
|
+
is_client_cert: bool = False,
|
365
|
+
) -> "X509Certificate":
|
366
|
+
"""Internal helper to build and sign the X.509 certificate object."""
|
367
|
+
if not hasattr(self, "_base"):
|
368
|
+
raise CertificateError("Cannot create certificate without base information")
|
369
|
+
|
370
|
+
try:
|
371
|
+
logger.debug("📜📝🚀 _create_x509_certificate: Building certificate")
|
372
|
+
|
373
|
+
actual_issuer_name = (
|
374
|
+
issuer_name_override if issuer_name_override else self._base.issuer
|
375
|
+
)
|
376
|
+
actual_signing_key = (
|
377
|
+
signing_key_override if signing_key_override else self._private_key
|
378
|
+
)
|
379
|
+
|
380
|
+
if not actual_signing_key:
|
381
|
+
raise CertificateError(
|
382
|
+
"Cannot sign certificate without a signing key (either own or override)"
|
383
|
+
)
|
384
|
+
|
385
|
+
builder = (
|
386
|
+
x509.CertificateBuilder()
|
387
|
+
.subject_name(self._base.subject)
|
388
|
+
.issuer_name(actual_issuer_name)
|
389
|
+
.public_key(self._base.public_key)
|
390
|
+
.serial_number(self._base.serial_number)
|
391
|
+
.not_valid_before(self._base.not_valid_before)
|
392
|
+
.not_valid_after(self._base.not_valid_after)
|
393
|
+
)
|
394
|
+
|
395
|
+
san_list = [x509.DNSName(name) for name in (self.alt_names or []) if name]
|
396
|
+
if san_list:
|
397
|
+
builder = builder.add_extension(
|
398
|
+
x509.SubjectAlternativeName(san_list), critical=False
|
399
|
+
)
|
400
|
+
logger.debug(f"📜📝✅ Added SANs: {self.alt_names or []}")
|
401
|
+
|
402
|
+
builder = builder.add_extension(
|
403
|
+
x509.BasicConstraints(ca=is_ca, path_length=None),
|
404
|
+
critical=True,
|
405
|
+
)
|
406
|
+
|
407
|
+
if is_ca:
|
408
|
+
builder = builder.add_extension(
|
409
|
+
x509.KeyUsage(
|
410
|
+
digital_signature=False,
|
411
|
+
key_encipherment=False,
|
412
|
+
key_agreement=False,
|
413
|
+
content_commitment=False,
|
414
|
+
data_encipherment=False,
|
415
|
+
key_cert_sign=True,
|
416
|
+
crl_sign=True,
|
417
|
+
encipher_only=False,
|
418
|
+
decipher_only=False,
|
419
|
+
),
|
420
|
+
critical=True,
|
421
|
+
)
|
422
|
+
else:
|
423
|
+
builder = builder.add_extension(
|
424
|
+
x509.KeyUsage(
|
425
|
+
digital_signature=True,
|
426
|
+
key_encipherment=(
|
427
|
+
True
|
428
|
+
if not is_client_cert
|
429
|
+
and isinstance(self._base.public_key, rsa.RSAPublicKey)
|
430
|
+
else False
|
431
|
+
),
|
432
|
+
key_agreement=(
|
433
|
+
True
|
434
|
+
if isinstance(
|
435
|
+
self._base.public_key, ec.EllipticCurvePublicKey
|
436
|
+
)
|
437
|
+
else False
|
438
|
+
),
|
439
|
+
content_commitment=False,
|
440
|
+
data_encipherment=False,
|
441
|
+
key_cert_sign=False,
|
442
|
+
crl_sign=False,
|
443
|
+
encipher_only=False,
|
444
|
+
decipher_only=False,
|
445
|
+
),
|
446
|
+
critical=True,
|
447
|
+
)
|
448
|
+
extended_usages = []
|
449
|
+
if is_client_cert:
|
450
|
+
extended_usages.append(ExtendedKeyUsageOID.CLIENT_AUTH)
|
451
|
+
else:
|
452
|
+
extended_usages.append(ExtendedKeyUsageOID.SERVER_AUTH)
|
453
|
+
|
454
|
+
if extended_usages:
|
455
|
+
builder = builder.add_extension(
|
456
|
+
x509.ExtendedKeyUsage(extended_usages),
|
457
|
+
critical=False,
|
458
|
+
)
|
459
|
+
|
460
|
+
logger.debug(
|
461
|
+
f"📜📝✅ Added BasicConstraints (is_ca={is_ca}), "
|
462
|
+
f"KeyUsage, ExtendedKeyUsage (is_client_cert={is_client_cert})"
|
463
|
+
)
|
464
|
+
|
465
|
+
signed_cert = builder.sign(
|
466
|
+
private_key=actual_signing_key,
|
467
|
+
algorithm=hashes.SHA256(),
|
468
|
+
backend=default_backend(),
|
469
|
+
)
|
470
|
+
logger.debug("📜📝✅ Certificate signed successfully")
|
471
|
+
return signed_cert
|
472
|
+
|
473
|
+
except Exception as e:
|
474
|
+
logger.error(
|
475
|
+
f"📜❌ _create_x509_certificate: Failed: {e}",
|
476
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
477
|
+
)
|
478
|
+
raise CertificateError("Failed to create X.509 certificate object") from e
|
479
|
+
|
480
|
+
@staticmethod
|
481
|
+
def _load_from_uri_or_pem(data: str) -> str:
|
482
|
+
"""Load PEM data either directly from a string or from a file URI."""
|
483
|
+
try:
|
484
|
+
if data.startswith("file://"):
|
485
|
+
path_str = data.removeprefix("file://")
|
486
|
+
if os.name == "nt" and path_str.startswith("//"):
|
487
|
+
path = Path(path_str)
|
488
|
+
else:
|
489
|
+
path_str = path_str.lstrip("/")
|
490
|
+
if os.name != "nt" and data.startswith("file:///"):
|
491
|
+
path_str = "/" + path_str
|
492
|
+
path = Path(path_str)
|
493
|
+
|
494
|
+
logger.debug(f"📜📂🚀 Loading data from file: {path}")
|
495
|
+
with path.open("r", encoding="utf-8") as f:
|
496
|
+
loaded_data = f.read().strip()
|
497
|
+
logger.debug("📜📂✅ Loaded data from file")
|
498
|
+
return loaded_data
|
499
|
+
|
500
|
+
loaded_data = data.strip()
|
501
|
+
if not loaded_data.startswith("-----BEGIN"):
|
502
|
+
logger.warning("📜📂⚠️ Data doesn't look like PEM format")
|
503
|
+
return loaded_data
|
504
|
+
except Exception as e:
|
505
|
+
logger.error(f"📜📂❌ Failed to load data: {e}", extra={"error": str(e)})
|
506
|
+
raise CertificateError(f"Failed to load data: {e}") from e
|
507
|
+
|
508
|
+
# Properties
|
509
|
+
@property
|
510
|
+
def trust_chain(self) -> list["Certificate"]:
|
511
|
+
"""Returns the list of trusted certificates associated with this one."""
|
512
|
+
return self._trust_chain
|
513
|
+
|
514
|
+
@trust_chain.setter
|
515
|
+
def trust_chain(self, value: list["Certificate"]) -> None:
|
516
|
+
"""Sets the list of trusted certificates."""
|
517
|
+
self._trust_chain = value
|
518
|
+
|
519
|
+
@cached_property
|
520
|
+
def is_valid(self) -> bool:
|
521
|
+
"""Checks if the certificate is currently valid based on its dates."""
|
522
|
+
if not hasattr(self, "_base"):
|
523
|
+
return False
|
524
|
+
now = datetime.now(UTC)
|
525
|
+
valid = self._base.not_valid_before <= now <= self._base.not_valid_after
|
526
|
+
return valid
|
527
|
+
|
528
|
+
@property
|
529
|
+
def is_ca(self) -> bool:
|
530
|
+
"""Checks if the certificate has the Basic Constraints CA flag set to True."""
|
531
|
+
if not hasattr(self, "_cert"):
|
532
|
+
return False
|
533
|
+
try:
|
534
|
+
ext = self._cert.extensions.get_extension_for_oid(
|
535
|
+
x509.oid.ExtensionOID.BASIC_CONSTRAINTS
|
536
|
+
)
|
537
|
+
if isinstance(ext.value, x509.BasicConstraints):
|
538
|
+
return ext.value.ca
|
539
|
+
return False
|
540
|
+
except x509.ExtensionNotFound:
|
541
|
+
logger.debug("📜🔍⚠️ is_ca: Basic Constraints extension not found")
|
542
|
+
return False
|
543
|
+
|
544
|
+
@property
|
545
|
+
def subject(self) -> str:
|
546
|
+
"""Returns the certificate subject as an RFC4514 string."""
|
547
|
+
if not hasattr(self, "_base"):
|
548
|
+
return "SubjectNotInitialized"
|
549
|
+
return self._base.subject.rfc4514_string()
|
550
|
+
|
551
|
+
@property
|
552
|
+
def issuer(self) -> str:
|
553
|
+
"""Returns the certificate issuer as an RFC4514 string."""
|
554
|
+
if not hasattr(self, "_base"):
|
555
|
+
return "IssuerNotInitialized"
|
556
|
+
return self._base.issuer.rfc4514_string()
|
557
|
+
|
558
|
+
@property
|
559
|
+
def public_key(self) -> "PublicKey | None":
|
560
|
+
"""Returns the public key object from the certificate."""
|
561
|
+
if not hasattr(self, "_base"):
|
562
|
+
return None
|
563
|
+
return self._base.public_key
|
564
|
+
|
565
|
+
@property
|
566
|
+
def serial_number(self) -> int | None:
|
567
|
+
"""Returns the certificate serial number."""
|
568
|
+
if not hasattr(self, "_base"):
|
569
|
+
return None
|
570
|
+
return self._base.serial_number
|
571
|
+
|
572
|
+
@classmethod
|
573
|
+
def create_ca(
|
574
|
+
cls,
|
575
|
+
common_name: str,
|
576
|
+
organization_name: str,
|
577
|
+
validity_days: int,
|
578
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
579
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
580
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
581
|
+
) -> "Certificate":
|
582
|
+
"""Creates a new self-signed CA certificate."""
|
583
|
+
logger.info(
|
584
|
+
f"📜🔑🏭 Creating new CA certificate: CN={common_name}, Org={organization_name}"
|
585
|
+
)
|
586
|
+
ca_cert_obj = cls(
|
587
|
+
generate_keypair=True,
|
588
|
+
common_name=common_name,
|
589
|
+
organization_name=organization_name,
|
590
|
+
validity_days=validity_days,
|
591
|
+
key_type=key_type,
|
592
|
+
key_size=key_size,
|
593
|
+
ecdsa_curve=ecdsa_curve,
|
594
|
+
alt_names=[common_name],
|
595
|
+
)
|
596
|
+
# Re-sign to ensure CA flags are correctly set for a CA
|
597
|
+
logger.info("📜🔑🏭 Re-signing generated CA certificate to ensure is_ca=True")
|
598
|
+
actual_ca_x509_cert = ca_cert_obj._create_x509_certificate(
|
599
|
+
is_ca=True,
|
600
|
+
is_client_cert=False,
|
601
|
+
)
|
602
|
+
ca_cert_obj._cert = actual_ca_x509_cert
|
603
|
+
ca_cert_obj.cert = actual_ca_x509_cert.public_bytes(
|
604
|
+
serialization.Encoding.PEM
|
605
|
+
).decode("utf-8")
|
606
|
+
return ca_cert_obj
|
607
|
+
|
608
|
+
@classmethod
|
609
|
+
def create_signed_certificate(
|
610
|
+
cls,
|
611
|
+
ca_certificate: "Certificate",
|
612
|
+
common_name: str,
|
613
|
+
organization_name: str,
|
614
|
+
validity_days: int,
|
615
|
+
alt_names: list[str] | None = None,
|
616
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
617
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
618
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
619
|
+
is_client_cert: bool = False,
|
620
|
+
) -> "Certificate":
|
621
|
+
"""Creates a new certificate signed by the provided CA certificate."""
|
622
|
+
logger.info(
|
623
|
+
f"📜🔑🏭 Creating new certificate signed by CA '{ca_certificate.subject}': "
|
624
|
+
f"CN={common_name}, Org={organization_name}, ClientCert={is_client_cert}"
|
625
|
+
)
|
626
|
+
if not ca_certificate._private_key:
|
627
|
+
raise CertificateError(
|
628
|
+
message="CA certificate's private key is not available for signing.",
|
629
|
+
hint="Ensure the CA certificate object was loaded or created with its private key.",
|
630
|
+
)
|
631
|
+
if not ca_certificate.is_ca:
|
632
|
+
logger.warning(
|
633
|
+
f"📜🔑⚠️ Signing certificate (Subject: {ca_certificate.subject}) "
|
634
|
+
"is not marked as a CA. This might lead to validation issues."
|
635
|
+
)
|
636
|
+
|
637
|
+
new_cert_obj = cls(
|
638
|
+
generate_keypair=True,
|
639
|
+
common_name=common_name,
|
640
|
+
organization_name=organization_name,
|
641
|
+
validity_days=validity_days,
|
642
|
+
alt_names=alt_names or [common_name],
|
643
|
+
key_type=key_type,
|
644
|
+
key_size=key_size,
|
645
|
+
ecdsa_curve=ecdsa_curve,
|
646
|
+
)
|
647
|
+
|
648
|
+
signed_x509_cert = new_cert_obj._create_x509_certificate(
|
649
|
+
issuer_name_override=ca_certificate._base.subject,
|
650
|
+
signing_key_override=ca_certificate._private_key,
|
651
|
+
is_ca=False,
|
652
|
+
is_client_cert=is_client_cert,
|
653
|
+
)
|
654
|
+
|
655
|
+
new_cert_obj._cert = signed_x509_cert
|
656
|
+
new_cert_obj.cert = signed_x509_cert.public_bytes(
|
657
|
+
serialization.Encoding.PEM
|
658
|
+
).decode("utf-8")
|
659
|
+
|
660
|
+
logger.info(
|
661
|
+
f"📜🔑✅ Successfully created and signed certificate for "
|
662
|
+
f"CN={common_name} by CA='{ca_certificate.subject}'"
|
663
|
+
)
|
664
|
+
return new_cert_obj
|
665
|
+
|
666
|
+
@classmethod
|
667
|
+
def create_self_signed_server_cert(
|
668
|
+
cls,
|
669
|
+
common_name: str,
|
670
|
+
organization_name: str,
|
671
|
+
validity_days: int,
|
672
|
+
alt_names: list[str] | None = None,
|
673
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
674
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
675
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
676
|
+
) -> "Certificate":
|
677
|
+
"""Creates a new self-signed end-entity certificate suitable for a server."""
|
678
|
+
logger.info(
|
679
|
+
f"📜🔑🏭 Creating new self-signed SERVER certificate: "
|
680
|
+
f"CN={common_name}, Org={organization_name}"
|
681
|
+
)
|
682
|
+
|
683
|
+
cert_obj = cls(
|
684
|
+
generate_keypair=True,
|
685
|
+
common_name=common_name,
|
686
|
+
organization_name=organization_name,
|
687
|
+
validity_days=validity_days,
|
688
|
+
alt_names=alt_names or [common_name],
|
689
|
+
key_type=key_type,
|
690
|
+
key_size=key_size,
|
691
|
+
ecdsa_curve=ecdsa_curve,
|
692
|
+
)
|
693
|
+
|
694
|
+
if not cert_obj._private_key:
|
695
|
+
raise CertificateError(
|
696
|
+
"Private key not generated for self-signed server certificate"
|
697
|
+
)
|
698
|
+
|
699
|
+
actual_x509_cert = cert_obj._create_x509_certificate(
|
700
|
+
is_ca=False,
|
701
|
+
is_client_cert=False,
|
702
|
+
)
|
703
|
+
|
704
|
+
cert_obj._cert = actual_x509_cert
|
705
|
+
cert_obj.cert = actual_x509_cert.public_bytes(
|
706
|
+
serialization.Encoding.PEM
|
707
|
+
).decode("utf-8")
|
708
|
+
|
709
|
+
logger.info(
|
710
|
+
f"📜🔑✅ Successfully created self-signed SERVER certificate for CN={common_name}"
|
711
|
+
)
|
712
|
+
return cert_obj
|
713
|
+
|
714
|
+
def verify_trust(self, other_cert: Self) -> bool:
|
715
|
+
"""Verifies if the `other_cert` is trusted based on this certificate's trust chain."""
|
716
|
+
if other_cert is None:
|
717
|
+
raise CertificateError("Cannot verify trust: other_cert is None")
|
718
|
+
|
719
|
+
logger.debug(
|
720
|
+
f"📜🔍🚀 Verifying trust for cert S/N {other_cert.serial_number} "
|
721
|
+
f"against chain of S/N {self.serial_number}"
|
722
|
+
)
|
723
|
+
|
724
|
+
if not other_cert.is_valid:
|
725
|
+
logger.debug(
|
726
|
+
"📜🔍⚠️ Trust verification failed: Other certificate is not valid"
|
727
|
+
)
|
728
|
+
return False
|
729
|
+
if not other_cert.public_key:
|
730
|
+
raise CertificateError(
|
731
|
+
"Cannot verify trust: Other certificate has no public key"
|
732
|
+
)
|
733
|
+
|
734
|
+
if self == other_cert:
|
735
|
+
logger.debug(
|
736
|
+
"📜🔍✅ Trust verified: Certificates are identical (based on subject/serial)"
|
737
|
+
)
|
738
|
+
return True
|
739
|
+
|
740
|
+
if other_cert in self._trust_chain:
|
741
|
+
logger.debug(
|
742
|
+
"📜🔍✅ Trust verified: Other certificate found in trust chain"
|
743
|
+
)
|
744
|
+
return True
|
745
|
+
|
746
|
+
for trusted_cert in self._trust_chain:
|
747
|
+
logger.debug(
|
748
|
+
f"📜🔍🔁 Checking signature against trusted cert S/N {trusted_cert.serial_number}"
|
749
|
+
)
|
750
|
+
if self._validate_signature(
|
751
|
+
signed_cert=other_cert, signing_cert=trusted_cert
|
752
|
+
):
|
753
|
+
logger.debug(
|
754
|
+
f"📜🔍✅ Trust verified: Other cert signed by trusted cert S/N "
|
755
|
+
f"{trusted_cert.serial_number}"
|
756
|
+
)
|
757
|
+
return True
|
758
|
+
|
759
|
+
logger.debug(
|
760
|
+
"📜🔍❌ Trust verification failed: Other certificate not identical, "
|
761
|
+
"not in chain, and not signed by any cert in chain"
|
762
|
+
)
|
763
|
+
return False
|
764
|
+
|
765
|
+
def _validate_signature(
|
766
|
+
self, signed_cert: "Certificate", signing_cert: "Certificate"
|
767
|
+
) -> bool:
|
768
|
+
"""Internal helper: Validates signature and issuer/subject match."""
|
769
|
+
if not hasattr(signed_cert, "_cert") or not hasattr(signing_cert, "_cert"):
|
770
|
+
logger.error(
|
771
|
+
"📜🔍❌ Cannot validate signature: Certificate object(s) not initialized"
|
772
|
+
)
|
773
|
+
return False
|
774
|
+
|
775
|
+
if signed_cert._cert.issuer != signing_cert._cert.subject:
|
776
|
+
logger.debug(
|
777
|
+
f"📜🔍❌ Signature validation failed: Issuer/Subject mismatch. "
|
778
|
+
f"Signed Issuer='{signed_cert._cert.issuer}', "
|
779
|
+
f"Signing Subject='{signing_cert._cert.subject}'"
|
780
|
+
)
|
781
|
+
return False
|
782
|
+
|
783
|
+
try:
|
784
|
+
signing_public_key = signing_cert.public_key
|
785
|
+
if not signing_public_key:
|
786
|
+
logger.error(
|
787
|
+
"📜🔍❌ Cannot validate signature: Signing certificate has no public key"
|
788
|
+
)
|
789
|
+
return False
|
790
|
+
|
791
|
+
signature = signed_cert._cert.signature
|
792
|
+
tbs_certificate_bytes = signed_cert._cert.tbs_certificate_bytes
|
793
|
+
signature_hash_algorithm = signed_cert._cert.signature_hash_algorithm
|
794
|
+
|
795
|
+
if not signature_hash_algorithm:
|
796
|
+
logger.error("📜🔍❌ Cannot validate signature: Unknown hash algorithm")
|
797
|
+
return False
|
798
|
+
|
799
|
+
if isinstance(signing_public_key, rsa.RSAPublicKey):
|
800
|
+
cast(rsa.RSAPublicKey, signing_public_key).verify(
|
801
|
+
signature,
|
802
|
+
tbs_certificate_bytes,
|
803
|
+
padding.PKCS1v15(),
|
804
|
+
signature_hash_algorithm,
|
805
|
+
)
|
806
|
+
elif isinstance(signing_public_key, ec.EllipticCurvePublicKey):
|
807
|
+
cast(ec.EllipticCurvePublicKey, signing_public_key).verify(
|
808
|
+
signature,
|
809
|
+
tbs_certificate_bytes,
|
810
|
+
ec.ECDSA(signature_hash_algorithm),
|
811
|
+
)
|
812
|
+
else:
|
813
|
+
logger.error(
|
814
|
+
f"📜🔍❌ Unsupported signing public key type: {type(signing_public_key)}"
|
815
|
+
)
|
816
|
+
return False
|
817
|
+
|
818
|
+
return True
|
819
|
+
|
820
|
+
except Exception as e:
|
821
|
+
logger.debug(f"📜🔍❌ Signature validation failed: {type(e).__name__}: {e}")
|
822
|
+
return False
|
823
|
+
|
824
|
+
def __eq__(self, other: object) -> bool:
|
825
|
+
"""Custom equality based on subject and serial number."""
|
826
|
+
if not isinstance(other, Certificate):
|
827
|
+
return NotImplemented
|
828
|
+
if not hasattr(self, "_base") or not hasattr(other, "_base"):
|
829
|
+
return False
|
830
|
+
eq = (
|
831
|
+
self._base.subject == other._base.subject
|
832
|
+
and self._base.serial_number == other._base.serial_number
|
833
|
+
)
|
834
|
+
return eq
|
835
|
+
|
836
|
+
def __hash__(self) -> int:
|
837
|
+
"""Custom hash based on subject and serial number."""
|
838
|
+
if not hasattr(self, "_base"):
|
839
|
+
logger.warning("📜🔍⚠️ __hash__ called before _base initialized")
|
840
|
+
return hash((None, None))
|
841
|
+
|
842
|
+
h = hash((self._base.subject, self._base.serial_number))
|
843
|
+
return h
|
844
|
+
|
845
|
+
def __repr__(self) -> str:
|
846
|
+
try:
|
847
|
+
subject_str = self.subject
|
848
|
+
issuer_str = self.issuer
|
849
|
+
valid_str = str(self.is_valid)
|
850
|
+
ca_str = str(self.is_ca)
|
851
|
+
except AttributeError:
|
852
|
+
subject_str = "PartiallyInitialized"
|
853
|
+
issuer_str = "PartiallyInitialized"
|
854
|
+
valid_str = "Unknown"
|
855
|
+
ca_str = "Unknown"
|
856
|
+
|
857
|
+
return (
|
858
|
+
f"Certificate(subject='{subject_str}', issuer='{issuer_str}', "
|
859
|
+
f"common_name='{self.common_name}', valid={valid_str}, ca={ca_str}, "
|
860
|
+
f"key_type='{self.key_type}')"
|
861
|
+
)
|
862
|
+
|
863
|
+
|
864
|
+
# Convenience functions for common use cases
|
865
|
+
def create_self_signed(
|
866
|
+
common_name: str = "localhost",
|
867
|
+
alt_names: list[str] | None = None,
|
868
|
+
organization: str = "Default Organization",
|
869
|
+
validity_days: int = DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
870
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
871
|
+
) -> "Certificate":
|
872
|
+
"""Create a self-signed certificate (convenience function)."""
|
873
|
+
_require_crypto()
|
874
|
+
return Certificate.create_self_signed_server_cert(
|
875
|
+
common_name=common_name,
|
876
|
+
organization_name=organization,
|
877
|
+
validity_days=validity_days,
|
878
|
+
alt_names=alt_names or [common_name],
|
879
|
+
key_type=key_type,
|
880
|
+
)
|
881
|
+
|
882
|
+
|
883
|
+
def create_ca(
|
884
|
+
common_name: str,
|
885
|
+
organization: str = "Default CA Organization",
|
886
|
+
validity_days: int = DEFAULT_CERTIFICATE_VALIDITY_DAYS * 2, # CAs live longer
|
887
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
888
|
+
) -> "Certificate":
|
889
|
+
"""Create a CA certificate (convenience function)."""
|
890
|
+
_require_crypto()
|
891
|
+
return Certificate.create_ca(
|
892
|
+
common_name=common_name,
|
893
|
+
organization_name=organization,
|
894
|
+
validity_days=validity_days,
|
895
|
+
key_type=key_type,
|
896
|
+
)
|