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.
When
browser_emulation.cipher_suitesis 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:
Observed behavior:
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 frombrowser_emulation.cipher_suitesinto the ClientHello. Later, server cipher selection is validated againstconfig.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:
After applying this locally, https://danawa.com/ works with ChromeV144 + Android impersonation without adding any fallback to non-impersonated TLS.