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.
Files changed (161) hide show
  1. provide/foundation/__init__.py +41 -23
  2. provide/foundation/archive/__init__.py +23 -0
  3. provide/foundation/archive/base.py +70 -0
  4. provide/foundation/archive/bzip2.py +157 -0
  5. provide/foundation/archive/gzip.py +159 -0
  6. provide/foundation/archive/operations.py +334 -0
  7. provide/foundation/archive/tar.py +164 -0
  8. provide/foundation/archive/zip.py +203 -0
  9. provide/foundation/cli/__init__.py +2 -2
  10. provide/foundation/cli/commands/deps.py +13 -7
  11. provide/foundation/cli/commands/logs/__init__.py +1 -1
  12. provide/foundation/cli/commands/logs/query.py +1 -1
  13. provide/foundation/cli/commands/logs/send.py +1 -1
  14. provide/foundation/cli/commands/logs/tail.py +1 -1
  15. provide/foundation/cli/decorators.py +11 -10
  16. provide/foundation/cli/main.py +1 -1
  17. provide/foundation/cli/testing.py +2 -35
  18. provide/foundation/cli/utils.py +21 -17
  19. provide/foundation/config/__init__.py +35 -2
  20. provide/foundation/config/base.py +2 -2
  21. provide/foundation/config/converters.py +479 -0
  22. provide/foundation/config/defaults.py +67 -0
  23. provide/foundation/config/env.py +4 -19
  24. provide/foundation/config/loader.py +9 -3
  25. provide/foundation/config/sync.py +19 -4
  26. provide/foundation/console/input.py +5 -5
  27. provide/foundation/console/output.py +35 -13
  28. provide/foundation/context/__init__.py +8 -4
  29. provide/foundation/context/core.py +85 -109
  30. provide/foundation/core.py +1 -2
  31. provide/foundation/crypto/__init__.py +2 -0
  32. provide/foundation/crypto/certificates/__init__.py +34 -0
  33. provide/foundation/crypto/certificates/base.py +173 -0
  34. provide/foundation/crypto/certificates/certificate.py +290 -0
  35. provide/foundation/crypto/certificates/factory.py +213 -0
  36. provide/foundation/crypto/certificates/generator.py +138 -0
  37. provide/foundation/crypto/certificates/loader.py +130 -0
  38. provide/foundation/crypto/certificates/operations.py +198 -0
  39. provide/foundation/crypto/certificates/trust.py +107 -0
  40. provide/foundation/errors/__init__.py +2 -3
  41. provide/foundation/errors/decorators.py +0 -231
  42. provide/foundation/errors/types.py +0 -97
  43. provide/foundation/eventsets/__init__.py +0 -0
  44. provide/foundation/eventsets/display.py +84 -0
  45. provide/foundation/eventsets/registry.py +160 -0
  46. provide/foundation/eventsets/resolver.py +192 -0
  47. provide/foundation/eventsets/sets/das.py +128 -0
  48. provide/foundation/eventsets/sets/database.py +125 -0
  49. provide/foundation/eventsets/sets/http.py +153 -0
  50. provide/foundation/eventsets/sets/llm.py +139 -0
  51. provide/foundation/eventsets/sets/task_queue.py +107 -0
  52. provide/foundation/eventsets/types.py +70 -0
  53. provide/foundation/file/directory.py +13 -22
  54. provide/foundation/file/lock.py +3 -1
  55. provide/foundation/hub/components.py +77 -515
  56. provide/foundation/hub/config.py +151 -0
  57. provide/foundation/hub/discovery.py +62 -0
  58. provide/foundation/hub/handlers.py +81 -0
  59. provide/foundation/hub/lifecycle.py +194 -0
  60. provide/foundation/hub/manager.py +4 -4
  61. provide/foundation/hub/processors.py +44 -0
  62. provide/foundation/integrations/__init__.py +11 -0
  63. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  64. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  65. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  66. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  67. provide/foundation/integrations/openobserve/config.py +37 -0
  68. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  70. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  71. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  72. provide/foundation/logger/__init__.py +3 -10
  73. provide/foundation/logger/config/logging.py +68 -298
  74. provide/foundation/logger/config/telemetry.py +41 -121
  75. provide/foundation/logger/core.py +0 -2
  76. provide/foundation/logger/custom_processors.py +1 -0
  77. provide/foundation/logger/factories.py +11 -2
  78. provide/foundation/logger/processors/main.py +20 -84
  79. provide/foundation/logger/setup/__init__.py +5 -1
  80. provide/foundation/logger/setup/coordinator.py +76 -24
  81. provide/foundation/logger/setup/processors.py +2 -9
  82. provide/foundation/logger/trace.py +27 -0
  83. provide/foundation/metrics/otel.py +10 -10
  84. provide/foundation/observability/__init__.py +2 -2
  85. provide/foundation/process/__init__.py +9 -0
  86. provide/foundation/process/exit.py +47 -0
  87. provide/foundation/process/lifecycle.py +115 -59
  88. provide/foundation/resilience/__init__.py +35 -0
  89. provide/foundation/resilience/circuit.py +164 -0
  90. provide/foundation/resilience/decorators.py +220 -0
  91. provide/foundation/resilience/fallback.py +193 -0
  92. provide/foundation/resilience/retry.py +325 -0
  93. provide/foundation/streams/config.py +79 -0
  94. provide/foundation/streams/console.py +7 -8
  95. provide/foundation/streams/core.py +6 -3
  96. provide/foundation/streams/file.py +12 -2
  97. provide/foundation/testing/__init__.py +84 -2
  98. provide/foundation/testing/archive/__init__.py +24 -0
  99. provide/foundation/testing/archive/fixtures.py +217 -0
  100. provide/foundation/testing/cli.py +30 -17
  101. provide/foundation/testing/common/__init__.py +32 -0
  102. provide/foundation/testing/common/fixtures.py +236 -0
  103. provide/foundation/testing/file/__init__.py +40 -0
  104. provide/foundation/testing/file/content_fixtures.py +316 -0
  105. provide/foundation/testing/file/directory_fixtures.py +107 -0
  106. provide/foundation/testing/file/fixtures.py +52 -0
  107. provide/foundation/testing/file/special_fixtures.py +153 -0
  108. provide/foundation/testing/logger.py +117 -11
  109. provide/foundation/testing/mocking/__init__.py +46 -0
  110. provide/foundation/testing/mocking/fixtures.py +331 -0
  111. provide/foundation/testing/process/__init__.py +48 -0
  112. provide/foundation/testing/process/async_fixtures.py +405 -0
  113. provide/foundation/testing/process/fixtures.py +56 -0
  114. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  115. provide/foundation/testing/threading/__init__.py +38 -0
  116. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  117. provide/foundation/testing/threading/data_fixtures.py +99 -0
  118. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  119. provide/foundation/testing/threading/fixtures.py +54 -0
  120. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  121. provide/foundation/testing/time/__init__.py +32 -0
  122. provide/foundation/testing/time/fixtures.py +409 -0
  123. provide/foundation/testing/transport/__init__.py +30 -0
  124. provide/foundation/testing/transport/fixtures.py +280 -0
  125. provide/foundation/tools/__init__.py +58 -0
  126. provide/foundation/tools/base.py +348 -0
  127. provide/foundation/tools/cache.py +268 -0
  128. provide/foundation/tools/downloader.py +224 -0
  129. provide/foundation/tools/installer.py +254 -0
  130. provide/foundation/tools/registry.py +223 -0
  131. provide/foundation/tools/resolver.py +321 -0
  132. provide/foundation/tools/verifier.py +186 -0
  133. provide/foundation/tracer/otel.py +7 -11
  134. provide/foundation/tracer/spans.py +2 -2
  135. provide/foundation/transport/__init__.py +155 -0
  136. provide/foundation/transport/base.py +171 -0
  137. provide/foundation/transport/client.py +266 -0
  138. provide/foundation/transport/config.py +140 -0
  139. provide/foundation/transport/errors.py +79 -0
  140. provide/foundation/transport/http.py +232 -0
  141. provide/foundation/transport/middleware.py +360 -0
  142. provide/foundation/transport/registry.py +167 -0
  143. provide/foundation/transport/types.py +45 -0
  144. provide/foundation/utils/deps.py +14 -12
  145. provide/foundation/utils/parsing.py +49 -4
  146. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
  147. provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
  148. provide/foundation/cli/commands/logs/generate_old.py +0 -569
  149. provide/foundation/crypto/certificates.py +0 -896
  150. provide/foundation/logger/emoji/__init__.py +0 -44
  151. provide/foundation/logger/emoji/matrix.py +0 -209
  152. provide/foundation/logger/emoji/sets.py +0 -458
  153. provide/foundation/logger/emoji/types.py +0 -56
  154. provide/foundation/logger/setup/emoji_resolver.py +0 -64
  155. provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
  156. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  157. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  158. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  159. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  160. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  161. {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