Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streamline unit-tests that use a TPM #4239

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 11 additions & 73 deletions pkg/pillar/cmd/msrv/activatecred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,102 +10,40 @@ import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"

"github.com/google/go-tpm/legacy/tpm2"
"github.com/google/go-tpm/legacy/tpm2/credactivation"
"github.com/lf-edge/eve/pkg/pillar/base"
"github.com/lf-edge/eve/pkg/pillar/cmd/msrv"
etpm "github.com/lf-edge/eve/pkg/pillar/evetpm"
"github.com/lf-edge/eve/pkg/pillar/evetpm"
"github.com/lf-edge/eve/pkg/pillar/pubsub"
"github.com/onsi/gomega"
"github.com/sirupsen/logrus"
)

const TpmSimPath = "/tmp/eve-tpm/srv.sock"

var log = base.NewSourceLogObject(logrus.StandardLogger(), "acitavatecred", 1234)

func waitForTpmReadyState() error {
for i := 0; i < 10; i++ {
rw, err := tpm2.OpenTPM(msrv.TPMDevicePath)
if err != nil {
return fmt.Errorf("Failed to open TPM: %w", err)
}

_, _, err = tpm2.GetCapability(rw, tpm2.CapabilityHandles, 1, uint32(tpm2.HandleTypeTransient)<<24)
if err != nil {
// this is RCRetry, so retry
if strings.Contains(err.Error(), "code 0x22") {
time.Sleep(100 * time.Millisecond)
continue
} else {
return fmt.Errorf("Something is wrong with the TPM : %w", err)
}
} else {
return nil
}
}

return fmt.Errorf("TPM did't become ready after 10 attempts, failing the test")
}

func isTpmAvailable(path string) bool {
_, err := os.Stat(path)
if err != nil {
log.Warnf("TPM device %s is not available: %v", msrv.TPMDevicePath, err)
return false
}
return true
}

func prepareTPM() error {
//Make sure the TPM is ready for use, swtpm takes some time to become ready
if err := waitForTpmReadyState(); err != nil {
return fmt.Errorf("Failed to wait for TPM ready state: %w", err)
}

// Create EK and AIK in case it is not already created
err := etpm.CreateKey(log, msrv.TPMDevicePath, etpm.TpmEKHdl, tpm2.HandleEndorsement, etpm.DefaultEkTemplate, false)
if err != nil {
return fmt.Errorf("error in creating Endorsement key: %w ", err)
}
err = etpm.CreateKey(log, msrv.TPMDevicePath, etpm.TpmAIKHdl, tpm2.HandleOwner, etpm.DefaultAikTemplate, false)
if err != nil {
return fmt.Errorf("error in creating Attestation key: %w ", err)
}

return nil
}

// TestTpmActivateCred contains TPM kong-fu, not for the faint of heart.
func TestTpmActivateCred(t *testing.T) {
t.Parallel()
g := gomega.NewGomegaWithT(t)

if !isTpmAvailable(msrv.TPMDevicePath) {
// no TPM device available, check for TPM socket, see if we ended up
// here from tests/tpm/prep-and-test.sh script.
if !isTpmAvailable(TpmSimPath) {
t.Skip("Neither HW or SW TPM device available, skipping the test")
}

// set the TPM device path to swtpm socket path
msrv.TPMDevicePath = TpmSimPath
// we should end up here from tests/tpm/prep-and-test.sh script,
// so set the TPM device path to swtpm socket path.
msrv.TPMDevicePath = evetpm.SimTpmPath

// make sure TPM is prepare it before running the test.
err := prepareTPM()
g.Expect(err).ToNot(gomega.HaveOccurred())
if !evetpm.SimTpmAvailable() {
t.Skip("SWTPM device not available, skipping the test")
}

// make sure TPM is prepare it before running the test.
err := evetpm.SimTpmWaitForTpmReadyState()
g.Expect(err).ToNot(gomega.HaveOccurred())

logger := logrus.StandardLogger()
log := base.NewSourceLogObject(logger, "pubsub", 1234)
log := base.NewSourceLogObject(logger, "acitavatecred_test", os.Getpid())
ps := pubsub.New(pubsub.NewMemoryDriver(), logger, log)

srv := &msrv.Msrv{
Expand Down
3 changes: 3 additions & 0 deletions pkg/pillar/cmd/tpmmgr/tpmmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,9 @@ func writeDeviceCertToFile(certBytes, keyBytes []byte) error {
return os.WriteFile(types.DeviceCertName, certBytes, 0644)
}

// These keys template and hierarchy are used in the tests/tpm/prep-and-test.sh
// to create same keys and run tpm required unit-tests, in a unlikely event of
// changing these values dont forget to update the test script.
func createOtherKeys(override bool) error {
if err := etpm.CreateKey(log, etpm.TpmDevicePath, etpm.TpmEKHdl, tpm2.HandleEndorsement, etpm.DefaultEkTemplate, override); err != nil {
return fmt.Errorf("error in creating Endorsement key: %w ", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/pillar/cmd/vcomlink/tpmcom.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func handleTpmGetEk() ([]byte, error) {
}

func getEkPub() (string, error) {
rw, err := tpm2.OpenTPM(etpm.TpmDevicePath)
rw, err := tpm2.OpenTPM(TpmDevicePath)
if err != nil {
return "", fmt.Errorf("unable to open TPM: %w", err)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/pillar/cmd/vcomlink/vcomlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/lf-edge/eve/pkg/pillar/agentbase"
"github.com/lf-edge/eve/pkg/pillar/agentlog"
"github.com/lf-edge/eve/pkg/pillar/base"
etpm "github.com/lf-edge/eve/pkg/pillar/evetpm"
"github.com/lf-edge/eve/pkg/pillar/pubsub"
"github.com/lf-edge/eve/pkg/pillar/types"
"github.com/sirupsen/logrus"
Expand All @@ -31,6 +32,9 @@ var (
logger *logrus.Logger
log *base.LogObject
listener SocketListener = vsockListener
// TpmDevicePath is the path to the TPM device, this is a variable so that
// it can be overridden in tests.
TpmDevicePath = etpm.TpmDevicePath
)

type vcomLinkContext struct {
Expand Down
29 changes: 16 additions & 13 deletions pkg/pillar/cmd/vcomlink/vcomlink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/lf-edge/eve/pkg/pillar/base"
"github.com/lf-edge/eve/pkg/pillar/evetpm"
"github.com/lf-edge/eve/pkg/pillar/vcom"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
Expand Down Expand Up @@ -74,15 +75,25 @@ func connect() (int, error) {
return sockfd, nil
}

func isTPMAvailable() bool {
_, err := os.Stat("/dev/tpm0")
return err == nil
}

func TestMain(m *testing.M) {
log = base.NewSourceLogObject(logrus.StandardLogger(), "vcomlink_test", os.Getpid())
res := 1

if !evetpm.SimTpmAvailable() {
fmt.Println("TPM not available, skipping test")
os.Exit(0)
}

// make sure TPM is prepare it before running the test.
err := evetpm.SimTpmWaitForTpmReadyState()
if err != nil {
fmt.Printf("Failed to wait for TPM ready state: %v", err)
os.Exit(1)
}

// use sim tpm for testing
TpmDevicePath = evetpm.SimTpmPath

// we can't connect to a vsock listener host<->host, so we use a tcp listener
// for testing.
go startVcomServer(tcpListener)
Expand All @@ -101,10 +112,6 @@ func TestMain(m *testing.M) {
}

func TestValidTPMRequest(t *testing.T) {
if !isTPMAvailable() {
t.Skip("TPM not available, skipping test")
}

request := &vcom.TpmRequest{
Base: vcom.Base{Channel: int(vcom.ChannelTpm)},
Request: uint(vcom.RequestTpmGetEk),
Expand Down Expand Up @@ -151,10 +158,6 @@ func TestValidTPMRequest(t *testing.T) {
}

func TestInvalidRequest(t *testing.T) {
if !isTPMAvailable() {
t.Skip("TPM not available, skipping test")
}

request := &vcom.Base{
Channel: math.MaxUint32,
}
Expand Down
49 changes: 49 additions & 0 deletions pkg/pillar/evetpm/testhelper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2024 Zededa, Inc.
// SPDX-License-Identifier: Apache-2.0

package evetpm

import (
"fmt"
"os"
"strings"
"time"

"github.com/google/go-tpm/legacy/tpm2"
)

// SimTpmPath is the path to the SWTPM socket, this path is hardcoded in
// tests/tpm/prep-and-test.sh, so if you change this, make sure to update
// the script as well.
const SimTpmPath = "/tmp/eve-tpm/srv.sock"
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved

// SimTpmWaitForTpmReadyState waits for the SWTPM to be ready
func SimTpmWaitForTpmReadyState() error {
for i := 0; i < 10; i++ {
rw, err := tpm2.OpenTPM(SimTpmPath)
if err != nil {
return fmt.Errorf("failed to open TPM: %w", err)
}

_, _, err = tpm2.GetCapability(rw, tpm2.CapabilityHandles, 1, uint32(tpm2.HandleTypeTransient)<<24)
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
// this is RCRetry, so retry
if strings.Contains(err.Error(), "code 0x22") {
time.Sleep(100 * time.Millisecond)
continue
} else {
return fmt.Errorf("something is wrong with the TPM : %w", err)
}
} else {
return nil
}
}

return fmt.Errorf("TPM did't become ready after 10 attempts, failing the test")
}

// SimTpmAvailable checks if the SWTPM socket is available
func SimTpmAvailable() bool {
_, err := os.Stat(SimTpmPath)
return err == nil
}
22 changes: 9 additions & 13 deletions pkg/pillar/evetpm/tpm_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Copyright (c) 2020 Zededa, Inc.
// Copyright (c) 2020-2024 Zededa, Inc.
// SPDX-License-Identifier: Apache-2.0

// unit-tests for evetpm

// unit-tests for evetpm package
package evetpm

import (
Expand All @@ -22,30 +21,27 @@ import (
"github.com/sirupsen/logrus"
)

var log = base.NewSourceLogObject(logrus.StandardLogger(), "test", 1234)
var log = base.NewSourceLogObject(logrus.StandardLogger(), "evetpm", os.Getpid())

func TestMain(m *testing.M) {
log.Tracef("Setup test environment")

// setup variables
TpmDevicePath = "/tmp/eve-tpm/srv.sock"
TpmDevicePath = SimTpmPath
measurementLogFile = "/tmp/eve-tpm/binary_bios_measurement"
measurefsTpmEventLog = "/tmp/eve-tpm/measurefs_tpm_event_log"
savedSealingPcrsFile = "/tmp/eve-tpm/sealingpcrs"
measurementLogSealSuccess = "/tmp/eve-tpm/tpm_measurement_seal_success"
measurementLogUnsealFail = "/tmp/eve-tpm/tpm_measurement_unseal_fail"

// check if we are running under the correct context and we end up here
// from tests/tpm/prep-and-test.sh.
_, err := os.Stat(TpmDevicePath)
if err != nil {
log.Warnf("Neither TPM device nor swtpm is available, skipping the test.")
return
if !SimTpmAvailable() {
log.Warnf("TPM is not available, skipping the test.")
os.Exit(0)
}

// for some reason unknown to me, TPM might return RCRetry for the first
// for some reason TPM might return RCRetry for the first
// few operations, so we need to wait for it to become ready.
if err := waitForTpmReadyState(); err != nil {
if err := SimTpmWaitForTpmReadyState(); err != nil {
log.Fatalf("Failed to wait for TPM ready state: %v", err)
}

Expand Down
36 changes: 19 additions & 17 deletions tests/tpm/prep-and-test.sh
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question of curiosity. In the commit message, you say:

This changes adds the creation of AIK key and
also changes other keys creation method to creates all keys under Owner
hirearchy rather than EK, just like we do in EVE.

Can you please point me to where it is done in EVE?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here :

func createOtherKeys(override bool) error {
if err := etpm.CreateKey(log, etpm.TpmDevicePath, etpm.TpmEKHdl, tpm2.HandleEndorsement, etpm.DefaultEkTemplate, override); err != nil {
return fmt.Errorf("error in creating Endorsement key: %w ", err)
}
if err := etpm.CreateKey(log, etpm.TpmDevicePath, etpm.TpmSRKHdl, tpm2.HandleOwner, etpm.DefaultSrkTemplate, override); err != nil {
return fmt.Errorf("error in creating SRK key: %w ", err)
}
if err := etpm.CreateKey(log, etpm.TpmDevicePath, etpm.TpmAIKHdl, tpm2.HandleOwner, etpm.DefaultAikTemplate, override); err != nil {
return fmt.Errorf("error in creating Attestation key: %w ", err)
}
if err := etpm.CreateKey(log, etpm.TpmDevicePath, etpm.TpmQuoteKeyHdl, tpm2.HandleOwner, etpm.DefaultQuoteKeyTemplate, override); err != nil {
return fmt.Errorf("error in creating Quote key: %w ", err)
}
if err := etpm.CreateKey(log, etpm.TpmDevicePath, etpm.TpmEcdhKeyHdl, tpm2.HandleOwner, etpm.DefaultEcdhKeyTemplate, override); err != nil {
return fmt.Errorf("error in creating ECDH key: %w ", err)
}
return nil
}

btw, the ownerHandle in CreateKey is unused so don't and we don't create the EK under tpm2.HandleEndorsement, and tpm2.HandleOwner is hardcoded in the function. I want to clean that up for ages :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!
It may make sense to add a comment to the prep script that it refers to this code (and vice versa), especially if you have a plan to change it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
CWD=$(pwd)
TPM_SRV_PORT=1337
TPM_CTR_PORT=$((TPM_SRV_PORT + 1))
ENDO_SEED=0x4000000B
EK_HANDLE=0x81000001
SRK_HANDLE=0x81000002
AIK_HANDLE=0x81000003
EVE_TPM_STATE=/tmp/eve-tpm
EVE_TPM_CTRL="$EVE_TPM_STATE/ctrl.sock"
# this path is hardcoded in the pkg/pillar/evetpm/testhelper.go, so if you change
# it here, change it there too.
EVE_TPM_SRV="$EVE_TPM_STATE/srv.sock"

echo "[+] Installing swtpm and tpm2-tools ..."
Expand Down Expand Up @@ -56,46 +58,45 @@ swtpm socket --tpm2 \
--server port="$TPM_SRV_PORT" \
--ctrl type=tcp,port="$TPM_CTR_PORT" \
--tpmstate dir="$EVE_TPM_STATE" \
--flags not-need-init,startup-clear &
--flags startup-clear &

PID=$!

# set Transmission Interface (TCTI) swtpm socket, so tpm2-tools use it
# Set Transmission Interface (TCTI) swtpm socket, so tpm2-tools use it
# instead of the default char device interface.
export TPM2TOOLS_TCTI="swtpm:host=localhost,port=$TPM_SRV_PORT"

# start fresh
tpm2 clear

# The ek, srk and aik are created here based on what we do in createOtherKeys
# in pkg/pillar/cmd/tpmmgr/tpmmgr.go.
# create Endorsement Key
tpm2 createek -c ek.ctx

# this setup seems very fragile, and quickly errors out with
# "out of memory for object contexts", so flush everything to be safe.
flushtpm

# create Storage Root Key
tpm2 startauthsession --policy-session -S session.ctx
tpm2 policysecret -S session.ctx -c $ENDO_SEED
OhmSpectator marked this conversation as resolved.
Show resolved Hide resolved
tpm2 create -C ek.ctx -P "session:session.ctx" -G rsa2048 -u srk.pub -r srk.priv \
-a 'restricted|decrypt|fixedtpm|fixedparent|sensitivedataorigin|userwithauth'
tpm2 flushcontext session.ctx
# create srk
tpm2 createprimary -C o -G rsa2048:aes128cfb -g sha256 -c srk.ctx \
-a 'restricted|decrypt|fixedtpm|fixedparent|sensitivedataorigin|userwithauth'
flushtpm

# load the srk
tpm2 startauthsession --policy-session -S session.ctx
tpm2 policysecret -S session.ctx -c $ENDO_SEED
tpm2 load -C ek.ctx -P "session:session.ctx" -u srk.pub -r srk.priv -c srk.ctx
tpm2 flushcontext session.ctx
# create aik
tpm2 createprimary -C o -G rsa:rsassa-sha256:null -g sha256 -c aik.ctx \
-a 'fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign|noda'
flushtpm

# make persisted know-good-handles out of ek and srk

# make persisted know-good-handles out of ek, srk and aik
tpm2 evictcontrol -C o -c ek.ctx $EK_HANDLE
tpm2 evictcontrol -C o -c srk.ctx $SRK_HANDLE
tpm2 evictcontrol -C o -c aik.ctx $AIK_HANDLE
flushtpm

# clean up
rm session.ctx ek.ctx srk.pub srk.priv srk.ctx
rm ek.ctx srk.ctx aik.ctx

# kill swtpm, we are going to start it again with unix sockets
kill $PID
Expand Down Expand Up @@ -123,8 +124,9 @@ echo "[+] Running tests ..."
echo "========================================================"

# we dont have many test that require the TPM, so hardcode test paths here.
cd "$CWD/pkg/pillar/evetpm" && go test -v -coverprofile="evtpm.coverage.txt" -covermode=atomic
cd "$CWD/pkg/pillar/evetpm" && go test -v -coverprofile="evetpm.coverage.txt" -covermode=atomic
cd "$CWD/pkg/pillar/cmd/msrv" && go test -v -test.run ^TestTpmActivateCred$ -coverprofile="actcred.coverage.txt" -covermode=atomic
cd "$CWD/pkg/pillar/cmd/vcomlink" && go test -v -coverprofile="vcomlink.coverage.txt" -covermode=atomic

# we are done, kill the swtpm
kill $PID
Loading