Repository: google/go-tpm
Pull request #345 (2023-09-13 22:30:05)

Introduce helper for formatting PCR selection bitmasks
This change introduces PCClientCompatible.PCRs(), a function that converts a variadic list of PCR indices (as ints) into a PCR selection bitmask. Because of the vagaries of TPM:

1. That the minimum size of a PCR selection bitmask is not 0, but related to the minimum number of PCRs specified by the profile
2. That the PC Client Platform TPM Profile specification mandates a minimum but not a maximum number of implementation PCR,

this change creates an interface that could be implemented for other TPM profiles that specify different amounts of PCRs. The vast majority of on-market TPMs will just work with PCClientCompatible.PCRs, even if they implement more than 24 PCRs.

PCRs() can panic if given invalid values; this is to allow it to be inlined into the definition of a structure that needs a PCR selection.


Patch:
diff --git a/tpm2/pcrs.go b/tpm2/pcrs.go
new file mode 100644
index 0000000..3a250c5
--- /dev/null
+++ b/tpm2/pcrs.go
@@ -0,0 +1,55 @@
+package tpm2
+
+// pcrSelectionFormatter is a Platform TPM Profile-specific interface for
+// formatting TPM PCR selections.
+// This interface isn't (yet) part of the go-tpm public interface. After we
+// add a second implementation, we should consider making it public.
+type pcrSelectionFormatter interface {
+	// PCRs returns the TPM PCR selection bitmask associated with the given PCR indices.
+	PCRs(pcrs ...uint) []byte
+}
+
+// PCClientCompatible is a pcrSelectionFormatter that formats PCR selections
+// suitable for use in PC Client PTP-compatible TPMs (the vast majority):
+// https://trustedcomputinggroup.org/resource/pc-client-platform-tpm-profile-ptp-specification/
+// PC Client mandates at least 24 PCRs but does not provide an upper limit.
+var PCClientCompatible pcrSelectionFormatter = pcClient{}
+
+type pcClient struct{}
+
+// The TPM requires all PCR selections to be at least big enough to select all
+// the PCRs in the minimum PCR allocation.
+const pcClientMinimumPCRCount = 24
+
+func (pcClient) PCRs(pcrs ...uint) []byte {
+	// Find the biggest PCR we selected.
+	maxPCR := uint(0)
+	for _, pcr := range pcrs {
+		if pcr > maxPCR {
+			maxPCR = pcr
+		}
+	}
+	selectionSize := maxPCR/8 + 1
+
+	// Enforce the minimum PCR selection size.
+	if selectionSize < (pcClientMinimumPCRCount / 8) {
+		selectionSize = (pcClientMinimumPCRCount / 8)
+	}
+
+	// Allocate a byte array to store the bitfield, that has at least
+	// enough bits to store our selections.
+	selection := make([]byte, selectionSize)
+	for _, pcr := range pcrs {
+		// The PCR selection mask is byte-wise little-endian:
+		//   select[0] contains bits representing the selection of PCRs 0 through 7
+		//   select[1] contains PCRs 8 through 15, and so on.
+		byteIdx := pcr / 8
+		// Within the byte, the PCR selection is bit-wise big-endian:
+		//   bit 0 of select[0] contains the selection of PCR 0
+		//   bit 1 of select[0] contains the selection of PCR 1, and so on.
+		bitIdx := pcr % 8
+
+		selection[byteIdx] |= (1 << bitIdx)
+	}
+	return selection
+}


Test patch:
diff --git a/tpm2/test/certify_test.go b/tpm2/test/certify_test.go
index d70a4bc..41955e9 100644
--- a/tpm2/test/certify_test.go
+++ b/tpm2/test/certify_test.go
@@ -21,10 +21,6 @@ func TestCertify(t *testing.T) {
 
 	Auth := []byte("password")
 
-	PCR7, err := CreatePCRSelection([]int{7})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
 	public := New2B(TPMTPublic{
 		Type:    TPMAlgRSA,
 		NameAlg: TPMAlgSHA256,
@@ -58,7 +54,7 @@ func TestCertify(t *testing.T) {
 		PCRSelections: []TPMSPCRSelection{
 			{
 				Hash:      TPMAlgSHA256,
-				PCRSelect: PCR7,
+				PCRSelect: PCClientCompatible.PCRs(7),
 			},
 		},
 	}
@@ -211,15 +207,11 @@ func TestCreateAndCertifyCreation(t *testing.T) {
 		),
 	})
 
-	PCR7, err := CreatePCRSelection([]int{7})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
 	pcrSelection := TPMLPCRSelection{
 		PCRSelections: []TPMSPCRSelection{
 			{
 				Hash:      TPMAlgSHA1,
-				PCRSelect: PCR7,
+				PCRSelect: PCClientCompatible.PCRs(7),
 			},
 		},
 	}
diff --git a/tpm2/test/combined_context_test.go b/tpm2/test/combined_context_test.go
index 3640431..d7cd40c 100644
--- a/tpm2/test/combined_context_test.go
+++ b/tpm2/test/combined_context_test.go
@@ -30,11 +30,6 @@ func TestCombinedContext(t *testing.T) {
 	}
 	defer thetpm.Close()
 
-	PCR7, err := CreatePCRSelection([]int{7})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
-
 	createPrimary := CreatePrimary{
 		PrimaryHandle: TPMRHOwner,
 
@@ -67,7 +62,7 @@ func TestCombinedContext(t *testing.T) {
 			PCRSelections: []TPMSPCRSelection{
 				{
 					Hash:      TPMAlgSHA1,
-					PCRSelect: PCR7,
+					PCRSelect: PCClientCompatible.PCRs(7),
 				},
 			},
 		},
diff --git a/tpm2/test/pcr_test.go b/tpm2/test/pcr_test.go
index 6421213..09a2bc9 100644
--- a/tpm2/test/pcr_test.go
+++ b/tpm2/test/pcr_test.go
@@ -12,6 +12,54 @@ import (
 	"github.com/google/go-tpm/tpm2/transport/simulator"
 )
 
+func TestPCRs(t *testing.T) {
+	for i, tc := range []struct {
+		pcrs       []uint
+		wantSelect []byte
+	}{
+		{
+			pcrs:       nil,
+			wantSelect: []byte{0x00, 0x00, 0x00},
+		},
+		{
+			pcrs:       []uint{0},
+			wantSelect: []byte{0x01, 0x00, 0x00},
+		},
+		{
+			pcrs:       []uint{0, 1, 2},
+			wantSelect: []byte{0x07, 0x00, 0x00},
+		},
+		{
+			pcrs:       []uint{0, 7},
+			wantSelect: []byte{0x81, 0x00, 0x00},
+		},
+		{
+			pcrs:       []uint{8},
+			wantSelect: []byte{0x00, 0x01, 0x00},
+		},
+		{
+			pcrs:       []uint{1, 8, 9},
+			wantSelect: []byte{0x02, 0x03, 0x00},
+		},
+		{
+			pcrs:       []uint{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23},
+			wantSelect: []byte{0xff, 0xff, 0xff},
+		},
+		{
+			pcrs: []uint{255},
+			wantSelect: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
+		},
+	} {
+		t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
+			selection := PCClientCompatible.PCRs(tc.pcrs...)
+			if !bytes.Equal(selection, tc.wantSelect) {
+				t.Errorf("PCRs() = 0x%x, want 0x%x", selection, tc.wantSelect)
+			}
+		})
+	}
+}
+
 var extendstpm2 = map[TPMAlgID][]struct {
 	digest []byte
 }{
@@ -45,7 +93,7 @@ func TestPCRReset(t *testing.T) {
 	}
 	defer thetpm.Close()
 
-	DebugPCR := 16
+	DebugPCR := uint(16)
 
 	cases := []struct {
 		name    string
@@ -58,11 +106,6 @@ func TestPCRReset(t *testing.T) {
 
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
-			PCRs, err := CreatePCRSelection([]int{DebugPCR})
-			if err != nil {
-				t.Fatalf("Failed to create PCRSelection")
-			}
-
 			authHandle := AuthHandle{
 				Handle: TPMHandle(DebugPCR),
 				Auth:   PasswordAuth(nil),
@@ -73,7 +116,7 @@ func TestPCRReset(t *testing.T) {
 					PCRSelections: []TPMSPCRSelection{
 						{
 							Hash:      c.hashalg,
-							PCRSelect: PCRs,
+							PCRSelect: PCClientCompatible.PCRs(DebugPCR),
 						},
 					},
 				},
@@ -146,17 +189,12 @@ func TestPCREvent(t *testing.T) {
 		t.Run(c.name, func(t *testing.T) {
 			for i := 0; i < 17; i++ {
 				t.Run(fmt.Sprintf("PCR%02d", i), func(t *testing.T) {
-					PCRs, err := CreatePCRSelection([]int{i})
-					if err != nil {
-						t.Fatalf("Failed to create PCRSelection")
-					}
-
 					pcrRead := PCRRead{
 						PCRSelectionIn: TPMLPCRSelection{
 							PCRSelections: []TPMSPCRSelection{
 								{
 									Hash:      c.hashalg,
-									PCRSelect: PCRs,
+									PCRSelect: PCClientCompatible.PCRs(20),
 								},
 							},
 						},
diff --git a/tpm2/test/policy_test.go b/tpm2/test/policy_test.go
index f3dc478..9720f45 100644
--- a/tpm2/test/policy_test.go
+++ b/tpm2/test/policy_test.go
@@ -366,16 +366,11 @@ func TestPolicyPCR(t *testing.T) {
 	}
 	defer thetpm.Close()
 
-	PCRs, err := CreatePCRSelection([]int{0, 1, 2, 3, 7})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
-
 	selection := TPMLPCRSelection{
 		PCRSelections: []TPMSPCRSelection{
 			{
 				Hash:      TPMAlgSHA1,
-				PCRSelect: PCRs,
+				PCRSelect: PCClientCompatible.PCRs(0, 1, 2, 3, 7),
 			},
 		},
 	}
diff --git a/tpm2/test/sign_test.go b/tpm2/test/sign_test.go
index 75c8557..c12caa2 100644
--- a/tpm2/test/sign_test.go
+++ b/tpm2/test/sign_test.go
@@ -4,55 +4,12 @@ import (
 	"crypto"
 	"crypto/rsa"
 	"crypto/sha256"
-	"fmt"
 	"testing"
 
-	"github.com/google/go-cmp/cmp"
-
 	. "github.com/google/go-tpm/tpm2"
 	"github.com/google/go-tpm/tpm2/transport/simulator"
-	"github.com/google/go-tpm/tpmutil"
 )
 
-func CreatePCRSelection(s []int) ([]byte, error) {
-
-	const sizeOfPCRSelect = 3
-
-	PCRs := make(tpmutil.RawBytes, sizeOfPCRSelect)
-
-	for _, n := range s {
-		if n >= 8*sizeOfPCRSelect {
-			return nil, fmt.Errorf("PCR index %d is out of range (exceeds maximum value %d)", n, 8*sizeOfPCRSelect-1)
-		}
-		byteNum := n / 8
-		bytePos := byte(1 << (n % 8))
-		PCRs[byteNum] |= bytePos
-	}
-
-	return PCRs, nil
-}
-
-func TestCreatePCRSelection(t *testing.T) {
-
-	emptyTest, err := CreatePCRSelection([]int{})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
-
-	if !cmp.Equal(emptyTest, []byte{0, 0, 0}) {
-		t.Fatalf("emptyTest does not return valid PCRs")
-	}
-
-	filledTest, err := CreatePCRSelection([]int{0, 1, 2})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
-
-	if !cmp.Equal(filledTest, []byte{7, 0, 0}) {
-		t.Fatalf("filledTest does not return valid PCRs")
-	}
-}
-
 func TestSign(t *testing.T) {
 
 	thetpm, err := simulator.OpenSimulator()
@@ -61,11 +18,6 @@ func TestSign(t *testing.T) {
 	}
 	defer thetpm.Close()
 
-	PCR7, err := CreatePCRSelection([]int{7})
-	if err != nil {
-		t.Fatalf("Failed to create PCRSelection")
-	}
-
 	createPrimary := CreatePrimary{
 		PrimaryHandle: TPMRHOwner,
 
@@ -99,7 +51,7 @@ func TestSign(t *testing.T) {
 			PCRSelections: []TPMSPCRSelection{
 				{
 					Hash:      TPMAlgSHA1,
-					PCRSelect: PCR7,
+					PCRSelect: PCClientCompatible.PCRs(7),
 				},
 			},
 		},