From 3721531ea99419d5cb355b7e660e9943fb32cce4 Mon Sep 17 00:00:00 2001 From: Gaukas Wang Date: Thu, 30 Mar 2023 09:13:47 -0600 Subject: [PATCH] Implement ClientHelloSpec JSON Unmarshaler (#176) * wip: staging work * wip: staging work * feat: ClientHello JSON Unmarshaler Allowing unmarshalling a JSON object into a ClientHelloSpec. * feat: ClientHello JSON Unmarshaler rev - Revised JSON ClientHello format - Implemented `TLSExtensionJSON` interface for some more extensions --- go.mod | 1 + go.sum | 2 + testdata/ClientHello-JSON-Chrome102.json | 75 ++ testdata/ClientHello-JSON-Edge106.json | 76 ++ testdata/ClientHello-JSON-Firefox105.json | 78 ++ testdata/ClientHello-JSON-iOS14.json | 85 ++ u_clienthello_json.go | 168 ++++ u_clienthello_json_test.go | 123 +++ u_common.go | 131 ++- u_fingerprinter.go | 98 +-- u_parrots.go | 8 +- u_public.go | 8 +- u_tls_extensions.go | 957 ++++++++++++++++------ 13 files changed, 1453 insertions(+), 357 deletions(-) create mode 100644 testdata/ClientHello-JSON-Chrome102.json create mode 100644 testdata/ClientHello-JSON-Edge106.json create mode 100644 testdata/ClientHello-JSON-Firefox105.json create mode 100644 testdata/ClientHello-JSON-iOS14.json create mode 100644 u_clienthello_json.go create mode 100644 u_clienthello_json_test.go diff --git a/go.mod b/go.mod index 45af541..9703f97 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/andybalholm/brotli v1.0.4 + github.com/gaukas/godicttls v0.0.3 github.com/klauspost/compress v1.15.15 golang.org/x/crypto v0.5.0 golang.org/x/net v0.7.0 diff --git a/go.sum b/go.sum index 0d526fe..85b80ed 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= +github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= diff --git a/testdata/ClientHello-JSON-Chrome102.json b/testdata/ClientHello-JSON-Chrome102.json new file mode 100644 index 0000000..2463906 --- /dev/null +++ b/testdata/ClientHello-JSON-Chrome102.json @@ -0,0 +1,75 @@ +{ + "cipher_suites": [ + "GREASE", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "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" + ], + "compression_methods": [ + "NULL" + ], + "extensions": [ + {"name": "GREASE"}, + {"name": "server_name"}, + {"name": "extended_master_secret"}, + {"name": "renegotiation_info"}, + {"name": "supported_groups", "named_group_list": [ + "GREASE", + "x25519", + "secp256r1", + "secp384r1" + ]}, + {"name": "ec_point_formats", "ec_point_format_list": [ + "uncompressed" + ]}, + {"name": "session_ticket"}, + {"name": "application_layer_protocol_negotiation", "protocol_name_list": [ + "h2", + "http/1.1" + ]}, + {"name": "status_request"}, + {"name": "signature_algorithms", "supported_signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512" + ]}, + {"name": "signed_certificate_timestamp"}, + {"name": "key_share", "client_shares": [ + {"group": "GREASE", "key_exchange": [0]}, + {"group": "x25519"} + ]}, + {"name": "psk_key_exchange_modes", "ke_modes": [ + "psk_dhe_ke" + ]}, + {"name": "supported_versions", "versions": [ + "GREASE", + "TLS 1.3", + "TLS 1.2" + ]}, + {"name": "compress_certificate", "algorithms": [ + "brotli" + ]}, + {"name": "application_settings", "supported_protocols": [ + "h2" + ]}, + {"name": "GREASE"}, + {"name": "padding", "len": 0} + ] +} \ No newline at end of file diff --git a/testdata/ClientHello-JSON-Edge106.json b/testdata/ClientHello-JSON-Edge106.json new file mode 100644 index 0000000..4f506ca --- /dev/null +++ b/testdata/ClientHello-JSON-Edge106.json @@ -0,0 +1,76 @@ +{ + "cipher_suites": [ + "GREASE", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "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" + ], + "compression_methods": [ + "NULL" + ], + "extensions": [ + {"name": "GREASE"}, + {"name": "server_name"}, + {"name": "extended_master_secret"}, + {"name": "renegotiation_info"}, + {"name": "supported_groups", "named_group_list": [ + "GREASE", + "x25519", + "secp256r1", + "secp384r1" + ]}, + {"name": "ec_point_formats", "ec_point_format_list": [ + "uncompressed" + ]}, + {"name": "session_ticket"}, + {"name": "application_layer_protocol_negotiation", "protocol_name_list": [ + "h2", + "http/1.1" + ]}, + {"name": "status_request"}, + {"name": "signature_algorithms", "supported_signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512" + ]}, + {"name": "signed_certificate_timestamp"}, + {"name": "key_share", "client_shares": [ + {"group": "GREASE", "key_exchange": [0]}, + {"group": "x25519"} + ]}, + {"name": "psk_key_exchange_modes", "ke_modes": [ + "psk_dhe_ke" + ]}, + {"name": "supported_versions", "versions": [ + "GREASE", + "TLS 1.3", + "TLS 1.2" + ]}, + {"name": "compress_certificate", "algorithms": [ + "brotli" + ]}, + {"name": "application_settings", "supported_protocols": [ + "h2" + ]}, + {"name": "GREASE"}, + {"name": "padding", "len": 0} + ] +} \ No newline at end of file diff --git a/testdata/ClientHello-JSON-Firefox105.json b/testdata/ClientHello-JSON-Firefox105.json new file mode 100644 index 0000000..fa0aac7 --- /dev/null +++ b/testdata/ClientHello-JSON-Firefox105.json @@ -0,0 +1,78 @@ +{ + "cipher_suites": [ + "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_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "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" + ], + "compression_methods": [ + "NULL" + ], + "extensions": [ + {"name": "server_name"}, + {"name": "extended_master_secret"}, + {"name": "renegotiation_info"}, + {"name": "supported_groups", "named_group_list": [ + "x25519", + "secp256r1", + "secp384r1", + "secp521r1", + "ffdhe2048", + "ffdhe3072" + ]}, + {"name": "ec_point_formats", "ec_point_format_list": [ + "uncompressed" + ]}, + {"name": "session_ticket"}, + {"name": "application_layer_protocol_negotiation", "protocol_name_list": [ + "h2", + "http/1.1" + ]}, + {"name": "status_request"}, + {"name": "delegated_credentials", "supported_signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512", + "ecdsa_sha1" + ]}, + {"name": "key_share", "client_shares": [ + {"group": "x25519"}, + {"group": "secp256r1"} + ]}, + {"name": "supported_versions", "versions": [ + "TLS 1.3", + "TLS 1.2" + ]}, + {"name": "signature_algorithms", "supported_signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512", + "rsa_pss_rsae_sha256", + "rsa_pss_rsae_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha256", + "rsa_pkcs1_sha384", + "rsa_pkcs1_sha512", + "ecdsa_sha1", + "rsa_pkcs1_sha1" + ]}, + {"name": "psk_key_exchange_modes", "ke_modes": [ + "psk_dhe_ke" + ]}, + {"name": "record_size_limit", "record_size_limit": 16385}, + {"name": "padding", "len": 0} + ] +} \ No newline at end of file diff --git a/testdata/ClientHello-JSON-iOS14.json b/testdata/ClientHello-JSON-iOS14.json new file mode 100644 index 0000000..f16725e --- /dev/null +++ b/testdata/ClientHello-JSON-iOS14.json @@ -0,0 +1,85 @@ +{ + "cipher_suites": [ + "GREASE", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA" + ], + "compression_methods": [ + "NULL" + ], + "extensions": [ + {"name": "GREASE"}, + {"name": "server_name"}, + {"name": "extended_master_secret"}, + {"name": "renegotiation_info"}, + {"name": "supported_groups", "named_group_list": [ + "GREASE", + "x25519", + "secp256r1", + "secp384r1", + "secp521r1" + ]}, + {"name": "ec_point_formats", "ec_point_format_list": [ + "uncompressed" + ]}, + {"name": "application_layer_protocol_negotiation", "protocol_name_list": [ + "h2", + "http/1.1" + ]}, + {"name": "status_request"}, + {"name": "signature_algorithms", "supported_signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_sha1", + "rsa_pss_rsae_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512", + "rsa_pkcs1_sha1" + ]}, + {"name": "signed_certificate_timestamp"}, + {"name": "key_share", "client_shares": [ + {"group": "GREASE", "key_exchange": [0]}, + {"group": "x25519"} + ]}, + {"name": "psk_key_exchange_modes", "ke_modes": [ + "psk_dhe_ke" + ]}, + {"name": "supported_versions", "versions": [ + "GREASE", + "TLS 1.3", + "TLS 1.2", + "TLS 1.1", + "TLS 1.0" + ]}, + {"name": "GREASE"}, + {"name": "padding"} + ] +} \ No newline at end of file diff --git a/u_clienthello_json.go b/u_clienthello_json.go new file mode 100644 index 0000000..2529bf7 --- /dev/null +++ b/u_clienthello_json.go @@ -0,0 +1,168 @@ +package tls + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/gaukas/godicttls" +) + +var ErrUnknownExtension = errors.New("extension name is unknown to the dictionary") + +type ClientHelloSpecJSONUnmarshaler struct { + CipherSuites *CipherSuitesJSONUnmarshaler `json:"cipher_suites"` + CompressionMethods *CompressionMethodsJSONUnmarshaler `json:"compression_methods"` + Extensions *TLSExtensionsJSONUnmarshaler `json:"extensions"` + TLSVersMin uint16 `json:"min_vers,omitempty"` // optional + TLSVersMax uint16 `json:"max_vers,omitempty"` // optional +} + +func (chsju *ClientHelloSpecJSONUnmarshaler) ClientHelloSpec() ClientHelloSpec { + return ClientHelloSpec{ + CipherSuites: chsju.CipherSuites.CipherSuites(), + CompressionMethods: chsju.CompressionMethods.CompressionMethods(), + Extensions: chsju.Extensions.Extensions(), + TLSVersMin: chsju.TLSVersMin, + TLSVersMax: chsju.TLSVersMax, + } +} + +type CipherSuitesJSONUnmarshaler struct { + cipherSuites []uint16 +} + +func (c *CipherSuitesJSONUnmarshaler) UnmarshalJSON(jsonStr []byte) error { + var cipherSuiteNames []string + if err := json.Unmarshal(jsonStr, &cipherSuiteNames); err != nil { + return err + } + + for _, name := range cipherSuiteNames { + if name == "GREASE" { + c.cipherSuites = append(c.cipherSuites, GREASE_PLACEHOLDER) + continue + } + + if id, ok := godicttls.DictCipherSuiteNameIndexed[name]; ok { + c.cipherSuites = append(c.cipherSuites, id) + } else { + return fmt.Errorf("unknown cipher suite name: %s", name) + } + } + + return nil +} + +func (c *CipherSuitesJSONUnmarshaler) CipherSuites() []uint16 { + return c.cipherSuites +} + +type CompressionMethodsJSONUnmarshaler struct { + compressionMethods []uint8 +} + +func (c *CompressionMethodsJSONUnmarshaler) UnmarshalJSON(jsonStr []byte) error { + var compressionMethodNames []string + if err := json.Unmarshal(jsonStr, &compressionMethodNames); err != nil { + return err + } + + for _, name := range compressionMethodNames { + if id, ok := godicttls.DictCompMethNameIndexed[name]; ok { + c.compressionMethods = append(c.compressionMethods, id) + } else { + return fmt.Errorf("unknown compression method name: %s", name) + } + } + + return nil +} + +func (c *CompressionMethodsJSONUnmarshaler) CompressionMethods() []uint8 { + return c.compressionMethods +} + +type TLSExtensionsJSONUnmarshaler struct { + extensions []TLSExtensionJSON +} + +func (e *TLSExtensionsJSONUnmarshaler) UnmarshalJSON(jsonStr []byte) error { + var accepters []tlsExtensionJSONAccepter + if err := json.Unmarshal(jsonStr, &accepters); err != nil { + return err + } + + var exts []TLSExtensionJSON = make([]TLSExtensionJSON, 0, len(accepters)) + for _, accepter := range accepters { + if accepter.extNameOnly.Name == "GREASE" { + exts = append(exts, &UtlsGREASEExtension{}) + continue + } + + if extID, ok := godicttls.DictExtTypeNameIndexed[accepter.extNameOnly.Name]; !ok { + return fmt.Errorf("%w: %s", ErrUnknownExtension, accepter.extNameOnly.Name) + } else { + // get extension type from ID + var ext TLSExtension = ExtensionFromID(extID) + if ext == nil { + // fallback to generic extension + ext = genericExtension(extID, accepter.extNameOnly.Name) + } + + if extJsonCompatible, ok := ext.(TLSExtensionJSON); ok { + exts = append(exts, extJsonCompatible) + } else { + return fmt.Errorf("extension %d (%s) is not JSON compatible", extID, accepter.extNameOnly.Name) + } + } + } + + // unmashal extensions + for idx, ext := range exts { + // json.Unmarshal will call the UnmarshalJSON method of the extension + if err := json.Unmarshal(accepters[idx].origJsonInput, ext); err != nil { + return err + } + } + + e.extensions = exts + return nil +} + +func (e *TLSExtensionsJSONUnmarshaler) Extensions() []TLSExtension { + var exts []TLSExtension = make([]TLSExtension, 0, len(e.extensions)) + for _, ext := range e.extensions { + exts = append(exts, ext) + } + return exts +} + +func genericExtension(id uint16, name string) TLSExtension { + var warningMsg string = "WARNING: extension " + warningMsg += fmt.Sprintf("%d ", id) + if len(name) > 0 { + warningMsg += fmt.Sprintf("(%s) ", name) + } + warningMsg += "is falling back to generic extension" + warningMsg += "\n" + + fmt.Fprint(os.Stderr, warningMsg) + + // fallback to generic extension + return &GenericExtension{Id: id} +} + +type tlsExtensionJSONAccepter struct { + extNameOnly struct { + Name string `json:"name"` + } + origJsonInput []byte +} + +func (t *tlsExtensionJSONAccepter) UnmarshalJSON(jsonStr []byte) error { + t.origJsonInput = make([]byte, len(jsonStr)) + copy(t.origJsonInput, jsonStr) + return json.Unmarshal(jsonStr, &t.extNameOnly) +} diff --git a/u_clienthello_json_test.go b/u_clienthello_json_test.go new file mode 100644 index 0000000..9ab86c7 --- /dev/null +++ b/u_clienthello_json_test.go @@ -0,0 +1,123 @@ +package tls + +import ( + "encoding/json" + "os" + "reflect" + "testing" +) + +func TestClientHelloSpecJSONUnmarshaler(t *testing.T) { + testClientHelloSpecJSONUnmarshaler(t, "testdata/ClientHello-JSON-Chrome102.json", HelloChrome_102) + testClientHelloSpecJSONUnmarshaler(t, "testdata/ClientHello-JSON-Firefox105.json", HelloFirefox_105) + testClientHelloSpecJSONUnmarshaler(t, "testdata/ClientHello-JSON-iOS14.json", HelloIOS_14) + testClientHelloSpecJSONUnmarshaler(t, "testdata/ClientHello-JSON-Edge106.json", HelloEdge_106) +} + +func testClientHelloSpecJSONUnmarshaler( + t *testing.T, + jsonFilepath string, + truthClientHelloID ClientHelloID, +) { + jsonCH, err := os.ReadFile(jsonFilepath) + if err != nil { + t.Fatal(err) + } + + var chsju ClientHelloSpecJSONUnmarshaler + if err := json.Unmarshal(jsonCH, &chsju); err != nil { + t.Fatal(err) + } + + truthSpec, _ := utlsIdToSpec(truthClientHelloID) + jsonSpec := chsju.ClientHelloSpec() + + // Compare CipherSuites + if !reflect.DeepEqual(jsonSpec.CipherSuites, truthSpec.CipherSuites) { + t.Errorf("JSONUnmarshaler %s: got %#v, want %#v", clientHelloSpecJSONTestIdentifier(truthClientHelloID), jsonSpec.CipherSuites, truthSpec.CipherSuites) + } + + // Compare CompressionMethods + if !reflect.DeepEqual(jsonSpec.CompressionMethods, truthSpec.CompressionMethods) { + t.Errorf("JSONUnmarshaler %s: got %#v, want %#v", clientHelloSpecJSONTestIdentifier(truthClientHelloID), jsonSpec.CompressionMethods, truthSpec.CompressionMethods) + } + + // Compare Extensions + if len(jsonSpec.Extensions) != len(truthSpec.Extensions) { + t.Errorf("JSONUnmarshaler %s: len(jsonExtensions) = %d != %d = len(truthExtensions)", clientHelloSpecJSONTestIdentifier(truthClientHelloID), len(jsonSpec.Extensions), len(truthSpec.Extensions)) + } + + for i := range jsonSpec.Extensions { + if !reflect.DeepEqual(jsonSpec.Extensions[i], truthSpec.Extensions[i]) { + if _, ok := jsonSpec.Extensions[i].(*UtlsPaddingExtension); ok { + testedPaddingExt := jsonSpec.Extensions[i].(*UtlsPaddingExtension) + savedPaddingExt := truthSpec.Extensions[i].(*UtlsPaddingExtension) + if testedPaddingExt.PaddingLen != savedPaddingExt.PaddingLen || testedPaddingExt.WillPad != savedPaddingExt.WillPad { + t.Errorf("got %#v, want %#v", testedPaddingExt, savedPaddingExt) + } else { + continue // UtlsPaddingExtension has non-nil function member + } + } + t.Errorf("JSONUnmarshaler %s: got %#v, want %#v", clientHelloSpecJSONTestIdentifier(truthClientHelloID), jsonSpec.Extensions[i], truthSpec.Extensions[i]) + } + } +} + +func TestClientHelloSpecUnmarshalJSON(t *testing.T) { + testClientHelloSpecUnmarshalJSON(t, "testdata/ClientHello-JSON-Chrome102.json", HelloChrome_102) + testClientHelloSpecUnmarshalJSON(t, "testdata/ClientHello-JSON-Firefox105.json", HelloFirefox_105) + testClientHelloSpecUnmarshalJSON(t, "testdata/ClientHello-JSON-iOS14.json", HelloIOS_14) + testClientHelloSpecUnmarshalJSON(t, "testdata/ClientHello-JSON-Edge106.json", HelloEdge_106) +} + +func testClientHelloSpecUnmarshalJSON( + t *testing.T, + jsonFilepath string, + truthClientHelloID ClientHelloID, +) { + var jsonSpec ClientHelloSpec + jsonCH, err := os.ReadFile(jsonFilepath) + if err != nil { + t.Fatal(err) + } + + if err := json.Unmarshal(jsonCH, &jsonSpec); err != nil { + t.Fatal(err) + } + + truthSpec, _ := utlsIdToSpec(truthClientHelloID) + + // Compare CipherSuites + if !reflect.DeepEqual(jsonSpec.CipherSuites, truthSpec.CipherSuites) { + t.Errorf("UnmarshalJSON %s: got %#v, want %#v", clientHelloSpecJSONTestIdentifier(truthClientHelloID), jsonSpec.CipherSuites, truthSpec.CipherSuites) + } + + // Compare CompressionMethods + if !reflect.DeepEqual(jsonSpec.CompressionMethods, truthSpec.CompressionMethods) { + t.Errorf("UnmarshalJSON %s: got %#v, want %#v", clientHelloSpecJSONTestIdentifier(truthClientHelloID), jsonSpec.CompressionMethods, truthSpec.CompressionMethods) + } + + // Compare Extensions + if len(jsonSpec.Extensions) != len(truthSpec.Extensions) { + t.Errorf("UnmarshalJSON %s: len(jsonExtensions) = %d != %d = len(truthExtensions)", jsonFilepath, len(jsonSpec.Extensions), len(truthSpec.Extensions)) + } + + for i := range jsonSpec.Extensions { + if !reflect.DeepEqual(jsonSpec.Extensions[i], truthSpec.Extensions[i]) { + if _, ok := jsonSpec.Extensions[i].(*UtlsPaddingExtension); ok { + testedPaddingExt := jsonSpec.Extensions[i].(*UtlsPaddingExtension) + savedPaddingExt := truthSpec.Extensions[i].(*UtlsPaddingExtension) + if testedPaddingExt.PaddingLen != savedPaddingExt.PaddingLen || testedPaddingExt.WillPad != savedPaddingExt.WillPad { + t.Errorf("got %#v, want %#v", testedPaddingExt, savedPaddingExt) + } else { + continue // UtlsPaddingExtension has non-nil function member + } + } + t.Errorf("UnmarshalJSON %s: got %#v, want %#v", clientHelloSpecJSONTestIdentifier(truthClientHelloID), jsonSpec.Extensions[i], truthSpec.Extensions[i]) + } + } +} + +func clientHelloSpecJSONTestIdentifier(id ClientHelloID) string { + return id.Client + id.Version +} diff --git a/u_common.go b/u_common.go index 87329f5..340d31a 100644 --- a/u_common.go +++ b/u_common.go @@ -210,8 +210,9 @@ func (chs *ClientHelloSpec) ReadTLSExtensions(b []byte, allowBluntMimicry bool) return fmt.Errorf("unable to read data for extension %x", extension) } - extWriter := ExtensionIDToExtension(extension) - if extWriter != nil { + ext := ExtensionFromID(extension) + extWriter, ok := ext.(TLSExtensionWriter) + if ext != nil && ok { // known extension and implements TLSExtensionWriter properly if extension == extensionSupportedVersions { chs.TLSVersMin = 0 chs.TLSVersMax = 0 @@ -293,8 +294,12 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { } for _, extType := range tlsExtensionTypes { - extension := ExtensionIDToExtension(extType) - if extension == nil { + extension := ExtensionFromID(extType) + extWriter, ok := extension.(TLSExtensionWriter) + if !ok { + return fmt.Errorf("unsupported extension %d", extType) + } + if extension == nil || !ok { log.Printf("[Warning] Unsupported extension %d added as a &GenericExtension without Data", extType) chs.Extensions = append(chs.Extensions, &GenericExtension{extType, []byte{}}) } else { @@ -303,7 +308,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { if data["pt_fmts"] == nil { return errors.New("pt_fmts is required") } - _, err = extension.Write(data["pt_fmts"]) + _, err = extWriter.Write(data["pt_fmts"]) if err != nil { return err } @@ -311,7 +316,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { if data["sig_algs"] == nil { return errors.New("sig_algs is required") } - _, err = extension.Write(data["sig_algs"]) + _, err = extWriter.Write(data["sig_algs"]) if err != nil { return err } @@ -327,7 +332,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { fixedData := make([]byte, len(data["supported_versions"])+1) fixedData[0] = uint8(len(data["supported_versions"]) & 0xff) copy(fixedData[1:], data["supported_versions"]) - _, err = extension.Write(fixedData) + _, err = extWriter.Write(fixedData) if err != nil { return err } @@ -336,7 +341,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { return errors.New("curves is required") } - _, err = extension.Write(data["curves"]) + _, err = extWriter.Write(data["curves"]) if err != nil { return err } @@ -345,7 +350,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { return errors.New("alpn is required") } - _, err = extension.Write(data["alpn"]) + _, err = extWriter.Write(data["alpn"]) if err != nil { return err } @@ -365,7 +370,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { // add uint16 length prefix fixedData = append([]byte{uint8(len(fixedData) >> 8), uint8(len(fixedData) & 0xff)}, fixedData...) - _, err = extension.Write(fixedData) + _, err = extWriter.Write(fixedData) if err != nil { return err } @@ -378,7 +383,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { fixedData := make([]byte, len(data["psk_key_exchange_modes"])+1) fixedData[0] = uint8(len(data["psk_key_exchange_modes"]) & 0xff) copy(fixedData[1:], data["psk_key_exchange_modes"]) - _, err = extension.Write(fixedData) + _, err = extWriter.Write(fixedData) if err != nil { return err } @@ -391,7 +396,7 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { fixedData := make([]byte, len(data["cert_compression_algs"])+1) fixedData[0] = uint8(len(data["cert_compression_algs"]) & 0xff) copy(fixedData[1:], data["cert_compression_algs"]) - _, err = extension.Write(fixedData) + _, err = extWriter.Write(fixedData) if err != nil { return err } @@ -400,13 +405,13 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { return errors.New("record_size_limit is required") } - _, err = extension.Write(data["record_size_limit"]) // uint16 as []byte + _, err = extWriter.Write(data["record_size_limit"]) // uint16 as []byte if err != nil { return err } case utlsExtensionApplicationSettings: // TODO: tlsfingerprint.io should record/provide application settings data - extension.(*ApplicationSettingsExtension).SupportedProtocols = []string{"h2"} + extWriter.(*ApplicationSettingsExtension).SupportedProtocols = []string{"h2"} case fakeExtensionPreSharedKey: log.Printf("[Warning] PSK extension added without data") default: @@ -416,12 +421,15 @@ func (chs *ClientHelloSpec) ImportTLSClientHello(data map[string][]byte) error { log.Printf("[Warning] GREASE extension added but ID/Data discarded. They will be automatically re-GREASEd on ApplyPreset() call.") }*/ } - chs.Extensions = append(chs.Extensions, extension) + chs.Extensions = append(chs.Extensions, extWriter) } } return nil } +// ImportTLSClientHelloFromJSON imports ClientHelloSpec from JSON data from client.tlsfingerprint.io format +// +// It calls ImportTLSClientHello internally after unmarshaling JSON data into map[string][]byte func (chs *ClientHelloSpec) ImportTLSClientHelloFromJSON(jsonB []byte) error { var data map[string][]byte err := json.Unmarshal(jsonB, &data) @@ -431,6 +439,99 @@ func (chs *ClientHelloSpec) ImportTLSClientHelloFromJSON(jsonB []byte) error { return chs.ImportTLSClientHello(data) } +// FromRaw converts a ClientHello message in the form of raw bytes into a ClientHelloSpec. +func (chs *ClientHelloSpec) FromRaw(raw []byte, allowBluntMimicry ...bool) error { + if chs == nil { + return errors.New("cannot unmarshal into nil ClientHelloSpec") + } + + var bluntMimicry = false + if len(allowBluntMimicry) == 1 { + bluntMimicry = allowBluntMimicry[0] + } + + *chs = ClientHelloSpec{} // reset + s := cryptobyte.String(raw) + + var contentType uint8 + var recordVersion uint16 + if !s.ReadUint8(&contentType) || // record type + !s.ReadUint16(&recordVersion) || !s.Skip(2) { // record version and length + return errors.New("unable to read record type, version, and length") + } + + if recordType(contentType) != recordTypeHandshake { + return errors.New("record is not a handshake") + } + + var handshakeVersion uint16 + var handshakeType uint8 + + if !s.ReadUint8(&handshakeType) || !s.Skip(3) || // message type and 3 byte length + !s.ReadUint16(&handshakeVersion) || !s.Skip(32) { // 32 byte random + return errors.New("unable to read handshake message type, length, and random") + } + + if handshakeType != typeClientHello { + return errors.New("handshake message is not a ClientHello") + } + + chs.TLSVersMin = recordVersion + chs.TLSVersMax = handshakeVersion + + var ignoredSessionID cryptobyte.String + if !s.ReadUint8LengthPrefixed(&ignoredSessionID) { + return errors.New("unable to read session id") + } + + // CipherSuites + var cipherSuitesBytes cryptobyte.String + if !s.ReadUint16LengthPrefixed(&cipherSuitesBytes) { + return errors.New("unable to read ciphersuites") + } + + if err := chs.ReadCipherSuites(cipherSuitesBytes); err != nil { + return err + } + + // CompressionMethods + var compressionMethods cryptobyte.String + if !s.ReadUint8LengthPrefixed(&compressionMethods) { + return errors.New("unable to read compression methods") + } + + if err := chs.ReadCompressionMethods(compressionMethods); err != nil { + return err + } + + if s.Empty() { + // Extensions are optional + return nil + } + + var extensions cryptobyte.String + if !s.ReadUint16LengthPrefixed(&extensions) { + return errors.New("unable to read extensions data") + } + + if err := chs.ReadTLSExtensions(extensions, bluntMimicry); err != nil { + return err + } + + return nil +} + +// UnmarshalJSON unmarshals a ClientHello message in the form of JSON into a ClientHelloSpec. +func (chs *ClientHelloSpec) UnmarshalJSON(jsonB []byte) error { + var chsju ClientHelloSpecJSONUnmarshaler + if err := json.Unmarshal(jsonB, &chsju); err != nil { + return err + } + + *chs = chsju.ClientHelloSpec() + return nil +} + var ( // HelloGolang will use default "crypto/tls" handshake marshaling codepath, which WILL // overwrite your changes to Hello(Config, Session are fine). diff --git a/u_fingerprinter.go b/u_fingerprinter.go index 478f222..1e1e1c8 100644 --- a/u_fingerprinter.go +++ b/u_fingerprinter.go @@ -4,12 +4,6 @@ package tls -import ( - "errors" - - "golang.org/x/crypto/cryptobyte" -) - // Fingerprinter is a struct largely for holding options for the FingerprintClientHello func type Fingerprinter struct { // AllowBluntMimicry will ensure that unknown extensions are @@ -36,72 +30,36 @@ type Fingerprinter struct { // as well as the handshake type/length/version header // https://tools.ietf.org/html/rfc5246#section-6.2 // https://tools.ietf.org/html/rfc5246#section-7.4 +// +// It calls UnmarshalClientHello internally, and is kept for backwards compatibility func (f *Fingerprinter) FingerprintClientHello(data []byte) (clientHelloSpec *ClientHelloSpec, err error) { + return f.RawClientHello(data) +} + +// RawClientHello returns a ClientHelloSpec which is based on the +// ClientHello raw bytes that is passed in as the raw argument. +// +// It was renamed from FingerprintClientHello in v1.3.1 and earlier versions +// as a more precise name for the function +func (f *Fingerprinter) RawClientHello(raw []byte) (clientHelloSpec *ClientHelloSpec, err error) { clientHelloSpec = &ClientHelloSpec{} - s := cryptobyte.String(data) - - var contentType uint8 - var recordVersion uint16 - if !s.ReadUint8(&contentType) || // record type - !s.ReadUint16(&recordVersion) || !s.Skip(2) { // record version and length - return nil, errors.New("unable to read record type, version, and length") - } - - if recordType(contentType) != recordTypeHandshake { - return nil, errors.New("record is not a handshake") - } - - var handshakeVersion uint16 - var handshakeType uint8 - - if !s.ReadUint8(&handshakeType) || !s.Skip(3) || // message type and 3 byte length - !s.ReadUint16(&handshakeVersion) || !s.Skip(32) { // 32 byte random - return nil, errors.New("unable to read handshake message type, length, and random") - } - - if handshakeType != typeClientHello { - return nil, errors.New("handshake message is not a ClientHello") - } - - clientHelloSpec.TLSVersMin = recordVersion - clientHelloSpec.TLSVersMax = handshakeVersion - - var ignoredSessionID cryptobyte.String - if !s.ReadUint8LengthPrefixed(&ignoredSessionID) { - return nil, errors.New("unable to read session id") - } - - // CipherSuites - var cipherSuitesBytes cryptobyte.String - if !s.ReadUint16LengthPrefixed(&cipherSuitesBytes) { - return nil, errors.New("unable to read ciphersuites") - } - err = clientHelloSpec.ReadCipherSuites(cipherSuitesBytes) - if err != nil { - return nil, err - } - - // CompressionMethods - var compressionMethods cryptobyte.String - if !s.ReadUint8LengthPrefixed(&compressionMethods) { - return nil, errors.New("unable to read compression methods") - } - err = clientHelloSpec.ReadCompressionMethods(compressionMethods) - if err != nil { - return nil, err - } - - if s.Empty() { - // ClientHello is optionally followed by extension data - return clientHelloSpec, nil - } - - var extensions cryptobyte.String - if !s.ReadUint16LengthPrefixed(&extensions) { - return nil, errors.New("unable to read extensions data") - } - - err = clientHelloSpec.ReadTLSExtensions(extensions, f.AllowBluntMimicry) + err = clientHelloSpec.FromRaw(raw, f.AllowBluntMimicry) + if err != nil { + return nil, err + } + + if f.AlwaysAddPadding { + clientHelloSpec.AlwaysAddPadding() + } + + return clientHelloSpec, nil +} + +// UnmarshalJSONClientHello returns a ClientHelloSpec which is based on the +// ClientHello JSON bytes that is passed in as the json argument. +func (f *Fingerprinter) UnmarshalJSONClientHello(json []byte) (clientHelloSpec *ClientHelloSpec, err error) { + clientHelloSpec = &ClientHelloSpec{} + err = clientHelloSpec.UnmarshalJSON(json) if err != nil { return nil, err } diff --git a/u_parrots.go b/u_parrots.go index cb3f1b6..70422d6 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -680,8 +680,8 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &SessionTicketExtension{}, &ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}}, //application_layer_protocol_negotiation &StatusRequestExtension{}, - &DelegatedCredentialsExtension{ - AlgorithmsSignature: []SignatureScheme{ //signature_algorithms + &FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []SignatureScheme{ //signature_algorithms ECDSAWithP256AndSHA256, ECDSAWithP384AndSHA384, ECDSAWithP521AndSHA512, @@ -761,8 +761,8 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &SessionTicketExtension{}, &ALPNExtension{AlpnProtocols: []string{"h2"}}, //application_layer_protocol_negotiation &StatusRequestExtension{}, - &DelegatedCredentialsExtension{ - AlgorithmsSignature: []SignatureScheme{ //signature_algorithms + &FakeDelegatedCredentialsExtension{ + SupportedSignatureAlgorithms: []SignatureScheme{ //signature_algorithms ECDSAWithP256AndSHA256, ECDSAWithP384AndSHA384, ECDSAWithP521AndSHA512, diff --git a/u_public.go b/u_public.go index 93f63f1..a9d39e2 100644 --- a/u_public.go +++ b/u_public.go @@ -543,8 +543,8 @@ func (fh *finishedHash) getPublicObj() FinishedHash { // TLS 1.3 Key Share. See RFC 8446, Section 4.2.8. type KeyShare struct { - Group CurveID - Data []byte + Group CurveID `json:"group"` + Data []byte `json:"key_exchange,omitempty"` // optional } type KeyShares []KeyShare @@ -568,8 +568,8 @@ func (KSS KeyShares) ToPrivate() []keyShare { // TLS 1.3 PSK Identity. Can be a Session Ticket, or a reference to a saved // session. See RFC 8446, Section 4.2.11. type PskIdentity struct { - Label []byte - ObfuscatedTicketAge uint32 + Label []byte `json:"identity"` + ObfuscatedTicketAge uint32 `json:"obfuscated_ticket_age"` } type PskIdentities []PskIdentity diff --git a/u_tls_extensions.go b/u_tls_extensions.go index 242fdde..3bc8826 100644 --- a/u_tls_extensions.go +++ b/u_tls_extensions.go @@ -5,15 +5,18 @@ package tls import ( + "encoding/json" "errors" + "fmt" "io" "strings" + "github.com/gaukas/godicttls" "golang.org/x/crypto/cryptobyte" ) -// ExtensionIDToExtension returns a TLSExtension for the given extension ID. -func ExtensionIDToExtension(id uint16) TLSExtensionWriter { +// ExtensionFromID returns a TLSExtension for the given extension ID. +func ExtensionFromID(id uint16) TLSExtension { // deep copy switch id { case extensionServerName: @@ -100,48 +103,18 @@ type TLSExtensionWriter interface { Write(b []byte) (n int, err error) } -type NPNExtension struct { - NextProtos []string -} - -func (e *NPNExtension) writeToUConn(uc *UConn) error { - uc.config.NextProtos = e.NextProtos - uc.HandshakeState.Hello.NextProtoNeg = true - return nil -} - -func (e *NPNExtension) Len() int { - return 4 -} - -func (e *NPNExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - b[0] = byte(extensionNextProtoNeg >> 8) - b[1] = byte(extensionNextProtoNeg & 0xff) - // The length is always 0 - return e.Len(), io.EOF -} - -// Write is a no-op for NPNExtension. NextProtos are not included in the -// ClientHello. -func (e *NPNExtension) Write(_ []byte) (int, error) { - return 0, nil +type TLSExtensionJSON interface { + TLSExtension + + // UnmarshalJSON unmarshals the JSON-encoded data into the extension. + UnmarshalJSON([]byte) error } +// SNIExtension implements server_name (0) type SNIExtension struct { ServerName string // not an array because go crypto/tls doesn't support multiple SNIs } -func (e *SNIExtension) writeToUConn(uc *UConn) error { - uc.config.ServerName = e.ServerName - hostName := hostnameInSNI(e.ServerName) - uc.HandshakeState.Hello.ServerName = hostName - - return nil -} - func (e *SNIExtension) Len() int { // Literal IP addresses, absolute FQDNs, and empty strings are not permitted as SNI values. // See RFC 6066, Section 3. @@ -176,6 +149,10 @@ func (e *SNIExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *SNIExtension) UnmarshalJSON(_ []byte) error { + return nil // no-op +} + // Write is a no-op for StatusRequestExtension. // SNI should not be fingerprinted and is user controlled. func (e *SNIExtension) Write(b []byte) (int, error) { @@ -212,12 +189,16 @@ func (e *SNIExtension) Write(b []byte) (int, error) { return fullLen, nil } -type StatusRequestExtension struct { +func (e *SNIExtension) writeToUConn(uc *UConn) error { + uc.config.ServerName = e.ServerName + hostName := hostnameInSNI(e.ServerName) + uc.HandshakeState.Hello.ServerName = hostName + + return nil } -func (e *StatusRequestExtension) writeToUConn(uc *UConn) error { - uc.HandshakeState.Hello.OcspStapling = true - return nil +// StatusRequestExtension implements status_request (5) +type StatusRequestExtension struct { } func (e *StatusRequestExtension) Len() int { @@ -238,6 +219,10 @@ func (e *StatusRequestExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *StatusRequestExtension) UnmarshalJSON(_ []byte) error { + return nil // no-op +} + // Write is a no-op for StatusRequestExtension. No data for this extension. func (e *StatusRequestExtension) Write(b []byte) (int, error) { fullLen := len(b) @@ -258,6 +243,225 @@ func (e *StatusRequestExtension) Write(b []byte) (int, error) { return fullLen, nil } +func (e *StatusRequestExtension) writeToUConn(uc *UConn) error { + uc.HandshakeState.Hello.OcspStapling = true + return nil +} + +// SupportedCurvesExtension implements supported_groups (renamed from "elliptic_curves") (10) +type SupportedCurvesExtension struct { + Curves []CurveID +} + +func (e *SupportedCurvesExtension) Len() int { + return 6 + 2*len(e.Curves) +} + +func (e *SupportedCurvesExtension) Read(b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + // http://tools.ietf.org/html/rfc4492#section-5.5.1 + b[0] = byte(extensionSupportedCurves >> 8) + b[1] = byte(extensionSupportedCurves) + b[2] = byte((2 + 2*len(e.Curves)) >> 8) + b[3] = byte(2 + 2*len(e.Curves)) + b[4] = byte((2 * len(e.Curves)) >> 8) + b[5] = byte(2 * len(e.Curves)) + for i, curve := range e.Curves { + b[6+2*i] = byte(curve >> 8) + b[7+2*i] = byte(curve) + } + return e.Len(), io.EOF +} + +func (e *SupportedCurvesExtension) UnmarshalJSON(data []byte) error { + var namedGroups struct { + NamedGroupList []string `json:"named_group_list"` + } + if err := json.Unmarshal(data, &namedGroups); err != nil { + return err + } + + for _, namedGroup := range namedGroups.NamedGroupList { + if namedGroup == "GREASE" { + e.Curves = append(e.Curves, GREASE_PLACEHOLDER) + continue + } + + if group, ok := godicttls.DictSupportedGroupsNameIndexed[namedGroup]; ok { + e.Curves = append(e.Curves, CurveID(group)) + } else { + return fmt.Errorf("unknown named group: %s", namedGroup) + } + } + return nil +} + +func (e *SupportedCurvesExtension) Write(b []byte) (int, error) { + fullLen := len(b) + extData := cryptobyte.String(b) + // RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7 + var curvesBytes cryptobyte.String + if !extData.ReadUint16LengthPrefixed(&curvesBytes) || curvesBytes.Empty() { + return 0, errors.New("unable to read supported curves extension data") + } + curves := []CurveID{} + for !curvesBytes.Empty() { + var curve uint16 + if !curvesBytes.ReadUint16(&curve) { + return 0, errors.New("unable to read supported curves extension data") + } + curves = append(curves, CurveID(unGREASEUint16(curve))) + } + e.Curves = curves + return fullLen, nil +} + +func (e *SupportedCurvesExtension) writeToUConn(uc *UConn) error { + uc.config.CurvePreferences = e.Curves + uc.HandshakeState.Hello.SupportedCurves = e.Curves + return nil +} + +// SupportedPointsExtension implements ec_point_formats (11) +type SupportedPointsExtension struct { + SupportedPoints []uint8 +} + +func (e *SupportedPointsExtension) Len() int { + return 5 + len(e.SupportedPoints) +} + +func (e *SupportedPointsExtension) Read(b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + // http://tools.ietf.org/html/rfc4492#section-5.5.2 + b[0] = byte(extensionSupportedPoints >> 8) + b[1] = byte(extensionSupportedPoints) + b[2] = byte((1 + len(e.SupportedPoints)) >> 8) + b[3] = byte(1 + len(e.SupportedPoints)) + b[4] = byte(len(e.SupportedPoints)) + for i, pointFormat := range e.SupportedPoints { + b[5+i] = pointFormat + } + return e.Len(), io.EOF +} + +func (e *SupportedPointsExtension) UnmarshalJSON(data []byte) error { + var pointFormatList struct { + ECPointFormatList []string `json:"ec_point_format_list"` + } + if err := json.Unmarshal(data, &pointFormatList); err != nil { + return err + } + + for _, pointFormat := range pointFormatList.ECPointFormatList { + if format, ok := godicttls.DictECPointFormatNameIndexed[pointFormat]; ok { + e.SupportedPoints = append(e.SupportedPoints, format) + } else { + return fmt.Errorf("unknown point format: %s", pointFormat) + } + } + return nil +} + +func (e *SupportedPointsExtension) Write(b []byte) (int, error) { + fullLen := len(b) + extData := cryptobyte.String(b) + // RFC 4492, Section 5.1.2 + supportedPoints := []uint8{} + if !readUint8LengthPrefixed(&extData, &supportedPoints) || + len(supportedPoints) == 0 { + return 0, errors.New("unable to read supported points extension data") + } + e.SupportedPoints = supportedPoints + return fullLen, nil +} + +func (e *SupportedPointsExtension) writeToUConn(uc *UConn) error { + uc.HandshakeState.Hello.SupportedPoints = e.SupportedPoints + return nil +} + +// SignatureAlgorithmsExtension implements signature_algorithms (13) +type SignatureAlgorithmsExtension struct { + SupportedSignatureAlgorithms []SignatureScheme +} + +func (e *SignatureAlgorithmsExtension) Len() int { + return 6 + 2*len(e.SupportedSignatureAlgorithms) +} + +func (e *SignatureAlgorithmsExtension) Read(b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + // https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 + b[0] = byte(extensionSignatureAlgorithms >> 8) + b[1] = byte(extensionSignatureAlgorithms) + b[2] = byte((2 + 2*len(e.SupportedSignatureAlgorithms)) >> 8) + b[3] = byte(2 + 2*len(e.SupportedSignatureAlgorithms)) + b[4] = byte((2 * len(e.SupportedSignatureAlgorithms)) >> 8) + b[5] = byte(2 * len(e.SupportedSignatureAlgorithms)) + for i, sigScheme := range e.SupportedSignatureAlgorithms { + b[6+2*i] = byte(sigScheme >> 8) + b[7+2*i] = byte(sigScheme) + } + return e.Len(), io.EOF +} + +func (e *SignatureAlgorithmsExtension) UnmarshalJSON(data []byte) error { + var signatureAlgorithms struct { + Algorithms []string `json:"supported_signature_algorithms"` + } + if err := json.Unmarshal(data, &signatureAlgorithms); err != nil { + return err + } + + for _, sigScheme := range signatureAlgorithms.Algorithms { + if sigScheme == "GREASE" { + e.SupportedSignatureAlgorithms = append(e.SupportedSignatureAlgorithms, GREASE_PLACEHOLDER) + continue + } + + if scheme, ok := godicttls.DictSignatureSchemeNameIndexed[sigScheme]; ok { + e.SupportedSignatureAlgorithms = append(e.SupportedSignatureAlgorithms, SignatureScheme(scheme)) + } else { + return fmt.Errorf("unknown signature scheme: %s", sigScheme) + } + } + return nil +} + +func (e *SignatureAlgorithmsExtension) Write(b []byte) (int, error) { + fullLen := len(b) + extData := cryptobyte.String(b) + // RFC 5246, Section 7.4.1.4.1 + var sigAndAlgs cryptobyte.String + if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() { + return 0, errors.New("unable to read signature algorithms extension data") + } + supportedSignatureAlgorithms := []SignatureScheme{} + for !sigAndAlgs.Empty() { + var sigAndAlg uint16 + if !sigAndAlgs.ReadUint16(&sigAndAlg) { + return 0, errors.New("unable to read signature algorithms extension data") + } + supportedSignatureAlgorithms = append( + supportedSignatureAlgorithms, SignatureScheme(sigAndAlg)) + } + e.SupportedSignatureAlgorithms = supportedSignatureAlgorithms + return fullLen, nil +} + +func (e *SignatureAlgorithmsExtension) writeToUConn(uc *UConn) error { + uc.HandshakeState.Hello.SupportedSignatureAlgorithms = e.SupportedSignatureAlgorithms + return nil +} + +// StatusRequestV2Extension implements status_request_v2 (17) type StatusRequestV2Extension struct { } @@ -310,161 +514,15 @@ func (e *StatusRequestV2Extension) Write(b []byte) (int, error) { return fullLen, nil } -type SupportedCurvesExtension struct { - Curves []CurveID -} - -func (e *SupportedCurvesExtension) writeToUConn(uc *UConn) error { - uc.config.CurvePreferences = e.Curves - uc.HandshakeState.Hello.SupportedCurves = e.Curves - return nil -} - -func (e *SupportedCurvesExtension) Len() int { - return 6 + 2*len(e.Curves) -} - -func (e *SupportedCurvesExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - // http://tools.ietf.org/html/rfc4492#section-5.5.1 - b[0] = byte(extensionSupportedCurves >> 8) - b[1] = byte(extensionSupportedCurves) - b[2] = byte((2 + 2*len(e.Curves)) >> 8) - b[3] = byte(2 + 2*len(e.Curves)) - b[4] = byte((2 * len(e.Curves)) >> 8) - b[5] = byte(2 * len(e.Curves)) - for i, curve := range e.Curves { - b[6+2*i] = byte(curve >> 8) - b[7+2*i] = byte(curve) - } - return e.Len(), io.EOF -} - -func (e *SupportedCurvesExtension) Write(b []byte) (int, error) { - fullLen := len(b) - extData := cryptobyte.String(b) - // RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7 - var curvesBytes cryptobyte.String - if !extData.ReadUint16LengthPrefixed(&curvesBytes) || curvesBytes.Empty() { - return 0, errors.New("unable to read supported curves extension data") - } - curves := []CurveID{} - for !curvesBytes.Empty() { - var curve uint16 - if !curvesBytes.ReadUint16(&curve) { - return 0, errors.New("unable to read supported curves extension data") - } - curves = append(curves, CurveID(unGREASEUint16(curve))) - } - e.Curves = curves - return fullLen, nil -} - -type SupportedPointsExtension struct { - SupportedPoints []uint8 -} - -func (e *SupportedPointsExtension) writeToUConn(uc *UConn) error { - uc.HandshakeState.Hello.SupportedPoints = e.SupportedPoints - return nil -} - -func (e *SupportedPointsExtension) Len() int { - return 5 + len(e.SupportedPoints) -} - -func (e *SupportedPointsExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - // http://tools.ietf.org/html/rfc4492#section-5.5.2 - b[0] = byte(extensionSupportedPoints >> 8) - b[1] = byte(extensionSupportedPoints) - b[2] = byte((1 + len(e.SupportedPoints)) >> 8) - b[3] = byte(1 + len(e.SupportedPoints)) - b[4] = byte(len(e.SupportedPoints)) - for i, pointFormat := range e.SupportedPoints { - b[5+i] = pointFormat - } - return e.Len(), io.EOF -} - -func (e *SupportedPointsExtension) Write(b []byte) (int, error) { - fullLen := len(b) - extData := cryptobyte.String(b) - // RFC 4492, Section 5.1.2 - supportedPoints := []uint8{} - if !readUint8LengthPrefixed(&extData, &supportedPoints) || - len(supportedPoints) == 0 { - return 0, errors.New("unable to read supported points extension data") - } - e.SupportedPoints = supportedPoints - return fullLen, nil -} - -type SignatureAlgorithmsExtension struct { - SupportedSignatureAlgorithms []SignatureScheme -} - -func (e *SignatureAlgorithmsExtension) writeToUConn(uc *UConn) error { - uc.HandshakeState.Hello.SupportedSignatureAlgorithms = e.SupportedSignatureAlgorithms - return nil -} - -func (e *SignatureAlgorithmsExtension) Len() int { - return 6 + 2*len(e.SupportedSignatureAlgorithms) -} - -func (e *SignatureAlgorithmsExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - // https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 - b[0] = byte(extensionSignatureAlgorithms >> 8) - b[1] = byte(extensionSignatureAlgorithms) - b[2] = byte((2 + 2*len(e.SupportedSignatureAlgorithms)) >> 8) - b[3] = byte(2 + 2*len(e.SupportedSignatureAlgorithms)) - b[4] = byte((2 * len(e.SupportedSignatureAlgorithms)) >> 8) - b[5] = byte(2 * len(e.SupportedSignatureAlgorithms)) - for i, sigAndHash := range e.SupportedSignatureAlgorithms { - b[6+2*i] = byte(sigAndHash >> 8) - b[7+2*i] = byte(sigAndHash) - } - return e.Len(), io.EOF -} - -func (e *SignatureAlgorithmsExtension) Write(b []byte) (int, error) { - fullLen := len(b) - extData := cryptobyte.String(b) - // RFC 5246, Section 7.4.1.4.1 - var sigAndAlgs cryptobyte.String - if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() { - return 0, errors.New("unable to read signature algorithms extension data") - } - supportedSignatureAlgorithms := []SignatureScheme{} - for !sigAndAlgs.Empty() { - var sigAndAlg uint16 - if !sigAndAlgs.ReadUint16(&sigAndAlg) { - return 0, errors.New("unable to read signature algorithms extension data") - } - supportedSignatureAlgorithms = append( - supportedSignatureAlgorithms, SignatureScheme(sigAndAlg)) - } - e.SupportedSignatureAlgorithms = supportedSignatureAlgorithms - return fullLen, nil +func (e *StatusRequestV2Extension) UnmarshalJSON(_ []byte) error { + return nil // no-op } +// SignatureAlgorithmsCertExtension implements signature_algorithms_cert (50) type SignatureAlgorithmsCertExtension struct { SupportedSignatureAlgorithms []SignatureScheme } -func (e *SignatureAlgorithmsCertExtension) writeToUConn(uc *UConn) error { - uc.HandshakeState.Hello.SupportedSignatureAlgorithms = e.SupportedSignatureAlgorithms - return nil -} - func (e *SignatureAlgorithmsCertExtension) Len() int { return 6 + 2*len(e.SupportedSignatureAlgorithms) } @@ -487,6 +545,30 @@ func (e *SignatureAlgorithmsCertExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +// Copied from SignatureAlgorithmsExtension.UnmarshalJSON +func (e *SignatureAlgorithmsCertExtension) UnmarshalJSON(data []byte) error { + var signatureAlgorithms struct { + Algorithms []string `json:"supported_signature_algorithms"` + } + if err := json.Unmarshal(data, &signatureAlgorithms); err != nil { + return err + } + + for _, sigScheme := range signatureAlgorithms.Algorithms { + if sigScheme == "GREASE" { + e.SupportedSignatureAlgorithms = append(e.SupportedSignatureAlgorithms, GREASE_PLACEHOLDER) + continue + } + + if scheme, ok := godicttls.DictSignatureSchemeNameIndexed[sigScheme]; ok { + e.SupportedSignatureAlgorithms = append(e.SupportedSignatureAlgorithms, SignatureScheme(scheme)) + } else { + return fmt.Errorf("unknown cert signature scheme: %s", sigScheme) + } + } + return nil +} + // Write implementation copied from SignatureAlgorithmsExtension.Write // // Warning: not tested. @@ -511,53 +593,12 @@ func (e *SignatureAlgorithmsCertExtension) Write(b []byte) (int, error) { return fullLen, nil } -type RenegotiationInfoExtension struct { - // Renegotiation field limits how many times client will perform renegotiation: no limit, once, or never. - // The extension still will be sent, even if Renegotiation is set to RenegotiateNever. - Renegotiation RenegotiationSupport -} - -func (e *RenegotiationInfoExtension) writeToUConn(uc *UConn) error { - uc.config.Renegotiation = e.Renegotiation - switch e.Renegotiation { - case RenegotiateOnceAsClient: - fallthrough - case RenegotiateFreelyAsClient: - uc.HandshakeState.Hello.SecureRenegotiationSupported = true - case RenegotiateNever: - default: - } +func (e *SignatureAlgorithmsCertExtension) writeToUConn(uc *UConn) error { + uc.HandshakeState.Hello.SupportedSignatureAlgorithms = e.SupportedSignatureAlgorithms return nil } -func (e *RenegotiationInfoExtension) Len() int { - return 5 -} - -func (e *RenegotiationInfoExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - - var extInnerBody []byte // inner body is empty - innerBodyLen := len(extInnerBody) - extBodyLen := innerBodyLen + 1 - - b[0] = byte(extensionRenegotiationInfo >> 8) - b[1] = byte(extensionRenegotiationInfo & 0xff) - b[2] = byte(extBodyLen >> 8) - b[3] = byte(extBodyLen) - b[4] = byte(innerBodyLen) - copy(b[5:], extInnerBody) - - return e.Len(), io.EOF -} - -func (e *RenegotiationInfoExtension) Write(_ []byte) (int, error) { - e.Renegotiation = RenegotiateOnceAsClient - return 0, nil -} - +// ALPNExtension implements application_layer_protocol_negotiation (16) type ALPNExtension struct { AlpnProtocols []string } @@ -604,6 +645,19 @@ func (e *ALPNExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *ALPNExtension) UnmarshalJSON(b []byte) error { + var protocolNames struct { + ProtocolNameList []string `json:"protocol_name_list"` + } + + if err := json.Unmarshal(b, &protocolNames); err != nil { + return err + } + + e.AlpnProtocols = protocolNames.ProtocolNameList + return nil +} + func (e *ALPNExtension) Write(b []byte) (int, error) { fullLen := len(b) extData := cryptobyte.String(b) @@ -674,6 +728,19 @@ func (e *ApplicationSettingsExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *ApplicationSettingsExtension) UnmarshalJSON(b []byte) error { + var applicationSettingsSupport struct { + SupportedProtocols []string `json:"supported_protocols"` + } + + if err := json.Unmarshal(b, &applicationSettingsSupport); err != nil { + return err + } + + e.SupportedProtocols = applicationSettingsSupport.SupportedProtocols + return nil +} + // Write implementation copied from ALPNExtension.Write func (e *ApplicationSettingsExtension) Write(b []byte) (int, error) { fullLen := len(b) @@ -696,6 +763,7 @@ func (e *ApplicationSettingsExtension) Write(b []byte) (int, error) { return fullLen, nil } +// SCTExtension implements signed_certificate_timestamp (18) type SCTExtension struct { } @@ -719,10 +787,15 @@ func (e *SCTExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *SCTExtension) UnmarshalJSON(_ []byte) error { + return nil // no-op +} + func (e *SCTExtension) Write(_ []byte) (int, error) { return 0, nil } +// SessionTicketExtension implements session_ticket (35) type SessionTicketExtension struct { Session *ClientSessionState } @@ -759,12 +832,18 @@ func (e *SessionTicketExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *SessionTicketExtension) UnmarshalJSON(_ []byte) error { + return nil // no-op +} + func (e *SessionTicketExtension) Write(_ []byte) (int, error) { // RFC 5077, Section 3.2 return 0, nil } // GenericExtension allows to include in ClientHello arbitrary unsupported extensions. +// It is not defined in TLS RFCs nor by IANA. +// If a server echoes this extension back, the handshake will likely fail due to no further support. type GenericExtension struct { Id uint16 Data []byte @@ -793,6 +872,26 @@ func (e *GenericExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *GenericExtension) UnmarshalJSON(b []byte) error { + var genericExtension struct { + Name string `json:"name"` + Data []byte `json:"data"` + } + if err := json.Unmarshal(b, &genericExtension); err != nil { + return err + } + + // lookup extension ID by name + if id, ok := godicttls.DictExtTypeNameIndexed[genericExtension.Name]; ok { + e.Id = id + } else { + return fmt.Errorf("unknown extension name %s", genericExtension.Name) + } + e.Data = genericExtension.Data + return nil +} + +// UtlsExtendedMasterSecretExtension implements extended_master_secret (23) type UtlsExtendedMasterSecretExtension struct { } @@ -818,6 +917,10 @@ func (e *UtlsExtendedMasterSecretExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *UtlsExtendedMasterSecretExtension) UnmarshalJSON(_ []byte) error { + return nil // no-op +} + func (e *UtlsExtendedMasterSecretExtension) Write(_ []byte) (int, error) { // https://tools.ietf.org/html/rfc7627 return 0, nil @@ -892,6 +995,36 @@ func (e *UtlsGREASEExtension) Write(b []byte) (int, error) { return n, nil } +func (e *UtlsGREASEExtension) UnmarshalJSON(b []byte) error { + var jsonObj struct { + Id uint16 `json:"id"` + Data []byte `json:"data"` + KeepID bool `json:"keep_id"` + KeepData bool `json:"keep_data"` + } + + if err := json.Unmarshal(b, &jsonObj); err != nil { + return err + } + + if jsonObj.Id == 0 { + return nil + } + + if isGREASEUint16(jsonObj.Id) { + if jsonObj.KeepID { + e.Value = jsonObj.Id + } + if jsonObj.KeepData { + e.Body = jsonObj.Data + } + return nil + } else { + return errors.New("GREASE extension id must be a GREASE value") + } +} + +// UtlsPaddingExtension implements padding (21) type UtlsPaddingExtension struct { PaddingLen int WillPad bool // set to false to disable extension @@ -934,6 +1067,24 @@ func (e *UtlsPaddingExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *UtlsPaddingExtension) UnmarshalJSON(b []byte) error { + var jsonObj struct { + Length uint `json:"len"` + } + if err := json.Unmarshal(b, &jsonObj); err != nil { + return err + } + + if jsonObj.Length == 0 { + e.GetPaddingLen = BoringPaddingStyle + } else { + e.PaddingLen = int(jsonObj.Length) + e.WillPad = true + } + + return nil +} + func (e *UtlsPaddingExtension) Write(_ []byte) (int, error) { e.GetPaddingLen = BoringPaddingStyle return 0, nil @@ -953,8 +1104,9 @@ func BoringPaddingStyle(unpaddedLen int) (int, bool) { return 0, false } -// UtlsCompressCertExtension is only implemented client-side, for server certificates. Alternate -// certificate message formats (https://datatracker.ietf.org/doc/html/rfc7250) are not supported. +// UtlsCompressCertExtension implements compress_certificate (27) and is only implemented client-side +// for server certificates. Alternate certificate message formats +// (https://datatracker.ietf.org/doc/html/rfc7250) are not supported. // // See https://datatracker.ietf.org/doc/html/rfc8879#section-3 type UtlsCompressCertExtension struct { @@ -1018,7 +1170,25 @@ func (e *UtlsCompressCertExtension) Write(b []byte) (int, error) { return fullLen, nil } -/* TLS 1.3 */ +func (e *UtlsCompressCertExtension) UnmarshalJSON(b []byte) error { + var certificateCompressionAlgorithms struct { + Algorithms []string `json:"algorithms"` + } + if err := json.Unmarshal(b, &certificateCompressionAlgorithms); err != nil { + return err + } + + for _, algorithm := range certificateCompressionAlgorithms.Algorithms { + if alg, ok := godicttls.DictCertificateCompressionAlgorithmNameIndexed[algorithm]; ok { + e.Algorithms = append(e.Algorithms, CertCompressionAlgo(alg)) + } else { + return fmt.Errorf("unknown certificate compression algorithm %s", algorithm) + } + } + return nil +} + +// KeyShareExtension implements key_share (51) and is for TLS 1.3 only. type KeyShareExtension struct { KeyShares []KeyShare } @@ -1095,6 +1265,40 @@ func (e *KeyShareExtension) writeToUConn(uc *UConn) error { return nil } +func (e *KeyShareExtension) UnmarshalJSON(b []byte) error { + var keyShareClientHello struct { + ClientShares []struct { + Group string `json:"group"` + KeyExchange []uint8 `json:"key_exchange"` + } `json:"client_shares"` + } + if err := json.Unmarshal(b, &keyShareClientHello); err != nil { + return err + } + + for _, clientShare := range keyShareClientHello.ClientShares { + if clientShare.Group == "GREASE" { + e.KeyShares = append(e.KeyShares, KeyShare{ + Group: GREASE_PLACEHOLDER, + Data: clientShare.KeyExchange, + }) + continue + } + + if groupID, ok := godicttls.DictSupportedGroupsNameIndexed[clientShare.Group]; ok { + ks := KeyShare{ + Group: CurveID(groupID), + Data: clientShare.KeyExchange, + } + e.KeyShares = append(e.KeyShares, ks) + } else { + return fmt.Errorf("unknown group %s", clientShare.Group) + } + } + return nil +} + +// PSKKeyExchangeModesExtension implements psk_key_exchange_modes (45). type PSKKeyExchangeModesExtension struct { Modes []uint8 } @@ -1148,6 +1352,25 @@ func (e *PSKKeyExchangeModesExtension) writeToUConn(uc *UConn) error { return nil } +func (e *PSKKeyExchangeModesExtension) UnmarshalJSON(b []byte) error { + var pskKeyExchangeModes struct { + Modes []string `json:"ke_modes"` + } + if err := json.Unmarshal(b, &pskKeyExchangeModes); err != nil { + return err + } + + for _, mode := range pskKeyExchangeModes.Modes { + if modeID, ok := godicttls.DictPSKKeyExchangeModeNameIndexed[mode]; ok { + e.Modes = append(e.Modes, modeID) + } else { + return fmt.Errorf("unknown PSK Key Exchange Mode %s", mode) + } + } + return nil +} + +// SupportedVersionsExtension implements supported_versions (43). type SupportedVersionsExtension struct { Versions []uint16 } @@ -1205,6 +1428,37 @@ func (e *SupportedVersionsExtension) Write(b []byte) (int, error) { return fullLen, nil } +func (e *SupportedVersionsExtension) UnmarshalJSON(b []byte) error { + var supportedVersions struct { + Versions []string `json:"versions"` + } + if err := json.Unmarshal(b, &supportedVersions); err != nil { + return err + } + + for _, version := range supportedVersions.Versions { + switch version { + case "GREASE": + e.Versions = append(e.Versions, GREASE_PLACEHOLDER) + case "TLS 1.3": + e.Versions = append(e.Versions, VersionTLS13) + case "TLS 1.2": + e.Versions = append(e.Versions, VersionTLS12) + case "TLS 1.1": + e.Versions = append(e.Versions, VersionTLS11) + case "TLS 1.0": + e.Versions = append(e.Versions, VersionTLS10) + case "SSL 3.0": // deprecated + // e.Versions = append(e.Versions, VersionSSL30) + return fmt.Errorf("SSL 3.0 is deprecated") + default: + return fmt.Errorf("unknown version %s", version) + } + } + return nil +} + +// CookieExtension implements cookie (44). // MUST NOT be part of initial ClientHello type CookieExtension struct { Cookie []byte @@ -1233,6 +1487,122 @@ func (e *CookieExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } +func (e *CookieExtension) UnmarshalJSON(data []byte) error { + var cookie struct { + Cookie []uint8 `json:"cookie"` + } + if err := json.Unmarshal(data, &cookie); err != nil { + return err + } + e.Cookie = []byte(cookie.Cookie) + return nil +} + +// NPNExtension implements next_protocol_negotiation (Not IANA assigned) +type NPNExtension struct { + NextProtos []string +} + +func (e *NPNExtension) writeToUConn(uc *UConn) error { + uc.config.NextProtos = e.NextProtos + uc.HandshakeState.Hello.NextProtoNeg = true + return nil +} + +func (e *NPNExtension) Len() int { + return 4 +} + +func (e *NPNExtension) Read(b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + b[0] = byte(extensionNextProtoNeg >> 8) + b[1] = byte(extensionNextProtoNeg & 0xff) + // The length is always 0 + return e.Len(), io.EOF +} + +// Write is a no-op for NPNExtension. NextProtos are not included in the +// ClientHello. +func (e *NPNExtension) Write(_ []byte) (int, error) { + return 0, nil +} + +// draft-agl-tls-nextprotoneg-04: +// The "extension_data" field of a "next_protocol_negotiation" extension +// in a "ClientHello" MUST be empty. +func (e *NPNExtension) UnmarshalJSON(_ []byte) error { + return nil +} + +// RenegotiationInfoExtension implements renegotiation_info (65281) +type RenegotiationInfoExtension struct { + // Renegotiation field limits how many times client will perform renegotiation: no limit, once, or never. + // The extension still will be sent, even if Renegotiation is set to RenegotiateNever. + Renegotiation RenegotiationSupport // [UTLS] added for internal use only + + // RenegotiatedConnection is not yet properly handled, now we + // are just copying it to the client hello. + // + // If this is the initial handshake for a connection, then the + // "renegotiated_connection" field is of zero length in both the + // ClientHello and the ServerHello. + // RenegotiatedConnection []byte +} + +func (e *RenegotiationInfoExtension) Len() int { + return 5 // + len(e.RenegotiatedConnection) +} + +func (e *RenegotiationInfoExtension) Read(b []byte) (int, error) { + if len(b) < e.Len() { + return 0, io.ErrShortBuffer + } + + // dataLen := len(e.RenegotiatedConnection) + extBodyLen := 1 // + len(dataLen) + + b[0] = byte(extensionRenegotiationInfo >> 8) + b[1] = byte(extensionRenegotiationInfo & 0xff) + b[2] = byte(extBodyLen >> 8) + b[3] = byte(extBodyLen) + // b[4] = byte(dataLen) + // copy(b[5:], e.RenegotiatedConnection) + + return e.Len(), io.EOF +} + +func (e *RenegotiationInfoExtension) UnmarshalJSON(_ []byte) error { + e.Renegotiation = RenegotiateOnceAsClient + return nil +} + +func (e *RenegotiationInfoExtension) Write(_ []byte) (int, error) { + e.Renegotiation = RenegotiateOnceAsClient // none empty or other modes are unsupported + // extData := cryptobyte.String(b) + // var renegotiatedConnection cryptobyte.String + // if !extData.ReadUint8LengthPrefixed(&renegotiatedConnection) || !extData.Empty() { + // return 0, errors.New("unable to read renegotiation info extension data") + // } + // e.RenegotiatedConnection = make([]byte, len(renegotiatedConnection)) + // copy(e.RenegotiatedConnection, renegotiatedConnection) + return 0, nil +} + +func (e *RenegotiationInfoExtension) writeToUConn(uc *UConn) error { + uc.config.Renegotiation = e.Renegotiation + switch e.Renegotiation { + case RenegotiateOnceAsClient: + fallthrough + case RenegotiateFreelyAsClient: + uc.HandshakeState.Hello.SecureRenegotiationSupported = true + case RenegotiateNever: + default: + } + return nil +} + /* FAKE EXTENSIONS */ @@ -1269,6 +1639,12 @@ func (e *FakeChannelIDExtension) Write(_ []byte) (int, error) { return 0, nil } +func (e *FakeChannelIDExtension) UnmarshalJSON(_ []byte) error { + return nil +} + +// FakeRecordSizeLimitExtension implements record_size_limit (28) +// but with no support. type FakeRecordSizeLimitExtension struct { Limit uint16 } @@ -1306,34 +1682,19 @@ func (e *FakeRecordSizeLimitExtension) Write(b []byte) (int, error) { return fullLen, nil } -type DelegatedCredentialsExtension struct { - AlgorithmsSignature []SignatureScheme -} +func (e *FakeRecordSizeLimitExtension) UnmarshalJSON(data []byte) error { + var limitAccepter struct { + Limit uint16 `json:"record_size_limit"` + } + if err := json.Unmarshal(data, &limitAccepter); err != nil { + return err + } -func (e *DelegatedCredentialsExtension) writeToUConn(uc *UConn) error { + e.Limit = limitAccepter.Limit return nil } -func (e *DelegatedCredentialsExtension) Len() int { - return 6 + 2*len(e.AlgorithmsSignature) -} - -func (e *DelegatedCredentialsExtension) Read(b []byte) (int, error) { - if len(b) < e.Len() { - return 0, io.ErrShortBuffer - } - b[0] = byte(extensionDelegatedCredentials >> 8) - b[1] = byte(extensionDelegatedCredentials) - b[2] = byte((2 + 2*len(e.AlgorithmsSignature)) >> 8) - b[3] = byte(2 + 2*len(e.AlgorithmsSignature)) - b[4] = byte((2 * len(e.AlgorithmsSignature)) >> 8) - b[5] = byte(2 * len(e.AlgorithmsSignature)) - for i, sigAndHash := range e.AlgorithmsSignature { - b[6+2*i] = byte(sigAndHash >> 8) - b[7+2*i] = byte(sigAndHash) - } - return e.Len(), io.EOF -} +type DelegatedCredentialsExtension = FakeDelegatedCredentialsExtension // https://tools.ietf.org/html/rfc8472#section-2 type FakeTokenBindingExtension struct { @@ -1381,6 +1742,35 @@ func (e *FakeTokenBindingExtension) Write(b []byte) (int, error) { return fullLen, nil } +func (e *FakeTokenBindingExtension) UnmarshalJSON(data []byte) error { + var tokenBindingAccepter struct { + TB_ProtocolVersion struct { + Major uint8 `json:"major"` + Minor uint8 `json:"minor"` + } `json:"token_binding_version"` + TokenBindingKeyParameters []string `json:"key_parameters_list"` + } + if err := json.Unmarshal(data, &tokenBindingAccepter); err != nil { + return err + } + + e.MajorVersion = tokenBindingAccepter.TB_ProtocolVersion.Major + e.MinorVersion = tokenBindingAccepter.TB_ProtocolVersion.Minor + for _, param := range tokenBindingAccepter.TokenBindingKeyParameters { + switch param { + case "rsa2048_pkcs1.5": + e.KeyParameters = append(e.KeyParameters, 0) + case "rsa2048_pss": + e.KeyParameters = append(e.KeyParameters, 1) + case "ecdsap256": + e.KeyParameters = append(e.KeyParameters, 2) + default: + return fmt.Errorf("unknown token binding key parameter: %s", param) + } + } + return nil +} + // https://datatracker.ietf.org/doc/html/draft-ietf-tls-subcerts-15#section-4.1.1 type FakeDelegatedCredentialsExtension struct { @@ -1434,14 +1824,38 @@ func (e *FakeDelegatedCredentialsExtension) Write(b []byte) (int, error) { return fullLen, nil } +// Implementation copied from SignatureAlgorithmsExtension.UnmarshalJSON +func (e *FakeDelegatedCredentialsExtension) UnmarshalJSON(data []byte) error { + var signatureAlgorithms struct { + Algorithms []string `json:"supported_signature_algorithms"` + } + if err := json.Unmarshal(data, &signatureAlgorithms); err != nil { + return err + } + + for _, sigScheme := range signatureAlgorithms.Algorithms { + if sigScheme == "GREASE" { + e.SupportedSignatureAlgorithms = append(e.SupportedSignatureAlgorithms, GREASE_PLACEHOLDER) + continue + } + + if scheme, ok := godicttls.DictSignatureSchemeNameIndexed[sigScheme]; ok { + e.SupportedSignatureAlgorithms = append(e.SupportedSignatureAlgorithms, SignatureScheme(scheme)) + } else { + return fmt.Errorf("unknown delegated credentials signature scheme: %s", sigScheme) + } + } + return nil +} + // FakePreSharedKeyExtension is an extension used to set the PSK extension in the // ClientHello. // // Unfortunately, even when the PSK extension is set, there will be no PSK-based // resumption since crypto/tls does not implement PSK. type FakePreSharedKeyExtension struct { - PskIdentities []PskIdentity - PskBinders [][]byte + PskIdentities []PskIdentity `json:"identities"` + PskBinders [][]byte `json:"binders"` } func (e *FakePreSharedKeyExtension) writeToUConn(uc *UConn) error { @@ -1584,3 +1998,18 @@ func (e *FakePreSharedKeyExtension) Write(b []byte) (n int, err error) { return fullLen, nil } + +func (e *FakePreSharedKeyExtension) UnmarshalJSON(data []byte) error { + var pskAccepter struct { + PskIdentities []PskIdentity `json:"identities"` + PskBinders [][]byte `json:"binders"` + } + + if err := json.Unmarshal(data, &pskAccepter); err != nil { + return err + } + + e.PskIdentities = pskAccepter.PskIdentities + e.PskBinders = pskAccepter.PskBinders + return nil +}