diff --git a/.gitignore b/.gitignore index daedfe76..3ad0f3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ coverage.xml pylint_report.txt build/ docs/_build/ -htmlcov/ \ No newline at end of file +htmlcov/ +.claude/ \ No newline at end of file diff --git a/README b/README index f841a5bc..a039a321 100644 --- a/README +++ b/README @@ -42,6 +42,8 @@ Functionality implemented include: - TLS Certificate Compression (RFC 8879) - Hybrid ML-KEM key exchage groups (draft-kwiatkowski-tls-ecdhe-mlkem-02) - support for Brainpool curves in TLS 1.2 and TLS 1.3 + - Delegated Credentials (RFC 9345) + - ML-DSA certificates suppport (draft-ietf-tls-mldsa-00) tlslite-ng aims to be a drop-in replacement for tlslite while providing more diff --git a/README.md b/README.md index 0b96bcb0..e21a53e8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -tlslite-ng version 0.8.2 (2025-01-22) +tlslite-ng version 0.9.0b2 (2025-09-26) [![GitHub CI](https://github.com/tlsfuzzer/tlslite-ng/actions/workflows/ci.yml/badge.svg)](https://github.com/tlsfuzzer/tlslite-ng/actions/workflows/ci.yml) [![Read the Docs](https://img.shields.io/readthedocs/tlslite-ng)](https://tlslite-ng.readthedocs.io/en/latest/) @@ -61,7 +61,7 @@ Implemented TLS features include: * Extended master secret * padding extension * keying material exporter -* RSA, RSA-PSS, DSA, ECDSA, and EdDSA certificates +* RSA, RSA-PSS, DSA, ECDSA, EdDSA, and ML-DSA certificates * ticket based session resumption * 1-RTT handshake, Hello Retry Request, middlebox compatibility mode, cookie extension, post-handshake authentication and KeyUpdate @@ -622,6 +622,10 @@ Similarly, while delegated credentials have a valid time option, it is not enfor 12 History =========== +0.9.0b2 - 2025-09-26 +* support for Delegated Credentials (Ganna Starovoytova) +* (Experimental) support for ML-DSA certificates in TLS + 0.8.2 - 2025-01-22 * additional test vectors for the RSA implicit rejection mechanism * fix negotiation of TLS 1.2 Brainpool key exchanges in TLS 1.3, only diff --git a/docs/conf.py b/docs/conf.py index 8a927f58..152b7525 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,9 +32,9 @@ # built documents. # # The short X.Y version. -version = u'0.8' +version = u'0.9' # The full version, including alpha/beta/rc tags. -release = u'0.8.2' +release = u'0.9.0b2' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 72f6a28d..ee187a81 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ README = f.read() setup(name="tlslite-ng", - version="0.8.2", + version="0.9.0b2", author="Alicja Kario", author_email="hkario@redhat.com", url="https://github.com/tlsfuzzer/tlslite-ng", @@ -24,7 +24,7 @@ 'package1': ['LICENSE', 'README.md']}, install_requires=['ecdsa>=0.18.0b1'], obsoletes=["tlslite"], - python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*", + python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -35,12 +35,14 @@ 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Security :: Cryptography', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Networking' diff --git a/tlslite/api.py b/tlslite/api.py index 4b1db819..01962236 100644 --- a/tlslite/api.py +++ b/tlslite/api.py @@ -4,7 +4,7 @@ # # See the LICENSE file for legal information regarding use of this file. -__version__ = "0.8.2" +__version__ = "0.9.0b2" # the whole module is about importing most commonly used methods, for use # by other applications # pylint: disable=unused-import diff --git a/tlslite/constants.py b/tlslite/constants.py index 17e49675..fe45c15d 100644 --- a/tlslite/constants.py +++ b/tlslite/constants.py @@ -158,30 +158,68 @@ class ExtensionType(TLSEnum): server_name = 0 # RFC 6066 / 4366 max_fragment_length = 1 # RFC 6066 / 4366 + client_certificate_url = 2 # RFC 6066 + trusted_ca_keys = 3 # RFC 6066 + truncated_hmac = 4 # RFC 6066 status_request = 5 # RFC 6066 / 4366 + user_mapping = 6 # RFC 4681 + client_authz = 7 # RFC 5878 + server_authz = 8 # RFC 5878 cert_type = 9 # RFC 6091 supported_groups = 10 # RFC 4492, RFC-ietf-tls-negotiated-ff-dhe-10 ec_point_formats = 11 # RFC 4492 srp = 12 # RFC 5054 signature_algorithms = 13 # RFC 5246 + use_srtp = 14 # RFC 5764 heartbeat = 15 # RFC 6520 alpn = 16 # RFC 7301 + status_request_v2 = 17 # RFC 6961 + signed_certificate_timestamp = 18 # RFC 6962 + client_certificate_type = 19 # RFC 7250 + server_certificate_type = 20 # RFC 7250 client_hello_padding = 21 # RFC 7685 encrypt_then_mac = 22 # RFC 7366 extended_master_secret = 23 # RFC 7627 + token_binding = 24 # RFC 8472 + cached_info = 25 # RFC 7924 + tls_lts = 26 # draft-gutmann-tls-lts compress_certificate = 27 # RFC 8879 record_size_limit = 28 # RFC 8449 - session_ticket = 35 # RFC 5077 + pwd_protect = 29 # RFC 8492 + pwd_clear = 30 # RFC 8492 + password_salt = 31 # RFC 8492 + ticket_pinning = 32 # RFC 8672 + tls_cert_with_extern_psk = 33 # RFC 8773 + delegated_credential = 34 # TLS 1.3, RFC 9345 + session_ticket = 35 # RFC 5077 + TLMSP = 36 # draft-gutmann-tls-lts + TLMSP_proxying = 37 # draft-gutmann-tls-lts + TLMSP_delegate = 38 # draft-gutmann-tls-lts + supported_ekt_ciphers = 39 # RFC 8870 extended_random = 40 # draft-rescorla-tls-extended-random-02 pre_shared_key = 41 # TLS 1.3 early_data = 42 # TLS 1.3 supported_versions = 43 # TLS 1.3 cookie = 44 # TLS 1.3 psk_key_exchange_modes = 45 # TLS 1.3 + certificate_authorities = 47 # RFC 8446 + oid_filters = 48 # RFC 8446 post_handshake_auth = 49 # TLS 1.3 signature_algorithms_cert = 50 # TLS 1.3 key_share = 51 # TLS 1.3 - delegated_credential = 34 # TLS 1.3, RFC 9345 + transparency_info = 52 # RFC 9162 + connection_id_deprecated = 53 # RFC 9146 (deprecated) + connection_id = 54 # RFC 9146 + external_id_hash = 55 # RFC 8844 + external_session_id = 56 # RFC 8844 + quic_transport_parameters = 57 # RFC 9001 + ticket_request = 58 # RFC 9149 + dnssec_chain = 59 # RFC 9102 + sequence_number_encryption_algorithms = 60 # draft-ietf-tls-dtls13 + rrc = 61 # RFC 9146 + tls_flags = 62 # draft-ietf-tls-tlsflags + ech_outer_extensions = 64768 # draft-ietf-tls-esni + encrypted_client_hello = 65037 # draft-ietf-tls-esni supports_npn = 13172 tack = 0xF300 renegotiation_info = 0xff01 # RFC 5746 diff --git a/tlslite/extensions.py b/tlslite/extensions.py index a45bb48d..868319ac 100644 --- a/tlslite/extensions.py +++ b/tlslite/extensions.py @@ -93,7 +93,7 @@ class TLSExtension(object): # actual definition at the end of file, after definitions of all classes _universalExtensions = {} _serverExtensions = {} - # _encryptedExtensions = {} + _encryptedExtensions = {} _certificateExtensions = {} _hrrExtensions = {} @@ -230,7 +230,7 @@ def parse(self, p): for handler_t, handlers in ( (self.cert, self._certificateExtensions), - # (self.encExtType, self._encryptedExtensions), + (self.encExtType, self._encryptedExtensions), (self.serverType, self._serverExtensions), (self.hrr, self._hrrExtensions), (True, self._universalExtensions)): @@ -2220,27 +2220,506 @@ def __init__(self): CertificateCompressionAlgorithm) +class StatusRequestV2Extension(TLSExtension): + """Handles status_request_v2 extension from RFC 6961.""" + + def __init__(self): + super(StatusRequestV2Extension, self).__init__( + extType=ExtensionType.status_request_v2) + self.status_request = bytearray(0) + + def create(self, status_request=None): + if status_request is None: + self.status_request = bytearray(0) + else: + self.status_request = status_request + return self + + @property + def extData(self): + return self.status_request + + def parse(self, parser): + self.status_request = parser.getFixBytes(parser.getRemainingLength()) + return self + + def __repr__(self): + return "StatusRequestV2Extension(status_request={0!r})".format( + self.status_request) + + +class SignedCertificateTimestampExtension(TLSExtension): + """Handles signed_certificate_timestamp extension from RFC 6962.""" + + def __init__(self): + super(SignedCertificateTimestampExtension, self).__init__( + extType=ExtensionType.signed_certificate_timestamp) + self.sct_data = bytearray(0) + + def create(self, sct_data=None): + if sct_data is None: + self.sct_data = bytearray(0) + else: + self.sct_data = sct_data + return self + + @property + def extData(self): + return self.sct_data + + def parse(self, parser): + self.sct_data = parser.getFixBytes(parser.getRemainingLength()) + return self + + def __repr__(self): + return "SignedCertificateTimestampExtension(sct_data={0!r})".format( + self.sct_data) + + +class EncryptThenMACExtension(TLSExtension): + """Handles encrypt_then_mac extension from RFC 7366.""" + + def __init__(self): + super(EncryptThenMACExtension, self).__init__( + extType=ExtensionType.encrypt_then_mac) + + def create(self): + return self + + @property + def extData(self): + return bytearray(0) + + def parse(self, parser): + if parser.getRemainingLength() != 0: + raise DecodeError("Non-empty encrypt_then_mac extension") + return self + + def __repr__(self): + return "EncryptThenMACExtension()" + + +class ExtendedMasterSecretExtension(TLSExtension): + """Handles extended_master_secret extension from RFC 7627.""" + + def __init__(self): + super(ExtendedMasterSecretExtension, self).__init__( + extType=ExtensionType.extended_master_secret) + + def create(self): + return self + + @property + def extData(self): + return bytearray(0) + + def parse(self, parser): + if parser.getRemainingLength() != 0: + raise DecodeError("Non-empty extended_master_secret extension") + return self + + def __repr__(self): + return "ExtendedMasterSecretExtension()" + + +class CertificateAuthoritiesExtension(TLSExtension): + """Handles certificate_authorities extension from RFC 8446.""" + + def __init__(self): + super(CertificateAuthoritiesExtension, self).__init__( + extType=ExtensionType.certificate_authorities) + self.certificate_authorities = None + + def create(self, certificate_authorities=None): + self.certificate_authorities = certificate_authorities + return self + + @property + def extData(self): + if self.certificate_authorities is None: + return bytearray(0) + + writer = Writer() + list_writer = Writer() + for cert_auth in self.certificate_authorities: + list_writer.add(len(cert_auth), 2) + list_writer.bytes += cert_auth + + writer.add(len(list_writer.bytes), 2) + writer.bytes += list_writer.bytes + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + self.certificate_authorities = None + return self + + p = Parser(parser.getFixBytes(parser.get(2))) + self.certificate_authorities = [] + while p.getRemainingLength() > 0: + cert_auth = p.getVarBytes(2) + self.certificate_authorities.append(cert_auth) + + return self + + def __repr__(self): + return "CertificateAuthoritiesExtension(certificate_authorities={0!r})".format( + self.certificate_authorities) + + +class OIDFiltersExtension(TLSExtension): + """Handles oid_filters extension from RFC 8446.""" + + def __init__(self): + super(OIDFiltersExtension, self).__init__( + extType=ExtensionType.oid_filters) + self.filters = [] + + def create(self, filters=None): + if filters is None: + self.filters = [] + else: + self.filters = filters + return self + + @property + def extData(self): + if not self.filters: + return bytearray(0) + + writer = Writer() + list_writer = Writer() + for cert_ext_oid, cert_ext_values in self.filters: + list_writer.add(len(cert_ext_oid), 1) + list_writer.bytes += cert_ext_oid + list_writer.add(len(cert_ext_values), 2) + list_writer.bytes += cert_ext_values + + writer.add(len(list_writer.bytes), 2) + writer.bytes += list_writer.bytes + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + self.filters = [] + return self + + p = Parser(parser.getFixBytes(parser.get(2))) + self.filters = [] + while p.getRemainingLength() > 0: + cert_ext_oid = p.getVarBytes(1) + cert_ext_values = p.getVarBytes(2) + self.filters.append((cert_ext_oid, cert_ext_values)) + + return self + + def __repr__(self): + return "OIDFiltersExtension(filters={0!r})".format(self.filters) + + +class PostHandshakeAuthExtension(TLSExtension): + """Handles post_handshake_auth extension from TLS 1.3.""" + + def __init__(self): + super(PostHandshakeAuthExtension, self).__init__( + extType=ExtensionType.post_handshake_auth) + + def create(self): + return self + + @property + def extData(self): + return bytearray(0) + + def parse(self, parser): + if parser.getRemainingLength() != 0: + raise DecodeError("Non-empty post_handshake_auth extension") + return self + + def __repr__(self): + return "PostHandshakeAuthExtension()" + + +class TruncatedHMACExtension(TLSExtension): + """Handles truncated_hmac extension from RFC 6066.""" + + def __init__(self): + super(TruncatedHMACExtension, self).__init__( + extType=ExtensionType.truncated_hmac) + + def create(self): + return self + + @property + def extData(self): + return bytearray(0) + + def parse(self, parser): + if parser.getRemainingLength() != 0: + raise DecodeError("Non-empty truncated_hmac extension") + return self + + def __repr__(self): + return "TruncatedHMACExtension()" + + +class UseSRTPExtension(TLSExtension): + """Handles use_srtp extension from RFC 5764 for DTLS-SRTP.""" + + def __init__(self): + super(UseSRTPExtension, self).__init__( + extType=ExtensionType.use_srtp) + self.protection_profiles = [] + self.mki = bytearray(0) + + def create(self, protection_profiles=None, mki=None): + self.protection_profiles = protection_profiles if protection_profiles else [] + self.mki = mki if mki else bytearray(0) + return self + + @property + def extData(self): + writer = Writer() + profile_writer = Writer() + for profile in self.protection_profiles: + profile_writer.add(profile, 2) + writer.add(len(profile_writer.bytes), 2) + writer.bytes += profile_writer.bytes + writer.add(len(self.mki), 1) + writer.bytes += self.mki + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + return self + profiles_len = parser.get(2) + self.protection_profiles = [] + for _ in range(profiles_len // 2): + self.protection_profiles.append(parser.get(2)) + mki_len = parser.get(1) + self.mki = parser.getFixBytes(mki_len) if mki_len > 0 else bytearray(0) + return self + + def __repr__(self): + return "UseSRTPExtension(protection_profiles={0!r}, mki={1!r})".format( + self.protection_profiles, self.mki) + + +class TokenBindingExtension(TLSExtension): + """Handles token_binding extension from RFC 8472.""" + + def __init__(self): + super(TokenBindingExtension, self).__init__( + extType=ExtensionType.token_binding) + self.key_parameters = [] + + def create(self, key_parameters=None): + self.key_parameters = key_parameters if key_parameters else [] + return self + + @property + def extData(self): + if not self.key_parameters: + return bytearray(0) + writer = Writer() + writer.add(len(self.key_parameters), 1) + for param in self.key_parameters: + writer.add(param, 1) + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + self.key_parameters = [] + return self + param_len = parser.get(1) + self.key_parameters = [] + for _ in range(param_len): + self.key_parameters.append(parser.get(1)) + return self + + def __repr__(self): + return "TokenBindingExtension(key_parameters={0!r})".format( + self.key_parameters) + + +class ClientCertificateTypeExtension(TLSExtension): + """Handles client_certificate_type extension from RFC 7250.""" + + def __init__(self): + super(ClientCertificateTypeExtension, self).__init__( + extType=ExtensionType.client_certificate_type) + self.certificate_types = [] + + def create(self, certificate_types=None): + self.certificate_types = certificate_types if certificate_types else [] + return self + + @property + def extData(self): + if not self.certificate_types: + return bytearray(0) + writer = Writer() + writer.add(len(self.certificate_types), 1) + for cert_type in self.certificate_types: + writer.add(cert_type, 1) + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + self.certificate_types = [] + return self + types_len = parser.get(1) + self.certificate_types = [] + for _ in range(types_len): + self.certificate_types.append(parser.get(1)) + return self + + def __repr__(self): + return "ClientCertificateTypeExtension(certificate_types={0!r})".format( + self.certificate_types) + + +class ServerCertificateTypeExtension(TLSExtension): + """Handles server_certificate_type extension from RFC 7250.""" + + def __init__(self): + super(ServerCertificateTypeExtension, self).__init__( + extType=ExtensionType.server_certificate_type) + self.certificate_types = [] + + def create(self, certificate_types=None): + self.certificate_types = certificate_types if certificate_types else [] + return self + + @property + def extData(self): + if not self.certificate_types: + return bytearray(0) + writer = Writer() + writer.add(len(self.certificate_types), 1) + for cert_type in self.certificate_types: + writer.add(cert_type, 1) + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + self.certificate_types = [] + return self + types_len = parser.get(1) + self.certificate_types = [] + for _ in range(types_len): + self.certificate_types.append(parser.get(1)) + return self + + def __repr__(self): + return "ServerCertificateTypeExtension(certificate_types={0!r})".format( + self.certificate_types) + + +class CachedInfoExtension(TLSExtension): + """Handles cached_info extension from RFC 7924.""" + + def __init__(self): + super(CachedInfoExtension, self).__init__( + extType=ExtensionType.cached_info) + self.cached_info = [] + + def create(self, cached_info=None): + self.cached_info = cached_info if cached_info else [] + return self + + @property + def extData(self): + if not self.cached_info: + return bytearray(0) + writer = Writer() + list_writer = Writer() + for info_type, hash_value in self.cached_info: + list_writer.add(info_type, 1) + list_writer.add(len(hash_value), 1) + list_writer.bytes += hash_value + writer.add(len(list_writer.bytes), 1) + writer.bytes += list_writer.bytes + return writer.bytes + + def parse(self, parser): + if parser.getRemainingLength() == 0: + self.cached_info = [] + return self + info_len = parser.get(1) + p = Parser(parser.getFixBytes(info_len)) + self.cached_info = [] + while p.getRemainingLength() > 0: + info_type = p.get(1) + hash_len = p.get(1) + hash_value = p.getFixBytes(hash_len) + self.cached_info.append((info_type, hash_value)) + return self + + def __repr__(self): + return "CachedInfoExtension(cached_info={0!r})".format( + self.cached_info) + + +class TransparencyInfoExtension(TLSExtension): + """Handles transparency_info extension from RFC 9162.""" + + def __init__(self): + super(TransparencyInfoExtension, self).__init__( + extType=ExtensionType.transparency_info) + self.transparency_info = bytearray(0) + + def create(self, transparency_info=None): + self.transparency_info = transparency_info if transparency_info else bytearray(0) + return self + + @property + def extData(self): + return self.transparency_info + + def parse(self, parser): + self.transparency_info = parser.getFixBytes(parser.getRemainingLength()) + return self + + def __repr__(self): + return "TransparencyInfoExtension(transparency_info={0!r})".format( + self.transparency_info) + + TLSExtension._universalExtensions = { ExtensionType.server_name: SNIExtension, ExtensionType.status_request: StatusRequestExtension, + ExtensionType.truncated_hmac: TruncatedHMACExtension, ExtensionType.cert_type: ClientCertTypeExtension, ExtensionType.supported_groups: SupportedGroupsExtension, ExtensionType.ec_point_formats: ECPointFormatsExtension, ExtensionType.srp: SRPExtension, ExtensionType.signature_algorithms: SignatureAlgorithmsExtension, + ExtensionType.use_srtp: UseSRTPExtension, ExtensionType.alpn: ALPNExtension, + ExtensionType.status_request_v2: StatusRequestV2Extension, + ExtensionType.signed_certificate_timestamp: SignedCertificateTimestampExtension, + ExtensionType.client_certificate_type: ClientCertificateTypeExtension, + ExtensionType.server_certificate_type: ServerCertificateTypeExtension, + ExtensionType.encrypt_then_mac: EncryptThenMACExtension, + ExtensionType.extended_master_secret: ExtendedMasterSecretExtension, + ExtensionType.token_binding: TokenBindingExtension, + ExtensionType.cached_info: CachedInfoExtension, ExtensionType.supports_npn: NPNExtension, ExtensionType.client_hello_padding: PaddingExtension, ExtensionType.renegotiation_info: RenegotiationInfoExtension, ExtensionType.heartbeat: HeartbeatExtension, ExtensionType.supported_versions: SupportedVersionsExtension, ExtensionType.key_share: ClientKeyShareExtension, - ExtensionType.signature_algorithms_cert: - SignatureAlgorithmsCertExtension, + ExtensionType.signature_algorithms_cert: SignatureAlgorithmsCertExtension, ExtensionType.pre_shared_key: PreSharedKeyExtension, ExtensionType.psk_key_exchange_modes: PskKeyExchangeModesExtension, ExtensionType.cookie: CookieExtension, + ExtensionType.certificate_authorities: CertificateAuthoritiesExtension, + ExtensionType.oid_filters: OIDFiltersExtension, + ExtensionType.post_handshake_auth: PostHandshakeAuthExtension, ExtensionType.record_size_limit: RecordSizeLimitExtension, + ExtensionType.transparency_info: TransparencyInfoExtension, ExtensionType.session_ticket: SessionTicketExtension, ExtensionType.compress_certificate: CompressedCertificateExtension, ExtensionType.delegated_credential: DelegatedCredentialExtension @@ -2254,13 +2733,19 @@ def __init__(self): ExtensionType.pre_shared_key: SrvPreSharedKeyExtension } +TLSExtension._encryptedExtensions = { + # Extensions with special EncryptedExtensions-specific parsers only + # Most extensions can reuse their ClientHello parsers via _universalExtensions +} + TLSExtension._certificateExtensions = { ExtensionType.status_request: CertificateStatusExtension, + ExtensionType.signed_certificate_timestamp: SignedCertificateTimestampExtension, ExtensionType.delegated_credential: DelegatedCredentialCertExtension - } TLSExtension._hrrExtensions = { - ExtensionType.key_share: HRRKeyShareExtension, - ExtensionType.supported_versions: SrvSupportedVersionsExtension + ExtensionType.cookie: CookieExtension, + ExtensionType.supported_versions: SrvSupportedVersionsExtension, + ExtensionType.key_share: HRRKeyShareExtension } diff --git a/tlslite/handshakesettings.py b/tlslite/handshakesettings.py index 7ea4822d..43642b27 100644 --- a/tlslite/handshakesettings.py +++ b/tlslite/handshakesettings.py @@ -478,7 +478,7 @@ def _init_misc_extensions(self): self.dc_sig_algs = [] self.dc_valid_time = DC_VALID_TIME - def __init__(self): + def __init__(self, **kwargs): """Initialise default values for settings.""" self._init_key_settings() self._init_misc_extensions() @@ -490,6 +490,11 @@ def __init__(self): self.keyExchangeNames = list(KEY_EXCHANGE_NAMES) self.cipherImplementations = list(CIPHER_IMPLEMENTATIONS) + # Custom attributes for exact JA3 control (added for httpx-tls compatibility) + self.cipher_order = kwargs.get("cipher_order", None) + self.extension_order = kwargs.get("extension_order", None) + self.groups_order = kwargs.get("groups_order", None) + @staticmethod def _sanityCheckKeySizes(other): """Check if key size limits are sane""" @@ -869,6 +874,11 @@ def validate(self): other.pskConfigs = self.pskConfigs other.psk_modes = self.psk_modes + # Copy custom JA3 control attributes (added for httpx-tls compatibility) + other.cipher_order = getattr(self, 'cipher_order', None) + other.extension_order = getattr(self, 'extension_order', None) + other.groups_order = getattr(self, 'groups_order', None) + if not other.certificateTypes: raise ValueError("No supported certificate types") diff --git a/tlslite/tlsconnection.py b/tlslite/tlsconnection.py index 856e10f2..80d373aa 100644 --- a/tlslite/tlsconnection.py +++ b/tlslite/tlsconnection.py @@ -712,21 +712,27 @@ def _clientSendClientHello(self, settings, session, srpUsername, srpParams, certParams, anonParams, serverName, nextProtos, reqTack, alpn): # Initialize acceptable ciphersuites - cipherSuites = [CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] - if srpParams: - cipherSuites += CipherSuite.getSrpAllSuites(settings) - elif certParams: - cipherSuites += CipherSuite.getTLS13Suites(settings) - cipherSuites += CipherSuite.getEcdsaSuites(settings) - cipherSuites += CipherSuite.getEcdheCertSuites(settings) - cipherSuites += CipherSuite.getDheCertSuites(settings) - cipherSuites += CipherSuite.getCertSuites(settings) - cipherSuites += CipherSuite.getDheDsaSuites(settings) - elif anonParams: - cipherSuites += CipherSuite.getEcdhAnonSuites(settings) - cipherSuites += CipherSuite.getAnonSuites(settings) + # Check if exact cipher order is specified (for JA3 fingerprint control) + if hasattr(settings, 'cipher_order') and settings.cipher_order is not None: + # Use exact cipher order specified by httpx-tls for precise JA3 control + cipherSuites = list(settings.cipher_order) else: - assert False + # Default behavior: add renegotiation info and standard cipher suites + cipherSuites = [CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV] + if srpParams: + cipherSuites += CipherSuite.getSrpAllSuites(settings) + elif certParams: + cipherSuites += CipherSuite.getTLS13Suites(settings) + cipherSuites += CipherSuite.getEcdsaSuites(settings) + cipherSuites += CipherSuite.getEcdheCertSuites(settings) + cipherSuites += CipherSuite.getDheCertSuites(settings) + cipherSuites += CipherSuite.getCertSuites(settings) + cipherSuites += CipherSuite.getDheDsaSuites(settings) + elif anonParams: + cipherSuites += CipherSuite.getEcdhAnonSuites(settings) + cipherSuites += CipherSuite.getAnonSuites(settings) + else: + assert False # Add any SCSVs. These are not real cipher suites, but signaling # values which reuse the cipher suite field in the ClientHello. @@ -878,6 +884,18 @@ def _clientSendClientHello(self, settings, session, srpUsername, extensions.append(CompressedCertificateExtension().create( algos_numbers)) + # Sort extensions according to extension_order if specified (for JA3 fingerprinting) + if hasattr(settings, 'extension_order') and settings.extension_order: + # Create a dict mapping extension type to extension object + ext_dict = {ext.extType: ext for ext in extensions} + + # Reorder based on extension_order, keeping only extensions that already exist + # We only reorder existing extensions to avoid sending unsupported ones + extensions = [ext_dict[ext_type] for ext_type in settings.extension_order if ext_type in ext_dict] + # Add any extensions not in extension_order at the end (to not lose any) + ordered_types = set(settings.extension_order) + extensions.extend([ext for ext in ext_dict.values() if ext.extType not in ordered_types]) + # don't send empty list of extensions or extensions in SSLv3 if not extensions or settings.maxVersion == (3, 0): extensions = None