provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev2__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/foundation/__init__.py +41 -23
- provide/foundation/archive/__init__.py +23 -0
- provide/foundation/archive/base.py +70 -0
- provide/foundation/archive/bzip2.py +157 -0
- provide/foundation/archive/gzip.py +159 -0
- provide/foundation/archive/operations.py +334 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +13 -7
- provide/foundation/cli/commands/logs/__init__.py +1 -1
- provide/foundation/cli/commands/logs/query.py +1 -1
- provide/foundation/cli/commands/logs/send.py +1 -1
- provide/foundation/cli/commands/logs/tail.py +1 -1
- provide/foundation/cli/decorators.py +11 -10
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -35
- provide/foundation/cli/utils.py +21 -17
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +479 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +4 -19
- provide/foundation/config/loader.py +9 -3
- provide/foundation/config/sync.py +19 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +35 -13
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +85 -109
- provide/foundation/core.py +1 -2
- provide/foundation/crypto/__init__.py +2 -0
- provide/foundation/crypto/certificates/__init__.py +34 -0
- provide/foundation/crypto/certificates/base.py +173 -0
- provide/foundation/crypto/certificates/certificate.py +290 -0
- provide/foundation/crypto/certificates/factory.py +213 -0
- provide/foundation/crypto/certificates/generator.py +138 -0
- provide/foundation/crypto/certificates/loader.py +130 -0
- provide/foundation/crypto/certificates/operations.py +198 -0
- provide/foundation/crypto/certificates/trust.py +107 -0
- provide/foundation/errors/__init__.py +2 -3
- provide/foundation/errors/decorators.py +0 -231
- provide/foundation/errors/types.py +0 -97
- provide/foundation/eventsets/__init__.py +0 -0
- provide/foundation/eventsets/display.py +84 -0
- provide/foundation/eventsets/registry.py +160 -0
- provide/foundation/eventsets/resolver.py +192 -0
- provide/foundation/eventsets/sets/das.py +128 -0
- provide/foundation/eventsets/sets/database.py +125 -0
- provide/foundation/eventsets/sets/http.py +153 -0
- provide/foundation/eventsets/sets/llm.py +139 -0
- provide/foundation/eventsets/sets/task_queue.py +107 -0
- provide/foundation/eventsets/types.py +70 -0
- provide/foundation/file/directory.py +13 -22
- provide/foundation/file/lock.py +3 -1
- provide/foundation/hub/components.py +77 -515
- provide/foundation/hub/config.py +151 -0
- provide/foundation/hub/discovery.py +62 -0
- provide/foundation/hub/handlers.py +81 -0
- provide/foundation/hub/lifecycle.py +194 -0
- provide/foundation/hub/manager.py +4 -4
- provide/foundation/hub/processors.py +44 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
- provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +68 -298
- provide/foundation/logger/config/telemetry.py +41 -121
- provide/foundation/logger/core.py +0 -2
- provide/foundation/logger/custom_processors.py +1 -0
- provide/foundation/logger/factories.py +11 -2
- provide/foundation/logger/processors/main.py +20 -84
- provide/foundation/logger/setup/__init__.py +5 -1
- provide/foundation/logger/setup/coordinator.py +76 -24
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/observability/__init__.py +2 -2
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +47 -0
- provide/foundation/process/lifecycle.py +115 -59
- provide/foundation/resilience/__init__.py +35 -0
- provide/foundation/resilience/circuit.py +164 -0
- provide/foundation/resilience/decorators.py +220 -0
- provide/foundation/resilience/fallback.py +193 -0
- provide/foundation/resilience/retry.py +325 -0
- provide/foundation/streams/config.py +79 -0
- provide/foundation/streams/console.py +7 -8
- provide/foundation/streams/core.py +6 -3
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +84 -2
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/cli.py +30 -17
- provide/foundation/testing/common/__init__.py +32 -0
- provide/foundation/testing/common/fixtures.py +236 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/content_fixtures.py +316 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +52 -0
- provide/foundation/testing/file/special_fixtures.py +153 -0
- provide/foundation/testing/logger.py +117 -11
- provide/foundation/testing/mocking/__init__.py +46 -0
- provide/foundation/testing/mocking/fixtures.py +331 -0
- provide/foundation/testing/process/__init__.py +48 -0
- provide/foundation/testing/process/async_fixtures.py +405 -0
- provide/foundation/testing/process/fixtures.py +56 -0
- provide/foundation/testing/process/subprocess_fixtures.py +209 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/basic_fixtures.py +101 -0
- provide/foundation/testing/threading/data_fixtures.py +99 -0
- provide/foundation/testing/threading/execution_fixtures.py +263 -0
- provide/foundation/testing/threading/fixtures.py +54 -0
- provide/foundation/testing/threading/sync_fixtures.py +97 -0
- provide/foundation/testing/time/__init__.py +32 -0
- provide/foundation/testing/time/fixtures.py +409 -0
- provide/foundation/testing/transport/__init__.py +30 -0
- provide/foundation/testing/transport/fixtures.py +280 -0
- provide/foundation/tools/__init__.py +58 -0
- provide/foundation/tools/base.py +348 -0
- provide/foundation/tools/cache.py +268 -0
- provide/foundation/tools/downloader.py +224 -0
- provide/foundation/tools/installer.py +254 -0
- provide/foundation/tools/registry.py +223 -0
- provide/foundation/tools/resolver.py +321 -0
- provide/foundation/tools/verifier.py +186 -0
- provide/foundation/tracer/otel.py +7 -11
- provide/foundation/tracer/spans.py +2 -2
- provide/foundation/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +140 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +360 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- provide/foundation/utils/deps.py +14 -12
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
- provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
- provide/foundation/cli/commands/logs/generate_old.py +0 -569
- provide/foundation/crypto/certificates.py +0 -896
- provide/foundation/logger/emoji/__init__.py +0 -44
- provide/foundation/logger/emoji/matrix.py +0 -209
- provide/foundation/logger/emoji/sets.py +0 -458
- provide/foundation/logger/emoji/types.py +0 -56
- provide/foundation/logger/setup/emoji_resolver.py +0 -64
- provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,290 @@
|
|
1
|
+
"""Main Certificate class."""
|
2
|
+
|
3
|
+
from datetime import UTC, datetime
|
4
|
+
from functools import cached_property
|
5
|
+
from typing import Self
|
6
|
+
|
7
|
+
from attrs import Factory, define, field
|
8
|
+
|
9
|
+
try:
|
10
|
+
from cryptography import x509
|
11
|
+
from cryptography.hazmat.primitives import serialization
|
12
|
+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
13
|
+
from cryptography.x509 import Certificate as X509Certificate
|
14
|
+
|
15
|
+
_HAS_CRYPTO = True
|
16
|
+
except ImportError:
|
17
|
+
x509 = None
|
18
|
+
serialization = None
|
19
|
+
ec = None
|
20
|
+
rsa = None
|
21
|
+
X509Certificate = None
|
22
|
+
_HAS_CRYPTO = False
|
23
|
+
|
24
|
+
from provide.foundation import logger
|
25
|
+
from provide.foundation.crypto.certificates.base import CertificateBase, CertificateError, PublicKey
|
26
|
+
from provide.foundation.crypto.certificates.generator import generate_certificate
|
27
|
+
from provide.foundation.crypto.certificates.loader import load_certificate_from_pem
|
28
|
+
from provide.foundation.crypto.certificates.operations import create_x509_certificate
|
29
|
+
from provide.foundation.crypto.certificates.trust import verify_trust as verify_trust_impl
|
30
|
+
from provide.foundation.crypto.constants import (
|
31
|
+
DEFAULT_CERTIFICATE_CURVE,
|
32
|
+
DEFAULT_CERTIFICATE_KEY_TYPE,
|
33
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
34
|
+
DEFAULT_RSA_KEY_SIZE,
|
35
|
+
)
|
36
|
+
|
37
|
+
|
38
|
+
@define(slots=True, eq=False, hash=False, repr=False)
|
39
|
+
class Certificate:
|
40
|
+
"""X.509 certificate management using attrs."""
|
41
|
+
|
42
|
+
cert_pem_or_uri: str | None = field(default=None, kw_only=True)
|
43
|
+
key_pem_or_uri: str | None = field(default=None, kw_only=True)
|
44
|
+
generate_keypair: bool = field(default=False, kw_only=True)
|
45
|
+
key_type: str = field(default=DEFAULT_CERTIFICATE_KEY_TYPE, kw_only=True)
|
46
|
+
key_size: int = field(default=DEFAULT_RSA_KEY_SIZE, kw_only=True)
|
47
|
+
ecdsa_curve: str = field(default=DEFAULT_CERTIFICATE_CURVE, kw_only=True)
|
48
|
+
common_name: str = field(default="localhost", kw_only=True)
|
49
|
+
alt_names: list[str] | None = field(
|
50
|
+
default=Factory(lambda: ["localhost"]), kw_only=True
|
51
|
+
)
|
52
|
+
organization_name: str = field(default="Default Organization", kw_only=True)
|
53
|
+
validity_days: int = field(default=DEFAULT_CERTIFICATE_VALIDITY_DAYS, kw_only=True)
|
54
|
+
|
55
|
+
_base: CertificateBase = field(init=False, repr=False)
|
56
|
+
_private_key: "KeyPair | None" = field(init=False, default=None, repr=False)
|
57
|
+
_cert: "X509Certificate" = field(init=False, repr=False)
|
58
|
+
_trust_chain: list["Certificate"] = field(init=False, factory=list, repr=False)
|
59
|
+
|
60
|
+
cert: str = field(init=False, default="", repr=True)
|
61
|
+
key: str | None = field(init=False, default=None, repr=False)
|
62
|
+
|
63
|
+
def __attrs_post_init__(self) -> None:
|
64
|
+
"""Handle loading or generation logic after attrs initialization."""
|
65
|
+
if self.generate_keypair:
|
66
|
+
# Generate new certificate
|
67
|
+
base, x509_cert, private_key, cert_pem, key_pem = generate_certificate(
|
68
|
+
common_name=self.common_name,
|
69
|
+
organization_name=self.organization_name,
|
70
|
+
validity_days=self.validity_days,
|
71
|
+
key_type=self.key_type,
|
72
|
+
key_size=self.key_size,
|
73
|
+
ecdsa_curve=self.ecdsa_curve,
|
74
|
+
alt_names=self.alt_names,
|
75
|
+
is_ca=False,
|
76
|
+
is_client_cert=True,
|
77
|
+
)
|
78
|
+
self._base = base
|
79
|
+
self._cert = x509_cert
|
80
|
+
self._private_key = private_key
|
81
|
+
self.cert = cert_pem
|
82
|
+
self.key = key_pem
|
83
|
+
else:
|
84
|
+
# Load existing certificate
|
85
|
+
if not self.cert_pem_or_uri:
|
86
|
+
raise CertificateError(
|
87
|
+
"cert_pem_or_uri required when not generating"
|
88
|
+
)
|
89
|
+
|
90
|
+
base, x509_cert, private_key, cert_pem, key_pem = load_certificate_from_pem(
|
91
|
+
self.cert_pem_or_uri,
|
92
|
+
self.key_pem_or_uri
|
93
|
+
)
|
94
|
+
self._base = base
|
95
|
+
self._cert = x509_cert
|
96
|
+
self._private_key = private_key
|
97
|
+
self.cert = cert_pem
|
98
|
+
self.key = key_pem
|
99
|
+
|
100
|
+
# Properties
|
101
|
+
@property
|
102
|
+
def trust_chain(self) -> list["Certificate"]:
|
103
|
+
"""Returns the list of trusted certificates associated with this one."""
|
104
|
+
return self._trust_chain
|
105
|
+
|
106
|
+
@trust_chain.setter
|
107
|
+
def trust_chain(self, value: list["Certificate"]) -> None:
|
108
|
+
"""Sets the list of trusted certificates."""
|
109
|
+
self._trust_chain = value
|
110
|
+
|
111
|
+
@cached_property
|
112
|
+
def is_valid(self) -> bool:
|
113
|
+
"""Checks if the certificate is currently valid based on its dates."""
|
114
|
+
if not hasattr(self, "_base"):
|
115
|
+
return False
|
116
|
+
now = datetime.now(UTC)
|
117
|
+
valid = self._base.not_valid_before <= now <= self._base.not_valid_after
|
118
|
+
return valid
|
119
|
+
|
120
|
+
@property
|
121
|
+
def is_ca(self) -> bool:
|
122
|
+
"""Checks if the certificate has the Basic Constraints CA flag set to True."""
|
123
|
+
if not hasattr(self, "_cert"):
|
124
|
+
return False
|
125
|
+
try:
|
126
|
+
ext = self._cert.extensions.get_extension_for_oid(
|
127
|
+
x509.oid.ExtensionOID.BASIC_CONSTRAINTS
|
128
|
+
)
|
129
|
+
if isinstance(ext.value, x509.BasicConstraints):
|
130
|
+
return ext.value.ca
|
131
|
+
return False
|
132
|
+
except x509.ExtensionNotFound:
|
133
|
+
logger.debug("📜🔍⚠️ is_ca: Basic Constraints extension not found")
|
134
|
+
return False
|
135
|
+
|
136
|
+
@property
|
137
|
+
def subject(self) -> str:
|
138
|
+
"""Returns the certificate subject as an RFC4514 string."""
|
139
|
+
if not hasattr(self, "_base"):
|
140
|
+
return "SubjectNotInitialized"
|
141
|
+
return self._base.subject.rfc4514_string()
|
142
|
+
|
143
|
+
@property
|
144
|
+
def issuer(self) -> str:
|
145
|
+
"""Returns the certificate issuer as an RFC4514 string."""
|
146
|
+
if not hasattr(self, "_base"):
|
147
|
+
return "IssuerNotInitialized"
|
148
|
+
return self._base.issuer.rfc4514_string()
|
149
|
+
|
150
|
+
@property
|
151
|
+
def public_key(self) -> "PublicKey | None":
|
152
|
+
"""Returns the public key object from the certificate."""
|
153
|
+
if not hasattr(self, "_base"):
|
154
|
+
return None
|
155
|
+
return self._base.public_key
|
156
|
+
|
157
|
+
@property
|
158
|
+
def serial_number(self) -> int | None:
|
159
|
+
"""Returns the certificate serial number."""
|
160
|
+
if not hasattr(self, "_base"):
|
161
|
+
return None
|
162
|
+
return self._base.serial_number
|
163
|
+
|
164
|
+
# Factory methods - moved to factory.py but kept as classmethods for compatibility
|
165
|
+
@classmethod
|
166
|
+
def create_ca(
|
167
|
+
cls,
|
168
|
+
common_name: str,
|
169
|
+
organization_name: str,
|
170
|
+
validity_days: int,
|
171
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
172
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
173
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
174
|
+
) -> "Certificate":
|
175
|
+
"""Creates a new self-signed CA certificate."""
|
176
|
+
from provide.foundation.crypto.certificates.factory import create_ca_certificate
|
177
|
+
return create_ca_certificate(
|
178
|
+
common_name, organization_name, validity_days,
|
179
|
+
key_type, key_size, ecdsa_curve
|
180
|
+
)
|
181
|
+
|
182
|
+
@classmethod
|
183
|
+
def create_signed_certificate(
|
184
|
+
cls,
|
185
|
+
ca_certificate: "Certificate",
|
186
|
+
common_name: str,
|
187
|
+
organization_name: str,
|
188
|
+
validity_days: int,
|
189
|
+
alt_names: list[str] | None = None,
|
190
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
191
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
192
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
193
|
+
is_client_cert: bool = False,
|
194
|
+
) -> "Certificate":
|
195
|
+
"""Creates a new certificate signed by the provided CA certificate."""
|
196
|
+
from provide.foundation.crypto.certificates.factory import create_signed_certificate
|
197
|
+
return create_signed_certificate(
|
198
|
+
ca_certificate, common_name, organization_name, validity_days,
|
199
|
+
alt_names, key_type, key_size, ecdsa_curve, is_client_cert
|
200
|
+
)
|
201
|
+
|
202
|
+
@classmethod
|
203
|
+
def create_self_signed_server_cert(
|
204
|
+
cls,
|
205
|
+
common_name: str,
|
206
|
+
organization_name: str,
|
207
|
+
validity_days: int,
|
208
|
+
alt_names: list[str] | None = None,
|
209
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
210
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
211
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
212
|
+
) -> "Certificate":
|
213
|
+
"""Creates a new self-signed end-entity certificate suitable for a server."""
|
214
|
+
from provide.foundation.crypto.certificates.factory import create_self_signed_server_cert
|
215
|
+
return create_self_signed_server_cert(
|
216
|
+
common_name, organization_name, validity_days,
|
217
|
+
alt_names, key_type, key_size, ecdsa_curve
|
218
|
+
)
|
219
|
+
|
220
|
+
def verify_trust(self, other_cert: Self) -> bool:
|
221
|
+
"""Verifies if the `other_cert` is trusted based on this certificate's trust chain."""
|
222
|
+
return verify_trust_impl(self, other_cert, self._trust_chain)
|
223
|
+
|
224
|
+
def _create_x509_certificate(
|
225
|
+
self,
|
226
|
+
issuer_name_override: "x509.Name | None" = None,
|
227
|
+
signing_key_override: "KeyPair | None" = None,
|
228
|
+
is_ca: bool = False,
|
229
|
+
is_client_cert: bool = False,
|
230
|
+
) -> "X509Certificate":
|
231
|
+
"""Internal helper to build and sign the X.509 certificate object."""
|
232
|
+
return create_x509_certificate(
|
233
|
+
base=self._base,
|
234
|
+
private_key=self._private_key,
|
235
|
+
alt_names=self.alt_names,
|
236
|
+
issuer_name_override=issuer_name_override,
|
237
|
+
signing_key_override=signing_key_override,
|
238
|
+
is_ca=is_ca,
|
239
|
+
is_client_cert=is_client_cert,
|
240
|
+
)
|
241
|
+
|
242
|
+
def _validate_signature(
|
243
|
+
self, signed_cert: "Certificate", signing_cert: "Certificate"
|
244
|
+
) -> bool:
|
245
|
+
"""Internal helper: Validates signature and issuer/subject match."""
|
246
|
+
from provide.foundation.crypto.certificates.trust import validate_signature_wrapper
|
247
|
+
return validate_signature_wrapper(signed_cert, signing_cert)
|
248
|
+
|
249
|
+
def __eq__(self, other: object) -> bool:
|
250
|
+
"""Custom equality based on subject and serial number."""
|
251
|
+
if not isinstance(other, Certificate):
|
252
|
+
return NotImplemented
|
253
|
+
if not hasattr(self, "_base") or not hasattr(other, "_base"):
|
254
|
+
return False
|
255
|
+
eq = (
|
256
|
+
self._base.subject == other._base.subject
|
257
|
+
and self._base.serial_number == other._base.serial_number
|
258
|
+
)
|
259
|
+
return eq
|
260
|
+
|
261
|
+
def __hash__(self) -> int:
|
262
|
+
"""Custom hash based on subject and serial number."""
|
263
|
+
if not hasattr(self, "_base"):
|
264
|
+
logger.warning("📜🔍⚠️ __hash__ called before _base initialized")
|
265
|
+
return hash((None, None))
|
266
|
+
|
267
|
+
h = hash((self._base.subject, self._base.serial_number))
|
268
|
+
return h
|
269
|
+
|
270
|
+
def __repr__(self) -> str:
|
271
|
+
try:
|
272
|
+
subject_str = self.subject
|
273
|
+
issuer_str = self.issuer
|
274
|
+
valid_str = str(self.is_valid)
|
275
|
+
ca_str = str(self.is_ca)
|
276
|
+
except AttributeError:
|
277
|
+
subject_str = "PartiallyInitialized"
|
278
|
+
issuer_str = "PartiallyInitialized"
|
279
|
+
valid_str = "Unknown"
|
280
|
+
ca_str = "Unknown"
|
281
|
+
|
282
|
+
return (
|
283
|
+
f"Certificate(subject='{subject_str}', issuer='{issuer_str}', "
|
284
|
+
f"common_name='{self.common_name}', valid={valid_str}, ca={ca_str}, "
|
285
|
+
f"key_type='{self.key_type}')"
|
286
|
+
)
|
287
|
+
|
288
|
+
|
289
|
+
# Type alias for backwards compatibility
|
290
|
+
KeyPair = rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey | None
|
@@ -0,0 +1,213 @@
|
|
1
|
+
"""Certificate factory methods."""
|
2
|
+
|
3
|
+
try:
|
4
|
+
from cryptography import x509
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
6
|
+
|
7
|
+
_HAS_CRYPTO = True
|
8
|
+
except ImportError:
|
9
|
+
x509 = None
|
10
|
+
serialization = None
|
11
|
+
_HAS_CRYPTO = False
|
12
|
+
|
13
|
+
from provide.foundation import logger
|
14
|
+
from provide.foundation.crypto.certificates.base import CertificateError, _require_crypto
|
15
|
+
from provide.foundation.crypto.certificates.operations import create_x509_certificate
|
16
|
+
from provide.foundation.crypto.constants import (
|
17
|
+
DEFAULT_CERTIFICATE_CURVE,
|
18
|
+
DEFAULT_CERTIFICATE_KEY_TYPE,
|
19
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
20
|
+
DEFAULT_RSA_KEY_SIZE,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
def create_ca_certificate(
|
25
|
+
common_name: str,
|
26
|
+
organization_name: str,
|
27
|
+
validity_days: int,
|
28
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
29
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
30
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
31
|
+
) -> "Certificate":
|
32
|
+
"""Creates a new self-signed CA certificate."""
|
33
|
+
# Import here to avoid circular dependency
|
34
|
+
from provide.foundation.crypto.certificates.certificate import Certificate
|
35
|
+
|
36
|
+
logger.info(
|
37
|
+
f"📜🔑🏭 Creating new CA certificate: CN={common_name}, Org={organization_name}"
|
38
|
+
)
|
39
|
+
ca_cert_obj = Certificate(
|
40
|
+
generate_keypair=True,
|
41
|
+
common_name=common_name,
|
42
|
+
organization_name=organization_name,
|
43
|
+
validity_days=validity_days,
|
44
|
+
key_type=key_type,
|
45
|
+
key_size=key_size,
|
46
|
+
ecdsa_curve=ecdsa_curve,
|
47
|
+
alt_names=[common_name],
|
48
|
+
)
|
49
|
+
# Re-sign to ensure CA flags are correctly set for a CA
|
50
|
+
logger.info("📜🔑🏭 Re-signing generated CA certificate to ensure is_ca=True")
|
51
|
+
actual_ca_x509_cert = create_x509_certificate(
|
52
|
+
base=ca_cert_obj._base,
|
53
|
+
private_key=ca_cert_obj._private_key,
|
54
|
+
alt_names=ca_cert_obj.alt_names,
|
55
|
+
is_ca=True,
|
56
|
+
is_client_cert=False,
|
57
|
+
)
|
58
|
+
ca_cert_obj._cert = actual_ca_x509_cert
|
59
|
+
ca_cert_obj.cert = actual_ca_x509_cert.public_bytes(
|
60
|
+
serialization.Encoding.PEM
|
61
|
+
).decode("utf-8")
|
62
|
+
return ca_cert_obj
|
63
|
+
|
64
|
+
|
65
|
+
def create_signed_certificate(
|
66
|
+
ca_certificate: "Certificate",
|
67
|
+
common_name: str,
|
68
|
+
organization_name: str,
|
69
|
+
validity_days: int,
|
70
|
+
alt_names: list[str] | None = None,
|
71
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
72
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
73
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
74
|
+
is_client_cert: bool = False,
|
75
|
+
) -> "Certificate":
|
76
|
+
"""Creates a new certificate signed by the provided CA certificate."""
|
77
|
+
# Import here to avoid circular dependency
|
78
|
+
from provide.foundation.crypto.certificates.certificate import Certificate
|
79
|
+
|
80
|
+
logger.info(
|
81
|
+
f"📜🔑🏭 Creating new certificate signed by CA '{ca_certificate.subject}': "
|
82
|
+
f"CN={common_name}, Org={organization_name}, ClientCert={is_client_cert}"
|
83
|
+
)
|
84
|
+
if not ca_certificate._private_key:
|
85
|
+
raise CertificateError(
|
86
|
+
message="CA certificate's private key is not available for signing.",
|
87
|
+
hint="Ensure the CA certificate object was loaded or created with its private key.",
|
88
|
+
)
|
89
|
+
if not ca_certificate.is_ca:
|
90
|
+
logger.warning(
|
91
|
+
f"📜🔑⚠️ Signing certificate (Subject: {ca_certificate.subject}) "
|
92
|
+
"is not marked as a CA. This might lead to validation issues."
|
93
|
+
)
|
94
|
+
|
95
|
+
new_cert_obj = Certificate(
|
96
|
+
generate_keypair=True,
|
97
|
+
common_name=common_name,
|
98
|
+
organization_name=organization_name,
|
99
|
+
validity_days=validity_days,
|
100
|
+
alt_names=alt_names or [common_name],
|
101
|
+
key_type=key_type,
|
102
|
+
key_size=key_size,
|
103
|
+
ecdsa_curve=ecdsa_curve,
|
104
|
+
)
|
105
|
+
|
106
|
+
signed_x509_cert = create_x509_certificate(
|
107
|
+
base=new_cert_obj._base,
|
108
|
+
private_key=new_cert_obj._private_key,
|
109
|
+
alt_names=new_cert_obj.alt_names,
|
110
|
+
issuer_name_override=ca_certificate._base.subject,
|
111
|
+
signing_key_override=ca_certificate._private_key,
|
112
|
+
is_ca=False,
|
113
|
+
is_client_cert=is_client_cert,
|
114
|
+
)
|
115
|
+
|
116
|
+
new_cert_obj._cert = signed_x509_cert
|
117
|
+
new_cert_obj.cert = signed_x509_cert.public_bytes(
|
118
|
+
serialization.Encoding.PEM
|
119
|
+
).decode("utf-8")
|
120
|
+
|
121
|
+
logger.info(
|
122
|
+
f"📜🔑✅ Successfully created and signed certificate for "
|
123
|
+
f"CN={common_name} by CA='{ca_certificate.subject}'"
|
124
|
+
)
|
125
|
+
return new_cert_obj
|
126
|
+
|
127
|
+
|
128
|
+
def create_self_signed_server_cert(
|
129
|
+
common_name: str,
|
130
|
+
organization_name: str,
|
131
|
+
validity_days: int,
|
132
|
+
alt_names: list[str] | None = None,
|
133
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
134
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
135
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
136
|
+
) -> "Certificate":
|
137
|
+
"""Creates a new self-signed end-entity certificate suitable for a server."""
|
138
|
+
# Import here to avoid circular dependency
|
139
|
+
from provide.foundation.crypto.certificates.certificate import Certificate
|
140
|
+
|
141
|
+
logger.info(
|
142
|
+
f"📜🔑🏭 Creating new self-signed SERVER certificate: "
|
143
|
+
f"CN={common_name}, Org={organization_name}"
|
144
|
+
)
|
145
|
+
|
146
|
+
cert_obj = Certificate(
|
147
|
+
generate_keypair=True,
|
148
|
+
common_name=common_name,
|
149
|
+
organization_name=organization_name,
|
150
|
+
validity_days=validity_days,
|
151
|
+
alt_names=alt_names or [common_name],
|
152
|
+
key_type=key_type,
|
153
|
+
key_size=key_size,
|
154
|
+
ecdsa_curve=ecdsa_curve,
|
155
|
+
)
|
156
|
+
|
157
|
+
if not cert_obj._private_key:
|
158
|
+
raise CertificateError(
|
159
|
+
"Private key not generated for self-signed server certificate"
|
160
|
+
)
|
161
|
+
|
162
|
+
actual_x509_cert = create_x509_certificate(
|
163
|
+
base=cert_obj._base,
|
164
|
+
private_key=cert_obj._private_key,
|
165
|
+
alt_names=cert_obj.alt_names,
|
166
|
+
is_ca=False,
|
167
|
+
is_client_cert=False,
|
168
|
+
)
|
169
|
+
|
170
|
+
cert_obj._cert = actual_x509_cert
|
171
|
+
cert_obj.cert = actual_x509_cert.public_bytes(
|
172
|
+
serialization.Encoding.PEM
|
173
|
+
).decode("utf-8")
|
174
|
+
|
175
|
+
logger.info(
|
176
|
+
f"📜🔑✅ Successfully created self-signed SERVER certificate for CN={common_name}"
|
177
|
+
)
|
178
|
+
return cert_obj
|
179
|
+
|
180
|
+
|
181
|
+
# Convenience functions for common use cases
|
182
|
+
def create_self_signed(
|
183
|
+
common_name: str = "localhost",
|
184
|
+
alt_names: list[str] | None = None,
|
185
|
+
organization: str = "Default Organization",
|
186
|
+
validity_days: int = DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
187
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
188
|
+
) -> "Certificate":
|
189
|
+
"""Create a self-signed certificate (convenience function)."""
|
190
|
+
_require_crypto()
|
191
|
+
return create_self_signed_server_cert(
|
192
|
+
common_name=common_name,
|
193
|
+
organization_name=organization,
|
194
|
+
validity_days=validity_days,
|
195
|
+
alt_names=alt_names or [common_name],
|
196
|
+
key_type=key_type,
|
197
|
+
)
|
198
|
+
|
199
|
+
|
200
|
+
def create_ca(
|
201
|
+
common_name: str,
|
202
|
+
organization: str = "Default CA Organization",
|
203
|
+
validity_days: int = DEFAULT_CERTIFICATE_VALIDITY_DAYS * 2, # CAs live longer
|
204
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
205
|
+
) -> "Certificate":
|
206
|
+
"""Create a CA certificate (convenience function)."""
|
207
|
+
_require_crypto()
|
208
|
+
return create_ca_certificate(
|
209
|
+
common_name=common_name,
|
210
|
+
organization_name=organization,
|
211
|
+
validity_days=validity_days,
|
212
|
+
key_type=key_type,
|
213
|
+
)
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"""Certificate generation utilities."""
|
2
|
+
|
3
|
+
from datetime import UTC, datetime, timedelta
|
4
|
+
import traceback
|
5
|
+
|
6
|
+
try:
|
7
|
+
from cryptography import x509
|
8
|
+
from cryptography.hazmat.primitives import serialization
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
10
|
+
|
11
|
+
_HAS_CRYPTO = True
|
12
|
+
except ImportError:
|
13
|
+
x509 = None
|
14
|
+
serialization = None
|
15
|
+
ec = None
|
16
|
+
rsa = None
|
17
|
+
_HAS_CRYPTO = False
|
18
|
+
|
19
|
+
from provide.foundation import logger
|
20
|
+
from provide.foundation.crypto.certificates.base import (
|
21
|
+
CertificateBase,
|
22
|
+
CertificateConfig,
|
23
|
+
CertificateError,
|
24
|
+
CurveType,
|
25
|
+
KeyType,
|
26
|
+
)
|
27
|
+
from provide.foundation.crypto.certificates.operations import create_x509_certificate
|
28
|
+
from provide.foundation.crypto.constants import (
|
29
|
+
DEFAULT_CERTIFICATE_CURVE,
|
30
|
+
DEFAULT_CERTIFICATE_KEY_TYPE,
|
31
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
32
|
+
DEFAULT_RSA_KEY_SIZE,
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
def generate_certificate(
|
37
|
+
common_name: str,
|
38
|
+
organization_name: str,
|
39
|
+
validity_days: int,
|
40
|
+
key_type: str = DEFAULT_CERTIFICATE_KEY_TYPE,
|
41
|
+
key_size: int = DEFAULT_RSA_KEY_SIZE,
|
42
|
+
ecdsa_curve: str = DEFAULT_CERTIFICATE_CURVE,
|
43
|
+
alt_names: list[str] | None = None,
|
44
|
+
is_ca: bool = False,
|
45
|
+
is_client_cert: bool = False,
|
46
|
+
) -> tuple[CertificateBase, "x509.Certificate", "rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey", str, str]:
|
47
|
+
"""
|
48
|
+
Generate a new certificate with a keypair.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Tuple of (CertificateBase, X509Certificate, private_key, cert_pem, key_pem)
|
52
|
+
"""
|
53
|
+
try:
|
54
|
+
logger.debug("📜🔑🚀 Generating new keypair")
|
55
|
+
|
56
|
+
now = datetime.now(UTC)
|
57
|
+
not_valid_before = now - timedelta(days=1)
|
58
|
+
not_valid_after = now + timedelta(days=validity_days)
|
59
|
+
|
60
|
+
# Parse key type
|
61
|
+
normalized_key_type_str = key_type.lower()
|
62
|
+
match normalized_key_type_str:
|
63
|
+
case "rsa":
|
64
|
+
gen_key_type = KeyType.RSA
|
65
|
+
case "ecdsa":
|
66
|
+
gen_key_type = KeyType.ECDSA
|
67
|
+
case _:
|
68
|
+
raise ValueError(
|
69
|
+
f"Unsupported key_type string: '{key_type}'. "
|
70
|
+
"Must be 'rsa' or 'ecdsa'."
|
71
|
+
)
|
72
|
+
|
73
|
+
# Configure key parameters
|
74
|
+
gen_curve: CurveType | None = None
|
75
|
+
gen_key_size = None
|
76
|
+
|
77
|
+
if gen_key_type == KeyType.ECDSA:
|
78
|
+
try:
|
79
|
+
gen_curve = CurveType[ecdsa_curve.upper()]
|
80
|
+
except KeyError as e_curve:
|
81
|
+
raise ValueError(
|
82
|
+
f"Unsupported ECDSA curve: {ecdsa_curve}"
|
83
|
+
) from e_curve
|
84
|
+
else: # RSA
|
85
|
+
gen_key_size = key_size
|
86
|
+
|
87
|
+
# Build configuration
|
88
|
+
conf: CertificateConfig = {
|
89
|
+
"common_name": common_name,
|
90
|
+
"organization": organization_name,
|
91
|
+
"alt_names": alt_names or ["localhost"],
|
92
|
+
"key_type": gen_key_type,
|
93
|
+
"not_valid_before": not_valid_before,
|
94
|
+
"not_valid_after": not_valid_after,
|
95
|
+
}
|
96
|
+
if gen_curve is not None:
|
97
|
+
conf["curve"] = gen_curve
|
98
|
+
if gen_key_size is not None:
|
99
|
+
conf["key_size"] = gen_key_size
|
100
|
+
logger.debug(f"📜🔑🚀 Generation config: {conf}")
|
101
|
+
|
102
|
+
# Generate base certificate and private key
|
103
|
+
base, private_key = CertificateBase.create(conf)
|
104
|
+
|
105
|
+
# Create X.509 certificate
|
106
|
+
x509_cert = create_x509_certificate(
|
107
|
+
base=base,
|
108
|
+
private_key=private_key,
|
109
|
+
alt_names=alt_names or ["localhost"],
|
110
|
+
is_ca=is_ca,
|
111
|
+
is_client_cert=is_client_cert,
|
112
|
+
)
|
113
|
+
|
114
|
+
if x509_cert is None:
|
115
|
+
raise CertificateError(
|
116
|
+
"Certificate object (_cert) is None after creation"
|
117
|
+
)
|
118
|
+
|
119
|
+
# Convert to PEM format
|
120
|
+
cert_pem = x509_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
121
|
+
key_pem = private_key.private_bytes(
|
122
|
+
encoding=serialization.Encoding.PEM,
|
123
|
+
format=serialization.PrivateFormat.PKCS8,
|
124
|
+
encryption_algorithm=serialization.NoEncryption(),
|
125
|
+
).decode("utf-8")
|
126
|
+
|
127
|
+
logger.debug("📜🔑✅ Generated cert and key")
|
128
|
+
|
129
|
+
return base, x509_cert, private_key, cert_pem, key_pem
|
130
|
+
|
131
|
+
except Exception as e:
|
132
|
+
logger.error(
|
133
|
+
f"📜❌ Failed to generate certificate. Error: {type(e).__name__}: {e}",
|
134
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
135
|
+
)
|
136
|
+
raise CertificateError(
|
137
|
+
f"Failed to initialize certificate. Original error: {type(e).__name__}"
|
138
|
+
) from e
|