provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev1__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 +12 -20
- 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 +336 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/sync.py +19 -4
- 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/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/hub/components.py +7 -133
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +6 -6
- 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 +75 -23
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/process/lifecycle.py +82 -26
- provide/foundation/testing/__init__.py +77 -0
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/common/__init__.py +34 -0
- provide/foundation/testing/common/fixtures.py +263 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/fixtures.py +523 -0
- provide/foundation/testing/logger.py +41 -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/fixtures.py +577 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/fixtures.py +520 -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 +266 -0
- provide/foundation/tools/downloader.py +213 -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/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +209 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +366 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/METADATA +5 -28
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/RECORD +85 -34
- 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 → provide_foundation-0.0.0.dev1.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/top_level.txt +0 -0
@@ -174,8 +174,8 @@ class BaseConfig:
|
|
174
174
|
Returns:
|
175
175
|
Configuration instance
|
176
176
|
"""
|
177
|
-
# Filter data to only include fields defined in the class
|
178
|
-
field_names = {f.name for f in fields(cls)}
|
177
|
+
# Filter data to only include fields defined in the class, excluding private fields
|
178
|
+
field_names = {f.name for f in fields(cls) if not f.name.startswith('_')}
|
179
179
|
filtered_data = {k: v for k, v in data.items() if k in field_names}
|
180
180
|
|
181
181
|
# Create instance
|
@@ -20,6 +20,7 @@ from provide.foundation.config.loader import (
|
|
20
20
|
)
|
21
21
|
from provide.foundation.config.manager import ConfigManager
|
22
22
|
from provide.foundation.config.types import ConfigDict, ConfigSource
|
23
|
+
from provide.foundation.config.loader import ConfigLoader
|
23
24
|
|
24
25
|
T = TypeVar("T", bound=BaseConfig)
|
25
26
|
|
@@ -227,9 +228,14 @@ class SyncConfigManager:
|
|
227
228
|
Provides a sync interface to the async ConfigManager.
|
228
229
|
"""
|
229
230
|
|
230
|
-
def __init__(self) -> None:
|
231
|
-
"""Initialize sync config manager.
|
231
|
+
def __init__(self, loader: ConfigLoader | None = None) -> None:
|
232
|
+
"""Initialize sync config manager.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
loader: Optional config loader for loading configurations.
|
236
|
+
"""
|
232
237
|
self._async_manager = ConfigManager()
|
238
|
+
self._loader = loader
|
233
239
|
|
234
240
|
def register(self, name: str, config: BaseConfig | None = None, **kwargs) -> None:
|
235
241
|
"""Register a configuration (sync)."""
|
@@ -239,8 +245,17 @@ class SyncConfigManager:
|
|
239
245
|
"""Get a configuration by name (sync)."""
|
240
246
|
return run_async(self._async_manager.get(name))
|
241
247
|
|
242
|
-
def load(self, name: str, config_class: type[T], loader=None) -> T:
|
243
|
-
"""Load a configuration (sync).
|
248
|
+
def load(self, name: str, config_class: type[T], loader: ConfigLoader | None = None) -> T:
|
249
|
+
"""Load a configuration (sync).
|
250
|
+
|
251
|
+
Args:
|
252
|
+
name: Configuration name
|
253
|
+
config_class: Configuration class
|
254
|
+
loader: Optional loader (uses registered if None)
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
Configuration instance
|
258
|
+
"""
|
244
259
|
return run_async(self._async_manager.load(name, config_class, loader))
|
245
260
|
|
246
261
|
def update(
|
provide/foundation/core.py
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
Foundation Telemetry Core Setup Functions.
|
6
6
|
"""
|
7
7
|
|
8
|
-
|
8
|
+
# Emoji resolver removed - using event sets now
|
9
9
|
from provide.foundation.setup import (
|
10
10
|
reset_foundation_setup_for_testing,
|
11
11
|
setup_telemetry,
|
@@ -13,7 +13,6 @@ from provide.foundation.setup import (
|
|
13
13
|
)
|
14
14
|
|
15
15
|
__all__ = [
|
16
|
-
"ResolvedEmojiConfig",
|
17
16
|
"reset_foundation_setup_for_testing",
|
18
17
|
"setup_telemetry",
|
19
18
|
"shutdown_foundation_telemetry",
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"""X.509 certificate generation and management."""
|
2
|
+
|
3
|
+
# Import from submodules using absolute imports
|
4
|
+
from provide.foundation.crypto.certificates.base import (
|
5
|
+
CertificateBase,
|
6
|
+
CertificateConfig,
|
7
|
+
CertificateError,
|
8
|
+
CurveType,
|
9
|
+
KeyPair,
|
10
|
+
KeyType,
|
11
|
+
PublicKey,
|
12
|
+
_HAS_CRYPTO,
|
13
|
+
_require_crypto,
|
14
|
+
)
|
15
|
+
from provide.foundation.crypto.certificates.certificate import Certificate
|
16
|
+
from provide.foundation.crypto.certificates.factory import create_ca, create_self_signed
|
17
|
+
from provide.foundation.crypto.certificates.operations import (
|
18
|
+
create_x509_certificate,
|
19
|
+
validate_signature,
|
20
|
+
)
|
21
|
+
|
22
|
+
# Re-export public types - maintaining exact same API
|
23
|
+
__all__ = [
|
24
|
+
"Certificate",
|
25
|
+
"CertificateBase",
|
26
|
+
"CertificateConfig",
|
27
|
+
"CertificateError",
|
28
|
+
"CurveType",
|
29
|
+
"KeyType",
|
30
|
+
"create_self_signed",
|
31
|
+
"create_ca",
|
32
|
+
"_HAS_CRYPTO", # For testing
|
33
|
+
"_require_crypto", # For testing
|
34
|
+
]
|
@@ -0,0 +1,173 @@
|
|
1
|
+
"""Certificate base classes, types, and utilities."""
|
2
|
+
|
3
|
+
from datetime import UTC, datetime
|
4
|
+
from enum import StrEnum, auto
|
5
|
+
import traceback
|
6
|
+
from typing import NotRequired, Self, TypeAlias, TypedDict
|
7
|
+
|
8
|
+
from attrs import define, field
|
9
|
+
|
10
|
+
try:
|
11
|
+
from cryptography import x509
|
12
|
+
from cryptography.hazmat.backends import default_backend
|
13
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
14
|
+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
15
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
16
|
+
from cryptography.x509 import Certificate as X509Certificate
|
17
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
18
|
+
|
19
|
+
_HAS_CRYPTO = True
|
20
|
+
except ImportError:
|
21
|
+
# Stub out cryptography types for type hints
|
22
|
+
x509 = None
|
23
|
+
default_backend = None
|
24
|
+
hashes = None
|
25
|
+
serialization = None
|
26
|
+
ec = None
|
27
|
+
rsa = None
|
28
|
+
load_pem_private_key = None
|
29
|
+
X509Certificate = None
|
30
|
+
ExtendedKeyUsageOID = None
|
31
|
+
NameOID = None
|
32
|
+
_HAS_CRYPTO = False
|
33
|
+
|
34
|
+
from provide.foundation import logger
|
35
|
+
from provide.foundation.crypto.constants import (
|
36
|
+
DEFAULT_RSA_KEY_SIZE,
|
37
|
+
)
|
38
|
+
from provide.foundation.errors.config import ValidationError
|
39
|
+
|
40
|
+
|
41
|
+
def _require_crypto():
|
42
|
+
"""Ensure cryptography is available for crypto operations."""
|
43
|
+
if not _HAS_CRYPTO:
|
44
|
+
raise ImportError(
|
45
|
+
"Cryptography features require optional dependencies. Install with: "
|
46
|
+
"pip install 'provide-foundation[crypto]'"
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
class CertificateError(ValidationError):
|
51
|
+
"""Certificate-related errors."""
|
52
|
+
|
53
|
+
def __init__(self, message: str, hint: str | None = None) -> None:
|
54
|
+
super().__init__(
|
55
|
+
message=message,
|
56
|
+
field="certificate",
|
57
|
+
value=None,
|
58
|
+
rule=hint or "Certificate operation failed",
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
class KeyType(StrEnum):
|
63
|
+
RSA = auto()
|
64
|
+
ECDSA = auto()
|
65
|
+
|
66
|
+
|
67
|
+
class CurveType(StrEnum):
|
68
|
+
SECP256R1 = auto()
|
69
|
+
SECP384R1 = auto()
|
70
|
+
SECP521R1 = auto()
|
71
|
+
|
72
|
+
|
73
|
+
class CertificateConfig(TypedDict):
|
74
|
+
common_name: str
|
75
|
+
organization: str
|
76
|
+
alt_names: list[str]
|
77
|
+
key_type: KeyType
|
78
|
+
not_valid_before: datetime
|
79
|
+
not_valid_after: datetime
|
80
|
+
# Optional key generation parameters
|
81
|
+
key_size: NotRequired[int]
|
82
|
+
curve: NotRequired[CurveType]
|
83
|
+
|
84
|
+
|
85
|
+
if _HAS_CRYPTO:
|
86
|
+
KeyPair: TypeAlias = rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey
|
87
|
+
PublicKey: TypeAlias = rsa.RSAPublicKey | ec.EllipticCurvePublicKey
|
88
|
+
else:
|
89
|
+
KeyPair: TypeAlias = None
|
90
|
+
PublicKey: TypeAlias = None
|
91
|
+
|
92
|
+
|
93
|
+
@define(slots=True, frozen=True)
|
94
|
+
class CertificateBase:
|
95
|
+
"""Immutable base certificate data."""
|
96
|
+
|
97
|
+
subject: "x509.Name"
|
98
|
+
issuer: "x509.Name"
|
99
|
+
public_key: "PublicKey"
|
100
|
+
not_valid_before: datetime
|
101
|
+
not_valid_after: datetime
|
102
|
+
serial_number: int
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def create(cls, config: CertificateConfig) -> tuple[Self, "KeyPair"]:
|
106
|
+
"""Create a new certificate base and private key."""
|
107
|
+
_require_crypto()
|
108
|
+
try:
|
109
|
+
logger.debug("📜📝🚀 CertificateBase.create: Starting base creation")
|
110
|
+
not_valid_before = config["not_valid_before"]
|
111
|
+
not_valid_after = config["not_valid_after"]
|
112
|
+
|
113
|
+
if not_valid_before.tzinfo is None:
|
114
|
+
not_valid_before = not_valid_before.replace(tzinfo=UTC)
|
115
|
+
if not_valid_after.tzinfo is None:
|
116
|
+
not_valid_after = not_valid_after.replace(tzinfo=UTC)
|
117
|
+
|
118
|
+
logger.debug(
|
119
|
+
f"📜⏳✅ CertificateBase.create: Using validity: "
|
120
|
+
f"{not_valid_before} to {not_valid_after}"
|
121
|
+
)
|
122
|
+
|
123
|
+
private_key: KeyPair
|
124
|
+
match config["key_type"]:
|
125
|
+
case KeyType.RSA:
|
126
|
+
key_size = config.get("key_size", DEFAULT_RSA_KEY_SIZE)
|
127
|
+
logger.debug(f"📜🔑🚀 Generating RSA key (size: {key_size})")
|
128
|
+
private_key = rsa.generate_private_key(
|
129
|
+
public_exponent=65537, key_size=key_size
|
130
|
+
)
|
131
|
+
case KeyType.ECDSA:
|
132
|
+
curve_choice = config.get("curve", CurveType.SECP384R1)
|
133
|
+
logger.debug(f"📜🔑🚀 Generating ECDSA key (curve: {curve_choice})")
|
134
|
+
curve = getattr(ec, curve_choice.name)()
|
135
|
+
private_key = ec.generate_private_key(curve)
|
136
|
+
case _:
|
137
|
+
raise ValueError(
|
138
|
+
f"Internal Error: Unsupported key type: {config['key_type']}"
|
139
|
+
)
|
140
|
+
|
141
|
+
subject = cls._create_name(config["common_name"], config["organization"])
|
142
|
+
issuer = cls._create_name(config["common_name"], config["organization"])
|
143
|
+
|
144
|
+
serial_number = x509.random_serial_number()
|
145
|
+
logger.debug(f"📜🔑✅ Generated serial number: {serial_number}")
|
146
|
+
|
147
|
+
base = cls(
|
148
|
+
subject=subject,
|
149
|
+
issuer=issuer,
|
150
|
+
public_key=private_key.public_key(),
|
151
|
+
not_valid_before=not_valid_before,
|
152
|
+
not_valid_after=not_valid_after,
|
153
|
+
serial_number=serial_number,
|
154
|
+
)
|
155
|
+
logger.debug("📜📝✅ CertificateBase.create: Base creation complete")
|
156
|
+
return base, private_key
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
logger.error(
|
160
|
+
f"📜❌ CertificateBase.create: Failed: {e}",
|
161
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
162
|
+
)
|
163
|
+
raise CertificateError(f"Failed to generate certificate base: {e}") from e
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def _create_name(common_name: str, org: str) -> "x509.Name":
|
167
|
+
"""Helper method to construct an X.509 name."""
|
168
|
+
return x509.Name(
|
169
|
+
[
|
170
|
+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
171
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
|
172
|
+
]
|
173
|
+
)
|
@@ -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
|