blob: a4a07e3e96c9cfd984807d807d3a76b7dccd23ee [file] [log] [blame]
// Copyright 2020 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
import (
"context"
"encoding/hex"
"fmt"
"regexp"
"strings"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
const (
// NVRAMAttributeWriteAuth is used by DefineSpace to indicate that writing this NVRAM index requires authorization with authValue.
NVRAMAttributeWriteAuth = "WRITE_AUTHORIZATION"
// NVRAMAttributeReadAuth is used by DefineSpace to indicate that reading this NVRAM index requires authorization with authValue.
NVRAMAttributeReadAuth = "READ_AUTHORIZATION"
// tpmManagerNVRAMSuccessMessage is the error code from NVRAM related tpm_manager API when the operation is successful.
tpmManagerNVRAMSuccessMessage = "NVRAM_RESULT_SUCCESS"
// tpmManagerStatusSuccessMessage is the error code from other tpm_manager API when the operation is successful.
tpmManagerStatusSuccessMessage = "STATUS_SUCCESS"
)
// Matches the owner password in status string.
// Example: "owner_password: 3344393734333945364646313534443137454434".
var ownerPasswordRegexp = regexp.MustCompile("owner_password: (?P<OwnerPassword>[0-9A-F]+)")
// TPMManagerClient wraps and the functions of tpmManagerBinary and parses the outputs to
// structured data.
type TPMManagerClient struct {
binary *tpmManagerBinary
}
// NewTPMManagerClient creates a new TPMManagerClient.
func NewTPMManagerClient(r CmdRunner) *TPMManagerClient {
return &TPMManagerClient{
binary: newTPMManagerBinary(r),
}
}
// checkCommandAndReturn is a simple helper that checks if binaryMsg returned is successful, and returns the corresponding message and error.
func checkCommandAndReturn(ctx context.Context, binaryMsg []byte, err error, command, successMsg string) (string, error) {
// Convert msg first because it's still used when there's an error.
msg := string(binaryMsg)
// Check if the command succeeds.
if err != nil {
// Command failed.
return msg, errors.Wrapf(err, "calling %s failed with message %q", command, msg)
}
// Check if the message make sense.
if !strings.Contains(msg, successMsg) {
return msg, errors.Errorf("calling %s failed due to missing wanted context %q in stdout %q", command, successMsg, msg)
}
return msg, nil
}
// checkNVRAMCommandAndReturn is a simple helper that checks if binaryMsg returned from NVRAM related command is successful, and returns the corresponding message and error.
func checkNVRAMCommandAndReturn(ctx context.Context, binaryMsg []byte, err error, command string) (string, error) {
return checkCommandAndReturn(ctx, binaryMsg, err, command, tpmManagerNVRAMSuccessMessage)
}
// checkStatusCommandAndReturn is a simple helper that checks if binaryMsg returned from status related command is successful, and returns the corresponding message and error.
func checkStatusCommandAndReturn(ctx context.Context, binaryMsg []byte, err error, command string) (string, error) {
return checkCommandAndReturn(ctx, binaryMsg, err, command, tpmManagerStatusSuccessMessage)
}
// DefineSpace defines (creates) an NVRAM space at index, of size size, with attributes attributes and password password, and the NVRAM space will be bound to PCR0 if bindToPCR0 is true.
// If password is "", it'll not be passed to the command. attributes should be a slice that contains only the const NVRAMAttribute*.
// Will return nil for error iff the operation completes successfully. The string returned, msg, is the message from the command line, if any.
func (u *TPMManagerClient) DefineSpace(ctx context.Context, size int, bindToPCR0 bool, index string, attributes []string, password string) (string, error) {
attributesParam := strings.Join(attributes, "|")
binaryMsg, err := u.binary.defineSpace(ctx, size, bindToPCR0, index, attributesParam, password)
return checkNVRAMCommandAndReturn(ctx, binaryMsg, err, "DefineSpace")
}
// DestroySpace destroys (removes) an NVRAM space at index.
// Will return nil for error iff the operation completes successfully. The string returned, msg, is the message from the command line, if any.
func (u *TPMManagerClient) DestroySpace(ctx context.Context, index string) (string, error) {
binaryMsg, err := u.binary.destroySpace(ctx, index)
return checkNVRAMCommandAndReturn(ctx, binaryMsg, err, "DestroySpace")
}
// WriteSpaceFromFile writes the content of file inputFile into the NVRAM space at index, with password password (if not empty).
func (u *TPMManagerClient) WriteSpaceFromFile(ctx context.Context, index, inputFile, password string) (string, error) {
binaryMsg, err := u.binary.writeSpace(ctx, index, inputFile, password)
return checkNVRAMCommandAndReturn(ctx, binaryMsg, err, "WriteSpace")
}
// ReadSpaceToFile reads the content of NVRAM space at index into the file outputFile, with password (if not empty).
func (u *TPMManagerClient) ReadSpaceToFile(ctx context.Context, index, outputFile, password string) (string, error) {
binaryMsg, err := u.binary.readSpace(ctx, index, outputFile, password)
return checkNVRAMCommandAndReturn(ctx, binaryMsg, err, "ReadSpace")
}
// TakeOwnership takes the TPM ownership.
func (u *TPMManagerClient) TakeOwnership(ctx context.Context) (string, error) {
binaryMsg, err := u.binary.takeOwnership(ctx)
return checkStatusCommandAndReturn(ctx, binaryMsg, err, "TakeOwnership")
}
// Status returns the status string.
func (u *TPMManagerClient) Status(ctx context.Context) (string, error) {
binaryMsg, err := u.binary.status(ctx)
return checkStatusCommandAndReturn(ctx, binaryMsg, err, "Status")
}
// GetOwnerPassword returns the owner password.
func (u *TPMManagerClient) GetOwnerPassword(ctx context.Context) (string, error) {
msg, err := u.Status(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to get TPM status")
}
result := ownerPasswordRegexp.FindStringSubmatch(msg)
if len(result) < 2 {
return "", nil
}
hexOwnerPassword := result[1]
dec := make([]byte, hex.DecodedLen(len(hexOwnerPassword)))
n, err := hex.Decode(dec, []byte(hexOwnerPassword))
if err != nil {
return "", errors.Wrap(err, "failed to call hex.Decode")
}
return string(dec[:n]), nil
}
// ClearOwnerPassword clears TPM owner password in the best effort.
func (u *TPMManagerClient) ClearOwnerPassword(ctx context.Context) (string, error) {
binaryMsg, err := u.binary.clearOwnerPassword(ctx)
return checkStatusCommandAndReturn(ctx, binaryMsg, err, "ClearOwnerPassword")
}
// NonsensitiveStatusInfo contains the dictionary attack related information.
type NonsensitiveStatusInfo struct {
// Whether a TPM is enabled on the system.
IsEnabled bool
// Whether the TPM has been owned.
IsOwned bool
// Whether the owner password is still retained.
IsOwnerPasswordPresent bool
// Whether tpm manager is capable of reset DA.
HasResetLockPermissions bool
}
func parseStringMap(ctx context.Context, msg string, checkMatch bool, prefixes []string) (map[string]string, error) {
lines := strings.Split(msg, "\n")
parsed := map[string]string{}
// TODO(yich): This compare is slow when we have big msg and prefixes.
for _, line := range lines {
for _, prefix := range prefixes {
if !strings.HasPrefix(line, prefix) {
continue
}
if _, found := parsed[prefix]; found {
testing.ContextLogf(ctx, "Command have duplicate prefix, message %q", msg)
return nil, errors.Errorf("duplicate prefix %q found", prefix)
}
parsed[prefix] = line[len(prefix):]
}
}
if checkMatch && len(parsed) != len(prefixes) {
return nil, errors.Errorf("missing attribute/prefix, message %q", msg)
}
return parsed, nil
}
// parseNonsensitiveStatusInfo tries to parse the output of NonsensitiveStatus from msg, if checkStatus is true, then we'll verify that the output of the command contains a success message.
func parseNonsensitiveStatusInfo(ctx context.Context, checkStatus bool, msg string) (info *NonsensitiveStatusInfo, returnedError error) {
const (
IsEnablePrefix = " is_enabled: "
IsOwnedPrefix = " is_owned: "
IsOwnerPasswordPresentPrefix = " is_owner_password_present: "
HasResetLockPermissionsPrefix = " has_reset_lock_permissions: "
StatusPrefix = " status: "
)
prefixes := []string{
IsEnablePrefix,
IsOwnedPrefix,
IsOwnerPasswordPresentPrefix,
HasResetLockPermissionsPrefix,
StatusPrefix,
}
parsed, err := parseStringMap(ctx, msg, checkStatus, prefixes)
if err != nil {
return nil, errors.Wrap(err, "failed to parse string map")
}
if checkStatus {
// We need to check the status.
if parsed[StatusPrefix] != tpmManagerStatusSuccessMessage {
return nil, errors.Errorf("incorrect status %q from NonsensitiveStatusInfo", parsed[StatusPrefix])
}
}
isEnabled := false
if _, err := fmt.Sscanf(parsed[IsEnablePrefix], "%t", &isEnabled); err != nil {
return nil, errors.Wrapf(err, "isEnabled doesn't start with a valid boolean %q", parsed[IsEnablePrefix])
}
isOwned := false
if _, err := fmt.Sscanf(parsed[IsOwnedPrefix], "%t", &isOwned); err != nil {
return nil, errors.Wrapf(err, "isOwned doesn't start with a valid boolean %q", parsed[IsOwnedPrefix])
}
ownerPass := false
if _, err := fmt.Sscanf(parsed[IsOwnerPasswordPresentPrefix], "%t", &ownerPass); err != nil {
return nil, errors.Wrapf(err, "ownerPass doesn't start with a valid boolean %q", parsed[IsOwnerPasswordPresentPrefix])
}
lockPerm := false
if _, err := fmt.Sscanf(parsed[HasResetLockPermissionsPrefix], "%t", &lockPerm); err != nil {
return nil, errors.Wrapf(err, "lockPerm doesn't start with a valid boolean %q", parsed[HasResetLockPermissionsPrefix])
}
return &NonsensitiveStatusInfo{
IsEnabled: isEnabled,
IsOwned: isOwned,
IsOwnerPasswordPresent: ownerPass,
HasResetLockPermissions: lockPerm,
}, nil
}
// GetNonsensitiveStatus retrieves the NonsensitiveStatusInfo.
func (u *TPMManagerClient) GetNonsensitiveStatus(ctx context.Context) (info *NonsensitiveStatusInfo, returnedError error) {
binaryMsg, err := u.binary.nonsensitiveStatus(ctx)
// Convert msg first because it's still used when there's an error.
msg := string(binaryMsg)
if err != nil {
return nil, errors.Wrapf(err, "calling NonsensitiveStatus failed with message %q", msg)
}
// Now try to parse everything.
return parseNonsensitiveStatusInfo(ctx, true, msg)
}
// GetNonsensitiveStatusIgnoreCache retrieves the NonsensitiveStatusInfo and ignore the cache.
func (u *TPMManagerClient) GetNonsensitiveStatusIgnoreCache(ctx context.Context) (info *NonsensitiveStatusInfo, returnedError error) {
binaryMsg, err := u.binary.nonsensitiveStatusIgnoreCache(ctx)
// Convert msg first because it's still used when there's an error.
msg := string(binaryMsg)
if err != nil {
return nil, errors.Wrapf(err, "calling NonsensitiveStatusIgnoreCache failed with message %q", msg)
}
// Now try to parse everything.
return parseNonsensitiveStatusInfo(ctx, true, msg)
}
// DAInfo contains the dictionary attack related information.
type DAInfo struct {
// Counter is the dictionary attack lockout counter.
Counter int
// Threshold is the dictionary attack lockout threshold.
Threshold int
// InEffect indicates if dictionary attack lockout is in effect.
InEffect bool
// Remaining is the seconds remaining until we can reset the lockout.
Remaining int
}
// parseDAInfo tries to parse the output of GetDAInfo from msg, if checkStatus is true, then we'll verify that the output of the command contains a success message.
func parseDAInfo(ctx context.Context, checkStatus bool, msg string) (info *DAInfo, returnedError error) {
const (
CounterPrefix = " dictionary_attack_counter: "
ThresholdPrefix = " dictionary_attack_threshold: "
InEffectPrefix = " dictionary_attack_lockout_in_effect: "
RemainingPrefix = " dictionary_attack_lockout_seconds_remaining: "
StatusPrefix = " status: "
)
prefixes := []string{CounterPrefix, ThresholdPrefix, InEffectPrefix, RemainingPrefix, StatusPrefix}
parsed, err := parseStringMap(ctx, msg, checkStatus, prefixes)
if err != nil {
return nil, errors.Wrap(err, "failed to parse string map")
}
if checkStatus {
// We need to check the status.
if parsed[StatusPrefix] != tpmManagerStatusSuccessMessage {
return nil, errors.Errorf("incorrect status %q from GetDAInfo", parsed[StatusPrefix])
}
}
counter := -1
if _, err := fmt.Sscanf(parsed[CounterPrefix], "%d", &counter); err != nil {
return nil, errors.Wrapf(err, "counter doesn't start with a valid integer %q", parsed[CounterPrefix])
}
threshold := -1
if _, err := fmt.Sscanf(parsed[ThresholdPrefix], "%d", &threshold); err != nil {
return nil, errors.Wrapf(err, "threshold doesn't start with a valid integer %q", parsed[ThresholdPrefix])
}
inEffect := false
if _, err := fmt.Sscanf(parsed[InEffectPrefix], "%t", &inEffect); err != nil {
return nil, errors.Wrapf(err, "in effect doesn't start with a valid boolean %q", parsed[InEffectPrefix])
}
remaining := -1
if _, err := fmt.Sscanf(parsed[RemainingPrefix], "%d", &remaining); err != nil {
return nil, errors.Wrapf(err, "remaining doesn't start with a valid integer %q", parsed[RemainingPrefix])
}
return &DAInfo{Counter: counter, Threshold: threshold, InEffect: inEffect, Remaining: remaining}, nil
}
// GetDAInfo retrieves the dictionary attack counter, threshold, if lockout is in effect and seconds remaining. The returned err is nil iff the operation is successful.
func (u *TPMManagerClient) GetDAInfo(ctx context.Context) (info *DAInfo, returnedError error) {
binaryMsg, err := u.binary.getDAInfo(ctx)
// Convert msg first because it's still used when there's an error.
msg := string(binaryMsg)
if err != nil {
return nil, errors.Wrapf(err, "calling GetDAInfo failed with message %q", msg)
}
// Now try to parse everything.
return parseDAInfo(ctx, true, msg)
}
// ResetDALock resets the dictionary attack lockout.
func (u *TPMManagerClient) ResetDALock(ctx context.Context) (string, error) {
binaryMsg, err := u.binary.resetDALock(ctx)
// Convert msg first because it's still used when there's an error.
msg := string(binaryMsg)
// Check if the command succeeds.
if err != nil {
// Command failed.
return msg, errors.Wrapf(err, "calling ResetDALock failed with message %q", msg)
}
// Check if the message make sense.
if !strings.Contains(msg, tpmManagerStatusSuccessMessage) {
return msg, errors.Errorf("calling ResetDALock failed due to unexpected stdout %q", msg)
}
return msg, nil
}