provablyfine-client 0.3.0__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,28 @@
1
+ from . import exceptions, schemas
2
+ from .account_client import AccountClient
3
+ from .aio import AsyncAccountClient, AsyncInvitationClient, AsyncPublicClient, AsyncSessionClient
4
+ from .directory import Directory
5
+ from .http_session import HttpSession
6
+ from .http_signatures import Auth
7
+ from .invitation_client import InvitationClient
8
+ from .public_client import PublicClient
9
+ from .session_client import SessionClient
10
+ from .signer import HmacSigner, Signer
11
+
12
+ __all__ = [
13
+ "AccountClient",
14
+ "AsyncAccountClient",
15
+ "AsyncInvitationClient",
16
+ "AsyncPublicClient",
17
+ "AsyncSessionClient",
18
+ "Auth",
19
+ "Directory",
20
+ "HmacSigner",
21
+ "HttpSession",
22
+ "InvitationClient",
23
+ "PublicClient",
24
+ "SessionClient",
25
+ "Signer",
26
+ "exceptions",
27
+ "schemas",
28
+ ]
@@ -0,0 +1,9 @@
1
+ import base64
2
+
3
+
4
+ def decode(s: str) -> bytes:
5
+ return base64.urlsafe_b64decode(s + "=======")
6
+
7
+
8
+ def encode(s: bytes) -> str:
9
+ return base64.urlsafe_b64encode(s).decode().rstrip("=")
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+
5
+ from . import directory, exceptions, http_session, http_signatures, signer
6
+
7
+
8
+ class AccountClient:
9
+ """API methods that require account + session authentication."""
10
+
11
+ def __init__(
12
+ self,
13
+ session: http_session.HttpSession,
14
+ _directory: directory.Directory,
15
+ account_signer: signer.Signer,
16
+ session_signer: signer.Signer,
17
+ ) -> None:
18
+ self._session = session
19
+ self._directory = _directory
20
+ self._account_signer = account_signer
21
+ self._session_signer = session_signer
22
+
23
+ def login_http_sig(self, session_public_key: dict[str, typing.Any]) -> None:
24
+ auth = http_signatures.Auth([self._account_signer, self._session_signer])
25
+ response = self._session.post(
26
+ self._directory.login,
27
+ auth=auth,
28
+ json={"session_public_key": session_public_key},
29
+ )
30
+ if response.status_code != 204:
31
+ raise exceptions.UI(f"Unable to login successfully: {response.text}")
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import typing
5
+
6
+ from . import account_client, invitation_client, public_client, schemas, session_client, signer
7
+
8
+
9
+ class AsyncPublicClient:
10
+ def __init__(self, inner: public_client.PublicClient) -> None:
11
+ self._inner = inner
12
+ self._lock = asyncio.Lock()
13
+
14
+ async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
15
+ async with self._lock:
16
+ return await asyncio.to_thread(fn)
17
+
18
+ async def get_user_trusted_keys_public(self) -> str:
19
+ return await self._run(self._inner.get_user_trusted_keys_public)
20
+
21
+ async def get_public_auth(self, auth_name: str) -> schemas.AuthPublic:
22
+ return await self._run(lambda: self._inner.get_public_auth(auth_name))
23
+
24
+ async def list_public_auths(self) -> list[schemas.AuthPublicSummary]:
25
+ return await self._run(self._inner.list_public_auths)
26
+
27
+ async def initialize(self, account_signer: signer.Signer, account_public_key: dict[str, typing.Any]) -> None:
28
+ return await self._run(lambda: self._inner.initialize(account_signer, account_public_key))
29
+
30
+
31
+ class AsyncSessionClient:
32
+ def __init__(self, inner: session_client.SessionClient) -> None:
33
+ self._inner = inner
34
+ self._lock = asyncio.Lock()
35
+
36
+ async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
37
+ async with self._lock:
38
+ return await asyncio.to_thread(fn)
39
+
40
+ async def list_ssh_hosts(self) -> schemas.SshHostsResponse:
41
+ return await self._run(self._inner.list_ssh_hosts)
42
+
43
+ async def get_host_trusted_keys(self) -> str:
44
+ return await self._run(self._inner.get_host_trusted_keys)
45
+
46
+ async def get_user_certificate(
47
+ self,
48
+ hostname: str,
49
+ username: str,
50
+ action: str,
51
+ public_key: dict[str, typing.Any],
52
+ command: str | None = None,
53
+ ) -> schemas.SshUserCertificateResponse:
54
+ return await self._run(
55
+ lambda: self._inner.get_user_certificate(hostname, username, action, public_key, command)
56
+ )
57
+
58
+ async def sign_host_certificates(
59
+ self, public_keys: list[dict[str, typing.Any]]
60
+ ) -> schemas.SshHostCertificateResponse:
61
+ return await self._run(lambda: self._inner.sign_host_certificates(public_keys))
62
+
63
+ async def list_self_bastions(self) -> schemas.IdentitySelfBastionListResponse:
64
+ return await self._run(self._inner.list_self_bastions)
65
+
66
+ async def get_self_token(self, service: str) -> schemas.IdentitySelfTokenResponse:
67
+ return await self._run(lambda: self._inner.get_self_token(service))
68
+
69
+ async def list_tags(
70
+ self,
71
+ id: int | None = None,
72
+ name: str | None = None,
73
+ value: str | None = None,
74
+ ) -> schemas.TagsResponse:
75
+ return await self._run(lambda: self._inner.list_tags(id, name, value))
76
+
77
+ async def create_tag(self, name: str, value: str) -> schemas.Tag:
78
+ return await self._run(lambda: self._inner.create_tag(name, value))
79
+
80
+ async def delete_tag(self, id: int) -> None:
81
+ return await self._run(lambda: self._inner.delete_tag(id))
82
+
83
+ async def list_tenants(self, id: int | None = None) -> schemas.TenantsResponse:
84
+ return await self._run(lambda: self._inner.list_tenants(id))
85
+
86
+ async def get_tenant(self, id: int) -> schemas.Tenant:
87
+ return await self._run(lambda: self._inner.get_tenant(id))
88
+
89
+ async def create_tenant(self, name: str, display_name: str) -> schemas.Tenant:
90
+ return await self._run(lambda: self._inner.create_tenant(name, display_name))
91
+
92
+ async def update_tenant(
93
+ self,
94
+ id: int,
95
+ display_name: str | None = None,
96
+ is_enabled: bool | None = None,
97
+ ) -> None:
98
+ return await self._run(lambda: self._inner.update_tenant(id, display_name, is_enabled))
99
+
100
+ async def delete_tenant(self, id: int) -> None:
101
+ return await self._run(lambda: self._inner.delete_tenant(id))
102
+
103
+ async def list_auths(self) -> schemas.AuthListResponse:
104
+ return await self._run(self._inner.list_auths)
105
+
106
+ async def get_auth(self, id: int) -> schemas.Auth:
107
+ return await self._run(lambda: self._inner.get_auth(id))
108
+
109
+ async def create_auth_http_sig(self, name: str, description: str, tags: list[dict[str, str]]) -> schemas.Auth:
110
+ return await self._run(lambda: self._inner.create_auth_http_sig(name, description, tags))
111
+
112
+ async def create_auth_oidc(
113
+ self,
114
+ name: str,
115
+ description: str,
116
+ tags: list[dict[str, str]],
117
+ issuer: str,
118
+ client_id: str,
119
+ client_secret: str | None,
120
+ ) -> schemas.Auth:
121
+ return await self._run(
122
+ lambda: self._inner.create_auth_oidc(name, description, tags, issuer, client_id, client_secret)
123
+ )
124
+
125
+ async def create_auth_oauth2_github(
126
+ self,
127
+ name: str,
128
+ description: str,
129
+ tags: list[dict[str, str]],
130
+ client_id: str,
131
+ client_secret: str,
132
+ ) -> schemas.Auth:
133
+ return await self._run(
134
+ lambda: self._inner.create_auth_oauth2_github(name, description, tags, client_id, client_secret)
135
+ )
136
+
137
+ async def update_auth(
138
+ self,
139
+ id: int,
140
+ name: str | None = None,
141
+ description: str | None = None,
142
+ is_enabled: bool | None = None,
143
+ tags: list[schemas.TagNameValue] | None = None,
144
+ ) -> None:
145
+ return await self._run(lambda: self._inner.update_auth(id, name, description, is_enabled, tags))
146
+
147
+ async def delete_auth(self, id: int) -> None:
148
+ return await self._run(lambda: self._inner.delete_auth(id))
149
+
150
+ async def list_bastions(self, id: int | None = None) -> schemas.BastionListResponse:
151
+ return await self._run(lambda: self._inner.list_bastions(id))
152
+
153
+ async def get_bastion(self, id: int) -> schemas.Bastion:
154
+ return await self._run(lambda: self._inner.get_bastion(id))
155
+
156
+ async def create_bastion(
157
+ self,
158
+ url: str,
159
+ ssh_proxy_jump: str | None,
160
+ tag_id_list: list[int],
161
+ tag_name_value_list: list[dict[str, str]],
162
+ ) -> schemas.Bastion:
163
+ return await self._run(
164
+ lambda: self._inner.create_bastion(url, ssh_proxy_jump, tag_id_list, tag_name_value_list)
165
+ )
166
+
167
+ async def update_bastion(
168
+ self,
169
+ id: int,
170
+ url: str | None = None,
171
+ ssh_proxy_jump: str | None = None,
172
+ tag_id_list: list[int] | None = None,
173
+ tag_name_value_list: list[schemas.TagNameValue] | None = None,
174
+ ) -> None:
175
+ return await self._run(
176
+ lambda: self._inner.update_bastion(id, url, ssh_proxy_jump, tag_id_list, tag_name_value_list)
177
+ )
178
+
179
+ async def delete_bastion(self, id: int) -> None:
180
+ return await self._run(lambda: self._inner.delete_bastion(id))
181
+
182
+ async def list_roles(self, id: int | None = None, name: str | None = None) -> schemas.RolesResponse:
183
+ return await self._run(lambda: self._inner.list_roles(id, name))
184
+
185
+ async def get_role(self, id: int) -> schemas.Role:
186
+ return await self._run(lambda: self._inner.get_role(id))
187
+
188
+ async def create_role(self, name: str, description: str) -> schemas.Role:
189
+ return await self._run(lambda: self._inner.create_role(name, description))
190
+
191
+ async def update_role(
192
+ self,
193
+ id: int,
194
+ name: str | None = None,
195
+ description: str | None = None,
196
+ grant_list: list[schemas.Grant] | None = None,
197
+ member_list: list[schemas.RoleMemberRef] | None = None,
198
+ ) -> None:
199
+ return await self._run(lambda: self._inner.update_role(id, name, description, grant_list, member_list))
200
+
201
+ async def delete_role(self, id: int) -> None:
202
+ return await self._run(lambda: self._inner.delete_role(id))
203
+
204
+ async def list_boundaries(self, id: int | None = None, name: str | None = None) -> schemas.BoundariesResponse:
205
+ return await self._run(lambda: self._inner.list_boundaries(id, name))
206
+
207
+ async def get_boundary(self, id: int) -> schemas.Boundary:
208
+ return await self._run(lambda: self._inner.get_boundary(id))
209
+
210
+ async def create_boundary(self, name: str, description: str) -> schemas.Boundary:
211
+ return await self._run(lambda: self._inner.create_boundary(name, description))
212
+
213
+ async def update_boundary(
214
+ self,
215
+ id: int,
216
+ name: str | None = None,
217
+ description: str | None = None,
218
+ ceiling_list: list[schemas.Grant] | None = None,
219
+ denied_list: list[schemas.Grant] | None = None,
220
+ ) -> None:
221
+ return await self._run(lambda: self._inner.update_boundary(id, name, description, ceiling_list, denied_list))
222
+
223
+ async def delete_boundary(self, id: int) -> None:
224
+ return await self._run(lambda: self._inner.delete_boundary(id))
225
+
226
+ async def list_identities(
227
+ self,
228
+ id: int | None = None,
229
+ name: str | None = None,
230
+ tag_id: list[str] | None = None,
231
+ tag_name: list[str] | None = None,
232
+ boundary_id: list[str] | None = None,
233
+ boundary_name: list[str] | None = None,
234
+ ) -> schemas.IdentitiesResponse:
235
+ return await self._run(
236
+ lambda: self._inner.list_identities(id, name, tag_id, tag_name, boundary_id, boundary_name)
237
+ )
238
+
239
+ async def get_identity(self, id: int) -> schemas.Identity:
240
+ return await self._run(lambda: self._inner.get_identity(id))
241
+
242
+ async def create_identity(
243
+ self,
244
+ name: str | None,
245
+ boundary_id_list: list[int],
246
+ boundary_name_list: list[str],
247
+ tag_id_list: list[int],
248
+ tag_name_value_list: list[dict[str, str]],
249
+ ) -> schemas.Identity:
250
+ return await self._run(
251
+ lambda: self._inner.create_identity(
252
+ name, boundary_id_list, boundary_name_list, tag_id_list, tag_name_value_list
253
+ )
254
+ )
255
+
256
+ async def invite_identity(self, id: int, delivery: str) -> str | None:
257
+ return await self._run(lambda: self._inner.invite_identity(id, delivery))
258
+
259
+ async def delete_identity(self, id: int) -> None:
260
+ return await self._run(lambda: self._inner.delete_identity(id))
261
+
262
+ async def update_identity(
263
+ self,
264
+ id: int,
265
+ name: str | None = None,
266
+ tags: list[schemas.IdentityTagOp] | None = None,
267
+ ) -> None:
268
+ return await self._run(lambda: self._inner.update_identity(id, name, tags))
269
+
270
+ async def login_oidc(
271
+ self,
272
+ auth_name: str,
273
+ id_token: str,
274
+ session_public_key: dict[str, typing.Any],
275
+ ) -> None:
276
+ return await self._run(lambda: self._inner.login_oidc(auth_name, id_token, session_public_key))
277
+
278
+ async def login_oauth2_start(
279
+ self,
280
+ auth_name: str,
281
+ session_public_key: dict[str, typing.Any],
282
+ client_redirect_uri: str,
283
+ ) -> str:
284
+ return await self._run(
285
+ lambda: self._inner.login_oauth2_start(auth_name, session_public_key, client_redirect_uri)
286
+ )
287
+
288
+ async def list_audit_log(
289
+ self,
290
+ level: int | None = None,
291
+ object_type: str | None = None,
292
+ by_identity_id: str | None = None,
293
+ start_time: int | None = None,
294
+ end_time: int | None = None,
295
+ ) -> schemas.AuditLogListResponse:
296
+ return await self._run(
297
+ lambda: self._inner.list_audit_log(level, object_type, by_identity_id, start_time, end_time)
298
+ )
299
+
300
+
301
+ class AsyncAccountClient:
302
+ def __init__(self, inner: account_client.AccountClient) -> None:
303
+ self._inner = inner
304
+ self._lock = asyncio.Lock()
305
+
306
+ async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
307
+ async with self._lock:
308
+ return await asyncio.to_thread(fn)
309
+
310
+ async def login_http_sig(self, session_public_key: dict[str, typing.Any]) -> None:
311
+ return await self._run(lambda: self._inner.login_http_sig(session_public_key))
312
+
313
+
314
+ class AsyncInvitationClient:
315
+ def __init__(self, inner: invitation_client.InvitationClient) -> None:
316
+ self._inner = inner
317
+ self._lock = asyncio.Lock()
318
+
319
+ async def _run(self, fn: typing.Callable[[], typing.Any]) -> typing.Any:
320
+ async with self._lock:
321
+ return await asyncio.to_thread(fn)
322
+
323
+ async def connect(self, account_public_key: dict[str, typing.Any]) -> None:
324
+ return await self._run(lambda: self._inner.connect(account_public_key))
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import requests
4
+
5
+ from . import exceptions
6
+
7
+
8
+ class Directory:
9
+ """Fetches and caches the server's endpoint URL map from a well-known URL."""
10
+
11
+ def __init__(self, url: str, timeout: float = 5.0) -> None:
12
+ self._url = url
13
+ self._timeout = timeout
14
+ self._data: dict[str, str] | None = None
15
+
16
+ def _load(self) -> dict[str, str]:
17
+ if self._data is None:
18
+ try:
19
+ response = requests.get(self._url, timeout=self._timeout)
20
+ except requests.exceptions.ConnectionError:
21
+ raise exceptions.UI("Unable to connect to server")
22
+ except requests.exceptions.ReadTimeout:
23
+ raise exceptions.UI("Request timed out")
24
+ if response.status_code != 200:
25
+ raise exceptions.UI("Unable to read directory from server")
26
+ self._data = response.json()
27
+ assert self._data is not None
28
+ return self._data
29
+
30
+ @property
31
+ def accept_invitation(self) -> str:
32
+ return self._load()["accept_invitation"]
33
+
34
+ @property
35
+ def audit_log(self) -> str:
36
+ return self._load()["audit_log"]
37
+
38
+ @property
39
+ def auth(self) -> str:
40
+ return self._load()["auth"]
41
+
42
+ @property
43
+ def bastion(self) -> str:
44
+ return self._load()["bastion"]
45
+
46
+ @property
47
+ def boundary(self) -> str:
48
+ return self._load()["boundary"]
49
+
50
+ @property
51
+ def identity(self) -> str:
52
+ return self._load()["identity"]
53
+
54
+ @property
55
+ def initialize(self) -> str:
56
+ return self._load()["initialize"]
57
+
58
+ @property
59
+ def login(self) -> str:
60
+ return self._load()["login"]
61
+
62
+ @property
63
+ def login_oauth2_start(self) -> str:
64
+ return self._load()["login_oauth2_start"]
65
+
66
+ @property
67
+ def login_oidc(self) -> str:
68
+ return self._load()["login_oidc"]
69
+
70
+ @property
71
+ def public_auth(self) -> str:
72
+ return self._load()["public_auth"]
73
+
74
+ @property
75
+ def role(self) -> str:
76
+ return self._load()["role"]
77
+
78
+ @property
79
+ def ssh(self) -> str:
80
+ return self._load()["ssh"]
81
+
82
+ @property
83
+ def tag(self) -> str:
84
+ return self._load()["tag"]
85
+
86
+ @property
87
+ def tenant(self) -> str:
88
+ return self._load()["tenant"]
@@ -0,0 +1,11 @@
1
+ class UI(Exception):
2
+ pass
3
+
4
+
5
+ class Forbidden(UI):
6
+ pass
7
+
8
+
9
+ class KeyExpired(Exception):
10
+ def __init__(self, key_type: str):
11
+ self.key_type = key_type
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing
5
+
6
+ import requests
7
+
8
+ from . import exceptions, http_signatures
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class HttpSession:
14
+ """Thin wrapper around requests.Session: logging, 400/422 error handling, per-request auth."""
15
+
16
+ def __init__(self, session: requests.Session, timeout: float = 5.0) -> None:
17
+ self._session = session
18
+ self._timeout = timeout
19
+
20
+ def request(
21
+ self,
22
+ method: str,
23
+ url: str,
24
+ *,
25
+ auth: http_signatures.Auth | None = None,
26
+ json: typing.Any = None,
27
+ data: typing.Any = None,
28
+ headers: dict[str, str] | None = None,
29
+ params: dict[str, typing.Any] | None = None,
30
+ timeout: float | None = None,
31
+ ) -> requests.Response:
32
+ req = requests.Request(method=method, url=url, json=json, data=data, headers=headers, params=params)
33
+ prepared = req.prepare()
34
+ if auth is not None:
35
+ prepared = auth(prepared)
36
+
37
+ effective_timeout = timeout if timeout is not None else self._timeout
38
+ logger.info(f"tx {prepared.method} {prepared.url}")
39
+ logger.debug(f"tx headers: {prepared.headers}")
40
+ logger.debug(f"tx body: {prepared.body}")
41
+ try:
42
+ response = self._session.send(prepared, timeout=effective_timeout)
43
+ except requests.exceptions.ConnectionError:
44
+ raise exceptions.UI("Unable to connect to server")
45
+ except requests.exceptions.ReadTimeout:
46
+ raise exceptions.UI("Request timed out")
47
+ logger.info(f"rx {response.status_code}")
48
+ logger.debug(f"rx headers: {response.headers}")
49
+ logger.debug(f"rx body: {response.content}")
50
+
51
+ if response.status_code in (400, 422):
52
+ try:
53
+ problem = response.json()
54
+ title = problem.get("title", "")
55
+ detail = problem.get("detail")
56
+ msg = f"{title} {detail}" if detail else title
57
+ except Exception:
58
+ msg = response.text
59
+ raise exceptions.UI(msg or response.text)
60
+
61
+ return response
62
+
63
+ def get(
64
+ self,
65
+ url: str,
66
+ *,
67
+ auth: http_signatures.Auth | None = None,
68
+ params: dict[str, typing.Any] | None = None,
69
+ ) -> requests.Response:
70
+ return self.request("GET", url, auth=auth, params=params)
71
+
72
+ def post(
73
+ self,
74
+ url: str,
75
+ *,
76
+ auth: http_signatures.Auth | None = None,
77
+ json: typing.Any = None,
78
+ ) -> requests.Response:
79
+ return self.request("POST", url, auth=auth, json=json)
80
+
81
+ def patch(
82
+ self,
83
+ url: str,
84
+ *,
85
+ auth: http_signatures.Auth | None = None,
86
+ json: typing.Any = None,
87
+ ) -> requests.Response:
88
+ return self.request("PATCH", url, auth=auth, json=json)
89
+
90
+ def delete(
91
+ self,
92
+ url: str,
93
+ *,
94
+ auth: http_signatures.Auth | None = None,
95
+ ) -> requests.Response:
96
+ return self.request("DELETE", url, auth=auth)
97
+
98
+ def put(
99
+ self,
100
+ url: str,
101
+ *,
102
+ auth: http_signatures.Auth | None = None,
103
+ json: typing.Any = None,
104
+ ) -> requests.Response:
105
+ return self.request("PUT", url, auth=auth, json=json)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import secrets
5
+ import time
6
+ import typing
7
+ import urllib.parse
8
+
9
+ import http_sfv
10
+ import requests
11
+
12
+ from . import signer
13
+
14
+ # http_sfv type stubs are incomplete (private imports, reportPrivateImportUsage)
15
+ # pyright: reportPrivateImportUsage=false
16
+
17
+
18
+ def _build_signature_base(
19
+ request: requests.PreparedRequest,
20
+ covered: tuple[str, ...],
21
+ sig_params: str,
22
+ ) -> str:
23
+ parts: list[str] = []
24
+ for c in covered:
25
+ match c:
26
+ case "@method":
27
+ parts.append(f'"@method": {request.method}')
28
+ case "@authority":
29
+ parts.append(f'"@authority": {urllib.parse.urlparse(request.url).netloc}')
30
+ case "@target-uri":
31
+ parts.append(f'"@target-uri": {request.url}')
32
+ case "@signature-params":
33
+ parts.append(f'"@signature-params": {sig_params}')
34
+ case _:
35
+ parts.append(f'"{c}": {request.headers[c]}')
36
+ return "\n".join(parts)
37
+
38
+
39
+ class Auth:
40
+ """HTTP Message Signatures (RFC 9421) — signs a request with one or more Signer instances."""
41
+
42
+ def __init__(self, signers: typing.Sequence[signer.Signer]) -> None:
43
+ self._signers = signers
44
+
45
+ def _sign(
46
+ self, signer: signer.Signer, request: requests.PreparedRequest, covered: tuple[str, ...]
47
+ ) -> tuple[str, str]:
48
+ key_id = f"{signer.prefix()}:{signer.thumbprint()}"
49
+
50
+ inner = http_sfv.InnerList([http_sfv.Item(c) for c in covered])
51
+ inner.params["created"] = int(time.time())
52
+ inner.params["keyid"] = key_id
53
+ inner.params["nonce"] = secrets.token_hex(16)
54
+
55
+ sig_params = str(inner)
56
+ sig_base = _build_signature_base(request, covered, sig_params)
57
+ sig_bytes = signer.sign(sig_base.encode())
58
+
59
+ return (
60
+ f"{signer.prefix()}={sig_params}",
61
+ f"{signer.prefix()}={http_sfv.Item(sig_bytes)}",
62
+ )
63
+
64
+ def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
65
+ if "Content-Digest" not in request.headers:
66
+ body = request.body or b""
67
+ if isinstance(body, str):
68
+ body_bytes = body.encode()
69
+ else:
70
+ body_bytes = body if isinstance(body, bytes) else b""
71
+ digest = hashlib.sha256(body_bytes).digest()
72
+ request.headers["Content-Digest"] = str(http_sfv.Dictionary({"sha-256": digest}))
73
+ covered = ("@method", "@authority", "@target-uri", "content-digest", "@signature-params")
74
+ signatures_input: list[str] = []
75
+ signatures: list[str] = []
76
+ for _signer in self._signers:
77
+ signature_input, signature = self._sign(_signer, request, covered)
78
+ signatures_input.append(signature_input)
79
+ signatures.append(signature)
80
+ request.headers["Signature-Input"] = ", ".join(signatures_input)
81
+ request.headers["Signature"] = ", ".join(signatures)
82
+ return request