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.
@@ -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,3 @@
1
+ from .AwsKmsCertService import AwsKmsCertService
2
+
3
+ __all__ = ["AwsKmsCertService"]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [swarmauri.cert_services]
2
+ AwsKmsCertService=swarmauri_certservice_aws_kms.AwsKmsCertService:AwsKmsCertService
3
+