blob: 07d93d3213351d4ead8a83c6c4c3bc26d93acf94 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package security
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"github.com/godbus/dbus"
"chromiumos/tast/common/testexec"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/dbusutil"
"chromiumos/tast/testing"
)
const (
cronistaName = "cronista"
cronistaUser = "cronista"
trichechusName = "trichechus"
dugongName = "dugong"
dugongUser = "dugong"
minijailPath = "/sbin/minijail0"
)
type manateeTestCase struct {
useSystemServices bool
}
func init() {
testing.AddTest(&testing.Test{
Func: Manatee,
Desc: "Checks basic functionality of core ManaTEE features",
Contacts: []string{
"allenwebb@chromium.org",
"cros-manatee@google.com",
},
Attr: []string{"group:mainline", "informational"},
Params: []testing.Param{
{
Name: "real",
Val: manateeTestCase{useSystemServices: true},
ExtraSoftwareDeps: []string{"manatee"},
ExtraAttr: []string{"informational"},
},
{
Name: "fake",
Val: manateeTestCase{useSystemServices: false},
ExtraSoftwareDeps: []string{"sirenia"},
},
},
})
}
func stopCmd(cmd *testexec.Cmd) error {
// SIGKILL (sent by Cmd.Kill()) does not allow cleanup hooks to run. Upstart uses SIGTERM to notify daemons when
// their job is being stopped, so it is used here.
if err := cmd.Signal(syscall.SIGTERM); err != nil {
return err
}
// Signal would have failed above if the process already exited.
err := cmd.Wait()
status, ok := testexec.GetWaitStatus(err)
// Handle the case the process catches the signal and returns 0.
if ok {
return nil
}
// Handle the case the process didn't catch the signal.
if status.Signaled() && status.Signal() == syscall.SIGTERM {
return nil
}
return err
}
// sireniaServices provides an interface to ManaTEE through Sirenia. It also handles bring-up and tear-down of a test
// sirenia environment on non-ManaTEE images.
type sireniaServices struct {
cronista *testexec.Cmd
cronistaStderr *bufio.Reader
trichechus *testexec.Cmd
trichechusStderr *bufio.Reader
dugong *testexec.Cmd
dugongStderr *bufio.Reader
}
// newSireniaServices constructs the an instance of sireniaServices.
func newSireniaServices() *sireniaServices {
return &sireniaServices{}
}
// Start brings up a test environment. This is needed on non-ManaTEE images with sirenia.
func (z *sireniaServices) Start(ctx context.Context) (err error) {
if z.cronista != nil || z.trichechus != nil || z.dugong != nil {
return errors.New("already initialized; only call start once")
}
cronistaPath, err := exec.LookPath(cronistaName)
if err != nil {
return errors.Wrap(err, "cannot find cronista")
}
trichechusPath, err := exec.LookPath(trichechusName)
if err != nil {
return errors.Wrap(err, "cannot find trichechus")
}
dugongPath, err := exec.LookPath(dugongName)
if err != nil {
return errors.Wrap(err, "cannot find dugong")
}
z.cronista = testexec.CommandContext(ctx, minijailPath, "-u", cronistaUser, "--", cronistaPath, "-U", "ip://127.0.0.1:0")
stderr, err := z.cronista.StderrPipe()
if err != nil {
return errors.Wrap(err, "failed to get cronista stderr")
}
z.cronistaStderr = bufio.NewReader(stderr)
err = z.cronista.Start()
if err != nil {
return errors.Wrap(err, "failed to start cronista")
}
defer func() {
if err == nil {
return
}
if err2 := z.stopCronista(); err2 != nil {
testing.ContextLog(ctx, "Failed to stop cronista: ", err2)
}
}()
line, err := z.cronistaStderr.ReadString('\n')
if err != nil {
return errors.Wrap(err, "failed initial read from cronista stderr")
}
if strings.HasSuffix(line, "starting cronista\n") {
if line, err = z.cronistaStderr.ReadString('\n'); err != nil {
return errors.Wrapf(err, "failed to read from cronista stderr; last line: %q", strings.TrimSpace(line))
}
// Do not fail here since it is ok if the lines to skip aren't printed.
}
if !strings.Contains(line, "waiting for connection at: ip://127.0.0.1:") {
return errors.Errorf("failed to locate listening URI; last line: %q", line)
}
cronistaPort, err := strconv.Atoi(line[strings.LastIndexByte(line, ':')+1 : len(line)-1])
if err != nil {
return errors.Wrapf(err, "failed to parse cronista port from line: %q", line)
}
z.trichechus = testexec.CommandContext(ctx, trichechusPath, "-U", "ip://127.0.0.1:0", "-C", fmt.Sprintf("ip://127.0.0.1:%d", cronistaPort))
stderr, err = z.trichechus.StderrPipe()
if err != nil {
return errors.Wrap(err, "failed to get trichechus stderr")
}
z.trichechusStderr = bufio.NewReader(stderr)
err = z.trichechus.Start()
if err != nil {
return errors.Wrap(err, "failed to start trichechus")
}
defer func() {
if err == nil {
return
}
if err2 := z.stopTrichechus(); err2 != nil {
testing.ContextLog(ctx, "Failed to stop trichechus: ", err2)
}
}()
line, err = z.trichechusStderr.ReadString('\n')
if err != nil {
return errors.Wrap(err, "failed initial read from trichechus stderr")
}
// Skip expected lines only so error messages are caught.
for _, condition := range []func(string) bool{
func(l string) bool {
return l == "Syslog exists.\n" || l == "Creating syslog.\n"
},
func(l string) bool {
return strings.Contains(l, "starting trichechus:")
},
func(l string) bool {
return strings.Contains(l, "Unable to start new process group:")
},
} {
if condition(line) {
if line, err = z.trichechusStderr.ReadString('\n'); err != nil {
return errors.Wrapf(err, "failed to read from trichechus stderr; last line: %q", strings.TrimSpace(line))
}
}
// Do not fail here since it is ok if the lines to skip aren't printed.
}
if !strings.Contains(line, "waiting for connection at: ip://127.0.0.1:") {
return errors.Errorf("failed to locate listening URI; last line: %q", line)
}
trichechusPort, err := strconv.Atoi(line[strings.LastIndexByte(line, ':')+1 : len(line)-1])
if err != nil {
return errors.Wrapf(err, "failed to parse trichechus port from line: %q", line)
}
z.dugong = testexec.CommandContext(ctx, minijailPath, "-u", dugongUser, "--", dugongPath, "-U", fmt.Sprintf("ip://127.0.0.1:%d", trichechusPort))
stderr, err = z.dugong.StderrPipe()
if err != nil {
return errors.Wrap(err, "failed to get dugong stderr")
}
z.dugongStderr = bufio.NewReader(stderr)
err = z.dugong.Start()
if err != nil {
return errors.Wrap(err, "failed to start dugong")
}
defer func() {
if err == nil {
return
}
if err2 := z.stopDugong(); err2 != nil {
testing.ContextLog(ctx, "Failed to stop dugong: ", err2)
}
}()
line, err = z.dugongStderr.ReadString('\n')
if err != nil {
return errors.Wrap(err, "failed initial read from dugong stderr")
}
// Skip expected lines only so error messages are caught.
for _, condition := range []func(string) bool{
func(l string) bool {
return strings.Contains(l, "Starting dugong:")
},
func(l string) bool {
return strings.HasSuffix(l, "Opening connection to trichechus\n")
},
func(l string) bool {
return strings.HasSuffix(l, "Starting rpc\n")
},
} {
if condition(line) {
if line, err = z.dugongStderr.ReadString('\n'); err != nil {
return errors.Wrapf(err, "failed to read from dugong stderr; last line: %q", strings.TrimSpace(line))
}
}
// Do not fail here since it is ok if the lines to skip aren't printed.
}
if !strings.HasSuffix(line, "Finished dbus setup, starting handler.\n") {
return errors.Wrapf(err, "dugong failed to setup D-Bus; last line: %q", line)
}
return nil
}
func (z *sireniaServices) stopCronista() error {
if z.cronista == nil {
return errors.New("cronista not initialized; call start before stop")
}
return stopCmd(z.cronista)
}
func (z *sireniaServices) stopTrichechus() error {
if z.trichechus == nil {
return errors.New("trichechus not initialized; call start before stop")
}
return stopCmd(z.trichechus)
}
func (z *sireniaServices) stopDugong() error {
if z.dugong == nil {
return errors.New("dugong not initialized; call start before stop")
}
return stopCmd(z.dugong)
}
// Stop tears down the test environment. Start() must be called first.
func (z *sireniaServices) Stop(ctx context.Context) (retErr error) {
// The services are stopped in the reverse order they are started to avoid triggering errors from connection drops.
// Strictly speaking trichechus can run without cronista, but depends on cronista for its storage API, so cronista is
// started first. Dugong strictly depends on trichechus so stopping trichechus first might cause dugong to exit with
// an error. Thus, dugong is stopped first, followed by trichechus, and cronista is stopped last.
if retErr = z.stopDugong(); retErr != nil {
testing.ContextLog(ctx, "Failed to stop dugong: ", retErr)
}
if err := z.stopTrichechus(); err != nil {
testing.ContextLog(ctx, "Failed to stop trichechus: ", err)
retErr = err
}
if err := z.stopCronista(); err != nil {
testing.ContextLog(ctx, "Failed to stop cronista: ", err)
retErr = err
}
return retErr
}
// Manatee implements the security.Manatee test.
func Manatee(ctx context.Context, s *testing.State) {
testCase := s.Param().(manateeTestCase)
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
// Set up services if necessary.
d := newSireniaServices()
if !testCase.useSystemServices {
if err := d.Start(ctx); err != nil {
s.Fatal("Failed to start sirenia services: ", err)
}
defer func() {
if err := d.Stop(cleanupCtx); err != nil {
s.Error("Failed to stop sirenia services: ", err)
}
}()
}
// D-Bus constants for use with dugong / ManaTEE.
const (
dbusName = "org.chromium.ManaTEE"
dbusPath = "/org/chromium/ManaTEE1"
dbusInterface = "org.chromium.ManaTEEInterface"
dbusMethodStartTEEApplication = "StartTEEApplication"
// Run test with the demo app that will read in a value, store
// it, then write it back.
testTEEAppID = "demo_app"
testValue = "Store Test Value\n"
)
// Set up and validate D-Bus connection.
conn, obj, err := dbusutil.Connect(ctx, dbusName, dbus.ObjectPath(dbusPath))
if err != nil {
s.Fatalf("Failed to connect to %s: %v", dbusName, err)
}
if conn.SupportsUnixFDs() != true {
s.Fatal("Connection needs Unix FD support: ", err)
}
// Invoke ManaTEE D-Bus API through dugong.
s.Log("Starting test TEE app")
var errorCode int32
var fdIn dbus.UnixFD
var fdOut dbus.UnixFD
if err := obj.CallWithContext(ctx, dbusInterface+"."+dbusMethodStartTEEApplication, 0, testTEEAppID, []string{}).Store(&errorCode, &fdIn, &fdOut); err != nil {
s.Fatal("Failed to start TEE app: ", err)
}
if errorCode != 0 {
s.Fatalf("Unexpected return code: got %d; expected 0", errorCode)
}
// Communicate over the file descriptors.
fileIn := os.NewFile(uintptr(fdIn), "TEE App In")
defer func() {
if err := fileIn.Close(); err != nil {
s.Error("Failed to close input handle: ", err)
}
}()
fileOut := os.NewFile(uintptr(fdOut), "TEE App Out")
defer func() {
if err := fileOut.Close(); err != nil {
s.Error("Failed to close output handle: ", err)
}
}()
s.Log("Writing string to app")
if _, err := fileOut.WriteString(testValue); err != nil {
s.Error("Failed to send data to TEE App: ", err)
}
buffer := new(bytes.Buffer)
if _, err = buffer.ReadFrom(fileIn); err != nil && err != io.EOF {
s.Fatal("Failed to read data from TEE App: ", err)
}
response := buffer.String()
if response != testValue {
s.Errorf("Got %q; expected %q", response, testValue)
}
}