blob: cc23b53386454b609a3fafe59e191d8684a4b4d6 [file] [log] [blame]
// Copyright 2019 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 hwsec
/*
This file implements miscellaneous and unsorted helpers.
*/
import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/golang/protobuf/proto"
tmpb "chromiumos/system_api/tpm_manager_proto"
"chromiumos/tast/errors"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
// CmdHelper provides various helper functions that could be shared across all
// hwsec integration test base on CmdRunner.
type CmdHelper struct {
cmdRunner CmdRunner
cryptohome *CryptohomeClient
tpmManager *TPMManagerClient
daemonController *DaemonController
}
// AttestationHelper provides various helper functions that could be shared across all
// hwsec integration test base on AttestationClient.
type AttestationHelper struct {
attestation *AttestationClient
}
// CmdTPMClearHelper provides various helper functions that could be shared across all
// hwsec integration test base on CmdHelper & TPMClearer.
type CmdTPMClearHelper struct {
CmdHelper
tpmClearer TPMClearer
}
// FullHelper is the full version of all kinds of helper that could be shared across all
// hwsec integration test regardless of run-type, i.e., remote or local.
type FullHelper struct {
CmdTPMClearHelper
AttestationHelper
}
// NewCmdHelper creates a new CmdHelper, with r responsible for CmdRunner.
func NewCmdHelper(r CmdRunner) *CmdHelper {
return &CmdHelper{
cmdRunner: r,
cryptohome: NewCryptohomeClient(r),
tpmManager: NewTPMManagerClient(r),
daemonController: NewDaemonController(r),
}
}
// NewAttestationHelper creates a new AttestationHelper, with ac responsible for AttestationDBus.
func NewAttestationHelper(ac AttestationDBus) *AttestationHelper {
return &AttestationHelper{
attestation: NewAttestationClient(ac),
}
}
// NewCmdTPMClearHelper creates a new CmdTPMClearHelper, with ch responsible for CmdHelper and th responsible for TPMClearer.
func NewCmdTPMClearHelper(ch *CmdHelper, tc TPMClearer) *CmdTPMClearHelper {
return &CmdTPMClearHelper{*ch, tc}
}
// NewFullHelper creates a new FullHelper, with ch responsible for CmdTPMClearHelper and ah responsible for AttestationHelper.
func NewFullHelper(ch *CmdTPMClearHelper, ah *AttestationHelper) *FullHelper {
return &FullHelper{*ch, *ah}
}
// CmdRunner exposes the cmdRunner of helper
func (h *CmdHelper) CmdRunner() CmdRunner { return h.cmdRunner }
// CryptohomeClient exposes the cryptohome of helper
func (h *CmdHelper) CryptohomeClient() *CryptohomeClient { return h.cryptohome }
// TPMManagerClient exposes the tpmManager of helper
func (h *CmdHelper) TPMManagerClient() *TPMManagerClient { return h.tpmManager }
// DaemonController exposes the daemonController of helper
func (h *CmdHelper) DaemonController() *DaemonController { return h.daemonController }
// AttestationClient exposes the attestation of helper
func (h *AttestationHelper) AttestationClient() *AttestationClient { return h.attestation }
// TPMClearer exposes the tpmClearer of helper
func (h *CmdTPMClearHelper) TPMClearer() TPMClearer { return h.tpmClearer }
// EnsureTPMIsReady ensures the TPM is ready when the function returns |nil|.
// Otherwise, returns any encountered error.
func (h *CmdHelper) EnsureTPMIsReady(ctx context.Context, timeout time.Duration) error {
info, err := h.tpmManager.GetNonsensitiveStatus(ctx)
if err != nil {
return errors.Wrap(err, "failed to ensure ownership due to error in |GetNonsensitiveStatus|")
}
if !info.IsOwned {
if _, err := h.tpmManager.TakeOwnership(ctx); err != nil {
return errors.Wrap(err, "failed to ensure ownership due to error in |TakeOwnership|")
}
}
return testing.Poll(ctx, func(context.Context) error {
info, err := h.tpmManager.GetNonsensitiveStatus(ctx)
if err != nil {
return errors.New("error during checking TPM readiness")
}
if info.IsOwned {
return nil
}
return errors.New("haven't confirmed to be owned")
}, &testing.PollOptions{
Timeout: timeout,
Interval: PollingInterval,
})
}
// EnsureIsPreparedForEnrollment ensures the DUT is prepareed for enrollment
// when the function returns |nil|. Otherwise, returns any encountered error.
func (h *AttestationHelper) EnsureIsPreparedForEnrollment(ctx context.Context, timeout time.Duration) error {
return testing.Poll(ctx, func(context.Context) error {
// intentionally ignores error; retry the operation until timeout.
isPrepared, err := h.attestation.IsPreparedForEnrollment(ctx)
if err != nil {
return err
}
if !isPrepared {
return errors.New("not prepared yet")
}
return nil
}, &testing.PollOptions{
Timeout: timeout,
Interval: PollingInterval,
})
}
// RemoveFile would delete the file
func (h *CmdHelper) RemoveFile(ctx context.Context, filename string) error {
_, err := h.cmdRunner.Run(ctx, "rm", "-f", "--", filename)
return err
}
// ReadFile would read data from the file
func (h *CmdHelper) ReadFile(ctx context.Context, filename string) ([]byte, error) {
return h.cmdRunner.Run(ctx, "cat", "--", filename)
}
// WriteFile would write data into the file
func (h *CmdHelper) WriteFile(ctx context.Context, filename string, data []byte) error {
// Because we may pass NULL('\0') byte in data, using base64 to encode the data could resolve the escape character issue.
// Using "echo" or "printf" would require a complex escaping rule to let it work correctly.
b64String := base64.StdEncoding.EncodeToString(data)
echoStrCmd := fmt.Sprintf("echo %s", shutil.Escape(b64String))
b64DecCmd := fmt.Sprintf("base64 -d > %s", shutil.Escape(filename))
cmd := fmt.Sprintf("%s | %s", echoStrCmd, b64DecCmd)
if _, err := h.cmdRunner.Run(ctx, "sh", "-c", cmd); err != nil {
return errors.Wrap(err, "failed to echo string")
}
return nil
}
// GetTPMManagerLocalData would read the tpm_manager local_tpm_data.
// Note: Get the data without stopping tpm_managerd may result stale data.
func (h *CmdHelper) GetTPMManagerLocalData(ctx context.Context) ([]byte, error) {
return h.ReadFile(ctx, "/var/lib/tpm_manager/local_tpm_data")
}
// SetTPMManagerLocalData would write the local_tpm_data.
// Because tpm_managerd may cache the local data in the memory, we would need to restart tpm_managerd after modifying the data.
func (h *CmdHelper) SetTPMManagerLocalData(ctx context.Context, data []byte) error {
return h.WriteFile(ctx, "/var/lib/tpm_manager/local_tpm_data", data)
}
// DropResetLockPermissions drops the reset lock permissions and return a callback to restore the permissions.
func (h *CmdHelper) DropResetLockPermissions(ctx context.Context) (restoreFunc func(ctx context.Context) error, retErr error) {
stopDaemon := func(daemon *DaemonInfo, retErr *error) {
if err := h.daemonController.Stop(ctx, daemon); err != nil {
*retErr = errors.Wrapf(err, "failed to stop %s", daemon.DaemonName)
}
}
startDaemon := func(daemon *DaemonInfo, retErr *error) {
if err := h.daemonController.Start(ctx, daemon); err != nil {
if retErr == nil {
*retErr = errors.Wrapf(err, "failed to start %s", daemon.DaemonName)
} else {
testing.ContextLogf(ctx, "Failed to start %s: %v", daemon.DaemonName, err)
}
}
}
// Stop Cryptohome because it contains TPM status cache.
if stopDaemon(CryptohomeDaemon, &retErr); retErr != nil {
return nil, retErr
}
// Restart it after finishing all operations.
defer startDaemon(CryptohomeDaemon, &retErr)
// Stop TPM Manager before modifying its local data.
if stopDaemon(TPMManagerDaemon, &retErr); retErr != nil {
return nil, retErr
}
// Restart it after finishing all operations.
defer startDaemon(TPMManagerDaemon, &retErr)
rawData, err := h.GetTPMManagerLocalData(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get local TPM data")
}
var data tmpb.LocalData
if err := proto.Unmarshal(rawData, &data); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal local TPM data")
}
// Drop the owner password, so tpm_manager couldn't use it to create owner delegate on TPM1.2 device.
data.OwnerPassword = []byte{}
// Drop the owner delegate, so tpm_manager couldn't use it to reset DA counter on TPM1.2 device.
data.OwnerDelegate = &tmpb.AuthDelegate{}
// Drop the lockout password, so tpm_manager couldn't use it to reset DA counter on TPM2.0 device.
data.LockoutPassword = []byte{}
newData, err := proto.Marshal(&data)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal local TPM data")
}
// Write back the data into the local data of tpm_manager.
if err := h.SetTPMManagerLocalData(ctx, newData); err != nil {
return nil, errors.Wrap(err, "failed to set local TPM data")
}
return func(ctx context.Context) (retErr error) {
// Stop Cryptohome because it contains TPM status cache.
if stopDaemon(CryptohomeDaemon, &retErr); retErr != nil {
return retErr
}
// Restart it after finishing all operations.
defer startDaemon(CryptohomeDaemon, &retErr)
// Stop TPM Manager before modifying its local data.
if stopDaemon(TPMManagerDaemon, &retErr); retErr != nil {
return retErr
}
// Restart it after finishing all operations.
defer startDaemon(TPMManagerDaemon, &retErr)
// Restore the local data.
if err := h.SetTPMManagerLocalData(ctx, rawData); err != nil {
return errors.Wrap(err, "failed to restore local TPM data")
}
return nil
}, nil
}
// GetTPMVersion would rteurn the TPM version, for example: "1.2", "2.0"
func (h *CmdHelper) GetTPMVersion(ctx context.Context) (string, error) {
out, err := h.cmdRunner.Run(ctx, "tpmc", "tpmver")
// Trailing newline char is trimmed.
return strings.TrimSpace(string(out)), err
}
// ErrIneffectiveReset is returned if the TPM is owned after reset attempt.
var ErrIneffectiveReset = errors.New("ineffective reset of TPM")
// ensureTPMIsReset ensures the TPM is reset when the function returns nil.
// Otherwise, returns any encountered error.
// Optionally removes files from the DUT to simulate a powerwash.
func (h *CmdTPMClearHelper) ensureTPMIsReset(ctx context.Context, removeFiles bool) error {
if err := h.daemonController.WaitForAllDBusServices(ctx); err != nil {
return errors.Wrap(err, "failed to wait for hwsec D-Bus services to be ready")
}
ownershipData, err := h.cmdRunner.Run(ctx, "hwsec-ownership-id", "id")
if err != nil {
return errors.Wrap(err, "failed to get ownership ID")
}
ownershipID := strings.TrimSpace(string(ownershipData))
// Wrap this section to a function, so we can ensure all daemons are up.
err = func(ctx context.Context) error {
if err := h.tpmClearer.PreClearTPM(ctx); err != nil {
return errors.Wrap(err, "failed to pre clear TPM")
}
if err := h.daemonController.TryStop(ctx, UIDaemon); err != nil {
// ui might not be running because there's no guarantee that it's running when we start the test.
// If we actually failed to stop ui and something ends up being wrong, then we can use the logging
// below to let whoever that's debugging this problem find out.
testing.ContextLog(ctx, "Failed to stop ui, this is normal if ui was not running: ", err)
}
defer func(ctx context.Context) {
if err := h.daemonController.Ensure(ctx, UIDaemon); err != nil {
testing.ContextLog(ctx, "Failed to ensure ui daemon: ", err)
}
}(ctx)
if err := h.daemonController.TryStopDaemons(ctx, HighLevelTPMDaemons); err != nil {
// High-level TPM daemons might not be running because there's no guarantee that it's running when we start the test.
// If we actually failed to stop them and something ends up being wrong, then we can use the logging
// below to let whoever that's debugging this problem find out.
testing.ContextLog(ctx, "Failed to stop High-level TPM daemons, this is normal if they were not running: ", err)
}
defer func(ctx context.Context) {
if err := h.daemonController.EnsureDaemons(ctx, HighLevelTPMDaemons); err != nil {
testing.ContextLog(ctx, "Failed to ensure High-level TPM daemons: ", err)
}
}(ctx)
if err := h.tpmClearer.ClearTPM(ctx); err != nil {
return errors.Wrap(err, "failed to clear TPM")
}
if removeFiles {
args := append([]string{"-rf", "--"}, SystemStateFiles...)
if out, err := h.cmdRunner.Run(ctx, "rm", args...); err != nil {
// TODO(b/173189029): Ignore errors on failure. This is a workaround to prevent Permission denied when removing a fscrypt directory.
testing.ContextLog(ctx, "Failed to remove files to clear ownership: ", err, string(out))
}
command := "rm " + strings.Join(SystemStateGlobs, " ")
if out, err := h.cmdRunner.Run(ctx, "bash", "-c", command); err != nil {
// TODO(b/173189029): Ignore errors on failure. This is a workaround to prevent Permission denied when removing a fscrypt directory.
testing.ContextLog(ctx, "Failed to remove files to clear ownership: ", err, string(out))
}
}
if err := h.tpmClearer.PostClearTPM(ctx); err != nil {
return errors.Wrap(err, "failed to post clear TPM")
}
return nil
}(ctx)
if err != nil {
if err := h.saveTPMClearLogs(ctx); err != nil {
testing.ContextLog(ctx, "Failed to save TPM clear logs: ", err)
}
return errors.Wrap(err, "failed to ensure TPM is reset")
}
testing.ContextLog(ctx, "Waiting for system to be ready after reset TPM")
if err := h.daemonController.WaitForAllDBusServices(ctx); err != nil {
return errors.Wrap(err, "failed to wait for hwsec D-Bus services to be ready")
}
if _, err = h.cmdRunner.Run(ctx, "hwsec-ownership-id", "diff", "--id="+ownershipID); err != nil {
if err := h.saveTPMClearLogs(ctx); err != nil {
testing.ContextLog(ctx, "Failed to save TPM clear logs: ", err)
}
// If the ownership ID is not changed, the reset was not successful
return ErrIneffectiveReset
}
return nil
}
// saveTPMClearLogs saves the logs which are related to the TPM clear failure to outdir.
// For more information, please see b/172876417.
func (h *CmdTPMClearHelper) saveTPMClearLogs(ctx context.Context) error {
dir, ok := testing.ContextOutDir(ctx)
if !ok || dir == "" {
return errors.New("failed to get name of directory")
}
if _, err := os.Stat(dir); err != nil {
return errors.Wrap(err, "output directory not found")
}
logDir := filepath.Join(dir, "tpm_clear_faillog")
if err := os.MkdirAll(logDir, 0755); err != nil {
return errors.Wrap(err, "failed to create directory for tpm clear faillog")
}
if _, err := h.cmdRunner.Run(ctx, "flashrom", "-r", "-i", "RW_NVRAM:/tmp/nvram.bin"); err != nil {
testing.ContextLog(ctx, "Failed to call flashrom command: ", err)
}
saveLogFile := func(from, to string) error {
rawOutput, err := h.ReadFile(ctx, from)
if err != nil {
return errors.Wrapf(err, "failed to get the %s", from)
}
path := filepath.Join(logDir, to)
f, err := os.Create(path)
if err != nil {
return errors.Wrapf(err, "failed to create %s", to)
}
defer f.Close()
if _, err := f.Write(rawOutput); err != nil {
return errors.Wrapf(err, "failed to write data to %s", to)
}
return nil
}
if err := saveLogFile("/tmp/nvram.bin", "nvram.bin"); err != nil {
testing.ContextLog(ctx, "Failed to save nvram.bin: ", err)
}
if err := saveLogFile("/sys/firmware/log", "firmware_log"); err != nil {
testing.ContextLog(ctx, "Failed to save firmware_log: ", err)
}
return nil
}
// EnsureTPMIsReset ensures the TPM is reset when the function returns nil.
// Otherwise, returns any encountered error.
func (h *CmdTPMClearHelper) EnsureTPMIsReset(ctx context.Context) error {
return h.ensureTPMIsReset(ctx, false)
}
// EnsureTPMAndSystemStateAreReset ensures the TPM is reset and simulates a Powerwash.
func (h *CmdTPMClearHelper) EnsureTPMAndSystemStateAreReset(ctx context.Context) error {
return h.ensureTPMIsReset(ctx, true)
}