Skip to content

Browser impersonation advertises unsupported cipher suites, causing SelectedUnofferedCipherSuite on some servers #156

Description

@ehfkwlfkwl

When browser_emulation.cipher_suites is used, the ClientHello appears to advertise the emulated cipher suite list directly. Some of those cipher suites, such as static RSA / legacy CBC / 3DES suites depending on the profile, are not actually supported by the configured rustls crypto provider.

As a result, if a server selects one of those advertised-but-unsupported cipher suites, the handshake fails with PeerMisbehaved(SelectedUnofferedCipherSuite).

One reproducible case is:

let client = primp::Client::builder()
    .no_proxy()
    .impersonate(primp::Impersonate::ChromeV144)
    .impersonate_os(primp::ImpersonateOS::Android)
    .build()
    .unwrap();

let response = client
    .get("https://danawa.com/")
    .send()
    .await?;

Observed behavior:

error sending request for url (https://danawa.com/):
client error (Connect):
peer misbehaved: SelectedUnofferedCipherSuite

The impersonated ClientHello should not advertise cipher suites that the implementation cannot actually negotiate, or the implementation should support all advertised suites.

I believe the relevant code is in primp-rustls/src/client/hs.rs, where the impersonation cipher suite list is copied from browser_emulation.cipher_suites into the ClientHello. Later, server cipher selection is validated against config.provider.cipher_suites, so an emulated-but-provider-unsupported cipher suite can fail as “unoffered” even though it was present in the ClientHello.

A possible fix is to filter the emulated cipher suite list against config.provider.cipher_suites, while preserving GREASE placeholders, before emitting the ClientHello. That prevents advertising cipher suites the provider cannot negotiate.

For example:

let cipher_suites = browser_cipher_suites
    .iter()
    .copied()
    .filter(|suite| {
        *suite == CipherSuite::TLS_RESERVED_GREASE
            || config.provider.cipher_suites.iter().any(|supported| {
                supported.suite() == *suite
                    && supported.usable_for_protocol(cx.common.protocol)
            })
    })
    .collect::<Vec<_>>();

After applying this locally, https://danawa.com/ works with ChromeV144 + Android impersonation without adding any fallback to non-impersonated TLS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions