diff --git a/loader/goroot.go b/loader/goroot.go index 0aab0a0e13..38d28a30e7 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -45,7 +45,7 @@ func GetCachedGoroot(config *compileopts.Config) (string, error) { } // Find the overrides needed for the goroot. - overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags())) + overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags()), needsTLSStubPackage(config.GOOS(), config.BuildTags())) // Resolve the merge links within the goroot. merge, err := listGorootMergeLinks(goroot, tinygoroot, overrides) @@ -225,14 +225,27 @@ func needsSyscallPackage(buildTags []string) bool { return false } +// Keep the netdev TLS wrapper except on hosted Linux and Darwin. +// The baremetal tag is needed because those targets also report GOOS=linux. +func needsTLSStubPackage(goos string, buildTags []string) bool { + if goos != "linux" && goos != "darwin" { + return true + } + for _, tag := range buildTags { + if tag == "baremetal" || tag == "nintendoswitch" || tag == "tinygo.wasm" || tag == "wasm_unknown" { + return true + } + } + return false +} + // The boolean indicates whether to merge the subdirs. True means merge, false // means use the TinyGo version. -func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { +func pathsToOverride(goMinor int, needsSyscallPackage, needsTLSStubPackage bool) map[string]bool { paths := map[string]bool{ "": true, "crypto/": true, "crypto/rand/": false, - "crypto/tls/": false, "crypto/x509/": true, "crypto/x509/internal/": true, "crypto/x509/internal/macos/": false, @@ -263,6 +276,12 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { "unique/": false, } + if needsTLSStubPackage { + // Without this entry crypto/tls falls under the "crypto/" merge above, + // which links in the package of the standard library. + paths["crypto/tls/"] = false + } + if goMinor >= 19 { paths["crypto/internal/"] = true paths["crypto/internal/boring/"] = true diff --git a/loader/goroot_test.go b/loader/goroot_test.go new file mode 100644 index 0000000000..f21fe24d7b --- /dev/null +++ b/loader/goroot_test.go @@ -0,0 +1,28 @@ +package loader + +import "testing" + +func TestNeedsTLSStubPackage(t *testing.T) { + tests := []struct { + name string + goos string + buildTags []string + want bool + }{ + {"hosted linux", "linux", []string{"linux", "amd64"}, false}, + {"hosted darwin", "darwin", []string{"darwin", "arm64"}, false}, + {"windows", "windows", []string{"windows", "amd64"}, true}, + {"wasip1", "wasip1", []string{"wasip1", "tinygo.wasm"}, true}, + {"wasip2", "wasip2", []string{"wasip2", "tinygo.wasm"}, true}, + // A baremetal target reports GOOS=linux, so the build tags have to + // keep the stub for it. + {"baremetal", "linux", []string{"linux", "arm", "baremetal"}, true}, + {"nintendoswitch", "linux", []string{"linux", "nintendoswitch"}, true}, + {"wasm_unknown", "linux", []string{"linux", "wasm_unknown"}, true}, + } + for _, test := range tests { + if got := needsTLSStubPackage(test.goos, test.buildTags); got != test.want { + t.Errorf("%s: wanted %v, got %v", test.name, test.want, got) + } + } +} diff --git a/loader/loader.go b/loader/loader.go index 5696abd065..7435e737e7 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -278,7 +278,7 @@ func (p *Program) getOriginalPath(path string) string { originalPath = realgorootPath } maybeInTinyGoRoot := false - for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags())) { + for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags()), needsTLSStubPackage(p.config.GOOS(), p.config.BuildTags())) { if runtime.GOOS == "windows" { prefix = strings.ReplaceAll(prefix, "/", "\\") } diff --git a/main_test.go b/main_test.go index 07c531a392..4ab96d565d 100644 --- a/main_test.go +++ b/main_test.go @@ -283,6 +283,19 @@ func TestTimerStopResetRace(t *testing.T) { runTest("timer_stop_reset_race.go", optionsFromTarget("", sema), t, nil, nil) } +// TestHostCryptoTLS checks that a hosted target gets the real crypto/tls and +// not the stub, whose handshake does nothing. Only linux and macOS do. +func TestHostCryptoTLS(t *testing.T) { + t.Parallel() + + switch runtime.GOOS { + case "darwin", "linux": + default: + t.Skipf("host GOOS %s keeps the crypto/tls stub", runtime.GOOS) + } + runTest("hostcryptotls.go", optionsFromTarget("", sema), t, nil, nil) +} + func TestESP32QEMU(t *testing.T) { t.Parallel() diff --git a/testdata/hostcryptotls.go b/testdata/hostcryptotls.go new file mode 100644 index 0000000000..462f19971c --- /dev/null +++ b/testdata/hostcryptotls.go @@ -0,0 +1,100 @@ +package main + +// A real TLS handshake over an in-memory pipe. The stub crypto/tls has a +// handshake that does nothing, so it cannot pass this. + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "io" + "math/big" + "net" + "time" +) + +func main() { + cert, pool := selfSigned() + + // A client that trusts the certificate completes the handshake and + // exchanges data. + client, server := net.Pipe() + go serve(server, cert) + conn := tls.Client(client, &tls.Config{RootCAs: pool, ServerName: "tinygo.test"}) + if err := conn.Handshake(); err != nil { + println("handshake failed:", err.Error()) + return + } + if v := conn.ConnectionState().Version; v < tls.VersionTLS12 { + println("negotiated an unexpected version:", v) + return + } + if _, err := conn.Write([]byte("ping")); err != nil { + println("write failed:", err.Error()) + return + } + buf := make([]byte, 4) + if _, err := io.ReadFull(conn, buf); err != nil { + println("read failed:", err.Error()) + return + } + println("got:", string(buf)) + conn.Close() + + // A client that does not trust the certificate must refuse it. + client, server = net.Pipe() + go serve(server, cert) + conn = tls.Client(client, &tls.Config{ServerName: "tinygo.test"}) + if err := conn.Handshake(); err == nil { + println("an unknown certificate was accepted") + return + } + conn.Close() + println("unknown certificate refused") +} + +func serve(conn net.Conn, cert tls.Certificate) { + server := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}}) + if err := server.Handshake(); err != nil { + conn.Close() + return + } + buf := make([]byte, 4) + if _, err := io.ReadFull(server, buf); err != nil { + server.Close() + return + } + server.Write([]byte("pong")) +} + +func selfSigned() (tls.Certificate, *x509.CertPool) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "tinygo.test"}, + DNSNames: []string{"tinygo.test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + panic(err) + } + leaf, err := x509.ParseCertificate(der) + if err != nil { + panic(err) + } + pool := x509.NewCertPool() + pool.AddCert(leaf) + return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}, pool +} diff --git a/testdata/hostcryptotls.txt b/testdata/hostcryptotls.txt new file mode 100644 index 0000000000..bc5c8865cf --- /dev/null +++ b/testdata/hostcryptotls.txt @@ -0,0 +1,2 @@ +got: pong +unknown certificate refused