swarmauri_certservice_aws_kms 0.3.0.dev4__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.
- swarmauri_certservice_aws_kms/AwsKmsCertService.py +730 -0
- swarmauri_certservice_aws_kms/__init__.py +3 -0
- swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/METADATA +50 -0
- swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/RECORD +6 -0
- swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/WHEEL +4 -0
- swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Dict,
|
|
10
|
+
Iterable,
|
|
11
|
+
Literal,
|
|
12
|
+
Mapping,
|
|
13
|
+
Optional,
|
|
14
|
+
Sequence,
|
|
15
|
+
Tuple,
|
|
16
|
+
Union,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from pydantic import Field
|
|
20
|
+
|
|
21
|
+
from swarmauri_base.ComponentBase import ComponentBase, ResourceTypes
|
|
22
|
+
from swarmauri_base.certs.CertServiceBase import CertServiceBase
|
|
23
|
+
from swarmauri_core.certs.ICertService import (
|
|
24
|
+
AltNameSpec,
|
|
25
|
+
CertBytes,
|
|
26
|
+
CsrBytes,
|
|
27
|
+
SubjectSpec,
|
|
28
|
+
CertExtensionSpec,
|
|
29
|
+
)
|
|
30
|
+
from swarmauri_core.crypto.types import KeyRef
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import boto3
|
|
34
|
+
except Exception: # pragma: no cover
|
|
35
|
+
boto3 = None
|
|
36
|
+
|
|
37
|
+
from cryptography import x509 as cx509
|
|
38
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
39
|
+
from cryptography.hazmat.primitives.asymmetric import ec, padding
|
|
40
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
41
|
+
|
|
42
|
+
from asn1crypto import algos as a_algos
|
|
43
|
+
from asn1crypto import core as a_core
|
|
44
|
+
from asn1crypto import x509 as ax509
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["AwsKmsCertService"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _now_utc() -> dt.datetime:
|
|
51
|
+
return dt.datetime.now(dt.timezone.utc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _to_utc(ts: Optional[int], *, default: Optional[int] = None) -> dt.datetime:
|
|
55
|
+
if ts is None:
|
|
56
|
+
ts = default if default is not None else int(_now_utc().timestamp())
|
|
57
|
+
return dt.datetime.fromtimestamp(int(ts), tz=dt.timezone.utc)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _pem(data_der: bytes, header: str) -> bytes:
|
|
61
|
+
b64 = base64.encodebytes(data_der).replace(b"\n", b"")
|
|
62
|
+
lines = [b64[i : i + 64] for i in range(0, len(b64), 64)]
|
|
63
|
+
body = b"\n".join(lines)
|
|
64
|
+
return (
|
|
65
|
+
b"-----BEGIN "
|
|
66
|
+
+ header.encode()
|
|
67
|
+
+ b"-----\n"
|
|
68
|
+
+ body
|
|
69
|
+
+ b"\n-----END "
|
|
70
|
+
+ header.encode()
|
|
71
|
+
+ b"-----\n"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _name_from_subject_spec(spec: SubjectSpec) -> ax509.Name:
|
|
76
|
+
mapping = (
|
|
77
|
+
("C", "country_name"),
|
|
78
|
+
("ST", "state_or_province_name"),
|
|
79
|
+
("L", "locality_name"),
|
|
80
|
+
("O", "organization_name"),
|
|
81
|
+
("OU", "organizational_unit_name"),
|
|
82
|
+
("CN", "common_name"),
|
|
83
|
+
("emailAddress", "email_address"),
|
|
84
|
+
)
|
|
85
|
+
attrs: dict[str, str] = {}
|
|
86
|
+
for short, long_name in mapping:
|
|
87
|
+
v = spec.get(short)
|
|
88
|
+
if v:
|
|
89
|
+
attrs[long_name] = v
|
|
90
|
+
extra = spec.get("extra_rdns") or {}
|
|
91
|
+
attrs.update(extra)
|
|
92
|
+
return ax509.Name.build(attrs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cryptography_name_from_spec(spec: SubjectSpec) -> cx509.Name:
|
|
96
|
+
attrs: list[cx509.NameAttribute] = []
|
|
97
|
+
oid_map = {
|
|
98
|
+
"C": cx509.NameOID.COUNTRY_NAME,
|
|
99
|
+
"ST": cx509.NameOID.STATE_OR_PROVINCE_NAME,
|
|
100
|
+
"L": cx509.NameOID.LOCALITY_NAME,
|
|
101
|
+
"O": cx509.NameOID.ORGANIZATION_NAME,
|
|
102
|
+
"OU": cx509.NameOID.ORGANIZATIONAL_UNIT_NAME,
|
|
103
|
+
"CN": cx509.NameOID.COMMON_NAME,
|
|
104
|
+
"emailAddress": cx509.NameOID.EMAIL_ADDRESS,
|
|
105
|
+
}
|
|
106
|
+
for k, oid in oid_map.items():
|
|
107
|
+
v = spec.get(k)
|
|
108
|
+
if v:
|
|
109
|
+
attrs.append(cx509.NameAttribute(oid, v))
|
|
110
|
+
extra = spec.get("extra_rdns") or {}
|
|
111
|
+
for k, v in extra.items():
|
|
112
|
+
attrs.append(cx509.NameAttribute(cx509.ObjectIdentifier(k), v))
|
|
113
|
+
return cx509.Name(attrs)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _spki_from_csr(csr: cx509.CertificateSigningRequest) -> ax509.PublicKeyInfo:
|
|
117
|
+
spki_der = csr.public_key().public_bytes(
|
|
118
|
+
Encoding.DER, PublicFormat.SubjectPublicKeyInfo
|
|
119
|
+
)
|
|
120
|
+
return ax509.PublicKeyInfo.load(spki_der)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _skid_from_pub(pub: Union[cx509.PublicKey, ax509.PublicKeyInfo]) -> bytes:
|
|
124
|
+
if isinstance(pub, ax509.PublicKeyInfo):
|
|
125
|
+
pk_bytes = pub["public_key"].native
|
|
126
|
+
else:
|
|
127
|
+
spki_der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
|
|
128
|
+
spki = ax509.PublicKeyInfo.load(spki_der)
|
|
129
|
+
pk_bytes = spki["public_key"].native
|
|
130
|
+
return hashlib.sha1(bytes(pk_bytes)).digest()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _akid_from_issuer_pub(
|
|
134
|
+
issuer_pub: Union[cx509.PublicKey, ax509.PublicKeyInfo],
|
|
135
|
+
) -> bytes:
|
|
136
|
+
return _skid_from_pub(issuer_pub)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _SigAlg:
|
|
140
|
+
def __init__(self, tbs_alg: a_algos.AlgorithmIdentifier, kms_alg: str) -> None:
|
|
141
|
+
self.tbs_alg = tbs_alg
|
|
142
|
+
self.kms_alg = kms_alg
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _rsa_pss_sha256_alg() -> _SigAlg:
|
|
146
|
+
params = a_algos.RSASSAPSSParams(
|
|
147
|
+
{
|
|
148
|
+
"hash_algorithm": a_algos.DigestAlgorithm({"algorithm": "sha256"}),
|
|
149
|
+
"mask_gen_algorithm": a_algos.MaskGenAlgorithm(
|
|
150
|
+
{
|
|
151
|
+
"algorithm": "mgf1",
|
|
152
|
+
"parameters": a_algos.DigestAlgorithm({"algorithm": "sha256"}),
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
"salt_length": 32,
|
|
156
|
+
"trailer_field": 1,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
alg = a_algos.AlgorithmIdentifier(
|
|
160
|
+
{"algorithm": "1.2.840.113549.1.1.10", "parameters": params}
|
|
161
|
+
)
|
|
162
|
+
return _SigAlg(alg, "RSASSA_PSS_SHA_256")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _rsa_pkcs1_sha256_alg() -> _SigAlg:
|
|
166
|
+
alg = a_algos.AlgorithmIdentifier({"algorithm": "1.2.840.113549.1.1.11"})
|
|
167
|
+
return _SigAlg(alg, "RSASSA_PKCS1_V1_5_SHA_256")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _ecdsa_sha256_alg() -> _SigAlg:
|
|
171
|
+
alg = a_algos.AlgorithmIdentifier({"algorithm": "ecdsa_sha256"})
|
|
172
|
+
return _SigAlg(alg, "ECDSA_SHA_256")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _pick_alg(sig_alg: Optional[str]) -> _SigAlg:
|
|
176
|
+
if sig_alg in (None, "RSA-PSS-SHA256", "RSASSA-PSS-SHA256"):
|
|
177
|
+
return _rsa_pss_sha256_alg()
|
|
178
|
+
if sig_alg in ("RSA-SHA256", "RSASSA-PKCS1v1_5-SHA256", "RS256"):
|
|
179
|
+
return _rsa_pkcs1_sha256_alg()
|
|
180
|
+
if sig_alg in ("ECDSA-P256-SHA256", "ECDSA-SHA256", "ES256"):
|
|
181
|
+
return _ecdsa_sha256_alg()
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"Unsupported signature algorithm '{sig_alg}'. "
|
|
184
|
+
"Use one of: RSA-PSS-SHA256 | RSA-SHA256 | ECDSA-P256-SHA256"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _ensure_boto3():
|
|
189
|
+
if boto3 is None:
|
|
190
|
+
raise RuntimeError(
|
|
191
|
+
"AwsKmsCertService requires boto3. Install with: pip install boto3"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _normalize_bytes_maybe_pem(data: bytes) -> Tuple[bytes, str]:
|
|
196
|
+
t = data.strip()
|
|
197
|
+
if t.startswith(b"-----BEGIN "):
|
|
198
|
+
header = t.splitlines()[0].decode()
|
|
199
|
+
if "CERTIFICATE REQUEST" in header or "NEW CERTIFICATE REQUEST" in header:
|
|
200
|
+
kind = "CERTIFICATE REQUEST"
|
|
201
|
+
elif "CERTIFICATE" in header:
|
|
202
|
+
kind = "CERTIFICATE"
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError("Unsupported PEM header")
|
|
205
|
+
lines = [ln for ln in t.splitlines() if b"-----" not in ln]
|
|
206
|
+
der = base64.b64decode(b"".join(lines))
|
|
207
|
+
return der, kind
|
|
208
|
+
try:
|
|
209
|
+
cx509.load_der_x509_csr(t)
|
|
210
|
+
return t, "CERTIFICATE REQUEST"
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
try:
|
|
214
|
+
cx509.load_der_x509_certificate(t)
|
|
215
|
+
return t, "CERTIFICATE"
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
return t, "CERTIFICATE"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _issuer_name_from_cert_pem(issuer_cert_pem: bytes) -> ax509.Name:
|
|
222
|
+
c = cx509.load_pem_x509_certificate(issuer_cert_pem)
|
|
223
|
+
name_der = c.subject.public_bytes(Encoding.DER)
|
|
224
|
+
return ax509.Name.load(name_der)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _cert_to_pem(cert: ax509.Certificate) -> bytes:
|
|
228
|
+
return _pem(cert.dump(), "CERTIFICATE")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _cx509_name_to_ax509_name(name: cx509.Name) -> ax509.Name:
|
|
232
|
+
return ax509.Name.load(name.public_bytes(Encoding.DER))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _mk_validity(not_before: dt.datetime, not_after: dt.datetime) -> ax509.Validity:
|
|
236
|
+
return ax509.Validity(
|
|
237
|
+
{
|
|
238
|
+
"not_before": ax509.Time({"utc_time": not_before}),
|
|
239
|
+
"not_after": ax509.Time({"utc_time": not_after}),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _rand_serial_160() -> int:
|
|
245
|
+
b = bytearray(os.urandom(20))
|
|
246
|
+
b[0] &= 0x7F
|
|
247
|
+
return int.from_bytes(bytes(b), "big")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _aws_kms_pubkey_info(kms_client, key_id: str) -> Tuple[ax509.PublicKeyInfo, str]:
|
|
251
|
+
resp = kms_client.get_public_key(KeyId=key_id)
|
|
252
|
+
spki = ax509.PublicKeyInfo.load(resp["PublicKey"])
|
|
253
|
+
key_spec: str = resp.get("KeySpec", "")
|
|
254
|
+
return spki, key_spec
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _csr_copy_extensions(csr: cx509.CertificateSigningRequest) -> list[ax509.Extension]:
|
|
258
|
+
exts: list[ax509.Extension] = []
|
|
259
|
+
for ext in csr.extensions:
|
|
260
|
+
exts.append(
|
|
261
|
+
ax509.Extension(
|
|
262
|
+
{
|
|
263
|
+
"extn_id": ax509.ExtensionId(ext.oid.dotted_string),
|
|
264
|
+
"critical": ext.critical,
|
|
265
|
+
"extn_value": a_core.OctetString(
|
|
266
|
+
ext.value.public_bytes(Encoding.DER)
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return exts
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _combine_extensions(
|
|
275
|
+
csr_exts: list[ax509.Extension],
|
|
276
|
+
extra: Optional[CertExtensionSpec],
|
|
277
|
+
subject_pub: ax509.PublicKeyInfo,
|
|
278
|
+
issuer_pub: Optional[ax509.PublicKeyInfo],
|
|
279
|
+
) -> ax509.Extensions:
|
|
280
|
+
by_oid: Dict[str, ax509.Extension] = {
|
|
281
|
+
ext["extn_id"].native: ext for ext in csr_exts
|
|
282
|
+
}
|
|
283
|
+
skid = _skid_from_pub(subject_pub)
|
|
284
|
+
by_oid["subject_key_identifier"] = ax509.Extension(
|
|
285
|
+
{
|
|
286
|
+
"extn_id": "subject_key_identifier",
|
|
287
|
+
"critical": False,
|
|
288
|
+
"extn_value": ax509.SubjectKeyIdentifier(skid),
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
if issuer_pub is not None:
|
|
292
|
+
akid = _akid_from_issuer_pub(issuer_pub)
|
|
293
|
+
by_oid["authority_key_identifier"] = ax509.Extension(
|
|
294
|
+
{
|
|
295
|
+
"extn_id": "authority_key_identifier",
|
|
296
|
+
"critical": False,
|
|
297
|
+
"extn_value": ax509.AuthorityKeyIdentifier({"key_identifier": akid}),
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
return ax509.Extensions(list(by_oid.values()))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _build_tbs_from_csr(
|
|
304
|
+
csr: cx509.CertificateSigningRequest,
|
|
305
|
+
issuer_name: ax509.Name,
|
|
306
|
+
serial: Optional[int],
|
|
307
|
+
not_before: dt.datetime,
|
|
308
|
+
not_after: dt.datetime,
|
|
309
|
+
sig_alg: _SigAlg,
|
|
310
|
+
issuer_spki: Optional[ax509.PublicKeyInfo],
|
|
311
|
+
) -> Tuple[ax509.TbsCertificate, ax509.PublicKeyInfo]:
|
|
312
|
+
subject_name_ax = _cx509_name_to_ax509_name(csr.subject)
|
|
313
|
+
sub_spki = _spki_from_csr(csr)
|
|
314
|
+
exts = _combine_extensions(
|
|
315
|
+
_csr_copy_extensions(csr),
|
|
316
|
+
extra=None,
|
|
317
|
+
subject_pub=sub_spki,
|
|
318
|
+
issuer_pub=issuer_spki,
|
|
319
|
+
)
|
|
320
|
+
tbs = ax509.TbsCertificate(
|
|
321
|
+
{
|
|
322
|
+
"version": "v3",
|
|
323
|
+
"serial_number": serial if serial is not None else _rand_serial_160(),
|
|
324
|
+
"signature": sig_alg.tbs_alg,
|
|
325
|
+
"issuer": issuer_name,
|
|
326
|
+
"validity": _mk_validity(not_before, not_after),
|
|
327
|
+
"subject": subject_name_ax,
|
|
328
|
+
"subject_public_key_info": sub_spki,
|
|
329
|
+
"extensions": exts,
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
return tbs, sub_spki
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _assemble_cert(
|
|
336
|
+
tbs: ax509.TbsCertificate, sig_alg: _SigAlg, signature_bytes: bytes
|
|
337
|
+
) -> ax509.Certificate:
|
|
338
|
+
return ax509.Certificate(
|
|
339
|
+
{
|
|
340
|
+
"tbs_certificate": tbs,
|
|
341
|
+
"signature_algorithm": sig_alg.tbs_alg,
|
|
342
|
+
"signature_value": a_core.BitString.from_bytes(signature_bytes),
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _kms_sign(kms, key_id: str, alg: _SigAlg, tbs_der: bytes) -> bytes:
|
|
348
|
+
resp = kms.sign(
|
|
349
|
+
KeyId=key_id, Message=tbs_der, MessageType="RAW", SigningAlgorithm=alg.kms_alg
|
|
350
|
+
)
|
|
351
|
+
return resp["Signature"]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _extract_kms_key_id_from_keyref(ca_key: KeyRef) -> str:
|
|
355
|
+
if ca_key.tags and "aws_kms_key_id" in ca_key.tags:
|
|
356
|
+
return str(ca_key.tags["aws_kms_key_id"])
|
|
357
|
+
if ca_key.tags and "kms_key_id" in ca_key.tags:
|
|
358
|
+
return str(ca_key.tags["kms_key_id"])
|
|
359
|
+
if ca_key.kid:
|
|
360
|
+
return str(ca_key.kid)
|
|
361
|
+
raise ValueError(
|
|
362
|
+
"AwsKmsCertService: unable to resolve KMS KeyId from KeyRef (expected tags['aws_kms_key_id'] or kid)."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@ComponentBase.register_type(CertServiceBase, "AwsKmsCertService")
|
|
367
|
+
class AwsKmsCertService(CertServiceBase):
|
|
368
|
+
resource: Optional[str] = Field(default=ResourceTypes.CRYPTO.value, frozen=True)
|
|
369
|
+
type: Literal["AwsKmsCertService"] = "AwsKmsCertService"
|
|
370
|
+
|
|
371
|
+
def __init__(
|
|
372
|
+
self,
|
|
373
|
+
*,
|
|
374
|
+
region_name: Optional[str] = None,
|
|
375
|
+
endpoint_url: Optional[str] = None,
|
|
376
|
+
session: Optional["boto3.session.Session"] = None, # type: ignore[name-defined]
|
|
377
|
+
default_sig_alg: str = "RSA-PSS-SHA256",
|
|
378
|
+
) -> None:
|
|
379
|
+
super().__init__()
|
|
380
|
+
self._region = region_name
|
|
381
|
+
self._endpoint = endpoint_url
|
|
382
|
+
self._session = session
|
|
383
|
+
self._default_sig_alg = default_sig_alg
|
|
384
|
+
self._kms = None
|
|
385
|
+
|
|
386
|
+
def _client(self):
|
|
387
|
+
_ensure_boto3()
|
|
388
|
+
if self._kms is not None:
|
|
389
|
+
return self._kms
|
|
390
|
+
sess = self._session or boto3.session.Session()
|
|
391
|
+
self._kms = sess.client(
|
|
392
|
+
"kms", region_name=self._region, endpoint_url=self._endpoint
|
|
393
|
+
)
|
|
394
|
+
return self._kms
|
|
395
|
+
|
|
396
|
+
def supports(self) -> Mapping[str, Iterable[str]]:
|
|
397
|
+
return {
|
|
398
|
+
"key_algs": ("RSA-2048", "RSA-3072", "RSA-4096", "EC-P256", "EC-P384"),
|
|
399
|
+
"sig_algs": ("RSA-PSS-SHA256", "RSA-SHA256", "ECDSA-P256-SHA256"),
|
|
400
|
+
"features": (
|
|
401
|
+
"sign_from_csr",
|
|
402
|
+
"self_signed",
|
|
403
|
+
"verify",
|
|
404
|
+
"parse",
|
|
405
|
+
"akid",
|
|
406
|
+
"skid",
|
|
407
|
+
),
|
|
408
|
+
"profiles": ("server", "client", "intermediate", "root"),
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async def create_csr(
|
|
412
|
+
self,
|
|
413
|
+
key: KeyRef,
|
|
414
|
+
subject: SubjectSpec,
|
|
415
|
+
*,
|
|
416
|
+
san: Optional[AltNameSpec] = None,
|
|
417
|
+
extensions: Optional[CertExtensionSpec] = None,
|
|
418
|
+
sig_alg: Optional[str] = None,
|
|
419
|
+
challenge_password: Optional[str] = None,
|
|
420
|
+
output_der: bool = False,
|
|
421
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
422
|
+
) -> CsrBytes:
|
|
423
|
+
if not key.material:
|
|
424
|
+
raise NotImplementedError(
|
|
425
|
+
"create_csr requires exportable private key material in KeyRef.material"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
priv = serialization.load_pem_private_key(key.material, password=None)
|
|
429
|
+
builder = cx509.CertificateSigningRequestBuilder().subject_name(
|
|
430
|
+
_cryptography_name_from_spec(subject)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
san_list: list[cx509.GeneralName] = []
|
|
434
|
+
if san:
|
|
435
|
+
for d in san.get("dns") or []:
|
|
436
|
+
san_list.append(cx509.DNSName(d))
|
|
437
|
+
for ip in san.get("ip") or []:
|
|
438
|
+
san_list.append(cx509.IPAddress(cx509.ipaddress.ip_address(ip)))
|
|
439
|
+
for uri in san.get("uri") or []:
|
|
440
|
+
san_list.append(cx509.UniformResourceIdentifier(uri))
|
|
441
|
+
for em in san.get("email") or []:
|
|
442
|
+
san_list.append(cx509.RFC822Name(em))
|
|
443
|
+
if san_list:
|
|
444
|
+
builder = builder.add_extension(
|
|
445
|
+
cx509.SubjectAlternativeName(san_list), critical=False
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
chosen_hash = hashes.SHA256()
|
|
449
|
+
csr = builder.sign(priv, chosen_hash)
|
|
450
|
+
der = csr.public_bytes(Encoding.DER)
|
|
451
|
+
return (
|
|
452
|
+
der
|
|
453
|
+
if output_der
|
|
454
|
+
else cx509.load_der_x509_csr(der).public_bytes(Encoding.PEM)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
async def create_self_signed(
|
|
458
|
+
self,
|
|
459
|
+
key: KeyRef,
|
|
460
|
+
subject: SubjectSpec,
|
|
461
|
+
*,
|
|
462
|
+
serial: Optional[int] = None,
|
|
463
|
+
not_before: Optional[int] = None,
|
|
464
|
+
not_after: Optional[int] = None,
|
|
465
|
+
extensions: Optional[CertExtensionSpec] = None,
|
|
466
|
+
sig_alg: Optional[str] = None,
|
|
467
|
+
output_der: bool = False,
|
|
468
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
469
|
+
) -> CertBytes:
|
|
470
|
+
kms = self._client()
|
|
471
|
+
key_id = _extract_kms_key_id_from_keyref(key)
|
|
472
|
+
sig = _pick_alg(sig_alg or self._default_sig_alg)
|
|
473
|
+
issuer_name_ax = _name_from_subject_spec(subject)
|
|
474
|
+
issuer_spki, _ = _aws_kms_pubkey_info(kms, key_id)
|
|
475
|
+
|
|
476
|
+
nbf = _to_utc(not_before, default=int(_now_utc().timestamp() - 300))
|
|
477
|
+
naf = _to_utc(
|
|
478
|
+
not_after, default=int((_now_utc() + dt.timedelta(days=365)).timestamp())
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
tbs = ax509.TbsCertificate(
|
|
482
|
+
{
|
|
483
|
+
"version": "v3",
|
|
484
|
+
"serial_number": serial if serial is not None else _rand_serial_160(),
|
|
485
|
+
"signature": sig.tbs_alg,
|
|
486
|
+
"issuer": issuer_name_ax,
|
|
487
|
+
"validity": _mk_validity(nbf, naf),
|
|
488
|
+
"subject": issuer_name_ax,
|
|
489
|
+
"subject_public_key_info": issuer_spki,
|
|
490
|
+
"extensions": ax509.Extensions(
|
|
491
|
+
[
|
|
492
|
+
ax509.Extension(
|
|
493
|
+
{
|
|
494
|
+
"extn_id": "basic_constraints",
|
|
495
|
+
"critical": True,
|
|
496
|
+
"extn_value": ax509.BasicConstraints(
|
|
497
|
+
{"ca": True, "path_len_constraint": 1}
|
|
498
|
+
),
|
|
499
|
+
}
|
|
500
|
+
),
|
|
501
|
+
ax509.Extension(
|
|
502
|
+
{
|
|
503
|
+
"extn_id": "subject_key_identifier",
|
|
504
|
+
"critical": False,
|
|
505
|
+
"extn_value": ax509.SubjectKeyIdentifier(
|
|
506
|
+
_skid_from_pub(issuer_spki)
|
|
507
|
+
),
|
|
508
|
+
}
|
|
509
|
+
),
|
|
510
|
+
]
|
|
511
|
+
),
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
tbs_der = tbs.dump()
|
|
516
|
+
signature = _kms_sign(kms, key_id, sig, tbs_der)
|
|
517
|
+
cert = _assemble_cert(tbs, sig, signature)
|
|
518
|
+
der = cert.dump()
|
|
519
|
+
return der if output_der else _cert_to_pem(cert)
|
|
520
|
+
|
|
521
|
+
async def sign_cert(
|
|
522
|
+
self,
|
|
523
|
+
csr: CsrBytes,
|
|
524
|
+
ca_key: KeyRef,
|
|
525
|
+
*,
|
|
526
|
+
issuer: Optional[SubjectSpec] = None,
|
|
527
|
+
ca_cert: Optional[CertBytes] = None,
|
|
528
|
+
serial: Optional[int] = None,
|
|
529
|
+
not_before: Optional[int] = None,
|
|
530
|
+
not_after: Optional[int] = None,
|
|
531
|
+
extensions: Optional[CertExtensionSpec] = None,
|
|
532
|
+
sig_alg: Optional[str] = None,
|
|
533
|
+
output_der: bool = False,
|
|
534
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
535
|
+
) -> CertBytes:
|
|
536
|
+
kms = self._client()
|
|
537
|
+
key_id = _extract_kms_key_id_from_keyref(ca_key)
|
|
538
|
+
sig = _pick_alg(sig_alg or self._default_sig_alg)
|
|
539
|
+
|
|
540
|
+
csr_der, kind = _normalize_bytes_maybe_pem(csr)
|
|
541
|
+
if kind != "CERTIFICATE REQUEST":
|
|
542
|
+
raise ValueError("sign_cert expects a CSR (PKCS#10)")
|
|
543
|
+
csr_obj = cx509.load_der_x509_csr(csr_der)
|
|
544
|
+
|
|
545
|
+
if ca_cert is not None:
|
|
546
|
+
ic_der, _ = _normalize_bytes_maybe_pem(ca_cert)
|
|
547
|
+
ic = cx509.load_der_x509_certificate(ic_der)
|
|
548
|
+
issuer_name_ax = _cx509_name_to_ax509_name(ic.subject)
|
|
549
|
+
issuer_spki = ax509.PublicKeyInfo.load(
|
|
550
|
+
ic.public_key().public_bytes(
|
|
551
|
+
Encoding.DER, PublicFormat.SubjectPublicKeyInfo
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
else:
|
|
555
|
+
if issuer is None:
|
|
556
|
+
raise ValueError(
|
|
557
|
+
"sign_cert: either 'ca_cert' or 'issuer' must be provided"
|
|
558
|
+
)
|
|
559
|
+
issuer_name_ax = _name_from_subject_spec(issuer)
|
|
560
|
+
issuer_spki, _ = _aws_kms_pubkey_info(kms, key_id)
|
|
561
|
+
|
|
562
|
+
nbf = _to_utc(not_before, default=int(_now_utc().timestamp() - 300))
|
|
563
|
+
naf = _to_utc(
|
|
564
|
+
not_after, default=int((_now_utc() + dt.timedelta(days=365)).timestamp())
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
tbs, _ = _build_tbs_from_csr(
|
|
568
|
+
csr_obj, issuer_name_ax, serial, nbf, naf, sig, issuer_spki
|
|
569
|
+
)
|
|
570
|
+
signature = _kms_sign(kms, key_id, sig, tbs.dump())
|
|
571
|
+
cert = _assemble_cert(tbs, sig, signature)
|
|
572
|
+
der = cert.dump()
|
|
573
|
+
return der if output_der else _cert_to_pem(cert)
|
|
574
|
+
|
|
575
|
+
async def verify_cert(
|
|
576
|
+
self,
|
|
577
|
+
cert: CertBytes,
|
|
578
|
+
*,
|
|
579
|
+
trust_roots: Optional[Sequence[CertBytes]] = None,
|
|
580
|
+
intermediates: Optional[Sequence[CertBytes]] = None,
|
|
581
|
+
check_time: Optional[int] = None,
|
|
582
|
+
check_revocation: bool = False,
|
|
583
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
584
|
+
) -> Dict[str, Any]:
|
|
585
|
+
der, _ = _normalize_bytes_maybe_pem(cert)
|
|
586
|
+
c = cx509.load_der_x509_certificate(der)
|
|
587
|
+
|
|
588
|
+
now = _to_utc(check_time) if check_time else _now_utc()
|
|
589
|
+
if c.not_valid_before > now or c.not_valid_after < now:
|
|
590
|
+
return {
|
|
591
|
+
"valid": False,
|
|
592
|
+
"reason": "time_window",
|
|
593
|
+
"not_before": int(c.not_valid_before.timestamp()),
|
|
594
|
+
"not_after": int(c.not_valid_after.timestamp()),
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
issuer_cert: Optional[cx509.Certificate] = None
|
|
598
|
+
seq = list(intermediates or []) + list(trust_roots or [])
|
|
599
|
+
for cand in seq:
|
|
600
|
+
cd, _ = _normalize_bytes_maybe_pem(cand)
|
|
601
|
+
try:
|
|
602
|
+
issuer_cert = cx509.load_der_x509_certificate(cd)
|
|
603
|
+
if issuer_cert.subject == c.issuer:
|
|
604
|
+
break
|
|
605
|
+
issuer_cert = None
|
|
606
|
+
except Exception:
|
|
607
|
+
continue
|
|
608
|
+
|
|
609
|
+
if issuer_cert is None:
|
|
610
|
+
return {
|
|
611
|
+
"valid": True,
|
|
612
|
+
"reason": None,
|
|
613
|
+
"issuer_known": False,
|
|
614
|
+
"subject": c.subject.rfc4514_string(),
|
|
615
|
+
"not_before": int(c.not_valid_before.timestamp()),
|
|
616
|
+
"not_after": int(c.not_valid_after.timestamp()),
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
pub = issuer_cert.public_key()
|
|
620
|
+
tbs = c.tbs_certificate_bytes
|
|
621
|
+
try:
|
|
622
|
+
sa = c.signature_algorithm_oid
|
|
623
|
+
if sa._name in ("sha256_rsa", "sha384_rsa", "sha512_rsa"):
|
|
624
|
+
pub.verify(
|
|
625
|
+
c.signature, tbs, padding.PKCS1v15(), c.signature_hash_algorithm
|
|
626
|
+
)
|
|
627
|
+
elif (
|
|
628
|
+
sa.dotted_string == cx509.SignatureAlgorithmOID.RSASSA_PSS.dotted_string
|
|
629
|
+
):
|
|
630
|
+
pub.verify(
|
|
631
|
+
c.signature,
|
|
632
|
+
tbs,
|
|
633
|
+
padding.PSS(
|
|
634
|
+
mgf=padding.MGF1(c.signature_hash_algorithm),
|
|
635
|
+
salt_length=c.signature_hash_algorithm.digest_size,
|
|
636
|
+
),
|
|
637
|
+
c.signature_hash_algorithm,
|
|
638
|
+
)
|
|
639
|
+
elif sa._name in (
|
|
640
|
+
"ecdsa_with_sha256",
|
|
641
|
+
"ecdsa_with_sha384",
|
|
642
|
+
"ecdsa_with_sha512",
|
|
643
|
+
):
|
|
644
|
+
pub.verify(c.signature, tbs, ec.ECDSA(c.signature_hash_algorithm))
|
|
645
|
+
else:
|
|
646
|
+
raise ValueError(
|
|
647
|
+
f"Unsupported signature algorithm for verify: {sa._name}"
|
|
648
|
+
)
|
|
649
|
+
except Exception as e: # pragma: no cover - errors tested via failure cases
|
|
650
|
+
return {"valid": False, "reason": f"signature:{e.__class__.__name__}"}
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
"valid": True,
|
|
654
|
+
"reason": None,
|
|
655
|
+
"issuer_known": True,
|
|
656
|
+
"issuer": issuer_cert.subject.rfc4514_string(),
|
|
657
|
+
"subject": c.subject.rfc4514_string(),
|
|
658
|
+
"not_before": int(c.not_valid_before.timestamp()),
|
|
659
|
+
"not_after": int(c.not_valid_after.timestamp()),
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async def parse_cert(
|
|
663
|
+
self,
|
|
664
|
+
cert: CertBytes,
|
|
665
|
+
*,
|
|
666
|
+
include_extensions: bool = True,
|
|
667
|
+
opts: Optional[Dict[str, Any]] = None,
|
|
668
|
+
) -> Dict[str, Any]:
|
|
669
|
+
der, _ = _normalize_bytes_maybe_pem(cert)
|
|
670
|
+
c = cx509.load_der_x509_certificate(der)
|
|
671
|
+
|
|
672
|
+
out: Dict[str, Any] = {
|
|
673
|
+
"serial": c.serial_number,
|
|
674
|
+
"sig_alg": c.signature_algorithm_oid.dotted_string,
|
|
675
|
+
"issuer": c.issuer.rfc4514_string(),
|
|
676
|
+
"subject": c.subject.rfc4514_string(),
|
|
677
|
+
"not_before": int(c.not_valid_before.timestamp()),
|
|
678
|
+
"not_after": int(c.not_valid_after.timestamp()),
|
|
679
|
+
"is_ca": False,
|
|
680
|
+
}
|
|
681
|
+
if include_extensions:
|
|
682
|
+
try:
|
|
683
|
+
bc = c.extensions.get_extension_for_class(cx509.BasicConstraints).value
|
|
684
|
+
out["is_ca"] = bool(bc.ca)
|
|
685
|
+
if bc.path_length is not None:
|
|
686
|
+
out["path_len"] = bc.path_length
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
689
|
+
try:
|
|
690
|
+
san = c.extensions.get_extension_for_class(
|
|
691
|
+
cx509.SubjectAlternativeName
|
|
692
|
+
).value
|
|
693
|
+
out["san"] = {
|
|
694
|
+
"dns": [n.value for n in san.get_values_for_type(cx509.DNSName)],
|
|
695
|
+
"ip": [
|
|
696
|
+
str(n.value) for n in san.get_values_for_type(cx509.IPAddress)
|
|
697
|
+
],
|
|
698
|
+
"uri": [
|
|
699
|
+
n.value
|
|
700
|
+
for n in san.get_values_for_type(
|
|
701
|
+
cx509.UniformResourceIdentifier
|
|
702
|
+
)
|
|
703
|
+
],
|
|
704
|
+
"email": [
|
|
705
|
+
n.value for n in san.get_values_for_type(cx509.RFC822Name)
|
|
706
|
+
],
|
|
707
|
+
}
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
710
|
+
try:
|
|
711
|
+
ku = c.extensions.get_extension_for_class(cx509.KeyUsage).value
|
|
712
|
+
out["key_usage"] = {
|
|
713
|
+
"digital_signature": ku.digital_signature,
|
|
714
|
+
"content_commitment": ku.content_commitment,
|
|
715
|
+
"key_encipherment": ku.key_encipherment,
|
|
716
|
+
"data_encipherment": ku.data_encipherment,
|
|
717
|
+
"key_agreement": ku.key_agreement,
|
|
718
|
+
"key_cert_sign": ku.key_cert_sign,
|
|
719
|
+
"crl_sign": ku.crl_sign,
|
|
720
|
+
"encipher_only": ku.encipher_only,
|
|
721
|
+
"decipher_only": ku.decipher_only,
|
|
722
|
+
}
|
|
723
|
+
except Exception:
|
|
724
|
+
pass
|
|
725
|
+
try:
|
|
726
|
+
eku = c.extensions.get_extension_for_class(cx509.ExtendedKeyUsage).value
|
|
727
|
+
out["eku"] = [oid.dotted_string for oid in eku]
|
|
728
|
+
except Exception:
|
|
729
|
+
pass
|
|
730
|
+
return out
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: swarmauri_certservice_aws_kms
|
|
3
|
+
Version: 0.3.0.dev4
|
|
4
|
+
Summary: AWS KMS backed CertService for Swarmauri
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: Swarmauri
|
|
7
|
+
Author-email: opensource@swarmauri.com
|
|
8
|
+
Requires-Python: >=3.10,<3.13
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Provides-Extra: docs
|
|
14
|
+
Provides-Extra: perf
|
|
15
|
+
Requires-Dist: asn1crypto
|
|
16
|
+
Requires-Dist: boto3 (>=1.28.0)
|
|
17
|
+
Requires-Dist: cryptography
|
|
18
|
+
Requires-Dist: mkdocs ; extra == "docs"
|
|
19
|
+
Requires-Dist: pytest-benchmark (>=4.0.0) ; extra == "perf"
|
|
20
|
+
Requires-Dist: swarmauri_base
|
|
21
|
+
Requires-Dist: swarmauri_core
|
|
22
|
+
Requires-Dist: swarmauri_standard
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# swarmauri_certservice_aws_kms
|
|
26
|
+
|
|
27
|
+
AWS KMS backed certificate service for Swarmauri.
|
|
28
|
+
|
|
29
|
+
This package provides an implementation of `CertServiceBase` that signs and verifies X.509 certificates using AWS Key Management Service.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Create CSRs from exportable key material.
|
|
34
|
+
- Issue certificates using AWS KMS `Sign` API.
|
|
35
|
+
- Create self‑signed certificates.
|
|
36
|
+
- Verify and parse certificates with RFC 5280 compliance.
|
|
37
|
+
|
|
38
|
+
## Extras
|
|
39
|
+
|
|
40
|
+
- `docs`: documentation helpers.
|
|
41
|
+
- `perf`: benchmarking support.
|
|
42
|
+
|
|
43
|
+
## Testing
|
|
44
|
+
|
|
45
|
+
Run unit, functional and performance tests in isolation from the repository root:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv run --package swarmauri_certservice_aws_kms --directory community/swarmauri_certservice_aws_kms pytest
|
|
49
|
+
```
|
|
50
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
swarmauri_certservice_aws_kms/AwsKmsCertService.py,sha256=wH5XL8gOhMKcx8_abFSLp_8SmwKwNopcwhffjWitGPE,25218
|
|
2
|
+
swarmauri_certservice_aws_kms/__init__.py,sha256=U5WPk0mZIkE7n-9qGr-aHhnDpLPyBjCQOfgH1M35XQk,82
|
|
3
|
+
swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/METADATA,sha256=kfem7Ne2WyZWWMrVx3RzZbOZ6gSkNZrPyr10RIhCdIk,1528
|
|
4
|
+
swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
5
|
+
swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/entry_points.txt,sha256=ihiP9ySNdcVg0EiVfQusQnF_O_-bH8v5r5FwncLE9iI,111
|
|
6
|
+
swarmauri_certservice_aws_kms-0.3.0.dev4.dist-info/RECORD,,
|