Skip to content

Commit f72b890

Browse files
authored
adding certificate_source to crt layer (#747)
1 parent 4a456f3 commit f72b890

7 files changed

Lines changed: 132 additions & 26 deletions

File tree

awscrt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
__all__ = [
88
'aio',
99
'auth',
10+
'aws_iot_metrics',
1011
'crypto',
1112
'http',
1213
'io',
@@ -16,7 +17,6 @@
1617
'mqtt_request_response',
1718
's3',
1819
'websocket',
19-
'aws_iot_metrics',
2020
]
2121

2222
__version__ = '1.0.0.dev0'

awscrt/aws_iot_metrics.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ class _MetricsFeatureId(str, Enum):
6363
# Feature Value Constants
6464

6565

66+
def _certificate_source_metrics_value(source):
67+
"""Map _CertificateSource to its single-character metrics value.
68+
69+
Mapping: CERTIFICATE_FILES->A, PKCS11->B, WINDOWS_CERT_STORE->C, PKCS12_FILE->E.
70+
"D" is reserved for Java KeyStore.
71+
Returns None if source is None or unrecognized.
72+
"""
73+
from awscrt.io import _CertificateSource
74+
mapping = {
75+
_CertificateSource.CERTIFICATE_FILES: "A",
76+
_CertificateSource.PKCS11: "B",
77+
_CertificateSource.WINDOWS_CERT_STORE: "C",
78+
_CertificateSource.PKCS12_FILE: "E",
79+
}
80+
return mapping.get(source)
81+
82+
6683
def _protocol_version_metrics_value(protocol):
6784
"""Map protocol version to its single-character metrics value.
6885
@@ -221,11 +238,10 @@ def _get_encoded_feature_list(client_options):
221238
- D (outbound_topic_alias_behavior): from topic_aliasing_options.outbound_behavior
222239
- E (inbound_topic_alias_behavior): from topic_aliasing_options.inbound_behavior
223240
- H (http_proxy_type): HTTP or HTTPS based on proxy TLS settings
241+
- I (certificate_source): detected from TlsContextOptions
224242
- J (tls_cipher_preference): mapped from TlsCipherPref on the TLS context
225243
- K (minimum_tls_version): mapped from TlsVersion on the TLS context
226244
227-
Feature I (certificate_source) is set at the IoT SDK level, not here.
228-
229245
Args:
230246
client_options: MQTT5 ClientOptions dataclass.
231247
Returns:
@@ -277,7 +293,11 @@ def _get_encoded_feature_list(client_options):
277293
val = _http_proxy_type_metrics_value(client_options.http_proxy_options)
278294
features.append(f"{_MetricsFeatureId.HTTP_PROXY_TYPE.value}/{val}")
279295

280-
# I: certificate_source - Would need to be tracked from TLS context setup. This is set at a IoT SDK level
296+
# I: certificate_source - detected from TlsContextOptions factory method
297+
if client_options.tls_ctx is not None:
298+
val = _certificate_source_metrics_value(client_options.tls_ctx._certificate_source)
299+
if val:
300+
features.append(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/{val}")
281301

282302
# J: tls_cipher_preference - security policy
283303
if client_options.tls_ctx is not None:
@@ -306,6 +326,7 @@ def _get_encoded_feature_list_mqtt3(proxy_options, tls_ctx=None):
306326
307327
Conditionally includes:
308328
- H (http_proxy_type): HTTP or HTTPS based on proxy TLS settings
329+
- I (certificate_source): detected from TlsContextOptions
309330
- J (tls_cipher_preference): mapped from TlsCipherPref on the TLS context
310331
- K (minimum_tls_version): mapped from TlsVersion on the TLS context
311332
@@ -324,6 +345,12 @@ def _get_encoded_feature_list_mqtt3(proxy_options, tls_ctx=None):
324345
val = _http_proxy_type_metrics_value(proxy_options)
325346
features.append(f"{_MetricsFeatureId.HTTP_PROXY_TYPE.value}/{val}")
326347

348+
# I: certificate_source - detected from TlsContextOptions factory method
349+
if tls_ctx is not None:
350+
val = _certificate_source_metrics_value(tls_ctx._certificate_source)
351+
if val:
352+
features.append(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/{val}")
353+
327354
# J: tls_cipher_preference - security policy
328355
if tls_ctx is not None:
329356
val = _tls_cipher_preference_metrics_value(tls_ctx._cipher_pref)
@@ -387,10 +414,8 @@ def _create_metrics(user_metrics, crt_feature_list):
387414
IoTSDKMetricsVersion, IoTSDKFeature) are passed through unchanged.
388415
389416
Args:
390-
user_metrics : Metrics configuration from
391-
the IoT SDK. May be None if no SDK-level metrics are provided.
392-
crt_feature_list : Encoded CRT feature list string generated
393-
by _get_encoded_feature_list or _get_encoded_feature_list_mqtt3.
417+
user_metrics: Metrics configuration from the IoT SDK. May be None if no SDK-level metrics are provided.
418+
crt_feature_list: Encoded CRT feature list string generated by _get_encoded_feature_list or _get_encoded_feature_list_mqtt3.
394419
Returns:
395420
AWSIoTMetrics: The final metrics object ready to be embedded in the
396421
MQTT CONNECT packet username field.
@@ -434,11 +459,12 @@ def _create_metrics(user_metrics, crt_feature_list):
434459
def _create_metrics_mqtt5(client_options):
435460
"""Create the final AWSIoTMetrics object for an MQTT5 client.
436461
437-
Generates the CRT feature list from the full set of MQTT5 ClientOptions
462+
Generates the CRT feature list from the full set of MQTT5 ClientOptions,
463+
including detected certificate source from the TLS context.
438464
439465
Args:
440466
client_options: MQTT5 ClientOptions dataclass containing all
441-
connection configuration and optional user metrics.
467+
connection configuration and optional user (AWSIoTMetrics) metrics.
442468
Returns:
443469
AWSIoTMetrics: The final metrics object with merged CRT and SDK features.
444470
"""
@@ -447,18 +473,15 @@ def _create_metrics_mqtt5(client_options):
447473

448474

449475
def _create_metrics_mqtt3(user_metrics=None, proxy_options=None, tls_ctx=None):
450-
"""Creates the final AWSIoTMetrics object for an MQTT3 connection.
476+
"""Create the final AWSIoTMetrics object for an MQTT3 connection.
451477
452-
Generates the CRT feature list from the MQTT3 connection parameters
478+
Generates the CRT feature list from the MQTT3 connection parameters,
479+
including detected certificate source from the TLS context.
453480
454481
Args:
455-
user_metrics : Optional metrics configuration
456-
provided by the IoT SDK. If None, defaults are used.
457-
proxy_options : Optional HTTP proxy options
458-
from the Connection, used to determine proxy type feature.
459-
tls_ctx : Optional TLS context from the
460-
connection, used to determine cipher preference and minimum TLS
461-
version features.
482+
user_metrics: Optional AWSIoTMetrics configuration provided by the IoT SDK.
483+
proxy_options: Optional HTTP proxy options from the Connection.
484+
tls_ctx: Optional ClientTlsContext from the connection.
462485
Returns:
463486
AWSIoTMetrics: The final metrics object with merged CRT and SDK features.
464487
"""

awscrt/io.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@
1515
from typing import Union
1616

1717

18+
class _CertificateSource(IntEnum):
19+
"""Certificate source types for mTLS authentication.
20+
21+
Used by TlsContextOptions to track which factory method created the
22+
TLS configuration. The metrics reads this and encodes it as
23+
feature ID "I" in the metrics string.
24+
"""
25+
CERTIFICATE_FILES = 0 # PEM cert + key files
26+
PKCS11 = 1 # Hardware security module via PKCS11
27+
WINDOWS_CERT_STORE = 2 # Windows certificate source
28+
# 3 is reserved for Java keystore (not applicable to Python)
29+
PKCS12_FILE = 4 # PKCS#12 (.p12/.pfx) files
30+
31+
1832
class LogLevel(IntEnum):
1933
NoLogs = 0 #:
2034
Fatal = 1 #:
@@ -340,6 +354,7 @@ class TlsContextOptions:
340354
'_pkcs11_cert_file_path',
341355
'_pkcs11_cert_file_contents',
342356
'_windows_cert_store_path',
357+
'_certificate_source',
343358
)
344359

345360
def __init__(self):
@@ -396,6 +411,7 @@ def create_client_with_mtls(cert_buffer, key_buffer):
396411
opt = TlsContextOptions()
397412
opt.certificate_buffer = cert_buffer
398413
opt.private_key_buffer = key_buffer
414+
opt._certificate_source = _CertificateSource.CERTIFICATE_FILES
399415

400416
return opt
401417

@@ -456,6 +472,7 @@ def create_client_with_mtls_pkcs11(*,
456472
opt._pkcs11_private_key_label = private_key_label
457473
opt._pkcs11_cert_file_path = cert_file_path
458474
opt._pkcs11_cert_file_contents = cert_file_contents
475+
opt._certificate_source = _CertificateSource.PKCS11
459476
return opt
460477

461478
@staticmethod
@@ -480,6 +497,7 @@ def create_client_with_mtls_pkcs12(pkcs12_filepath, pkcs12_password):
480497
opt = TlsContextOptions()
481498
opt.pkcs12_filepath = pkcs12_filepath
482499
opt.pkcs12_password = pkcs12_password
500+
opt._certificate_source = _CertificateSource.PKCS12_FILE
483501
return opt
484502

485503
@staticmethod
@@ -501,6 +519,7 @@ def create_client_with_mtls_windows_cert_store_path(cert_path):
501519
assert isinstance(cert_path, str)
502520
opt = TlsContextOptions()
503521
opt._windows_cert_store_path = cert_path
522+
opt._certificate_source = _CertificateSource.WINDOWS_CERT_STORE
504523
return opt
505524

506525
@staticmethod
@@ -614,7 +633,7 @@ class ClientTlsContext(NativeResource):
614633
Args:
615634
options (TlsContextOptions): Configuration options.
616635
"""
617-
__slots__ = ('_min_tls_ver', '_cipher_pref')
636+
__slots__ = ('_min_tls_ver', '_cipher_pref', '_certificate_source')
618637

619638
def __init__(self, options):
620639
assert isinstance(options, TlsContextOptions)
@@ -623,6 +642,7 @@ def __init__(self, options):
623642

624643
self._min_tls_ver = options.min_tls_ver
625644
self._cipher_pref = options.cipher_pref
645+
self._certificate_source = options._certificate_source
626646

627647
self._binding = _awscrt.client_tls_ctx_new(
628648
options.min_tls_ver.value,

awscrt/mqtt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from awscrt.io import ClientBootstrap, ClientTlsContext, SocketOptions
1717
from dataclasses import dataclass
1818
from awscrt.mqtt5 import Client as Mqtt5Client
19-
from awscrt.aws_iot_metrics import AWSIoTMetrics, IoTMetricsMetadata, _create_metrics_mqtt3
19+
from awscrt.aws_iot_metrics import AWSIoTMetrics, _create_metrics_mqtt3
2020

2121

2222
class QoS(IntEnum):
@@ -334,7 +334,7 @@ class Connection(NativeResource):
334334
335335
disable_metrics (bool): Disable IoT SDK metrics in MQTT CONNECT packet username field, including SDK name, version, and platform. Default to False.
336336
337-
metrics (Optional[AWSIoTMetrics]) : Optional metrics configuration for IoT SDK metrics reporting. If provided, the CRT will use the given metrics. If None, a default AWSIoTMetrics will be created.
337+
metrics (Optional[AWSIoTMetrics]): Optional metrics configuration for IoT SDK metrics reporting. If provided, the CRT will use the given metrics. If None, a default AWSIoTMetrics will be created.
338338
"""
339339

340340
def __init__(self,

awscrt/mqtt5.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from dataclasses import dataclass
1616
from collections.abc import Sequence
1717
from inspect import signature
18-
from awscrt.aws_iot_metrics import AWSIoTMetrics, IoTMetricsMetadata, _create_metrics_mqtt5
18+
from awscrt.aws_iot_metrics import AWSIoTMetrics, _create_metrics_mqtt5
1919

2020

2121
class QoS(IntEnum):
@@ -1372,7 +1372,7 @@ class ClientOptions:
13721372
on_lifecycle_event_connection_failure_fn (Callable[[LifecycleConnectFailureData],]): Callback for Lifecycle Event Connection Failure.
13731373
on_lifecycle_event_disconnection_fn (Callable[[LifecycleDisconnectData],]): Callback for Lifecycle Event Disconnection.
13741374
disable_metrics (bool): Disable IoT SDK metrics in MQTT CONNECT packet username field, including SDK name, version, and platform. Default to False.
1375-
metrics (Optional[AWSIoTMetrics]) : Optional metrics configuration for IoT SDK metrics reporting. If provided, the CRT will use the given metrics. If None, a default AWSIoTMetrics will be created.
1375+
metrics (Optional[AWSIoTMetrics]): Optional metrics configuration for IoT SDK metrics reporting. If provided, the CRT will use the given metrics. If None, a default AWSIoTMetrics will be created.
13761376
13771377
"""
13781378
host_name: str

docsrc/source/api/aws_iot_metrics.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@ awscrt.aws_iot_metrics
33

44
.. automodule:: awscrt.aws_iot_metrics
55
:members:
6-

test/test_aws_iot_metrics.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
InboundTopicAliasBehaviorType,
2727
TopicAliasingOptions,
2828
)
29-
from awscrt.io import ClientTlsContext, TlsContextOptions, TlsConnectionOptions, TlsVersion, TlsCipherPref
29+
from awscrt.io import ClientTlsContext, TlsContextOptions, TlsConnectionOptions, TlsVersion, TlsCipherPref, _CertificateSource
3030
from awscrt.http import HttpProxyOptions
3131

3232

@@ -419,5 +419,69 @@ def test_custom_library_name(self):
419419
self.assertEqual("MyCustomSDK/1.0", result.library_name)
420420

421421

422+
class TestCertificateSourceMetrics(NativeResourceTest):
423+
"""Test certificate source metrics encoding through the TLS context binding path."""
424+
425+
def _make_tls_ctx_with_source(self, source):
426+
tls_options = TlsContextOptions()
427+
tls_options._certificate_source = source
428+
return ClientTlsContext(tls_options)
429+
430+
def test_certificate_source_certificate_files(self):
431+
"""CERTIFICATE_FILES source encodes as 'A' in feature list."""
432+
tls_ctx = self._make_tls_ctx_with_source(_CertificateSource.CERTIFICATE_FILES)
433+
434+
options = ClientOptions(host_name="localhost", port=8883, tls_ctx=tls_ctx)
435+
result = _get_encoded_feature_list(options)
436+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/A", result)
437+
438+
result_mqtt3 = _get_encoded_feature_list_mqtt3(proxy_options=None, tls_ctx=tls_ctx)
439+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/A", result_mqtt3)
440+
441+
def test_certificate_source_pkcs11(self):
442+
"""PKCS11 source encodes as 'B' in feature list."""
443+
tls_ctx = self._make_tls_ctx_with_source(_CertificateSource.PKCS11)
444+
445+
options = ClientOptions(host_name="localhost", port=8883, tls_ctx=tls_ctx)
446+
result = _get_encoded_feature_list(options)
447+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/B", result)
448+
449+
result_mqtt3 = _get_encoded_feature_list_mqtt3(proxy_options=None, tls_ctx=tls_ctx)
450+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/B", result_mqtt3)
451+
452+
def test_certificate_source_windows_cert_store(self):
453+
"""WINDOWS_CERT_STORE source encodes as 'C' in feature list."""
454+
tls_ctx = self._make_tls_ctx_with_source(_CertificateSource.WINDOWS_CERT_STORE)
455+
456+
options = ClientOptions(host_name="localhost", port=8883, tls_ctx=tls_ctx)
457+
result = _get_encoded_feature_list(options)
458+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/C", result)
459+
460+
result_mqtt3 = _get_encoded_feature_list_mqtt3(proxy_options=None, tls_ctx=tls_ctx)
461+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/C", result_mqtt3)
462+
463+
def test_certificate_source_pkcs12(self):
464+
"""PKCS12_FILE source encodes as 'E' in feature list."""
465+
tls_ctx = self._make_tls_ctx_with_source(_CertificateSource.PKCS12_FILE)
466+
467+
options = ClientOptions(host_name="localhost", port=8883, tls_ctx=tls_ctx)
468+
result = _get_encoded_feature_list(options)
469+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/E", result)
470+
471+
result_mqtt3 = _get_encoded_feature_list_mqtt3(proxy_options=None, tls_ctx=tls_ctx)
472+
self.assertIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/E", result_mqtt3)
473+
474+
def test_no_certificate_source_omits_feature(self):
475+
"""No certificate source (None) should omit feature I from metrics."""
476+
tls_ctx = self._make_tls_ctx_with_source(None)
477+
478+
options = ClientOptions(host_name="localhost", port=8883, tls_ctx=tls_ctx)
479+
result = _get_encoded_feature_list(options)
480+
self.assertNotIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/", result)
481+
482+
result_mqtt3 = _get_encoded_feature_list_mqtt3(proxy_options=None, tls_ctx=tls_ctx)
483+
self.assertNotIn(f"{_MetricsFeatureId.CERTIFICATE_SOURCE.value}/", result_mqtt3)
484+
485+
422486
if __name__ == '__main__':
423487
unittest.main()

0 commit comments

Comments
 (0)