From 3272935aeb437655958dbc54bfd380305b99c471 Mon Sep 17 00:00:00 2001 From: Mingye Chen Date: Sat, 26 Jul 2025 17:51:10 -0600 Subject: [PATCH 1/2] feat: add firefox 141 spec Compared to firefox 120, changes seem to include: - added `X25519MLKEM768` in keyshare and supported curves - added signed certificate timestamp extension - added compress certificate extension --- u_common.go | 3 +- u_parrots.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/u_common.go b/u_common.go index e98c7ce2..b7876af0 100644 --- a/u_common.go +++ b/u_common.go @@ -602,7 +602,7 @@ var ( HelloRandomizedNoALPN = ClientHelloID{helloRandomizedNoALPN, helloAutoVers, nil, nil} // The rest will will parrot given browser. - HelloFirefox_Auto = HelloFirefox_120 + HelloFirefox_Auto = HelloFirefox_141 HelloFirefox_55 = ClientHelloID{helloFirefox, "55", nil, nil} HelloFirefox_56 = ClientHelloID{helloFirefox, "56", nil, nil} HelloFirefox_63 = ClientHelloID{helloFirefox, "63", nil, nil} @@ -611,6 +611,7 @@ var ( HelloFirefox_102 = ClientHelloID{helloFirefox, "102", nil, nil} HelloFirefox_105 = ClientHelloID{helloFirefox, "105", nil, nil} HelloFirefox_120 = ClientHelloID{helloFirefox, "120", nil, nil} + HelloFirefox_141 = ClientHelloID{helloFirefox, "141", nil, nil} HelloChrome_Auto = HelloChrome_133 HelloChrome_58 = ClientHelloID{helloChrome, "58", nil, nil} diff --git a/u_parrots.go b/u_parrots.go index d5f60710..aa2fc17e 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -1455,6 +1455,131 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { }, }, }, nil + case HelloFirefox_141: + return ClientHelloSpec{ + TLSVersMin: VersionTLS12, + TLSVersMax: VersionTLS13, + CipherSuites: []uint16{ + TLS_AES_128_GCM_SHA256, + TLS_CHACHA20_POLY1305_SHA256, + TLS_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + }, + CompressionMethods: []uint8{ + 0x0, // no compression + }, + Extensions: []TLSExtension{ + &SNIExtension{}, + &ExtendedMasterSecretExtension{}, + &RenegotiationInfoExtension{ + Renegotiation: RenegotiateOnceAsClient, + }, + &SupportedCurvesExtension{ + Curves: []CurveID{ + X25519MLKEM768, + X25519, + CurveP256, + CurveP384, + CurveP521, + 256, + 257, + }, + }, + &SupportedPointsExtension{ + SupportedPoints: []uint8{ + 0x0, // uncompressed + }, + }, + &SessionTicketExtension{}, + &ALPNExtension{ + AlpnProtocols: []string{ + "h2", + "http/1.1", + }, + }, + &StatusRequestExtension{}, + &FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + ECDSAWithP384AndSHA384, + ECDSAWithP521AndSHA512, + ECDSAWithSHA1, + }, + }, + &SCTExtension{}, + &KeyShareExtension{ + KeyShares: []KeyShare{ + { + Group: X25519MLKEM768, + }, + { + Group: X25519, + }, + { + Group: CurveP256, + }, + }, + }, + &SupportedVersionsExtension{ + Versions: []uint16{ + VersionTLS13, + VersionTLS12, + }, + }, + &SignatureAlgorithmsExtension{ + SupportedSignatureAlgorithms: []SignatureScheme{ + ECDSAWithP256AndSHA256, + ECDSAWithP384AndSHA384, + ECDSAWithP521AndSHA512, + PSSWithSHA256, + PSSWithSHA384, + PSSWithSHA512, + PKCS1WithSHA256, + PKCS1WithSHA384, + PKCS1WithSHA512, + ECDSAWithSHA1, + PKCS1WithSHA1, + }, + }, + &PSKKeyExchangeModesExtension{[]uint8{ + PskModeDHE, + }}, + &FakeRecordSizeLimitExtension{ + Limit: 0x4001, + }, + &UtlsCompressCertExtension{[]CertCompressionAlgo{ + CertCompressionZlib, + CertCompressionBrotli, + CertCompressionZstd, + }}, + &GREASEEncryptedClientHelloExtension{ + CandidateCipherSuites: []HPKESymmetricCipherSuite{ + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_AES_128_GCM, + }, + { + KdfId: dicttls.HKDF_SHA256, + AeadId: dicttls.AEAD_CHACHA20_POLY1305, + }, + }, + CandidatePayloadLens: []uint16{223}, // +16: 239 + }, + }, + }, nil case HelloIOS_11_1: return ClientHelloSpec{ TLSVersMax: VersionTLS12, From 1130c16e1117009f43d332445c51a4f393a704ac Mon Sep 17 00:00:00 2001 From: Mingye Chen Date: Sat, 26 Jul 2025 19:47:25 -0600 Subject: [PATCH 2/2] feat: add option to reuse keyshares for hybrid key --- u_parrots.go | 117 +++++++++++++++++++++++++------------------- u_tls_extensions.go | 10 ++++ 2 files changed, 78 insertions(+), 49 deletions(-) diff --git a/u_parrots.go b/u_parrots.go index aa2fc17e..5f093d72 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -1520,8 +1520,8 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { }, }, &SCTExtension{}, - &KeyShareExtension{ - KeyShares: []KeyShare{ + &KeyShareExtensionExtended{ + KeyShareExtension: &KeyShareExtension{KeyShares: []KeyShare{ { Group: X25519MLKEM768, }, @@ -1531,7 +1531,8 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { { Group: CurveP256, }, - }, + }}, + HybridReuseKey: true, }, &SupportedVersionsExtension{ Versions: []uint16{ @@ -2997,52 +2998,12 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { } } case *KeyShareExtension: - preferredCurveIsSet := false - for i := range ext.KeyShares { - curveID := ext.KeyShares[i].Group - if isGREASEUint16(uint16(curveID)) { // just in case the user set a GREASE value instead of unGREASEd - ext.KeyShares[i].Group = CurveID(GetBoringGREASEValue(uconn.greaseSeed, ssl_grease_group)) - continue - } - if len(ext.KeyShares[i].Data) > 1 { - continue - } - - if curveID == X25519MLKEM768 || curveID == X25519Kyber768Draft00 { - ecdheKey, err := generateECDHEKey(uconn.config.rand(), X25519) - if err != nil { - return err - } - seed := make([]byte, mlkem.SeedSize) - if _, err := io.ReadFull(uconn.config.rand(), seed); err != nil { - return err - } - mlkemKey, err := mlkem.NewDecapsulationKey768(seed) - if err != nil { - return err - } - - if curveID == X25519Kyber768Draft00 { - ext.KeyShares[i].Data = append(ecdheKey.PublicKey().Bytes(), mlkemKey.EncapsulationKey().Bytes()...) - } else { - ext.KeyShares[i].Data = append(mlkemKey.EncapsulationKey().Bytes(), ecdheKey.PublicKey().Bytes()...) - } - uconn.HandshakeState.State13.KeyShareKeys.Mlkem = mlkemKey - uconn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe = ecdheKey - } else { - ecdheKey, err := generateECDHEKey(uconn.config.rand(), curveID) - if err != nil { - return fmt.Errorf("unsupported Curve in KeyShareExtension: %v."+ - "To mimic it, fill the Data(key) field manually", curveID) - } - - ext.KeyShares[i].Data = ecdheKey.PublicKey().Bytes() - if !preferredCurveIsSet { - // only do this once for the first non-grease curve - uconn.HandshakeState.State13.KeyShareKeys.Ecdhe = ecdheKey - preferredCurveIsSet = true - } - } + if err := uconn.setKeyShare(&KeyShareExtensionExtended{KeyShareExtension: ext}); err != nil { + return err + } + case *KeyShareExtensionExtended: + if err := uconn.setKeyShare(ext); err != nil { + return err } case *SupportedVersionsExtension: for i := range ext.Versions { @@ -3363,3 +3324,61 @@ func removeRC4Ciphers(s []uint16) []uint16 { } return s[:sliceLen] } + +func (uconn *UConn) setKeyShare(ext *KeyShareExtensionExtended) error { + preferredCurveIsSet := false + for i := range ext.KeyShares { + curveID := ext.KeyShares[i].Group + if isGREASEUint16(uint16(curveID)) { // just in case the user set a GREASE value instead of unGREASEd + ext.KeyShares[i].Group = CurveID(GetBoringGREASEValue(uconn.greaseSeed, ssl_grease_group)) + continue + } + if len(ext.KeyShares[i].Data) > 1 { + continue + } + + if curveID == X25519MLKEM768 || curveID == X25519Kyber768Draft00 { + ecdheKey, err := generateECDHEKey(uconn.config.rand(), X25519) + if err != nil { + return err + } + seed := make([]byte, mlkem.SeedSize) + if _, err := io.ReadFull(uconn.config.rand(), seed); err != nil { + return err + } + mlkemKey, err := mlkem.NewDecapsulationKey768(seed) + if err != nil { + return err + } + + if curveID == X25519Kyber768Draft00 { + ext.KeyShares[i].Data = append(ecdheKey.PublicKey().Bytes(), mlkemKey.EncapsulationKey().Bytes()...) + } else { + ext.KeyShares[i].Data = append(mlkemKey.EncapsulationKey().Bytes(), ecdheKey.PublicKey().Bytes()...) + } + uconn.HandshakeState.State13.KeyShareKeys.Mlkem = mlkemKey + uconn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe = ecdheKey + + if ext.HybridReuseKey && len(ext.KeyShares) > i+1 && ext.KeyShares[i+1].Group == X25519 { + preferredCurveIsSet = true + uconn.HandshakeState.State13.KeyShareKeys.Ecdhe = ecdheKey + ext.KeyShares[i+1].Data = ecdheKey.PublicKey().Bytes() + } + } else { + ecdheKey, err := generateECDHEKey(uconn.config.rand(), curveID) + if err != nil { + return fmt.Errorf("unsupported Curve in KeyShareExtension: %v."+ + "To mimic it, fill the Data(key) field manually", curveID) + } + + ext.KeyShares[i].Data = ecdheKey.PublicKey().Bytes() + if !preferredCurveIsSet { + // only do this once for the first non-grease curve + uconn.HandshakeState.State13.KeyShareKeys.Ecdhe = ecdheKey + preferredCurveIsSet = true + } + } + } + + return nil +} diff --git a/u_tls_extensions.go b/u_tls_extensions.go index c03fdb68..9ccf395c 100644 --- a/u_tls_extensions.go +++ b/u_tls_extensions.go @@ -1222,6 +1222,16 @@ func (e *UtlsCompressCertExtension) UnmarshalJSON(b []byte) error { return nil } +// Same as KeyShareExtension with extra options added without breaking users that +// use positional struct literal for KeyShareExtension +type KeyShareExtensionExtended struct { + *KeyShareExtension + + // whether to reuse keys for the same algorithm for hybrid key shares with traditional key shares + // according to draft-ietf-tls-hybrid-design-14 section 3.2 + HybridReuseKey bool +} + // KeyShareExtension implements key_share (51) and is for TLS 1.3 only. type KeyShareExtension struct { KeyShares []KeyShare