From 3dd1c268b0650e9fcdd2e08ac82601bf962077b9 Mon Sep 17 00:00:00 2001 From: Yage Hu Date: Mon, 1 Jun 2026 16:11:54 +0000 Subject: [PATCH] Make guest-agent buildable on FreeBSD This commit, when paired with corresponding dependency patches, makes guest-agent buildable on FreeBSD. The plan is to incrementally fix guest-agent's behavior on FreeBSD. Because this commit only introduces files with the `_freebsd` suffix, it does not impact the current behavior on Linux or Windows. The list of required dependency patches: - https://github.com/google/go-sev-guest/pull/189 - https://github.com/google/go-tdx-guest/pull/101 - https://github.com/tarm/serial/pull/134 --- .../agentcrypto/mtls_mds_freebsd.go | 13 ++ .../agentcrypto/mtls_mds_linux.go | 107 ------------- .../agentcrypto/mtls_mds_unix.go | 127 ++++++++++++++++ google_guest_agent/command/command_freebsd.go | 18 +++ google_guest_agent/command/command_linux.go | 124 --------------- google_guest_agent/command/command_unix.go | 141 ++++++++++++++++++ ...rustedca_linux.go => sshtrustedca_unix.go} | 4 +- 7 files changed, 302 insertions(+), 232 deletions(-) create mode 100644 google_guest_agent/agentcrypto/mtls_mds_freebsd.go create mode 100644 google_guest_agent/agentcrypto/mtls_mds_unix.go create mode 100644 google_guest_agent/command/command_freebsd.go create mode 100644 google_guest_agent/command/command_unix.go rename google_guest_agent/events/sshtrustedca/{sshtrustedca_linux.go => sshtrustedca_unix.go} (98%) diff --git a/google_guest_agent/agentcrypto/mtls_mds_freebsd.go b/google_guest_agent/agentcrypto/mtls_mds_freebsd.go new file mode 100644 index 00000000..3ad88b28 --- /dev/null +++ b/google_guest_agent/agentcrypto/mtls_mds_freebsd.go @@ -0,0 +1,13 @@ +package agentcrypto + +const ( + // defaultCredsDir is the directory location for MTLS MDS credentials. + defaultCredsDir = "/var/run/google-mds-mtls" +) + +var ( + // certUpdaters is a map of known CA certificate updaters with the local directory paths for certificates. + certUpdaters = map[string][]string{ + "certctl": {"/usr/local/share/certs"}, + } +) diff --git a/google_guest_agent/agentcrypto/mtls_mds_linux.go b/google_guest_agent/agentcrypto/mtls_mds_linux.go index 1c461a4a..bfe479e5 100644 --- a/google_guest_agent/agentcrypto/mtls_mds_linux.go +++ b/google_guest_agent/agentcrypto/mtls_mds_linux.go @@ -14,26 +14,9 @@ package agentcrypto -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run" - "github.com/GoogleCloudPlatform/guest-agent/utils" - "github.com/GoogleCloudPlatform/guest-logging-go/logger" -) - const ( // defaultCredsDir is the directory location for MTLS MDS credentials. defaultCredsDir = "/run/google-mds-mtls" - // rootCACertFileName is the root CA cert. - rootCACertFileName = "root.crt" - // clientCredsFileName are client credentials, its basically the file - // that has the EC private key and the client certificate concatenated. - clientCredsFileName = "client.key" ) var ( @@ -48,93 +31,3 @@ var ( "update-ca-trust": {"/etc/pki/ca-trust/source/anchors"}, } ) - -// writeRootCACert writes Root CA cert from UEFI variable to output file. -func (j *CredsJob) writeRootCACert(ctx context.Context, content []byte, outputFile string) error { - // The directory should be executable, but the file does not need to be. - if err := os.MkdirAll(filepath.Dir(outputFile), 0655); err != nil { - return err - } - if err := utils.SaferWriteFile(content, outputFile, 0644); err != nil { - return err - } - - if !j.useNativeStore.Load() { - logger.Debugf("SkipNativeStore is enabled, will not write root cert to system store") - return nil - } - - // Best effort to update system store, don't fail. - if err := updateSystemStore(ctx, outputFile); err != nil { - logger.Errorf("Failed add Root MDS cert to system trust store with error: %v", err) - } - - return nil -} - -// writeClientCredentials stores client credentials (certificate and private key). -func (j *CredsJob) writeClientCredentials(plaintext []byte, outputFile string) error { - // The directory should be executable, but the file does not need to be. - if err := os.MkdirAll(filepath.Dir(outputFile), 0655); err != nil { - return err - } - return utils.SaferWriteFile(plaintext, outputFile, 0644) -} - -// getCAStoreUpdater interates over known system trust store updaters and returns the first found. -func getCAStoreUpdater() (string, error) { - var errs []string - - for u := range certUpdaters { - _, err := exec.LookPath(u) - if err == nil { - return u, nil - } - errs = append(errs, fmt.Sprintf("lookup for %q failed with error: %v", u, err)) - } - - return "", fmt.Errorf("no known trust updaters were found: %v", errs) -} - -// certificateDirFromUpdater returns directory of local CA certificates for the given updater tool. -func certificateDirFromUpdater(updater string) (string, error) { - dirs, ok := certUpdaters[updater] - if !ok { - return "", fmt.Errorf("unknown updater %q, no local trusted CA certificate directory found", updater) - } - - for _, dir := range dirs { - fi, err := os.Stat(dir) - if err == nil && fi.IsDir() { - return dir, nil - } - } - return "", fmt.Errorf("no of the known directories %v found for updater %q", dirs, updater) -} - -// updateSystemStore updates the local system store with the cert. -func updateSystemStore(ctx context.Context, cert string) error { - cmd, err := getCAStoreUpdater() - if err != nil { - return err - } - - dir, err := certificateDirFromUpdater(cmd) - if err != nil { - return err - } - - dest := filepath.Join(dir, filepath.Base(cert)) - - if err := utils.CopyFile(cert, dest, 0644); err != nil { - return err - } - - res := run.WithOutput(ctx, cmd) - if res.ExitCode != 0 { - return fmt.Errorf("command %q failed with error: %s", cmd, res.Error()) - } - - logger.Infof("Certificate %q added to system store successfully %s", cert, res.StdOut) - return nil -} diff --git a/google_guest_agent/agentcrypto/mtls_mds_unix.go b/google_guest_agent/agentcrypto/mtls_mds_unix.go new file mode 100644 index 00000000..32e2631d --- /dev/null +++ b/google_guest_agent/agentcrypto/mtls_mds_unix.go @@ -0,0 +1,127 @@ +//go:build linux || freebsd + +// Copyright 2023 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agentcrypto + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run" + "github.com/GoogleCloudPlatform/guest-agent/utils" + "github.com/GoogleCloudPlatform/guest-logging-go/logger" +) + +const ( + // rootCACertFileName is the root CA cert. + rootCACertFileName = "root.crt" + // clientCredsFileName are client credentials, its basically the file + // that has the EC private key and the client certificate concatenated. + clientCredsFileName = "client.key" +) + +// writeRootCACert writes Root CA cert from UEFI variable to output file. +func (j *CredsJob) writeRootCACert(ctx context.Context, content []byte, outputFile string) error { + // The directory should be executable, but the file does not need to be. + if err := os.MkdirAll(filepath.Dir(outputFile), 0655); err != nil { + return err + } + if err := utils.SaferWriteFile(content, outputFile, 0644); err != nil { + return err + } + + if !j.useNativeStore.Load() { + logger.Debugf("SkipNativeStore is enabled, will not write root cert to system store") + return nil + } + + // Best effort to update system store, don't fail. + if err := updateSystemStore(ctx, outputFile); err != nil { + logger.Errorf("Failed add Root MDS cert to system trust store with error: %v", err) + } + + return nil +} + +// writeClientCredentials stores client credentials (certificate and private key). +func (j *CredsJob) writeClientCredentials(plaintext []byte, outputFile string) error { + // The directory should be executable, but the file does not need to be. + if err := os.MkdirAll(filepath.Dir(outputFile), 0655); err != nil { + return err + } + return utils.SaferWriteFile(plaintext, outputFile, 0644) +} + +// getCAStoreUpdater interates over known system trust store updaters and returns the first found. +func getCAStoreUpdater() (string, error) { + var errs []string + + for u := range certUpdaters { + _, err := exec.LookPath(u) + if err == nil { + return u, nil + } + errs = append(errs, fmt.Sprintf("lookup for %q failed with error: %v", u, err)) + } + + return "", fmt.Errorf("no known trust updaters were found: %v", errs) +} + +// certificateDirFromUpdater returns directory of local CA certificates for the given updater tool. +func certificateDirFromUpdater(updater string) (string, error) { + dirs, ok := certUpdaters[updater] + if !ok { + return "", fmt.Errorf("unknown updater %q, no local trusted CA certificate directory found", updater) + } + + for _, dir := range dirs { + fi, err := os.Stat(dir) + if err == nil && fi.IsDir() { + return dir, nil + } + } + return "", fmt.Errorf("no of the known directories %v found for updater %q", dirs, updater) +} + +// updateSystemStore updates the local system store with the cert. +func updateSystemStore(ctx context.Context, cert string) error { + cmd, err := getCAStoreUpdater() + if err != nil { + return err + } + + dir, err := certificateDirFromUpdater(cmd) + if err != nil { + return err + } + + dest := filepath.Join(dir, filepath.Base(cert)) + + if err := utils.CopyFile(cert, dest, 0644); err != nil { + return err + } + + res := run.WithOutput(ctx, cmd) + if res.ExitCode != 0 { + return fmt.Errorf("command %q failed with error: %s", cmd, res.Error()) + } + + logger.Infof("Certificate %q added to system store successfully %s", cert, res.StdOut) + return nil +} diff --git a/google_guest_agent/command/command_freebsd.go b/google_guest_agent/command/command_freebsd.go new file mode 100644 index 00000000..a2224b47 --- /dev/null +++ b/google_guest_agent/command/command_freebsd.go @@ -0,0 +1,18 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +// DefaultPipePath is the default unix socket path for FreeBSD. +const DefaultPipePath = "/var/run/google-guest-agent/commands.sock" diff --git a/google_guest_agent/command/command_linux.go b/google_guest_agent/command/command_linux.go index cba895c0..feb33a99 100644 --- a/google_guest_agent/command/command_linux.go +++ b/google_guest_agent/command/command_linux.go @@ -14,129 +14,5 @@ package command -import ( - "context" - "fmt" - "net" - "os" - "os/user" - "path" - "runtime" - "strconv" - "syscall" - - "github.com/GoogleCloudPlatform/guest-logging-go/logger" -) - // DefaultPipePath is the default unix socket path for linux. const DefaultPipePath = "/run/google-guest-agent/commands.sock" - -func mkdirpWithPerms(dir string, p os.FileMode, uid, gid int) error { - parent := path.Dir(dir) - stat, err := os.Stat(dir) - if err == nil { - if parent != "/" && parent != "" { - statT, ok := stat.Sys().(*syscall.Stat_t) - if !ok { - return fmt.Errorf("could not determine owner of %s", dir) - } - if !stat.IsDir() { - return fmt.Errorf("%s exists and is not a directory", dir) - } - if morePermissive(int(stat.Mode()), int(p)) { - if err := os.Chmod(dir, p); err != nil { - return fmt.Errorf("could not correct %s permissions to %d: %v", dir, p, err) - } - } - if statT.Uid != 0 && statT.Uid != uint32(uid) { - if err := os.Chown(dir, uid, -1); err != nil { - return fmt.Errorf("could not correct %s owner to %d: %v", dir, uid, err) - } - } - if statT.Gid != 0 && statT.Gid != uint32(gid) { - if err := os.Chown(dir, -1, gid); err != nil { - return fmt.Errorf("could not correct %s group to %d: %v", dir, gid, err) - } - } - } - } else { - if parent != "/" && parent != "" { - if err := mkdirpWithPerms(parent, p, uid, gid); err != nil { - return err - } - } - if err := os.Mkdir(dir, p); err != nil { - return err - } - } - return nil -} - -func morePermissive(i, j int) bool { - for k := 0; k < 3; k++ { - if (i % 010) > (j % 10) { - return true - } - i = i / 010 - j = j / 010 - } - return false -} - -func listen(ctx context.Context, pipe string, filemode int, grp string) (net.Listener, error) { - // If grp is an int, use it as a GID - gid, err := strconv.Atoi(grp) - if err != nil { - // Otherwise lookup GID - group, err := user.LookupGroup(grp) - if err != nil { - logger.Errorf("guest agent command pipe group %s is not a GID nor a valid group, not changing socket ownership", grp) - gid = -1 - } else { - gid, err = strconv.Atoi(group.Gid) - if err != nil { - logger.Errorf("os reported group %s has gid %s which is not a valid int, not changing socket ownership. this should never happen", grp, group.Gid) - gid = -1 - } - } - } - // socket owner group does not need to have permissions to everything in the directory containing it, whatever user and group we are should own that - user, err := user.Current() - if err != nil { - return nil, fmt.Errorf("could not lookup current user") - } - currentuid, err := strconv.Atoi(user.Uid) - if err != nil { - return nil, fmt.Errorf("os reported user %s has uid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Uid) - } - currentgid, err := strconv.Atoi(user.Gid) - if err != nil { - return nil, fmt.Errorf("os reported user %s has gid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Gid) - } - if err := mkdirpWithPerms(path.Dir(pipe), os.FileMode(filemode), currentuid, currentgid); err != nil { - return nil, err - } - // Mutating the umask of the process for this is not ideal, but tightening permissions with chown after creation is not really secure. - // Lock OS thread while mutating umask so we don't lose a thread with a mutated mask. - runtime.LockOSThread() - oldmask := syscall.Umask(0777 - filemode) - var lc net.ListenConfig - l, err := lc.Listen(ctx, "unix", pipe) - syscall.Umask(oldmask) - runtime.UnlockOSThread() - if err != nil { - return nil, err - } - // But we need to chown anyway to loosen permissions to include whatever group the user has configured - err = os.Chown(pipe, int(currentuid), gid) - if err != nil { - l.Close() - return nil, err - } - return l, nil -} - -func dialPipe(ctx context.Context, pipe string) (net.Conn, error) { - var dialer net.Dialer - return dialer.DialContext(ctx, "unix", pipe) -} diff --git a/google_guest_agent/command/command_unix.go b/google_guest_agent/command/command_unix.go new file mode 100644 index 00000000..6b2d1b8e --- /dev/null +++ b/google_guest_agent/command/command_unix.go @@ -0,0 +1,141 @@ +//go:build linux || freebsd + +// Copyright 2026 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "fmt" + "net" + "os" + "os/user" + "path" + "runtime" + "strconv" + "syscall" + + "github.com/GoogleCloudPlatform/guest-logging-go/logger" +) + +func mkdirpWithPerms(dir string, p os.FileMode, uid, gid int) error { + parent := path.Dir(dir) + stat, err := os.Stat(dir) + if err == nil { + if parent != "/" && parent != "" { + statT, ok := stat.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("could not determine owner of %s", dir) + } + if !stat.IsDir() { + return fmt.Errorf("%s exists and is not a directory", dir) + } + if morePermissive(int(stat.Mode()), int(p)) { + if err := os.Chmod(dir, p); err != nil { + return fmt.Errorf("could not correct %s permissions to %d: %v", dir, p, err) + } + } + if statT.Uid != 0 && statT.Uid != uint32(uid) { + if err := os.Chown(dir, uid, -1); err != nil { + return fmt.Errorf("could not correct %s owner to %d: %v", dir, uid, err) + } + } + if statT.Gid != 0 && statT.Gid != uint32(gid) { + if err := os.Chown(dir, -1, gid); err != nil { + return fmt.Errorf("could not correct %s group to %d: %v", dir, gid, err) + } + } + } + } else { + if parent != "/" && parent != "" { + if err := mkdirpWithPerms(parent, p, uid, gid); err != nil { + return err + } + } + if err := os.Mkdir(dir, p); err != nil { + return err + } + } + return nil +} + +func morePermissive(i, j int) bool { + for k := 0; k < 3; k++ { + if (i % 010) > (j % 10) { + return true + } + i = i / 010 + j = j / 010 + } + return false +} + +func listen(ctx context.Context, pipe string, filemode int, grp string) (net.Listener, error) { + // If grp is an int, use it as a GID + gid, err := strconv.Atoi(grp) + if err != nil { + // Otherwise lookup GID + group, err := user.LookupGroup(grp) + if err != nil { + logger.Errorf("guest agent command pipe group %s is not a GID nor a valid group, not changing socket ownership", grp) + gid = -1 + } else { + gid, err = strconv.Atoi(group.Gid) + if err != nil { + logger.Errorf("os reported group %s has gid %s which is not a valid int, not changing socket ownership. this should never happen", grp, group.Gid) + gid = -1 + } + } + } + // socket owner group does not need to have permissions to everything in the directory containing it, whatever user and group we are should own that + user, err := user.Current() + if err != nil { + return nil, fmt.Errorf("could not lookup current user") + } + currentuid, err := strconv.Atoi(user.Uid) + if err != nil { + return nil, fmt.Errorf("os reported user %s has uid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Uid) + } + currentgid, err := strconv.Atoi(user.Gid) + if err != nil { + return nil, fmt.Errorf("os reported user %s has gid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Gid) + } + if err := mkdirpWithPerms(path.Dir(pipe), os.FileMode(filemode), currentuid, currentgid); err != nil { + return nil, err + } + // Mutating the umask of the process for this is not ideal, but tightening permissions with chown after creation is not really secure. + // Lock OS thread while mutating umask so we don't lose a thread with a mutated mask. + runtime.LockOSThread() + oldmask := syscall.Umask(0777 - filemode) + var lc net.ListenConfig + l, err := lc.Listen(ctx, "unix", pipe) + syscall.Umask(oldmask) + runtime.UnlockOSThread() + if err != nil { + return nil, err + } + // But we need to chown anyway to loosen permissions to include whatever group the user has configured + err = os.Chown(pipe, int(currentuid), gid) + if err != nil { + l.Close() + return nil, err + } + return l, nil +} + +func dialPipe(ctx context.Context, pipe string) (net.Conn, error) { + var dialer net.Dialer + return dialer.DialContext(ctx, "unix", pipe) +} diff --git a/google_guest_agent/events/sshtrustedca/sshtrustedca_linux.go b/google_guest_agent/events/sshtrustedca/sshtrustedca_unix.go similarity index 98% rename from google_guest_agent/events/sshtrustedca/sshtrustedca_linux.go rename to google_guest_agent/events/sshtrustedca/sshtrustedca_unix.go index f2ac7d0a..ede7f02b 100644 --- a/google_guest_agent/events/sshtrustedca/sshtrustedca_linux.go +++ b/google_guest_agent/events/sshtrustedca/sshtrustedca_unix.go @@ -1,4 +1,6 @@ -// Copyright 2023 Google LLC +//go:build linux || freebsd + +// Copyright 2026 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.