Skip to content

Commit 2d41706

Browse files
committed
loader: use the real crypto/tls on hosted linux and darwin
TinyGo replaces crypto/tls with a stub whose handshake does nothing, so a program that dials https gets a plaintext connection behind the TLS API. That stub is correct for a target with no OS below it, which has neither the code size for a full TLS implementation nor usually a socket to speak it over. Hosted linux and macOS have both, and there the crypto/tls of the Go standard library compiles and runs. Make the override conditional. Without an entry in the map, crypto/tls falls under the "crypto/" merge, which links the package of the standard library into the synthetic GOROOT. GOOS alone cannot decide this, because a baremetal target reports GOOS=linux, so the build tags decide as well. The goroot cache key is a hash of the merge links, so the two variants get separate cache entries. testdata/hostcryptotls.go does a TLS handshake over an in-memory pipe with a certificate that it makes at run time. On the current dev branch it prints "negotiated an unexpected version: 0", because the stub does no handshake. With this change the handshake completes, the data goes through, and a client that does not trust the certificate refuses it. loader/goroot_test.go covers the targets that keep the stub, the baremetal one that reports GOOS=linux included.
1 parent 1388449 commit 2d41706

6 files changed

Lines changed: 171 additions & 4 deletions

File tree

loader/goroot.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func GetCachedGoroot(config *compileopts.Config) (string, error) {
4545
}
4646

4747
// Find the overrides needed for the goroot.
48-
overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags()))
48+
overrides := pathsToOverride(config.GoMinorVersion, needsSyscallPackage(config.BuildTags()), needsTLSStubPackage(config.GOOS(), config.BuildTags()))
4949

5050
// Resolve the merge links within the goroot.
5151
merge, err := listGorootMergeLinks(goroot, tinygoroot, overrides)
@@ -225,14 +225,33 @@ func needsSyscallPackage(buildTags []string) bool {
225225
return false
226226
}
227227

228+
// needsTLSStubPackage returns whether the crypto/tls package should be
229+
// overridden with the TinyGo stub version, whose handshake is a no-op. A target
230+
// with no OS below it has neither the code size for a full TLS implementation
231+
// nor usually a socket to speak it over.
232+
//
233+
// Hosted linux and macOS have both, so they use the real crypto/tls of the Go
234+
// standard library. GOOS alone cannot decide this, because a baremetal target
235+
// reports GOOS=linux, so the build tags decide as well.
236+
func needsTLSStubPackage(goos string, buildTags []string) bool {
237+
if goos != "linux" && goos != "darwin" {
238+
return true
239+
}
240+
for _, tag := range buildTags {
241+
if tag == "baremetal" || tag == "nintendoswitch" || tag == "tinygo.wasm" || tag == "wasm_unknown" {
242+
return true
243+
}
244+
}
245+
return false
246+
}
247+
228248
// The boolean indicates whether to merge the subdirs. True means merge, false
229249
// means use the TinyGo version.
230-
func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
250+
func pathsToOverride(goMinor int, needsSyscallPackage, needsTLSStubPackage bool) map[string]bool {
231251
paths := map[string]bool{
232252
"": true,
233253
"crypto/": true,
234254
"crypto/rand/": false,
235-
"crypto/tls/": false,
236255
"crypto/x509/": true,
237256
"crypto/x509/internal/": true,
238257
"crypto/x509/internal/macos/": false,
@@ -263,6 +282,12 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
263282
"unique/": false,
264283
}
265284

285+
if needsTLSStubPackage {
286+
// Without this entry crypto/tls falls under the "crypto/" merge above,
287+
// which links in the package of the standard library.
288+
paths["crypto/tls/"] = false
289+
}
290+
266291
if goMinor >= 19 {
267292
paths["crypto/internal/"] = true
268293
paths["crypto/internal/boring/"] = true

loader/goroot_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package loader
2+
3+
import "testing"
4+
5+
func TestNeedsTLSStubPackage(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
goos string
9+
buildTags []string
10+
want bool
11+
}{
12+
{"hosted linux", "linux", []string{"linux", "amd64"}, false},
13+
{"hosted darwin", "darwin", []string{"darwin", "arm64"}, false},
14+
{"windows", "windows", []string{"windows", "amd64"}, true},
15+
{"wasip1", "wasip1", []string{"wasip1", "tinygo.wasm"}, true},
16+
// A baremetal target reports GOOS=linux, so the build tags have to
17+
// keep the stub for it.
18+
{"baremetal", "linux", []string{"linux", "arm", "baremetal"}, true},
19+
{"nintendoswitch", "linux", []string{"linux", "nintendoswitch"}, true},
20+
{"wasm_unknown", "linux", []string{"linux", "wasm_unknown"}, true},
21+
}
22+
for _, test := range tests {
23+
if got := needsTLSStubPackage(test.goos, test.buildTags); got != test.want {
24+
t.Errorf("%s: wanted %v, got %v", test.name, test.want, got)
25+
}
26+
}
27+
}

loader/loader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ func (p *Program) getOriginalPath(path string) string {
278278
originalPath = realgorootPath
279279
}
280280
maybeInTinyGoRoot := false
281-
for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags())) {
281+
for prefix := range pathsToOverride(p.config.GoMinorVersion, needsSyscallPackage(p.config.BuildTags()), needsTLSStubPackage(p.config.GOOS(), p.config.BuildTags())) {
282282
if runtime.GOOS == "windows" {
283283
prefix = strings.ReplaceAll(prefix, "/", "\\")
284284
}

main_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,19 @@ func TestTimerStopResetRace(t *testing.T) {
284284
runTest("timer_stop_reset_race.go", optionsFromTarget("", sema), t, nil, nil)
285285
}
286286

287+
// TestHostCryptoTLS checks that a hosted target gets the real crypto/tls and
288+
// not the stub, whose handshake does nothing. Only linux and macOS do.
289+
func TestHostCryptoTLS(t *testing.T) {
290+
t.Parallel()
291+
292+
switch runtime.GOOS {
293+
case "darwin", "linux":
294+
default:
295+
t.Skipf("host GOOS %s keeps the crypto/tls stub", runtime.GOOS)
296+
}
297+
runTest("hostcryptotls.go", optionsFromTarget("", sema), t, nil, nil)
298+
}
299+
287300
func TestESP32QEMU(t *testing.T) {
288301
t.Parallel()
289302

testdata/hostcryptotls.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package main
2+
3+
// A real TLS handshake over an in-memory pipe. The stub crypto/tls has a
4+
// handshake that does nothing, so it cannot pass this.
5+
6+
import (
7+
"crypto/ecdsa"
8+
"crypto/elliptic"
9+
"crypto/rand"
10+
"crypto/tls"
11+
"crypto/x509"
12+
"crypto/x509/pkix"
13+
"io"
14+
"math/big"
15+
"net"
16+
"time"
17+
)
18+
19+
func main() {
20+
cert, pool := selfSigned()
21+
22+
// A client that trusts the certificate completes the handshake and
23+
// exchanges data.
24+
client, server := net.Pipe()
25+
go serve(server, cert)
26+
conn := tls.Client(client, &tls.Config{RootCAs: pool, ServerName: "tinygo.test"})
27+
if err := conn.Handshake(); err != nil {
28+
println("handshake failed:", err.Error())
29+
return
30+
}
31+
if v := conn.ConnectionState().Version; v < tls.VersionTLS12 {
32+
println("negotiated an unexpected version:", v)
33+
return
34+
}
35+
if _, err := conn.Write([]byte("ping")); err != nil {
36+
println("write failed:", err.Error())
37+
return
38+
}
39+
buf := make([]byte, 4)
40+
if _, err := io.ReadFull(conn, buf); err != nil {
41+
println("read failed:", err.Error())
42+
return
43+
}
44+
println("got:", string(buf))
45+
conn.Close()
46+
47+
// A client that does not trust the certificate must refuse it.
48+
client, server = net.Pipe()
49+
go serve(server, cert)
50+
conn = tls.Client(client, &tls.Config{ServerName: "tinygo.test"})
51+
if err := conn.Handshake(); err == nil {
52+
println("an unknown certificate was accepted")
53+
return
54+
}
55+
conn.Close()
56+
println("unknown certificate refused")
57+
}
58+
59+
func serve(conn net.Conn, cert tls.Certificate) {
60+
server := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
61+
if err := server.Handshake(); err != nil {
62+
conn.Close()
63+
return
64+
}
65+
buf := make([]byte, 4)
66+
if _, err := io.ReadFull(server, buf); err != nil {
67+
server.Close()
68+
return
69+
}
70+
server.Write([]byte("pong"))
71+
}
72+
73+
func selfSigned() (tls.Certificate, *x509.CertPool) {
74+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
75+
if err != nil {
76+
panic(err)
77+
}
78+
template := &x509.Certificate{
79+
SerialNumber: big.NewInt(1),
80+
Subject: pkix.Name{CommonName: "tinygo.test"},
81+
DNSNames: []string{"tinygo.test"},
82+
NotBefore: time.Now().Add(-time.Hour),
83+
NotAfter: time.Now().Add(time.Hour),
84+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
85+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
86+
BasicConstraintsValid: true,
87+
IsCA: true,
88+
}
89+
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
90+
if err != nil {
91+
panic(err)
92+
}
93+
leaf, err := x509.ParseCertificate(der)
94+
if err != nil {
95+
panic(err)
96+
}
97+
pool := x509.NewCertPool()
98+
pool.AddCert(leaf)
99+
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}, pool
100+
}

testdata/hostcryptotls.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
got: pong
2+
unknown certificate refused

0 commit comments

Comments
 (0)