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
@@ -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
|
@@ -0,0 +1,130 @@
|
|
1
|
+
"""Certificate loading utilities."""
|
2
|
+
|
3
|
+
from datetime import UTC
|
4
|
+
import os
|
5
|
+
from pathlib import Path
|
6
|
+
import traceback
|
7
|
+
|
8
|
+
try:
|
9
|
+
from cryptography import x509
|
10
|
+
from cryptography.hazmat.primitives import serialization
|
11
|
+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
12
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
13
|
+
|
14
|
+
_HAS_CRYPTO = True
|
15
|
+
except ImportError:
|
16
|
+
x509 = None
|
17
|
+
serialization = None
|
18
|
+
ec = None
|
19
|
+
rsa = None
|
20
|
+
load_pem_private_key = None
|
21
|
+
_HAS_CRYPTO = False
|
22
|
+
|
23
|
+
from provide.foundation import logger
|
24
|
+
from provide.foundation.crypto.certificates.base import CertificateBase, CertificateError
|
25
|
+
|
26
|
+
|
27
|
+
def load_from_uri_or_pem(data: str) -> str:
|
28
|
+
"""Load PEM data either directly from a string or from a file URI."""
|
29
|
+
try:
|
30
|
+
if data.startswith("file://"):
|
31
|
+
path_str = data.removeprefix("file://")
|
32
|
+
if os.name == "nt" and path_str.startswith("//"):
|
33
|
+
path = Path(path_str)
|
34
|
+
else:
|
35
|
+
path_str = path_str.lstrip("/")
|
36
|
+
if os.name != "nt" and data.startswith("file:///"):
|
37
|
+
path_str = "/" + path_str
|
38
|
+
path = Path(path_str)
|
39
|
+
|
40
|
+
logger.debug(f"📜📂🚀 Loading data from file: {path}")
|
41
|
+
with path.open("r", encoding="utf-8") as f:
|
42
|
+
loaded_data = f.read().strip()
|
43
|
+
logger.debug("📜📂✅ Loaded data from file")
|
44
|
+
return loaded_data
|
45
|
+
|
46
|
+
loaded_data = data.strip()
|
47
|
+
if not loaded_data.startswith("-----BEGIN"):
|
48
|
+
logger.warning("📜📂⚠️ Data doesn't look like PEM format")
|
49
|
+
return loaded_data
|
50
|
+
except Exception as e:
|
51
|
+
logger.error(f"📜📂❌ Failed to load data: {e}", extra={"error": str(e)})
|
52
|
+
raise CertificateError(f"Failed to load data: {e}") from e
|
53
|
+
|
54
|
+
|
55
|
+
def load_certificate_from_pem(
|
56
|
+
cert_pem_or_uri: str,
|
57
|
+
key_pem_or_uri: str | None = None
|
58
|
+
) -> tuple[CertificateBase, "x509.Certificate", "rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey | None", str, str | None]:
|
59
|
+
"""
|
60
|
+
Load a certificate and optionally its private key from PEM data or file URIs.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Tuple of (CertificateBase, X509Certificate, private_key, cert_pem, key_pem)
|
64
|
+
"""
|
65
|
+
try:
|
66
|
+
logger.debug("📜🔑🚀 Loading certificate from provided data")
|
67
|
+
cert_data = load_from_uri_or_pem(cert_pem_or_uri)
|
68
|
+
|
69
|
+
logger.debug("📜🔑🔍 Loading X.509 certificate from PEM data")
|
70
|
+
x509_cert = x509.load_pem_x509_certificate(cert_data.encode("utf-8"))
|
71
|
+
logger.debug("📜🔑✅ X.509 certificate object loaded from PEM")
|
72
|
+
|
73
|
+
private_key = None
|
74
|
+
key_data = None
|
75
|
+
|
76
|
+
if key_pem_or_uri:
|
77
|
+
logger.debug("📜🔑🚀 Loading private key")
|
78
|
+
key_data = load_from_uri_or_pem(key_pem_or_uri)
|
79
|
+
|
80
|
+
loaded_priv_key = load_pem_private_key(
|
81
|
+
key_data.encode("utf-8"), password=None
|
82
|
+
)
|
83
|
+
if not isinstance(
|
84
|
+
loaded_priv_key,
|
85
|
+
rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey,
|
86
|
+
):
|
87
|
+
raise CertificateError(
|
88
|
+
f"Loaded private key is of unsupported type: {type(loaded_priv_key)}. "
|
89
|
+
"Expected RSA or ECDSA private key."
|
90
|
+
)
|
91
|
+
private_key = loaded_priv_key
|
92
|
+
logger.debug("📜🔑✅ Private key object loaded and type validated")
|
93
|
+
|
94
|
+
# Extract certificate details for CertificateBase
|
95
|
+
loaded_not_valid_before = x509_cert.not_valid_before_utc
|
96
|
+
loaded_not_valid_after = x509_cert.not_valid_after_utc
|
97
|
+
if loaded_not_valid_before.tzinfo is None:
|
98
|
+
loaded_not_valid_before = loaded_not_valid_before.replace(tzinfo=UTC)
|
99
|
+
if loaded_not_valid_after.tzinfo is None:
|
100
|
+
loaded_not_valid_after = loaded_not_valid_after.replace(tzinfo=UTC)
|
101
|
+
|
102
|
+
cert_public_key = x509_cert.public_key()
|
103
|
+
if not isinstance(
|
104
|
+
cert_public_key, rsa.RSAPublicKey | ec.EllipticCurvePublicKey
|
105
|
+
):
|
106
|
+
raise CertificateError(
|
107
|
+
f"Certificate's public key is of unsupported type: {type(cert_public_key)}. "
|
108
|
+
"Expected RSA or ECDSA public key."
|
109
|
+
)
|
110
|
+
|
111
|
+
base = CertificateBase(
|
112
|
+
subject=x509_cert.subject,
|
113
|
+
issuer=x509_cert.issuer,
|
114
|
+
public_key=cert_public_key,
|
115
|
+
not_valid_before=loaded_not_valid_before,
|
116
|
+
not_valid_after=loaded_not_valid_after,
|
117
|
+
serial_number=x509_cert.serial_number,
|
118
|
+
)
|
119
|
+
logger.debug("📜🔑✅ Reconstructed CertificateBase from loaded cert")
|
120
|
+
|
121
|
+
return base, x509_cert, private_key, cert_data, key_data
|
122
|
+
|
123
|
+
except Exception as e:
|
124
|
+
logger.error(
|
125
|
+
f"📜❌ Failed to load certificate. Error: {type(e).__name__}: {e}",
|
126
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
127
|
+
)
|
128
|
+
raise CertificateError(
|
129
|
+
f"Failed to initialize certificate. Original error: {type(e).__name__}"
|
130
|
+
) from e
|
@@ -0,0 +1,198 @@
|
|
1
|
+
"""Certificate operations: CA creation, signing, and trust verification."""
|
2
|
+
|
3
|
+
import traceback
|
4
|
+
from typing import cast
|
5
|
+
|
6
|
+
try:
|
7
|
+
from cryptography import x509
|
8
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
|
10
|
+
from cryptography.x509 import Certificate as X509Certificate
|
11
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID
|
12
|
+
|
13
|
+
_HAS_CRYPTO = True
|
14
|
+
except ImportError:
|
15
|
+
# Stub out cryptography types for type hints
|
16
|
+
x509 = None
|
17
|
+
hashes = None
|
18
|
+
serialization = None
|
19
|
+
ec = None
|
20
|
+
padding = None
|
21
|
+
rsa = None
|
22
|
+
X509Certificate = None
|
23
|
+
ExtendedKeyUsageOID = None
|
24
|
+
_HAS_CRYPTO = False
|
25
|
+
|
26
|
+
from provide.foundation import logger
|
27
|
+
from provide.foundation.crypto.constants import (
|
28
|
+
DEFAULT_CERTIFICATE_CURVE,
|
29
|
+
DEFAULT_CERTIFICATE_KEY_TYPE,
|
30
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
31
|
+
DEFAULT_RSA_KEY_SIZE,
|
32
|
+
)
|
33
|
+
from .base import CertificateBase, CertificateError, KeyPair, PublicKey
|
34
|
+
|
35
|
+
|
36
|
+
def create_x509_certificate(
|
37
|
+
base: CertificateBase,
|
38
|
+
private_key: "KeyPair",
|
39
|
+
alt_names: list[str] | None = None,
|
40
|
+
issuer_name_override: "x509.Name | None" = None,
|
41
|
+
signing_key_override: "KeyPair | None" = None,
|
42
|
+
is_ca: bool = False,
|
43
|
+
is_client_cert: bool = False,
|
44
|
+
) -> "X509Certificate":
|
45
|
+
"""Internal helper to build and sign the X.509 certificate object."""
|
46
|
+
try:
|
47
|
+
logger.debug("📜📝🚀 create_x509_certificate: Building certificate")
|
48
|
+
|
49
|
+
actual_issuer_name = issuer_name_override if issuer_name_override else base.issuer
|
50
|
+
actual_signing_key = signing_key_override if signing_key_override else private_key
|
51
|
+
|
52
|
+
if not actual_signing_key:
|
53
|
+
raise CertificateError(
|
54
|
+
"Cannot sign certificate without a signing key (either own or override)"
|
55
|
+
)
|
56
|
+
|
57
|
+
builder = (
|
58
|
+
x509.CertificateBuilder()
|
59
|
+
.subject_name(base.subject)
|
60
|
+
.issuer_name(actual_issuer_name)
|
61
|
+
.public_key(base.public_key)
|
62
|
+
.serial_number(base.serial_number)
|
63
|
+
.not_valid_before(base.not_valid_before)
|
64
|
+
.not_valid_after(base.not_valid_after)
|
65
|
+
)
|
66
|
+
|
67
|
+
san_list = [x509.DNSName(name) for name in (alt_names or []) if name]
|
68
|
+
if san_list:
|
69
|
+
builder = builder.add_extension(
|
70
|
+
x509.SubjectAlternativeName(san_list), critical=False
|
71
|
+
)
|
72
|
+
logger.debug(f"📜📝✅ Added SANs: {alt_names or []}")
|
73
|
+
|
74
|
+
builder = builder.add_extension(
|
75
|
+
x509.BasicConstraints(ca=is_ca, path_length=None),
|
76
|
+
critical=True,
|
77
|
+
)
|
78
|
+
|
79
|
+
if is_ca:
|
80
|
+
builder = builder.add_extension(
|
81
|
+
x509.KeyUsage(
|
82
|
+
digital_signature=False,
|
83
|
+
key_encipherment=False,
|
84
|
+
key_agreement=False,
|
85
|
+
content_commitment=False,
|
86
|
+
data_encipherment=False,
|
87
|
+
key_cert_sign=True,
|
88
|
+
crl_sign=True,
|
89
|
+
encipher_only=False,
|
90
|
+
decipher_only=False,
|
91
|
+
),
|
92
|
+
critical=True,
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
builder = builder.add_extension(
|
96
|
+
x509.KeyUsage(
|
97
|
+
digital_signature=True,
|
98
|
+
key_encipherment=(
|
99
|
+
True
|
100
|
+
if not is_client_cert
|
101
|
+
and isinstance(base.public_key, rsa.RSAPublicKey)
|
102
|
+
else False
|
103
|
+
),
|
104
|
+
key_agreement=(
|
105
|
+
True
|
106
|
+
if isinstance(base.public_key, ec.EllipticCurvePublicKey)
|
107
|
+
else False
|
108
|
+
),
|
109
|
+
content_commitment=False,
|
110
|
+
data_encipherment=False,
|
111
|
+
key_cert_sign=False,
|
112
|
+
crl_sign=False,
|
113
|
+
encipher_only=False,
|
114
|
+
decipher_only=False,
|
115
|
+
),
|
116
|
+
critical=True,
|
117
|
+
)
|
118
|
+
extended_usages = []
|
119
|
+
if is_client_cert:
|
120
|
+
extended_usages.append(ExtendedKeyUsageOID.CLIENT_AUTH)
|
121
|
+
else:
|
122
|
+
extended_usages.append(ExtendedKeyUsageOID.SERVER_AUTH)
|
123
|
+
|
124
|
+
if extended_usages:
|
125
|
+
builder = builder.add_extension(
|
126
|
+
x509.ExtendedKeyUsage(extended_usages),
|
127
|
+
critical=False,
|
128
|
+
)
|
129
|
+
|
130
|
+
logger.debug(
|
131
|
+
f"📜📝✅ Added BasicConstraints (is_ca={is_ca}), "
|
132
|
+
f"KeyUsage, ExtendedKeyUsage (is_client_cert={is_client_cert})"
|
133
|
+
)
|
134
|
+
|
135
|
+
signed_cert = builder.sign(
|
136
|
+
private_key=actual_signing_key,
|
137
|
+
algorithm=hashes.SHA256(),
|
138
|
+
)
|
139
|
+
logger.debug("📜📝✅ Certificate signed successfully")
|
140
|
+
return signed_cert
|
141
|
+
|
142
|
+
except Exception as e:
|
143
|
+
logger.error(
|
144
|
+
f"📜❌ create_x509_certificate: Failed: {e}",
|
145
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
146
|
+
)
|
147
|
+
raise CertificateError("Failed to create X.509 certificate object") from e
|
148
|
+
|
149
|
+
|
150
|
+
def validate_signature(signed_cert_obj: "X509Certificate", signing_cert_obj: "X509Certificate", signing_public_key: "PublicKey") -> bool:
|
151
|
+
"""Internal helper: Validates signature and issuer/subject match."""
|
152
|
+
if signed_cert_obj.issuer != signing_cert_obj.subject:
|
153
|
+
logger.debug(
|
154
|
+
f"📜🔍❌ Signature validation failed: Issuer/Subject mismatch. "
|
155
|
+
f"Signed Issuer='{signed_cert_obj.issuer}', "
|
156
|
+
f"Signing Subject='{signing_cert_obj.subject}'"
|
157
|
+
)
|
158
|
+
return False
|
159
|
+
|
160
|
+
try:
|
161
|
+
if not signing_public_key:
|
162
|
+
logger.error(
|
163
|
+
"📜🔍❌ Cannot validate signature: Signing certificate has no public key"
|
164
|
+
)
|
165
|
+
return False
|
166
|
+
|
167
|
+
signature = signed_cert_obj.signature
|
168
|
+
tbs_certificate_bytes = signed_cert_obj.tbs_certificate_bytes
|
169
|
+
signature_hash_algorithm = signed_cert_obj.signature_hash_algorithm
|
170
|
+
|
171
|
+
if not signature_hash_algorithm:
|
172
|
+
logger.error("📜🔍❌ Cannot validate signature: Unknown hash algorithm")
|
173
|
+
return False
|
174
|
+
|
175
|
+
if isinstance(signing_public_key, rsa.RSAPublicKey):
|
176
|
+
cast(rsa.RSAPublicKey, signing_public_key).verify(
|
177
|
+
signature,
|
178
|
+
tbs_certificate_bytes,
|
179
|
+
padding.PKCS1v15(),
|
180
|
+
signature_hash_algorithm,
|
181
|
+
)
|
182
|
+
elif isinstance(signing_public_key, ec.EllipticCurvePublicKey):
|
183
|
+
cast(ec.EllipticCurvePublicKey, signing_public_key).verify(
|
184
|
+
signature,
|
185
|
+
tbs_certificate_bytes,
|
186
|
+
ec.ECDSA(signature_hash_algorithm),
|
187
|
+
)
|
188
|
+
else:
|
189
|
+
logger.error(
|
190
|
+
f"📜🔍❌ Unsupported signing public key type: {type(signing_public_key)}"
|
191
|
+
)
|
192
|
+
return False
|
193
|
+
|
194
|
+
return True
|
195
|
+
|
196
|
+
except Exception as e:
|
197
|
+
logger.debug(f"📜🔍❌ Signature validation failed: {type(e).__name__}: {e}")
|
198
|
+
return False
|