swarmauri_transport_https_unicast 0.11.0.dev5__tar.gz

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 @@
1
+ Apache-2.0
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: swarmauri_transport_https_unicast
3
+ Version: 0.11.0.dev5
4
+ Summary: Policy-bound HTTPS unicast transport for Swarmauri.
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE
7
+ Keywords: swarmauri,sdk,standards,transport,https,httpx,tls,tls13,x509,certificate-validation,http-signature,jws,bearer-token,unicast
8
+ Author: Jacob Stewart
9
+ Author-email: jacob@swarmauri.com
10
+ Requires-Python: >=3.10,<3.15
11
+ Classifier: Development Status :: 1 - Planning
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Natural Language :: English
15
+ Classifier: Topic :: Internet :: WWW/HTTP
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Requires-Dist: httpx (>=0.27.0,<1)
27
+ Requires-Dist: swarmauri_base (>=0.11.0.dev5)
28
+ Requires-Dist: swarmauri_certs_x509verify
29
+ Requires-Dist: swarmauri_cipher_suite_tls13
30
+ Requires-Dist: swarmauri_core (>=0.11.0.dev5)
31
+ Requires-Dist: swarmauri_middleware_httpsig
32
+ Requires-Dist: swarmauri_signing_jws
33
+ Project-URL: Documentation, https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast#readme
34
+ Project-URL: Homepage, https://github.com/swarmauri/swarmauri-sdk
35
+ Project-URL: Source, https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast
36
+ Description-Content-Type: text/markdown
37
+
38
+ ![Swarmauri Logo](https://github.com/swarmauri/swarmauri-sdk/blob/3d4d1cfa949399d7019ae9d8f296afba773dfb7f/assets/swarmauri.brand.theme.svg)
39
+
40
+ <p align="center">
41
+ <a href="https://pepy.tech/project/swarmauri_transport_https_unicast/">
42
+ <img src="https://static.pepy.tech/badge/swarmauri_transport_https_unicast/month" alt="PyPI - Downloads"/></a>
43
+ <a href="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast/">
44
+ <img alt="Hits" src="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast.svg"/></a>
45
+ <a href="https://pypi.org/project/swarmauri_transport_https_unicast/">
46
+ <img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue" alt="PyPI - Python Version"/></a>
47
+ <a href="https://pypi.org/project/swarmauri_transport_https_unicast/">
48
+ <img src="https://img.shields.io/pypi/l/swarmauri_transport_https_unicast" alt="PyPI - License"/></a>
49
+ <a href="https://pypi.org/project/swarmauri_transport_https_unicast/">
50
+ <img src="https://img.shields.io/pypi/v/swarmauri_transport_https_unicast?label=swarmauri_transport_https_unicast&color=green" alt="PyPI - swarmauri_transport_https_unicast"/></a>
51
+ </p>
52
+
53
+ # swarmauri_transport_https_unicast
54
+
55
+ `swarmauri_transport_https_unicast` provides a policy-bound HTTPS client
56
+ transport that composes HTTPX with Swarmauri TLS 1.3 cipher policy, X.509
57
+ certificate verification, HTTP signature headers, bearer-token forwarding, and
58
+ JWS body signing or verification.
59
+
60
+ ## Features
61
+
62
+ - Advertises HTTPS-over-TLS unicast transport capabilities through
63
+ `TransportBase`.
64
+ - Uses HTTPX for concrete HTTPS request dispatch.
65
+ - Applies bearer tokens, `HttpSigMiddleware`-compatible HMAC headers, and JWS
66
+ request-body signatures.
67
+ - Verifies response-body JWS payloads before returning transport results.
68
+ - Exposes X.509 preflight verification through `X509VerifyService`.
69
+ - Publishes TLS 1.3 cipher-suite policy metadata through `Tls13CipherSuite`.
70
+
71
+ ## Installation
72
+
73
+ With `uv`:
74
+
75
+ ```bash
76
+ uv add swarmauri_transport_https_unicast
77
+ ```
78
+
79
+ With `pip`:
80
+
81
+ ```bash
82
+ pip install swarmauri_transport_https_unicast
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ ```python
88
+ import asyncio
89
+
90
+ from swarmauri_transport_https_unicast import (
91
+ HttpsSecurityPolicy,
92
+ HttpsUnicastTransport,
93
+ )
94
+
95
+
96
+ async def main() -> None:
97
+ body = b'{"order_id":"ord_1001"}'
98
+ jws_key = {"kind": "raw", "key": b"transport-demo-secret-32-bytes!!", "kid": "body.1"}
99
+ policy = HttpsSecurityPolicy(
100
+ bearer_token="jwt-or-other-bearer-token",
101
+ http_signature_secret="shared-secret",
102
+ request_jws_key=jws_key,
103
+ request_jws_kid="body.1",
104
+ response_jws_key=jws_key,
105
+ )
106
+ transport = HttpsUnicastTransport(
107
+ base_url="https://127.0.0.1:8443",
108
+ verify="/path/to/trust-anchor.pem",
109
+ security_policy=policy,
110
+ require_response_jws=True,
111
+ )
112
+
113
+ async with transport.client():
114
+ status, headers, response_body = await transport.request(
115
+ "POST", "/secure/orders", body=body
116
+ )
117
+
118
+ print(status, headers["content-type"], response_body)
119
+ print(transport.last_evidence)
120
+
121
+
122
+ asyncio.run(main())
123
+ ```
124
+
125
+ Expected output for a service that returns `application/json` and body
126
+ `b'{"ok":true}'`:
127
+
128
+ ```text
129
+ 200 application/json b'{"ok":true}'
130
+ HttpsTransportEvidence(status_code=200, response_jws_verified=True, certificate_valid=None, certificate_subject=None, cipher_suite='tls13', cipher_alg='TLS_AES_256_GCM_SHA384')
131
+ ```
132
+
133
+ Use `verify_certificate()` when a service has a PEM certificate or chain that
134
+ should be validated against explicit trust roots before making transport calls.
135
+ Use `allowed_tls_cipher()` on server instrumentation to confirm the negotiated
136
+ TLS cipher is within the Swarmauri TLS 1.3 policy.
137
+
138
+ ## Project Links
139
+
140
+ - Source:
141
+ <https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast>
142
+ - Issues: <https://github.com/swarmauri/swarmauri-sdk/issues>
143
+
@@ -0,0 +1,105 @@
1
+ ![Swarmauri Logo](https://github.com/swarmauri/swarmauri-sdk/blob/3d4d1cfa949399d7019ae9d8f296afba773dfb7f/assets/swarmauri.brand.theme.svg)
2
+
3
+ <p align="center">
4
+ <a href="https://pepy.tech/project/swarmauri_transport_https_unicast/">
5
+ <img src="https://static.pepy.tech/badge/swarmauri_transport_https_unicast/month" alt="PyPI - Downloads"/></a>
6
+ <a href="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast/">
7
+ <img alt="Hits" src="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast.svg"/></a>
8
+ <a href="https://pypi.org/project/swarmauri_transport_https_unicast/">
9
+ <img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue" alt="PyPI - Python Version"/></a>
10
+ <a href="https://pypi.org/project/swarmauri_transport_https_unicast/">
11
+ <img src="https://img.shields.io/pypi/l/swarmauri_transport_https_unicast" alt="PyPI - License"/></a>
12
+ <a href="https://pypi.org/project/swarmauri_transport_https_unicast/">
13
+ <img src="https://img.shields.io/pypi/v/swarmauri_transport_https_unicast?label=swarmauri_transport_https_unicast&color=green" alt="PyPI - swarmauri_transport_https_unicast"/></a>
14
+ </p>
15
+
16
+ # swarmauri_transport_https_unicast
17
+
18
+ `swarmauri_transport_https_unicast` provides a policy-bound HTTPS client
19
+ transport that composes HTTPX with Swarmauri TLS 1.3 cipher policy, X.509
20
+ certificate verification, HTTP signature headers, bearer-token forwarding, and
21
+ JWS body signing or verification.
22
+
23
+ ## Features
24
+
25
+ - Advertises HTTPS-over-TLS unicast transport capabilities through
26
+ `TransportBase`.
27
+ - Uses HTTPX for concrete HTTPS request dispatch.
28
+ - Applies bearer tokens, `HttpSigMiddleware`-compatible HMAC headers, and JWS
29
+ request-body signatures.
30
+ - Verifies response-body JWS payloads before returning transport results.
31
+ - Exposes X.509 preflight verification through `X509VerifyService`.
32
+ - Publishes TLS 1.3 cipher-suite policy metadata through `Tls13CipherSuite`.
33
+
34
+ ## Installation
35
+
36
+ With `uv`:
37
+
38
+ ```bash
39
+ uv add swarmauri_transport_https_unicast
40
+ ```
41
+
42
+ With `pip`:
43
+
44
+ ```bash
45
+ pip install swarmauri_transport_https_unicast
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ import asyncio
52
+
53
+ from swarmauri_transport_https_unicast import (
54
+ HttpsSecurityPolicy,
55
+ HttpsUnicastTransport,
56
+ )
57
+
58
+
59
+ async def main() -> None:
60
+ body = b'{"order_id":"ord_1001"}'
61
+ jws_key = {"kind": "raw", "key": b"transport-demo-secret-32-bytes!!", "kid": "body.1"}
62
+ policy = HttpsSecurityPolicy(
63
+ bearer_token="jwt-or-other-bearer-token",
64
+ http_signature_secret="shared-secret",
65
+ request_jws_key=jws_key,
66
+ request_jws_kid="body.1",
67
+ response_jws_key=jws_key,
68
+ )
69
+ transport = HttpsUnicastTransport(
70
+ base_url="https://127.0.0.1:8443",
71
+ verify="/path/to/trust-anchor.pem",
72
+ security_policy=policy,
73
+ require_response_jws=True,
74
+ )
75
+
76
+ async with transport.client():
77
+ status, headers, response_body = await transport.request(
78
+ "POST", "/secure/orders", body=body
79
+ )
80
+
81
+ print(status, headers["content-type"], response_body)
82
+ print(transport.last_evidence)
83
+
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ Expected output for a service that returns `application/json` and body
89
+ `b'{"ok":true}'`:
90
+
91
+ ```text
92
+ 200 application/json b'{"ok":true}'
93
+ HttpsTransportEvidence(status_code=200, response_jws_verified=True, certificate_valid=None, certificate_subject=None, cipher_suite='tls13', cipher_alg='TLS_AES_256_GCM_SHA384')
94
+ ```
95
+
96
+ Use `verify_certificate()` when a service has a PEM certificate or chain that
97
+ should be validated against explicit trust roots before making transport calls.
98
+ Use `allowed_tls_cipher()` on server instrumentation to confirm the negotiated
99
+ TLS cipher is within the Swarmauri TLS 1.3 policy.
100
+
101
+ ## Project Links
102
+
103
+ - Source:
104
+ <https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast>
105
+ - Issues: <https://github.com/swarmauri/swarmauri-sdk/issues>
@@ -0,0 +1,99 @@
1
+ [project]
2
+ name = "swarmauri_transport_https_unicast"
3
+ version = "0.11.0.dev5"
4
+ description = "Policy-bound HTTPS unicast transport for Swarmauri."
5
+ license = "Apache-2.0"
6
+ readme = "README.md"
7
+ requires-python = ">=3.10,<3.15"
8
+ authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
9
+ classifiers = [
10
+ "Development Status :: 1 - Planning",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: Apache Software License",
13
+ "Natural Language :: English",
14
+ "Topic :: Internet :: WWW/HTTP",
15
+ "Topic :: Security :: Cryptography",
16
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ ]
26
+ dependencies = [
27
+ "httpx>=0.27.0,<1",
28
+ "swarmauri_core>=0.11.0.dev5",
29
+ "swarmauri_base>=0.11.0.dev5",
30
+ "swarmauri_certs_x509verify",
31
+ "swarmauri_cipher_suite_tls13",
32
+ "swarmauri_middleware_httpsig",
33
+ "swarmauri_signing_jws",
34
+ ]
35
+ keywords = [
36
+ "swarmauri",
37
+ "sdk",
38
+ "standards",
39
+ "transport",
40
+ "https",
41
+ "httpx",
42
+ "tls",
43
+ "tls13",
44
+ "x509",
45
+ "certificate-validation",
46
+ "http-signature",
47
+ "jws",
48
+ "bearer-token",
49
+ "unicast",
50
+ ]
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/swarmauri/swarmauri-sdk"
54
+ Source = "https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast"
55
+ Documentation = "https://github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_transport_https_unicast#readme"
56
+
57
+ [project.entry-points."swarmauri.transports"]
58
+ HttpsUnicastTransport = "swarmauri_transport_https_unicast:HttpsUnicastTransport"
59
+
60
+ [tool.uv.sources]
61
+ swarmauri_core = { workspace = true }
62
+ swarmauri_base = { workspace = true }
63
+ swarmauri_certs_x509verify = { workspace = true }
64
+ swarmauri_cipher_suite_tls13 = { workspace = true }
65
+ swarmauri_middleware_httpsig = { workspace = true }
66
+ swarmauri_signing_jws = { workspace = true }
67
+
68
+ [tool.pytest.ini_options]
69
+ norecursedirs = ["combined", "scripts", ".uv-cache"]
70
+ markers = [
71
+ "test: standard test",
72
+ "unit: Unit tests",
73
+ "i9n: Integration tests",
74
+ "r8n: Regression tests",
75
+ "acceptance: Acceptance tests",
76
+ "perf: Performance tests",
77
+ "example: Documentation examples",
78
+ ]
79
+ timeout = 300
80
+ log_cli = true
81
+ log_cli_level = "INFO"
82
+ log_cli_format = "%(asctime)s [%(levelname)s] %(message)s"
83
+ log_cli_date_format = "%Y-%m-%d %H:%M:%S"
84
+ asyncio_default_fixture_loop_scope = "function"
85
+
86
+ [dependency-groups]
87
+ dev = [
88
+ "pytest>=8.0",
89
+ "pytest-asyncio>=0.24.0",
90
+ "pytest-xdist>=3.6.1",
91
+ "pytest-json-report>=1.5.0",
92
+ "pytest-timeout>=2.3.1",
93
+ "pytest-benchmark>=4.0.0",
94
+ "ruff>=0.9.9",
95
+ ]
96
+
97
+ [build-system]
98
+ requires = ["poetry-core>=1.0.0"]
99
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,301 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import ssl
7
+ from dataclasses import dataclass
8
+ from typing import Any, Literal, Mapping
9
+
10
+ import httpx
11
+ from pydantic import Field, PrivateAttr
12
+ from swarmauri_base.ComponentBase import ComponentBase
13
+ from swarmauri_base.transports import TransportBase
14
+ from swarmauri_certs_x509verify import X509VerifyService
15
+ from swarmauri_cipher_suite_tls13 import Tls13CipherSuite
16
+ from swarmauri_core.crypto.types import JWAAlg
17
+ from swarmauri_core.transports import (
18
+ AddressScheme,
19
+ Cast,
20
+ Feature,
21
+ IOModel,
22
+ Protocol,
23
+ SecurityMode,
24
+ TransportCapabilities,
25
+ )
26
+ from swarmauri_middleware_httpsig import HttpSigMiddleware
27
+ from swarmauri_signing_jws import JwsSignerVerifier
28
+
29
+ Bytes = bytes
30
+ Headers = dict[str, str]
31
+ JwsKey = Mapping[str, Any]
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class HttpsSecurityPolicy:
36
+ """Application-layer security material applied around HTTPS requests."""
37
+
38
+ bearer_token: str | None = None
39
+ http_signature_secret: str | bytes | None = None
40
+ http_signature_header: str = "X-Signature"
41
+ request_jws_key: JwsKey | None = None
42
+ request_jws_kid: str | None = None
43
+ request_jws_header: str = "X-Body-JWS"
44
+ response_jws_key: JwsKey | None = None
45
+ response_jws_header: str = "X-Response-JWS"
46
+ jws_alg: JWAAlg = JWAAlg.HS256
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class HttpsTransportEvidence:
51
+ """Verification evidence captured by a secure HTTPS request."""
52
+
53
+ status_code: int | None = None
54
+ response_jws_verified: bool = False
55
+ certificate_valid: bool | None = None
56
+ certificate_subject: str | None = None
57
+ cipher_suite: str = "tls13"
58
+ cipher_alg: str | None = None
59
+
60
+
61
+ def _secret_bytes(secret: str | bytes) -> bytes:
62
+ return secret.encode("utf-8") if isinstance(secret, str) else secret
63
+
64
+
65
+ def http_signature(secret: str | bytes, body: bytes) -> str:
66
+ """Return the HMAC-SHA256 signature used by ``HttpSigMiddleware``."""
67
+
68
+ return base64.b64encode(
69
+ hmac.new(_secret_bytes(secret), body, hashlib.sha256).digest()
70
+ ).decode("ascii")
71
+
72
+
73
+ @ComponentBase.register_type(TransportBase, "HttpsUnicastTransport")
74
+ class HttpsUnicastTransport(TransportBase):
75
+ """
76
+ HTTPS unicast transport with Swarmauri trust and request-signing policy.
77
+ """
78
+
79
+ type: Literal["HttpsUnicastTransport"] = "HttpsUnicastTransport"
80
+
81
+ base_url: str = ""
82
+ verify: bool | str | ssl.SSLContext = True
83
+ timeout: float = 10.0
84
+ trust_env: bool = False
85
+ security_policy: HttpsSecurityPolicy = Field(
86
+ default_factory=HttpsSecurityPolicy
87
+ )
88
+ require_response_jws: bool = False
89
+ httpx_transport: httpx.AsyncBaseTransport | None = Field(
90
+ default=None, exclude=True
91
+ )
92
+
93
+ _client: httpx.AsyncClient | None = PrivateAttr(default=None)
94
+ _jws: JwsSignerVerifier = PrivateAttr(default_factory=JwsSignerVerifier)
95
+ _cipher_suite: Tls13CipherSuite = PrivateAttr(
96
+ default_factory=Tls13CipherSuite
97
+ )
98
+ _last_evidence: HttpsTransportEvidence = PrivateAttr(
99
+ default_factory=HttpsTransportEvidence
100
+ )
101
+
102
+ def supports(self) -> TransportCapabilities:
103
+ return TransportCapabilities(
104
+ protocols=frozenset({Protocol.HTTP1, Protocol.TLS}),
105
+ io=IOModel.STREAM,
106
+ casts=frozenset({Cast.UNICAST}),
107
+ features=frozenset(
108
+ {
109
+ Feature.RELIABLE,
110
+ Feature.ORDERED,
111
+ Feature.ENCRYPTED,
112
+ Feature.AUTHENTICATED,
113
+ }
114
+ ),
115
+ security=SecurityMode.TLS,
116
+ schemes=frozenset({AddressScheme.HTTPS}),
117
+ )
118
+
119
+ @property
120
+ def last_evidence(self) -> HttpsTransportEvidence:
121
+ return self._last_evidence
122
+
123
+ @property
124
+ def cipher_descriptor(self) -> Mapping[str, Any]:
125
+ return self._cipher_suite.normalize(op="encrypt")
126
+
127
+ def allowed_tls_cipher(self, cipher_name: str) -> bool:
128
+ return cipher_name in set(self._cipher_suite.supports()["encrypt"])
129
+
130
+ def http_signature_middleware(self) -> HttpSigMiddleware:
131
+ secret = self.security_policy.http_signature_secret
132
+ if secret is None:
133
+ raise ValueError("http_signature_secret is required")
134
+ if isinstance(secret, bytes):
135
+ secret = secret.decode("utf-8")
136
+ return HttpSigMiddleware(
137
+ secret_key=secret,
138
+ header_name=self.security_policy.http_signature_header,
139
+ )
140
+
141
+ async def verify_certificate(
142
+ self,
143
+ cert_pem: bytes | str,
144
+ *,
145
+ trust_roots: list[bytes | str] | None = None,
146
+ ) -> dict[str, Any]:
147
+ result = await X509VerifyService().verify_cert(
148
+ cert_pem, trust_roots=trust_roots
149
+ )
150
+ self._last_evidence = HttpsTransportEvidence(
151
+ certificate_valid=bool(result.get("valid")),
152
+ certificate_subject=result.get("subject"),
153
+ cipher_suite=self._cipher_suite.suite_id(),
154
+ cipher_alg=self.cipher_descriptor["alg"],
155
+ )
156
+ return result
157
+
158
+ async def prepare_headers(
159
+ self,
160
+ body: bytes,
161
+ headers: Headers | None = None,
162
+ *,
163
+ bearer_token: str | None = None,
164
+ http_signature_secret: str | bytes | None = None,
165
+ request_jws_key: JwsKey | None = None,
166
+ ) -> Headers:
167
+ policy = self.security_policy
168
+ prepared = dict(headers or {})
169
+
170
+ token = (
171
+ bearer_token if bearer_token is not None else policy.bearer_token
172
+ )
173
+ if token:
174
+ prepared.setdefault("Authorization", f"Bearer {token}")
175
+
176
+ secret = (
177
+ http_signature_secret
178
+ if http_signature_secret is not None
179
+ else policy.http_signature_secret
180
+ )
181
+ if secret is not None:
182
+ prepared[policy.http_signature_header] = http_signature(
183
+ secret, body
184
+ )
185
+
186
+ jws_key = (
187
+ request_jws_key
188
+ if request_jws_key is not None
189
+ else policy.request_jws_key
190
+ )
191
+ if jws_key is not None:
192
+ prepared[policy.request_jws_header] = await self._jws.sign_compact(
193
+ payload=body,
194
+ alg=policy.jws_alg,
195
+ key=jws_key,
196
+ kid=policy.request_jws_kid,
197
+ typ="JWS",
198
+ )
199
+
200
+ return prepared
201
+
202
+ async def verify_body_jws(
203
+ self,
204
+ compact_jws: str,
205
+ body: bytes,
206
+ *,
207
+ hmac_keys: list[JwsKey] | None = None,
208
+ ) -> None:
209
+ keys = hmac_keys or [self.security_policy.response_jws_key]
210
+ usable_keys = [key for key in keys if key is not None]
211
+ result = await self._jws.verify_compact(
212
+ compact_jws,
213
+ hmac_keys=usable_keys,
214
+ alg_allowlist=[self.security_policy.jws_alg],
215
+ )
216
+ if result.payload != body:
217
+ raise ValueError("JWS payload does not match body")
218
+
219
+ async def _start_server(self, **bind_kwargs: Any) -> None:
220
+ raise NotImplementedError(
221
+ (
222
+ "HttpsUnicastTransport provides HTTPS client transport "
223
+ "orchestration"
224
+ )
225
+ )
226
+
227
+ async def _stop_server(self) -> None:
228
+ return None
229
+
230
+ async def _open_client(self, **connect_kwargs: Any) -> None:
231
+ self._client = httpx.AsyncClient(**self._client_kwargs(connect_kwargs))
232
+
233
+ async def _close_client(self) -> None:
234
+ if self._client is not None:
235
+ await self._client.aclose()
236
+ self._client = None
237
+
238
+ async def request(
239
+ self,
240
+ method: str,
241
+ path: str,
242
+ headers: Headers | None = None,
243
+ body: Bytes = b"",
244
+ **kwargs: Any,
245
+ ) -> tuple[int, Headers, Bytes]:
246
+ request_headers = await self.prepare_headers(body, headers)
247
+ response_jws_verified = False
248
+ client = self._client
249
+
250
+ if client is None:
251
+ async with httpx.AsyncClient(
252
+ **self._client_kwargs({})
253
+ ) as one_shot:
254
+ response = await one_shot.request(
255
+ method,
256
+ path,
257
+ headers=request_headers,
258
+ content=body,
259
+ **kwargs,
260
+ )
261
+ else:
262
+ response = await client.request(
263
+ method, path, headers=request_headers, content=body, **kwargs
264
+ )
265
+
266
+ response_jws = response.headers.get(
267
+ self.security_policy.response_jws_header
268
+ )
269
+ if response_jws:
270
+ await self.verify_body_jws(response_jws, response.content)
271
+ response_jws_verified = True
272
+ elif self.require_response_jws:
273
+ raise ValueError(
274
+ (
275
+ f"missing {self.security_policy.response_jws_header} "
276
+ f"response header"
277
+ )
278
+ )
279
+
280
+ descriptor = self.cipher_descriptor
281
+ self._last_evidence = HttpsTransportEvidence(
282
+ status_code=response.status_code,
283
+ response_jws_verified=response_jws_verified,
284
+ cipher_suite=self._cipher_suite.suite_id(),
285
+ cipher_alg=descriptor["alg"],
286
+ )
287
+ return response.status_code, dict(response.headers), response.content
288
+
289
+ def _client_kwargs(self, overrides: Mapping[str, Any]) -> dict[str, Any]:
290
+ kwargs: dict[str, Any] = {
291
+ "verify": overrides.get("verify", self.verify),
292
+ "timeout": overrides.get("timeout", self.timeout),
293
+ "trust_env": overrides.get("trust_env", self.trust_env),
294
+ }
295
+ base_url = overrides.get("base_url", self.base_url)
296
+ if base_url:
297
+ kwargs["base_url"] = base_url
298
+ transport = overrides.get("transport", self.httpx_transport)
299
+ if transport is not None:
300
+ kwargs["transport"] = transport
301
+ return kwargs
@@ -0,0 +1,15 @@
1
+ """Policy-bound HTTPS unicast transport."""
2
+
3
+ from .HttpsUnicastTransport import (
4
+ HttpsSecurityPolicy,
5
+ HttpsTransportEvidence,
6
+ HttpsUnicastTransport,
7
+ http_signature,
8
+ )
9
+
10
+ __all__ = [
11
+ "HttpsSecurityPolicy",
12
+ "HttpsTransportEvidence",
13
+ "HttpsUnicastTransport",
14
+ "http_signature",
15
+ ]