diff --git a/tinygo/v1/examples/utls/lib/lib.go b/tinygo/v1/examples/utls/lib/lib.go index b2d73c0..e0b04d9 100644 --- a/tinygo/v1/examples/utls/lib/lib.go +++ b/tinygo/v1/examples/utls/lib/lib.go @@ -1,6 +1,11 @@ package lib import ( + "crypto/x509" + "log" + "os" + "strings" + tls "github.com/refraction-networking/utls" ) @@ -9,7 +14,7 @@ type TLSConfig struct { NextProtos []string `json:"next_protos"` ApplicationSettings map[string][]byte `json:"application_settings"` ServerName string `json:"server_name"` - InsecureSkipVerify bool `json:"insecure_skip_verify"` + InsecureSkipVerify bool `json:"insecure_skip_verify"` // if not set, host must supply a root CA certificate via root_ca or root_ca_dirs InsecureSkipTimeVerify bool `json:"insecure_skip_time_verify"` OmitEmptyPsk bool `json:"omit_empty_psk"` InsecureServerNameToVerify string `json:"insecure_server_name_to_verify"` @@ -17,34 +22,110 @@ type TLSConfig struct { PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled"` DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled"` ECHConfigs []byte `json:"ech_configs"` + + RootCAPath string `json:"root_ca_path"` // if set, will be parsed as a x509 Root CA certificate + RootCADirs []string `json:"root_ca_dirs"` // if non-empty, all x509 certs found in the directory specified by the list will be used to verify the host +} + +func (tlsConf *TLSConfig) GetConfig() *tls.Config { + conf := &tls.Config{ + NextProtos: tlsConf.NextProtos, + ApplicationSettings: tlsConf.ApplicationSettings, + ServerName: tlsConf.ServerName, + InsecureSkipVerify: tlsConf.InsecureSkipVerify, + InsecureSkipTimeVerify: tlsConf.InsecureSkipTimeVerify, + OmitEmptyPsk: tlsConf.OmitEmptyPsk, + InsecureServerNameToVerify: tlsConf.InsecureServerNameToVerify, + SessionTicketsDisabled: tlsConf.SessionTicketsDisabled, + PQSignatureSchemesEnabled: tlsConf.PQSignatureSchemesEnabled, + DynamicRecordSizingDisabled: tlsConf.DynamicRecordSizingDisabled, + } + + echConfigs, err := tls.UnmarshalECHConfigs(tlsConf.ECHConfigs) + if err == nil { // otherwise do we need to return an error or just ignore it? + conf.ECHConfigs = echConfigs + } + + if !tlsConf.InsecureSkipVerify { + rootCAs, err := tlsConf.loadRootCAs() + if err == nil { + conf.RootCAs = rootCAs + } else { + panic("failed to load root CAs: " + err.Error()) + } + } + + return conf +} + +// loadRootCAs loads the root CA certificates from the RootCA and RootCADirs fields. +// +// Derived from crypto/x509.loadSystemRoots +func (tlsConf *TLSConfig) loadRootCAs() (*x509.CertPool, error) { + roots := x509.NewCertPool() + + // if RootCA is set, use it as the first cert + var files []string + if tlsConf.RootCAPath != "" { + log.Printf("UTLSClientWrappingTransport: loading root CA certificate set via config: %s\n", tlsConf.RootCAPath) + files = append(files, tlsConf.RootCAPath) + } + + if f := os.Getenv(certFileEnv); f != "" { + log.Printf("UTLSClientWrappingTransport: loading root CA certificate set via ENV: %s\n", tlsConf.RootCAPath) + files = append(files, f) + } + + var firstErr error + for _, file := range files { + data, err := os.ReadFile(file) + if err == nil { + roots.AppendCertsFromPEM(data) + break + } + if firstErr == nil && !os.IsNotExist(err) { + firstErr = err + } + } + + var dirs []string = tlsConf.RootCADirs + if d := os.Getenv(certDirEnv); d != "" { + // OpenSSL and BoringSSL both use ":" as the SSL_CERT_DIR separator. + // See: + // * https://golang.org/issue/35325 + // * https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html + dirs = strings.Split(d, ":") + } + + for _, directory := range dirs { + fis, err := readUniqueDirectoryEntries(directory) + if err != nil { + if firstErr == nil && !os.IsNotExist(err) { + firstErr = err + } + continue + } + for _, fi := range fis { + data, err := os.ReadFile(directory + "/" + fi.Name()) + if err == nil { + roots.AppendCertsFromPEM(data) + } + } + } + + return roots, firstErr } //tinyjson:json type Configurables struct { - TLSConfig *TLSConfig `json:"tls_config"` // will be converted to tls.Config - ClientHelloID string `json:"client_hello_id"` // will be converted to tls.ClientHelloID + TLSConfig *TLSConfig `json:"tls_config"` // will be converted to tls.Config + ClientHelloID string `json:"client_hello_id"` // will be converted to tls.ClientHelloID + InternalBufferSize int `json:"internal_buffer_size"` // will be used to allocate internal temporary buffer + BackgroundWorkerFairness bool `json:"background_worker_fairness"` // if true, use fairWorker, otherwise use unfairWorker } func (c *Configurables) GetTLSConfig() *tls.Config { - config := &tls.Config{ - NextProtos: c.TLSConfig.NextProtos, - ApplicationSettings: c.TLSConfig.ApplicationSettings, - ServerName: c.TLSConfig.ServerName, - InsecureSkipVerify: c.TLSConfig.InsecureSkipVerify, - InsecureSkipTimeVerify: c.TLSConfig.InsecureSkipTimeVerify, - OmitEmptyPsk: c.TLSConfig.OmitEmptyPsk, - InsecureServerNameToVerify: c.TLSConfig.InsecureServerNameToVerify, - SessionTicketsDisabled: c.TLSConfig.SessionTicketsDisabled, - PQSignatureSchemesEnabled: c.TLSConfig.PQSignatureSchemesEnabled, - DynamicRecordSizingDisabled: c.TLSConfig.DynamicRecordSizingDisabled, - } - - echConfigs, err := tls.UnmarshalECHConfigs(c.TLSConfig.ECHConfigs) - if err == nil { // otherwise do we need to return an error or just ignore it? - config.ECHConfigs = echConfigs - } - - return config + return c.TLSConfig.GetConfig() } func (c *Configurables) GetClientHelloID() tls.ClientHelloID { @@ -57,6 +138,14 @@ func (c *Configurables) GetClientHelloID() tls.ClientHelloID { return tls.HelloFirefox_Auto case "HelloSafari_Auto", "HelloSafari", "Safari", "safari": return tls.HelloSafari_Auto + case "HelloRandomized", "Randomized", "randomized", "Random", "random": + return tls.HelloRandomized + case "HelloRandomizedALPN", "RandomizedALPN", "randomized_alpn", "RandomALPN", "random_alpn": + return tls.HelloRandomizedALPN + case "HelloRandomizedNoALPN", "RandomizedNoALPN", "randomized_no_alpn", "RandomNoALPN", "random_no_alpn": + return tls.HelloRandomizedNoALPN + case "HelloGolang", "Golang", "golang", "Go", "go", "crypto/tls", "Default", "default", "": + return tls.HelloGolang default: panic("unknown client hello id") } diff --git a/tinygo/v1/examples/utls/lib/lib_tinyjson.go b/tinygo/v1/examples/utls/lib/lib_tinyjson.go index f828880..21e87ac 100644 --- a/tinygo/v1/examples/utls/lib/lib_tinyjson.go +++ b/tinygo/v1/examples/utls/lib/lib_tinyjson.go @@ -15,7 +15,7 @@ var ( _ tinyjson.Marshaler ) -func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib(in *jlexer.Lexer, out *TLSConfig) { +func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib(in *jlexer.Lexer, out *TLSConfig) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -101,6 +101,31 @@ func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls } else { out.ECHConfigs = in.Bytes() } + case "root_ca_path": + out.RootCAPath = string(in.String()) + case "root_ca_dirs": + if in.IsNull() { + in.Skip() + out.RootCADirs = nil + } else { + in.Delim('[') + if out.RootCADirs == nil { + if !in.IsDelim(']') { + out.RootCADirs = make([]string, 0, 4) + } else { + out.RootCADirs = []string{} + } + } else { + out.RootCADirs = (out.RootCADirs)[:0] + } + for !in.IsDelim(']') { + var v5 string + v5 = string(in.String()) + out.RootCADirs = append(out.RootCADirs, v5) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -111,7 +136,7 @@ func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls in.Consumed() } } -func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib(out *jwriter.Writer, in TLSConfig) { +func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib(out *jwriter.Writer, in TLSConfig) { out.RawByte('{') first := true _ = first @@ -122,11 +147,11 @@ func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls out.RawString("null") } else { out.RawByte('[') - for v5, v6 := range in.NextProtos { - if v5 > 0 { + for v6, v7 := range in.NextProtos { + if v6 > 0 { out.RawByte(',') } - out.String(string(v6)) + out.String(string(v7)) } out.RawByte(']') } @@ -138,16 +163,16 @@ func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls out.RawString(`null`) } else { out.RawByte('{') - v7First := true - for v7Name, v7Value := range in.ApplicationSettings { - if v7First { - v7First = false + v8First := true + for v8Name, v8Value := range in.ApplicationSettings { + if v8First { + v8First = false } else { out.RawByte(',') } - out.String(string(v7Name)) + out.String(string(v8Name)) out.RawByte(':') - out.Base64Bytes(v7Value) + out.Base64Bytes(v8Value) } out.RawByte('}') } @@ -197,33 +222,54 @@ func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls out.RawString(prefix) out.Base64Bytes(in.ECHConfigs) } + { + const prefix string = ",\"root_ca_path\":" + out.RawString(prefix) + out.String(string(in.RootCAPath)) + } + { + const prefix string = ",\"root_ca_dirs\":" + out.RawString(prefix) + if in.RootCADirs == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v13, v14 := range in.RootCADirs { + if v13 > 0 { + out.RawByte(',') + } + out.String(string(v14)) + } + out.RawByte(']') + } + } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface func (v TLSConfig) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib(&w, v) + tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalTinyJSON supports tinyjson.Marshaler interface func (v TLSConfig) MarshalTinyJSON(w *jwriter.Writer) { - tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib(w, v) + tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *TLSConfig) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib(&r, v) + tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib(&r, v) return r.Error() } // UnmarshalTinyJSON supports tinyjson.Unmarshaler interface func (v *TLSConfig) UnmarshalTinyJSON(l *jlexer.Lexer) { - tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib(l, v) + tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib(l, v) } -func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib1(in *jlexer.Lexer, out *Configurables) { +func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib1(in *jlexer.Lexer, out *Configurables) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -254,6 +300,10 @@ func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls } case "client_hello_id": out.ClientHelloID = string(in.String()) + case "internal_buffer_size": + out.InternalBufferSize = int(in.Int()) + case "background_worker_fairness": + out.BackgroundWorkerFairness = bool(in.Bool()) default: in.SkipRecursive() } @@ -264,7 +314,7 @@ func tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls in.Consumed() } } -func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib1(out *jwriter.Writer, in Configurables) { +func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib1(out *jwriter.Writer, in Configurables) { out.RawByte('{') first := true _ = first @@ -282,29 +332,39 @@ func tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtls out.RawString(prefix) out.String(string(in.ClientHelloID)) } + { + const prefix string = ",\"internal_buffer_size\":" + out.RawString(prefix) + out.Int(int(in.InternalBufferSize)) + } + { + const prefix string = ",\"background_worker_fairness\":" + out.RawString(prefix) + out.Bool(bool(in.BackgroundWorkerFairness)) + } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface func (v Configurables) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib1(&w, v) + tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib1(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalTinyJSON supports tinyjson.Marshaler interface func (v Configurables) MarshalTinyJSON(w *jwriter.Writer) { - tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib1(w, v) + tinyjsonAded76e7EncodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib1(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *Configurables) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib1(&r, v) + tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib1(&r, v) return r.Error() } // UnmarshalTinyJSON supports tinyjson.Unmarshaler interface func (v *Configurables) UnmarshalTinyJSON(l *jlexer.Lexer) { - tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV0ExamplesUtlsLib1(l, v) + tinyjsonAded76e7DecodeGithubComRefractionNetworkingWatmTinygoV1ExamplesUtlsLib1(l, v) } diff --git a/tinygo/v1/examples/utls/lib/x509_root.go b/tinygo/v1/examples/utls/lib/x509_root.go new file mode 100644 index 0000000..9a74c65 --- /dev/null +++ b/tinygo/v1/examples/utls/lib/x509_root.go @@ -0,0 +1,46 @@ +package lib + +import ( + "io/fs" + "os" + "path/filepath" + "strings" +) + +const ( + // certFileEnv is the environment variable which identifies where to locate + // the SSL certificate file. If set this overrides the system default. + certFileEnv = "SSL_CERT_FILE" + + // certDirEnv is the environment variable which identifies which directory + // to check for SSL certificate files. If set this overrides the system default. + // It is a colon separated list of directories. + // See https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html. + certDirEnv = "SSL_CERT_DIR" +) + +// readUniqueDirectoryEntries is like os.ReadDir but omits +// symlinks that point within the directory. +func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + uniq := files[:0] + for _, f := range files { + if !isSameDirSymlink(f, dir) { + uniq = append(uniq, f) + } + } + return uniq, nil +} + +// isSameDirSymlink reports whether fi in dir is a symlink with a +// target not containing a slash. +func isSameDirSymlink(f fs.DirEntry, dir string) bool { + if f.Type()&fs.ModeSymlink == 0 { + return false + } + target, err := os.Readlink(filepath.Join(dir, f.Name())) + return err == nil && !strings.Contains(target, "/") +} diff --git a/tinygo/v1/examples/utls/watm.json b/tinygo/v1/examples/utls/watm.json new file mode 100644 index 0000000..ee9f3a5 --- /dev/null +++ b/tinygo/v1/examples/utls/watm.json @@ -0,0 +1,9 @@ +{ + "tls_config": { + "server_name": "example.com", + "root_ca_path": "/etc/ssl/certs/ca-certificates.crt" + }, + "client_hello_id": "HelloChrome_Auto", + "internal_buffer_size": 1024, + "background_worker_fairness": true +} \ No newline at end of file diff --git a/tinygo/v1/examples/utls/wrapper_transport.go b/tinygo/v1/examples/utls/wrapper_transport.go index 5abcacc..7252a43 100644 --- a/tinygo/v1/examples/utls/wrapper_transport.go +++ b/tinygo/v1/examples/utls/wrapper_transport.go @@ -1,6 +1,8 @@ package main import ( + "log" + "github.com/CosmWasm/tinyjson" tls "github.com/refraction-networking/utls" v1 "github.com/refraction-networking/watm/tinygo/v1" @@ -18,6 +20,7 @@ type UTLSClientWrappingTransport struct { func (uwt *UTLSClientWrappingTransport) Wrap(conn v1net.Conn) (v1net.Conn, error) { if uwt.tlsConfig == nil { + log.Println("UTLSClientWrappingTransport: tlsConfig is nil, using default config") uwt.tlsConfig = &tls.Config{InsecureSkipVerify: true} } @@ -52,6 +55,13 @@ func (uwt *UTLSClientWrappingTransport) Configure(config []byte) error { uwt.tlsConfig = configurables.GetTLSConfig() uwt.clientHelloID = configurables.GetClientHelloID() + v1.WorkerFairness(configurables.BackgroundWorkerFairness) + log.Printf("UTLSClientWrappingTransport: set worker fairness to %v\n", configurables.BackgroundWorkerFairness) + if configurables.InternalBufferSize > 0 { + v1.SetReadBufferSize(configurables.InternalBufferSize) + log.Printf("UTLSClientWrappingTransport: set resizing internal buffer to %d Bytes\n", configurables.InternalBufferSize) + } + return nil }