boto3-refresh-session 2.0.5__py3-none-any.whl → 7.0.1__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.

Potentially problematic release.


This version of boto3-refresh-session might be problematic. Click here for more details.

@@ -0,0 +1,614 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """IoT Core X.509 refreshable session implementation."""
6
+
7
+ __all__ = ["IOTX509RefreshableSession"]
8
+
9
+ import json
10
+ import re
11
+ from atexit import register
12
+ from pathlib import Path
13
+ from tempfile import NamedTemporaryFile
14
+ from typing import cast, get_args
15
+ from urllib.parse import ParseResult, urlparse
16
+
17
+ from awscrt import auth, io
18
+ from awscrt.exceptions import AwsCrtError
19
+ from awscrt.http import HttpClientConnection, HttpRequest
20
+ from awscrt.io import (
21
+ ClientBootstrap,
22
+ ClientTlsContext,
23
+ DefaultHostResolver,
24
+ EventLoopGroup,
25
+ LogLevel,
26
+ Pkcs11Lib,
27
+ TlsConnectionOptions,
28
+ TlsContextOptions,
29
+ init_logging,
30
+ )
31
+ from awscrt.mqtt import Connection
32
+ from awsiot import mqtt_connection_builder
33
+
34
+ from ...exceptions import (
35
+ BRSConfigurationError,
36
+ BRSConnectionError,
37
+ BRSRequestError,
38
+ BRSValidationError,
39
+ BRSWarning,
40
+ )
41
+ from ...utils import (
42
+ PKCS11,
43
+ AWSCRTResponse,
44
+ Identity,
45
+ TemporaryCredentials,
46
+ Transport,
47
+ refreshable_session,
48
+ )
49
+ from .core import BaseIoTRefreshableSession
50
+
51
+ _TEMP_PATHS: list[str] = []
52
+
53
+
54
+ @refreshable_session
55
+ class IOTX509RefreshableSession(
56
+ BaseIoTRefreshableSession, registry_key="x509"
57
+ ):
58
+ """A :class:`boto3.session.Session` object that automatically refreshes
59
+ temporary credentials returned by the IoT Core credential provider.
60
+
61
+ Parameters
62
+ ----------
63
+ endpoint : str
64
+ The endpoint URL for the IoT Core credential provider. Must contain
65
+ '.credentials.iot.'.
66
+ role_alias : str
67
+ The IAM role alias to use when requesting temporary credentials.
68
+ certificate : str | bytes
69
+ The X.509 certificate to use when requesting temporary credentials.
70
+ ``str`` represents the file path to the certificate, while ``bytes``
71
+ represents the actual certificate data.
72
+ thing_name : str, optional
73
+ The name of the IoT thing to use when requesting temporary
74
+ credentials. Default is None.
75
+ private_key : str | bytes | None, optional
76
+ The private key to use when requesting temporary credentials. ``str``
77
+ represents the file path to the private key, while ``bytes``
78
+ represents the actual private key data. Optional only if ``pkcs11``
79
+ is provided. Default is None.
80
+ pkcs11 : PKCS11, optional
81
+ The PKCS#11 library to use when requesting temporary credentials. If
82
+ provided, ``private_key`` must be None.
83
+ ca : str | bytes | None, optional
84
+ The CA certificate to use when verifying the IoT Core endpoint. ``str``
85
+ represents the file path to the CA certificate, while ``bytes``
86
+ represents the actual CA certificate data. Default is None.
87
+ verify_peer : bool, optional
88
+ Whether to verify the CA certificate when establishing the TLS
89
+ connection. Default is True.
90
+ timeout : float | int | None, optional
91
+ The timeout for the TLS connection in seconds. Default is 10.0.
92
+ duration_seconds : int | None, optional
93
+ The duration for which the temporary credentials are valid, in
94
+ seconds. Cannot exceed the value declared in the IAM policy.
95
+ Default is None.
96
+ awscrt_log_level : awscrt.LogLevel | None, optional
97
+ The logging level for the AWS CRT library, e.g.
98
+ ``awscrt.LogLevel.INFO``. Default is None.
99
+ defer_refresh : bool, optional
100
+ If ``True`` then temporary credentials are not automatically refreshed
101
+ until they are explicitly needed. If ``False`` then temporary
102
+ credentials refresh immediately upon expiration. It is highly
103
+ recommended that you use ``True``. Default is ``True``.
104
+ advisory_timeout : int, optional
105
+ USE THIS ARGUMENT WITH CAUTION!!!
106
+
107
+ Botocore will attempt to refresh credentials early according to
108
+ this value (in seconds), but will continue using the existing
109
+ credentials if refresh fails. Default is 15 minutes (900 seconds).
110
+ mandatory_timeout : int, optional
111
+ USE THIS ARGUMENT WITH CAUTION!!!
112
+
113
+ Botocore requires a successful refresh before continuing. If
114
+ refresh fails in this window (in seconds), API calls may fail.
115
+ Default is 10 minutes (600 seconds).
116
+ cache_clients : bool, optional
117
+ If ``True`` then clients created by this session will be cached and
118
+ reused for subsequent calls to :meth:`client()` with the same
119
+ parameter signatures. Due to the memory overhead of clients, the
120
+ default is ``True`` in order to protect system resources.
121
+
122
+ Other Parameters
123
+ ----------------
124
+ kwargs : dict, optional
125
+ Optional keyword arguments for the :class:`boto3.session.Session`
126
+ object.
127
+
128
+ Notes
129
+ -----
130
+ Gavin Adams at AWS was a major influence on this implementation.
131
+ Thank you, Gavin!
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ endpoint: str,
137
+ role_alias: str,
138
+ certificate: str | bytes,
139
+ thing_name: str | None = None,
140
+ private_key: str | bytes | None = None,
141
+ pkcs11: PKCS11 | None = None,
142
+ ca: str | bytes | None = None,
143
+ verify_peer: bool = True,
144
+ timeout: float | int | None = None,
145
+ duration_seconds: int | None = None,
146
+ awscrt_log_level: LogLevel | None = None,
147
+ **kwargs,
148
+ ):
149
+ # initializing BRSSession
150
+ super().__init__(refresh_method="iot-x509", **kwargs)
151
+
152
+ # logging
153
+ if awscrt_log_level:
154
+ init_logging(log_level=awscrt_log_level, file_name="stdout")
155
+
156
+ # initializing public attributes
157
+ self.endpoint = self._normalize_iot_credential_endpoint(
158
+ endpoint=endpoint
159
+ )
160
+ self.role_alias = role_alias
161
+ self.certificate = self._read_maybe_path_to_bytes(
162
+ certificate, fallback=None, name="certificate"
163
+ )
164
+ self.thing_name = thing_name
165
+ self.private_key = self._read_maybe_path_to_bytes(
166
+ private_key, fallback=None, name="private_key"
167
+ )
168
+ self.pkcs11 = self._validate_pkcs11(pkcs11) if pkcs11 else None
169
+ self.ca = self._read_maybe_path_to_bytes(ca, fallback=None, name="ca")
170
+ self.verify_peer = verify_peer
171
+ self.timeout = 10.0 if timeout is None else timeout
172
+ self.duration_seconds = duration_seconds
173
+
174
+ # either private_key or pkcs11 must be provided
175
+ if self.private_key is None and self.pkcs11 is None:
176
+ raise BRSConfigurationError(
177
+ "Either 'private_key' or 'pkcs11' must be provided.",
178
+ param="private_key/pkcs11",
179
+ )
180
+
181
+ # . . . but both cannot be provided!
182
+ if self.private_key is not None and self.pkcs11 is not None:
183
+ raise BRSConfigurationError(
184
+ "Only one of 'private_key' or 'pkcs11' can be provided.",
185
+ param="private_key/pkcs11",
186
+ )
187
+
188
+ def _get_credentials(self) -> TemporaryCredentials:
189
+ url = urlparse(
190
+ f"https://{self.endpoint}/role-aliases/{self.role_alias}"
191
+ "/credentials"
192
+ )
193
+ request = HttpRequest("GET", url.path)
194
+ request.headers.add("host", str(url.hostname))
195
+ if self.thing_name:
196
+ request.headers.add("x-amzn-iot-thingname", self.thing_name)
197
+ if self.duration_seconds:
198
+ request.headers.add(
199
+ "x-amzn-iot-credential-duration-seconds",
200
+ str(self.duration_seconds),
201
+ )
202
+ response = AWSCRTResponse()
203
+ port = 443 if not url.port else url.port
204
+ connection = (
205
+ self._mtls_client_connection(url=url, port=port)
206
+ if not self.pkcs11
207
+ else self._mtls_pkcs11_client_connection(url=url, port=port)
208
+ )
209
+
210
+ try:
211
+ stream = connection.request(
212
+ request, response.on_response, response.on_body
213
+ )
214
+ stream.activate()
215
+ stream.completion_future.result(float(self.timeout))
216
+ finally:
217
+ try:
218
+ connection.close()
219
+ except Exception:
220
+ ...
221
+
222
+ if response.status_code == 200:
223
+ credentials = json.loads(response.body.decode("utf-8"))[
224
+ "credentials"
225
+ ]
226
+ return {
227
+ "access_key": credentials["accessKeyId"],
228
+ "secret_key": credentials["secretAccessKey"],
229
+ "token": credentials["sessionToken"],
230
+ "expiry_time": credentials["expiration"],
231
+ }
232
+ else:
233
+ response_body = json.loads(response.body.decode())
234
+ raise BRSRequestError(
235
+ "Error getting credentials from IoT credential provider.",
236
+ status_code=response.status_code,
237
+ details=response_body,
238
+ )
239
+
240
+ def _mtls_client_connection(
241
+ self, url: ParseResult, port: int
242
+ ) -> HttpClientConnection:
243
+ event_loop_group: EventLoopGroup = EventLoopGroup()
244
+ host_resolver: DefaultHostResolver = DefaultHostResolver(
245
+ event_loop_group
246
+ )
247
+ bootstrap: ClientBootstrap = ClientBootstrap(
248
+ event_loop_group, host_resolver
249
+ )
250
+ tls_ctx_opt = TlsContextOptions.create_client_with_mtls(
251
+ cert_buffer=self.certificate, key_buffer=self.private_key
252
+ )
253
+
254
+ if self.ca:
255
+ tls_ctx_opt.override_default_trust_store(self.ca)
256
+
257
+ tls_ctx_opt.verify_peer = self.verify_peer
258
+ tls_ctx = ClientTlsContext(tls_ctx_opt)
259
+ tls_conn_opt: TlsConnectionOptions = cast(
260
+ TlsConnectionOptions, tls_ctx.new_connection_options()
261
+ )
262
+ tls_conn_opt.set_server_name(str(url.hostname))
263
+
264
+ try:
265
+ connection_future = HttpClientConnection.new(
266
+ host_name=str(url.hostname),
267
+ port=port,
268
+ bootstrap=bootstrap,
269
+ tls_connection_options=tls_conn_opt,
270
+ )
271
+ return connection_future.result(self.timeout)
272
+ except AwsCrtError as err:
273
+ raise BRSConnectionError(
274
+ "Error completing mTLS connection to endpoint "
275
+ f"'{url.hostname}'",
276
+ param="endpoint",
277
+ value=str(url.hostname),
278
+ ) from err
279
+
280
+ def _mtls_pkcs11_client_connection(
281
+ self, url: ParseResult, port: int
282
+ ) -> HttpClientConnection:
283
+ event_loop_group: EventLoopGroup = EventLoopGroup()
284
+ host_resolver: DefaultHostResolver = DefaultHostResolver(
285
+ event_loop_group
286
+ )
287
+ bootstrap: ClientBootstrap = ClientBootstrap(
288
+ event_loop_group, host_resolver
289
+ )
290
+
291
+ if not self.pkcs11:
292
+ raise BRSConfigurationError(
293
+ "Attempting to establish mTLS connection using PKCS#11 "
294
+ "but 'pkcs11' parameter is 'None'!",
295
+ param="pkcs11",
296
+ )
297
+
298
+ tls_ctx_opt = TlsContextOptions.create_client_with_mtls_pkcs11(
299
+ pkcs11_lib=Pkcs11Lib(file=self.pkcs11["pkcs11_lib"]),
300
+ user_pin=self.pkcs11["user_pin"],
301
+ slot_id=self.pkcs11["slot_id"],
302
+ token_label=self.pkcs11["token_label"],
303
+ private_key_label=self.pkcs11["private_key_label"],
304
+ cert_file_contents=self.certificate,
305
+ )
306
+
307
+ if self.ca:
308
+ tls_ctx_opt.override_default_trust_store(self.ca)
309
+
310
+ tls_ctx_opt.verify_peer = self.verify_peer
311
+ tls_ctx = ClientTlsContext(tls_ctx_opt)
312
+ tls_conn_opt: TlsConnectionOptions = cast(
313
+ TlsConnectionOptions, tls_ctx.new_connection_options()
314
+ )
315
+ tls_conn_opt.set_server_name(str(url.hostname))
316
+
317
+ try:
318
+ connection_future = HttpClientConnection.new(
319
+ host_name=str(url.hostname),
320
+ port=port,
321
+ bootstrap=bootstrap,
322
+ tls_connection_options=tls_conn_opt,
323
+ )
324
+ return connection_future.result(self.timeout)
325
+ except AwsCrtError as err:
326
+ raise BRSConnectionError(
327
+ "Error completing mTLS connection.",
328
+ param="endpoint",
329
+ value=str(url.hostname),
330
+ ) from err
331
+
332
+ def get_identity(self) -> Identity:
333
+ """Returns metadata about the current caller identity.
334
+
335
+ Returns
336
+ -------
337
+ Identity
338
+ Dict containing information about the current calleridentity.
339
+ """
340
+
341
+ return self.client("sts").get_caller_identity()
342
+
343
+ @staticmethod
344
+ def _normalize_iot_credential_endpoint(endpoint: str) -> str:
345
+ if ".credentials.iot." in endpoint:
346
+ return endpoint
347
+
348
+ if ".iot." in endpoint and "-ats." in endpoint:
349
+ logged_data_endpoint = re.sub(r"^[^. -]+", "***", endpoint)
350
+ logged_credential_endpoint = re.sub(
351
+ r"^[^. -]+",
352
+ "***",
353
+ (endpoint := endpoint.replace("-ats.iot", ".credentials.iot")),
354
+ )
355
+ BRSWarning.warn(
356
+ "The 'endpoint' parameter you provided represents the data "
357
+ "endpoint for IoT not the credentials endpoint! The endpoint "
358
+ "you provided was therefore modified from "
359
+ f"'{logged_data_endpoint}' -> '{logged_credential_endpoint}'"
360
+ )
361
+ return endpoint
362
+
363
+ raise BRSValidationError(
364
+ "Invalid IoT endpoint provided for credentials provider. "
365
+ "Expected '<id>.credentials.iot.<region>.amazonaws.com'",
366
+ param="endpoint",
367
+ value=endpoint,
368
+ )
369
+
370
+ @staticmethod
371
+ def _validate_pkcs11(pkcs11: PKCS11) -> PKCS11:
372
+ if "pkcs11_lib" not in pkcs11:
373
+ raise BRSConfigurationError(
374
+ "PKCS#11 library path must be provided as 'pkcs11_lib'"
375
+ " in 'pkcs11'.",
376
+ param="pkcs11_lib",
377
+ )
378
+ elif not Path(pkcs11["pkcs11_lib"]).expanduser().resolve().is_file():
379
+ raise BRSValidationError(
380
+ f"'{pkcs11['pkcs11_lib']}' is not a valid file path for "
381
+ "'pkcs11_lib' in 'pkcs11'.",
382
+ param="pkcs11_lib",
383
+ value=pkcs11.get("pkcs11_lib"),
384
+ )
385
+ pkcs11.setdefault("user_pin", None)
386
+ pkcs11.setdefault("slot_id", None)
387
+ pkcs11.setdefault("token_label", None)
388
+ pkcs11.setdefault("private_key_label", None)
389
+ return pkcs11
390
+
391
+ @staticmethod
392
+ def _read_maybe_path_to_bytes(
393
+ v: str | bytes | None, fallback: bytes | None, name: str
394
+ ) -> bytes | None:
395
+ match v:
396
+ case None:
397
+ return fallback
398
+ case bytes():
399
+ return v
400
+ case str() as p if Path(p).expanduser().resolve().is_file():
401
+ return Path(p).expanduser().resolve().read_bytes()
402
+ case _:
403
+ value = type(v).__name__
404
+ raise BRSValidationError(
405
+ f"Invalid {name} provided.",
406
+ param=name,
407
+ value=value,
408
+ )
409
+
410
+ @staticmethod
411
+ def _bytes_to_tempfile(b: bytes, suffix: str = ".pem") -> str:
412
+ f = NamedTemporaryFile("wb", suffix=suffix, delete=False)
413
+ f.write(b)
414
+ f.flush()
415
+ f.close()
416
+ _TEMP_PATHS.append(f.name)
417
+ return f.name
418
+
419
+ @staticmethod
420
+ @register
421
+ def _cleanup_tempfiles():
422
+ for p in _TEMP_PATHS:
423
+ try:
424
+ Path(p).unlink(missing_ok=True)
425
+ except Exception:
426
+ ...
427
+
428
+ def mqtt(
429
+ self,
430
+ *,
431
+ endpoint: str,
432
+ client_id: str,
433
+ transport: Transport = "x509",
434
+ certificate: str | bytes | None = None,
435
+ private_key: str | bytes | None = None,
436
+ ca: str | bytes | None = None,
437
+ pkcs11: PKCS11 | None = None,
438
+ region: str | None = None,
439
+ keep_alive_secs: int = 60,
440
+ clean_start: bool = True,
441
+ port: int | None = None,
442
+ use_alpn: bool = False,
443
+ ) -> Connection:
444
+ """Establishes an MQTT connection using the specified parameters.
445
+
446
+ .. versionadded:: 5.1.0
447
+
448
+ Parameters
449
+ ----------
450
+ endpoint: str
451
+ The MQTT endpoint to connect to.
452
+ client_id: str
453
+ The client ID to use for the MQTT connection.
454
+ transport: Transport
455
+ The transport protocol to use (e.g., "x509" or "ws").
456
+ certificate: str | bytes | None, optional
457
+ The client certificate to use for the connection. Defaults to the
458
+ session certificate.
459
+ private_key: str | bytes | None, optional
460
+ The private key to use for the connection. Defaults to the
461
+ session private key.
462
+ ca: str | bytes | None, optional
463
+ The CA certificate to use for the connection. Defaults to the
464
+ session CA certificate.
465
+ pkcs11: PKCS11 | None, optional
466
+ PKCS#11 configuration for hardware-backed keys. Defaults to the
467
+ session PKCS#11 configuration.
468
+ region: str | None, optional
469
+ The AWS region to use for the connection. Defaults to the
470
+ session region.
471
+ keep_alive_secs: int, optional
472
+ The keep-alive interval for the MQTT connection. Default is 60
473
+ seconds.
474
+ clean_start: bool, optional
475
+ Whether to start a clean session. Default is True.
476
+ port: int | None, optional
477
+ The port to use for the MQTT connection. Default is 8883 if not
478
+ using ALPN, otherwise 443.
479
+ use_alpn: bool, optional
480
+ Whether to use ALPN for the connection. Default is False.
481
+
482
+ Returns
483
+ -------
484
+ awscrt.mqtt.Connection
485
+ The established MQTT connection.
486
+ """
487
+
488
+ # Validate transport
489
+ if transport not in list(get_args(Transport)):
490
+ raise BRSValidationError(
491
+ "Transport must be 'x509' or 'ws'",
492
+ param="transport",
493
+ value=transport,
494
+ )
495
+
496
+ # Region default (WS only)
497
+ if region is None:
498
+ region = self.region_name
499
+
500
+ # Normalize inputs to bytes using session defaults
501
+ cert_bytes = self._read_maybe_path_to_bytes(
502
+ certificate, getattr(self, "certificate", None), "certificate"
503
+ )
504
+ key_bytes = self._read_maybe_path_to_bytes(
505
+ private_key, getattr(self, "private_key", None), "private_key"
506
+ )
507
+ ca_bytes = self._read_maybe_path_to_bytes(
508
+ ca, getattr(self, "ca", None), "ca"
509
+ )
510
+
511
+ # Validate PKCS#11
512
+ match pkcs11:
513
+ case None:
514
+ pkcs11 = getattr(self, "pkcs11", None)
515
+ case dict():
516
+ pkcs11 = self._validate_pkcs11(pkcs11)
517
+ case _:
518
+ raise BRSValidationError(
519
+ "Invalid PKCS#11 configuration provided.",
520
+ param="pkcs11",
521
+ value=type(pkcs11).__name__,
522
+ )
523
+
524
+ # X.509 invariants
525
+ if transport == "x509":
526
+ has_key = key_bytes is not None
527
+ has_hsm = pkcs11 is not None
528
+ if not has_key and not has_hsm:
529
+ raise BRSConfigurationError(
530
+ "For transport='x509', provide either 'private_key' "
531
+ "(bytes/path) or 'pkcs11'.",
532
+ param="private_key/pkcs11",
533
+ )
534
+ if has_key and has_hsm:
535
+ raise BRSConfigurationError(
536
+ "Provide only one of 'private_key' or 'pkcs11' for "
537
+ "transport='x509'.",
538
+ param="private_key/pkcs11",
539
+ )
540
+ if cert_bytes is None:
541
+ raise BRSConfigurationError(
542
+ "Certificate is required for transport='x509'",
543
+ param="certificate",
544
+ )
545
+
546
+ # CRT bootstrap
547
+ event_loop = io.EventLoopGroup(1)
548
+ host_resolver = io.DefaultHostResolver(event_loop)
549
+ bootstrap = io.ClientBootstrap(event_loop, host_resolver)
550
+
551
+ # Build connection
552
+ if transport == "x509":
553
+ if pkcs11 is not None:
554
+ # Cert must be a filepath for PKCS#11 builder → write temp
555
+ cert_path = self._bytes_to_tempfile(
556
+ cast(bytes, cert_bytes), ".crt"
557
+ )
558
+ ca_path = (
559
+ self._bytes_to_tempfile(ca_bytes, ".pem")
560
+ if ca_bytes
561
+ else None
562
+ )
563
+
564
+ return mqtt_connection_builder.mtls_with_pkcs11(
565
+ endpoint=endpoint,
566
+ client_bootstrap=bootstrap,
567
+ pkcs11_lib=Pkcs11Lib(file=pkcs11["pkcs11_lib"]),
568
+ user_pin=pkcs11.get("user_pin"),
569
+ slot_id=pkcs11.get("slot_id"),
570
+ token_label=pkcs11.get("token_label"),
571
+ private_key_object=pkcs11.get("private_key_label"),
572
+ cert_filepath=cert_path,
573
+ ca_filepath=ca_path,
574
+ client_id=client_id,
575
+ clean_session=clean_start,
576
+ keep_alive_secs=keep_alive_secs,
577
+ port=port or (443 if use_alpn else 8883),
578
+ alpn_list=["x-amzn-mqtt-ca"] if use_alpn else None,
579
+ )
580
+ else:
581
+ # pure mTLS with in-memory cert/key/CA
582
+ return mqtt_connection_builder.mtls_from_bytes(
583
+ endpoint=endpoint,
584
+ cert_bytes=cert_bytes,
585
+ pri_key_bytes=key_bytes,
586
+ ca_bytes=ca_bytes,
587
+ client_bootstrap=bootstrap,
588
+ client_id=client_id,
589
+ clean_session=clean_start,
590
+ keep_alive_secs=keep_alive_secs,
591
+ port=port or (443 if use_alpn else 8883),
592
+ alpn_list=["x-amzn-mqtt-ca"] if use_alpn else None,
593
+ )
594
+
595
+ else: # transport == "ws"
596
+ # WebSockets + SigV4
597
+ creds_provider = auth.AwsCredentialsProvider.new_delegate(
598
+ self._credentials
599
+ )
600
+ ca_path = (
601
+ self._bytes_to_tempfile(ca_bytes, ".pem") if ca_bytes else None
602
+ )
603
+
604
+ return mqtt_connection_builder.websockets_with_default_aws_signing(
605
+ endpoint=endpoint,
606
+ client_bootstrap=bootstrap,
607
+ region=region,
608
+ credentials_provider=creds_provider,
609
+ client_id=client_id,
610
+ clean_session=clean_start,
611
+ keep_alive_secs=keep_alive_secs,
612
+ ca_filepath=ca_path,
613
+ port=port or 443,
614
+ )