blob: c1a00c747b56b57e6508f1f5f5776d8bcef50e45 [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 contains TPM-related utility functions for local tests.
package hwsec
import (
"context"
"os"
"regexp"
"strings"
"chromiumos/tast/errors"
"chromiumos/tast/local/session"
"chromiumos/tast/local/testexec"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
const (
systemKeyBackupFile = "/mnt/stateful_partition/unencrypted/preserve/system.key"
trunksd = "trunksd"
)
// High-level TPM daemons that need to be stopped/restarted before/after we soft-clear the TPM and reset system states.
// All TPM daemons except for trunksd are considered high-level daemons.
//
// The order those jobs start matters. Make sure you know what you are doing before modifying this slice.
var highLevelTPMDaemonsToRestart = []string{
"tpm_managerd", "chapsd", "bootlockboxd", "attestationd", "u2fd", "cryptohomed",
}
// OOBE and TPM-related files that should be cleared after TPM is soft-cleared.
var filesToRemove = []string{
"/mnt/stateful_partition/.tpm_owned",
"/mnt/stateful_partition/.tpm_status",
"/mnt/stateful_partition/.tpm_status.sum",
"/home/.shadow/.can_attempt_ownership",
"/home/.shadow/attestation.epb",
"/home/.shadow/cryptohome.key",
"/home/.shadow/cryptohome.key.sum",
"/home/.shadow/install_attributes.pb",
"/home/.shadow/install_attributes.pb.sum",
"/home/.shadow/salt",
"/home/.shadow/salt.sum",
"/home/chronos/.oobe_completed",
"/var/lib/public_mount_salt",
}
// Dirs where TPM-related daemons cache data/states. Those dirs should be removed after TPM is soft-cleared.
var dirsToRemove = []string{
"/home/.shadow/low_entropy_creds",
"/run/cryptohome",
"/run/lockbox",
"/run/tpm_manager",
"/var/lib/bootlockbox",
"/var/lib/boot-lockbox",
"/var/lib/chaps",
"/var/lib/cryptohome",
"/var/lib/tpm_manager",
"/var/lib/u2f",
}
// ResetTPMAndSystemStates soft-clears the TPM, resets the OOBE state, device ownership, and
// TPM-related states, and restarts UI and TPM-related daemons. System key used by encstateful is
// restored after TPM is soft-cleared.
//
// There might be multiple errors happening in this function. All but the first error will be logged,
// and only the first error will be returned.
func ResetTPMAndSystemStates(ctx context.Context) (firstErr error) {
// Makes sure this is a TPM 2.0 device.
version, err := GetTPMVersion(ctx)
if err != nil {
return errors.Wrap(err, "failed to get TPM version")
} else if version != "2.0" {
return errors.Errorf("we don't support TPM version %s for TPM soft-clearing yet", version)
}
// Checks if system key exists in NVRAM.
hasSysKey, err := hasSystemKey(ctx)
if err != nil {
return errors.Wrap(err, "failed to check system key in NVRAM")
}
// If system key exists, checks if we also have the system key backup.
if hasSysKey {
if hasSysKeyBackup, err := hasSystemKeyBackup(); err != nil {
return errors.Wrap(err, "failed to check the system key backup file")
} else if !hasSysKeyBackup {
return errors.New("there is a system key but not its backup; we shouldn't soft-clear the TPM")
}
}
// Stops ui and all hwsec daemons except for trunksd before soft-clearing the TPM so that they
// don't run into weird states. Restarts those daemons before returning.
//
// trunksd is needed by the tpm_softclear command below and is stopped/started separately.
copyOfHighLevelDaemons := append([]string(nil), highLevelTPMDaemonsToRestart...)
jobsToRestart := append(copyOfHighLevelDaemons, "ui")
defer func() {
if err := ensureJobsStarted(ctx, jobsToRestart); err != nil {
logOrCopyErr(ctx, err, &firstErr)
}
}()
jobsToStop := reverseStringSlice(jobsToRestart)
if err = stopJobs(ctx, jobsToStop); err != nil {
logOrCopyErr(ctx, err, &firstErr)
return firstErr
}
// Actually clears the TPM.
if err = testexec.CommandContext(ctx, "tpm_softclear").Run(); err != nil {
logOrCopyErr(ctx, err, &firstErr)
return firstErr
}
defer func() {
if err := upstart.EnsureJobRunning(ctx, trunksd); err != nil {
logOrCopyErr(ctx, err, &firstErr)
}
}()
if err = upstart.StopJob(ctx, trunksd); err != nil {
logOrCopyErr(ctx, err, &firstErr)
return firstErr
}
if hasSysKey {
if err = restoreSystemKey(ctx); err != nil {
logOrCopyErr(ctx, err, &firstErr)
// Continues to reset daemons and system states even if we failed to restore system key,
// since the TPM is already cleared.
}
}
if err = resetDaemonsAndSystemStates(ctx); err != nil {
logOrCopyErr(ctx, err, &firstErr)
}
return firstErr
}
// GetTPMVersion gets TPM version number from tpmc and returns the result. If the tpmc command
// fails, an error is returned instead.
func GetTPMVersion(ctx context.Context) (version string, err error) {
out, err := testexec.CommandContext(ctx, "tpmc", "tpmver").Output()
// Trailing newline char is trimmed.
return strings.TrimSpace(string(out)), err
}
// RestartTPMDaemons restarts all TPM-related daemons.
//
// There might be multiple errors happening in this function. All but the first error will be
// logged, and only the first error will be returned.
func RestartTPMDaemons(ctx context.Context) (firstErr error) {
// Trunksd must restart first prior to other TPM daemons.
daemonsToRestart := append([]string{trunksd}, highLevelTPMDaemonsToRestart...)
daemonsToStop := reverseStringSlice(daemonsToRestart)
defer func() {
if err := ensureJobsStarted(ctx, daemonsToRestart); err != nil {
logOrCopyErr(ctx, err, &firstErr)
}
}()
if err := stopJobs(ctx, daemonsToStop); err != nil {
logOrCopyErr(ctx, err, &firstErr)
}
return firstErr
}
// logOrCopyErr sets errOfInterest to newErr if errOfInterest is nil; otherwise, logs it.
// This is for functions that may have multiple errors and want to make sure none of the errors is
// silently suppressed but only return errOfInterest.
func logOrCopyErr(ctx context.Context, newErr error, errOfInterest *error) {
if *errOfInterest == nil {
*errOfInterest = newErr
} else {
testing.ContextLog(ctx, "Ignoring due to earlier errors: ", newErr)
}
}
// hasSystemKey returns if the system key for encstateful exists in NVRAM or an error on any failure.
func hasSystemKey(ctx context.Context) (spaceExists bool, err error) {
var nvSpaceInfoRegexp = regexp.MustCompile(`(?m)^\s*result:\s*NVRAM_RESULT_SUCCESS\s*$`)
out, err := testexec.CommandContext(
ctx, "tpm_manager_client", "get_space_info", "--index=0x800005").Output()
return nvSpaceInfoRegexp.Match(out), err
}
// hasSystemKeyBackup returns if the system key backup exists on the disk or an error on any failure.
func hasSystemKeyBackup() (backupExists bool, err error) {
fileInfo, err := os.Stat(systemKeyBackupFile)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
if fileInfo.IsDir() {
return false, errors.Errorf("%s is a dir", systemKeyBackupFile)
}
return true, nil
}
// restoreSystemKey restores encstateful system key to NVRAM.
func restoreSystemKey(ctx context.Context) error {
if err := testexec.CommandContext(
ctx, "mount-encrypted", "set", systemKeyBackupFile).Run(); err != nil {
return errors.Wrapf(err, "failed to restore system key into NVRAM from %s", systemKeyBackupFile)
}
return nil
}
// ensureJobsStarted ensures the given jobs are started or returns an error if any one of the jobs failed to start.
func ensureJobsStarted(ctx context.Context, jobs []string) error {
for _, job := range jobs {
if err := upstart.EnsureJobRunning(ctx, job); err != nil {
return errors.Wrapf(err, "failed to start %s", job)
}
}
return nil
}
// stopJobs ensures the given jobs are stopped or returns an error if any one of the jobs failed to
// stop. It's not an error if a given job is already stopped.
func stopJobs(ctx context.Context, jobs []string) error {
for _, job := range jobs {
if err := upstart.StopJob(ctx, job); err != nil {
return errors.Wrapf(err, "failed to stop %s", job)
}
}
return nil
}
// reverseStringSlice returns the reverse of the input slice. This function doesn't change the input slice.
func reverseStringSlice(elements []string) []string {
length := len(elements)
newElements := make([]string, length)
for i := 0; i < length; i++ {
newElements[i] = elements[length-i-1]
}
return newElements
}
// resetDaemonsAndSystemStates removes TPM-related caches, device policies, and local states.
func resetDaemonsAndSystemStates(ctx context.Context) error {
for _, filename := range filesToRemove {
if err := os.Remove(filename); err != nil && !os.IsNotExist(err) {
return errors.Wrapf(err, "failed to remove file %s", filename)
}
}
for _, dirname := range dirsToRemove {
if err := os.RemoveAll(dirname); err != nil {
return errors.Wrapf(err, "failed to remove dir %s", dirname)
}
}
// Clears /var/lib/whitelist and /home/chronos/Local States.
return session.ClearDeviceOwnership(ctx)
}