Repository: google/go-tpm (go)

Issue:
tpm2: Sign with hash ticket
The TPM2_Sign command includes a validation hashcheck ticket parameter, but this isn't exposed via the tpm2.Sign function API. This is relevant to restricted keys (i.e., tpm2.FlagRestricted, which is included in tpm2.FlagSignerDefault), because they need to make sure the hashed data did not start with TPM_GENERATED_VALUE.

Pull request description:
Enable signing arbitrary data with attestation keys
Fixes #101 

This change update two functions in tpm2:

Hash now accepts the `hierarchy` parameter to TPM2_Hash, and returns `validation`.

Sign now accepts the `validation` parameter to TPM2_Sign. This allows a restricted signing key to sign arbitrary data (that does not begin with TPM_GENERATED_VALUE) if Hash is used to allow the TPM to perform this check on the data.

This change also adds warnings to the Hash function, pointing out that amounts of data over 1,024 bytes may not work on all TPMs.

Patch:
diff --git a/tpm2/tpm2.go b/tpm2/tpm2.go
index 70ccf8e..6753a2b 100644
--- a/tpm2/tpm2.go
+++ b/tpm2/tpm2.go
@@ -1381,18 +1381,30 @@ func NVReadLock(rw io.ReadWriter, owner, handle tpmutil.Handle, authString strin
 	return err
 }
 
-// Hash computes a hash of data in buf using the TPM.
-func Hash(rw io.ReadWriter, alg Algorithm, buf tpmutil.U16Bytes) ([]byte, error) {
-	resp, err := runCommand(rw, TagNoSessions, cmdHash, buf, alg, HandleNull)
-	if err != nil {
-		return nil, err
+// decodeHash unpacks a successful response to TPM2_Hash, returning the computed digest and
+// validation ticket.
+func decodeHash(resp []byte) ([]byte, *Ticket, error) {
+	var digest tpmutil.U16Bytes
+	var validation Ticket
+
+	buf := bytes.NewBuffer(resp)
+	if err := tpmutil.UnpackBuf(buf, &digest, &validation); err != nil {
+		return nil, nil, err
 	}
+	return digest, &validation, nil
+}
 
-	var digest tpmutil.U16Bytes
-	if _, err = tpmutil.Unpack(resp, &digest); err != nil {
-		return nil, err
+// Hash computes a hash of data in buf using TPM2_Hash, returning the computed
+// digest and validation ticket. The validation ticket serves as confirmation
+// from the TPM that the data in buf did not begin with TPM_GENERATED_VALUE.
+// NOTE: TPM2_Hash can only accept data up to MAX_DIGEST_BUFFER in size, which
+// is implementation-dependent, but guaranteed to be at least 1024 octets.
+func Hash(rw io.ReadWriter, alg Algorithm, buf tpmutil.U16Bytes, hierarchy tpmutil.Handle) (digest []byte, validation *Ticket, err error) {
+	resp, err := runCommand(rw, TagNoSessions, cmdHash, buf, alg, hierarchy)
+	if err != nil {
+		return nil, nil, err
 	}
-	return digest, nil
+	return decodeHash(resp)
 }
 
 // Startup initializes a TPM (usually done by the OS).
@@ -1407,7 +1419,16 @@ func Shutdown(rw io.ReadWriter, typ StartupType) error {
 	return err
 }
 
-func encodeSign(sessionHandle, key tpmutil.Handle, password string, digest tpmutil.U16Bytes, sigScheme *SigScheme) ([]byte, error) {
+// nullTicket is a hard-coded null ticket of type TPMT_TK_HASHCHECK.
+// It is for Sign commands that do not require the TPM to verify that the digest
+// is not from data that started with TPM_GENERATED_VALUE.
+var nullTicket = Ticket{
+	Type:      TagHashCheck,
+	Hierarchy: uint32(HandleNull),
+	Digest:    tpmutil.U16Bytes{},
+}
+
+func encodeSign(sessionHandle, key tpmutil.Handle, password string, digest tpmutil.U16Bytes, sigScheme *SigScheme, validation *Ticket) ([]byte, error) {
 	ha, err := tpmutil.Pack(key)
 	if err != nil {
 		return nil, err
@@ -1424,16 +1445,15 @@ func encodeSign(sessionHandle, key tpmutil.Handle, password string, digest tpmut
 	if err != nil {
 		return nil, err
 	}
-	hc, err := tpmutil.Pack(TagHashCheck)
-	if err != nil {
-		return nil, err
+	if validation == nil {
+		validation = &nullTicket
 	}
-	params, err := tpmutil.Pack(HandleNull, tpmutil.U16Bytes(nil))
+	v, err := tpmutil.Pack(validation)
 	if err != nil {
 		return nil, err
 	}
 
-	return concat(ha, auth, d, s, hc, params)
+	return concat(ha, auth, d, s, v)
 }
 
 func decodeSign(buf []byte) (*Signature, error) {
@@ -1447,8 +1467,11 @@ func decodeSign(buf []byte) (*Signature, error) {
 
 // SignWithSession computes a signature for digest using a given loaded key. Signature
 // algorithm depends on the key type. Used for keys with non-password authorization policies.
-func SignWithSession(rw io.ReadWriter, sessionHandle, key tpmutil.Handle, password string, digest []byte, sigScheme *SigScheme) (*Signature, error) {
-	cmd, err := encodeSign(sessionHandle, key, password, digest, sigScheme)
+// If 'key' references a Restricted Decryption key, 'validation' must be a valid hash verification
+// ticket from the TPM, which can be obtained by using Hash() to hash the data with the TPM.
+// If 'validation' is nil, a NULL ticket is passed to TPM2_Sign.
+func SignWithSession(rw io.ReadWriter, sessionHandle, key tpmutil.Handle, password string, digest []byte, validation *Ticket, sigScheme *SigScheme) (*Signature, error) {
+	cmd, err := encodeSign(sessionHandle, key, password, digest, sigScheme, validation)
 	if err != nil {
 		return nil, err
 	}
@@ -1461,8 +1484,11 @@ func SignWithSession(rw io.ReadWriter, sessionHandle, key tpmutil.Handle, passwo
 
 // Sign computes a signature for digest using a given loaded key. Signature
 // algorithm depends on the key type.
-func Sign(rw io.ReadWriter, key tpmutil.Handle, password string, digest []byte, sigScheme *SigScheme) (*Signature, error) {
-	return SignWithSession(rw, HandlePasswordSession, key, password, digest, sigScheme)
+// If 'key' references a Restricted Decryption key, 'validation' must be a valid hash verification
+// ticket from the TPM, which can be obtained by using Hash() to hash the data with the TPM.
+// If 'validation' is nil, a NULL ticket is passed to TPM2_Sign.
+func Sign(rw io.ReadWriter, key tpmutil.Handle, password string, digest []byte, validation *Ticket, sigScheme *SigScheme) (*Signature, error) {
+	return SignWithSession(rw, HandlePasswordSession, key, password, digest, validation, sigScheme)
 }
 
 func encodeCertify(parentAuth, ownerAuth string, object, signer tpmutil.Handle, qualifyingData tpmutil.U16Bytes) ([]byte, error) {


Test patch:
diff --git a/tpm2/encoding_test.go b/tpm2/encoding_test.go
index 22da5b2..9d969db 100644
--- a/tpm2/encoding_test.go
+++ b/tpm2/encoding_test.go
@@ -450,7 +450,7 @@ func TestSignEncode(t *testing.T) {
 			t.Fatal(err)
 		}
 		digest := []byte{0x01, 0x02, 0x03}
-		cmdBytes, err := encodeSign(HandlePasswordSession, tpmutil.Handle(0x80000001), defaultPassword, digest, nil)
+		cmdBytes, err := encodeSign(HandlePasswordSession, tpmutil.Handle(0x80000001), defaultPassword, digest, nil, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -468,7 +468,7 @@ func TestSignEncode(t *testing.T) {
 		cmdBytes, err := encodeSign(HandlePasswordSession, tpmutil.Handle(0x80000001), defaultPassword, digest, &SigScheme{
 			Alg:  AlgRSASSA,
 			Hash: AlgSHA256,
-		})
+		}, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
diff --git a/tpm2/test/benchmark_test.go b/tpm2/test/benchmark_test.go
index b9c0295..0f57b89 100644
--- a/tpm2/test/benchmark_test.go
+++ b/tpm2/test/benchmark_test.go
@@ -49,7 +49,7 @@ func BenchmarkRSA2048Signing(b *testing.B) {
 
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
-		if _, err := Sign(rw, signerHandle, defaultPassword, digest[:], pub.RSAParameters.Sign); err != nil {
+		if _, err := Sign(rw, signerHandle, defaultPassword, digest[:], nil, pub.RSAParameters.Sign); err != nil {
 			b.Fatalf("Signing failed: %v", err)
 		}
 	}
@@ -84,7 +84,7 @@ func BenchmarkECCNISTP256Signing(b *testing.B) {
 
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
-		if _, err := Sign(rw, signerHandle, defaultPassword, digest[:], pub.ECCParameters.Sign); err != nil {
+		if _, err := Sign(rw, signerHandle, defaultPassword, digest[:], nil, pub.ECCParameters.Sign); err != nil {
 			b.Fatalf("Signing failed: %v", err)
 		}
 	}
diff --git a/tpm2/test/tpm2_test.go b/tpm2/test/tpm2_test.go
index 4bf6d27..fda7fd0 100644
--- a/tpm2/test/tpm2_test.go
+++ b/tpm2/test/tpm2_test.go
@@ -349,16 +349,32 @@ func TestHash(t *testing.T) {
 	rw := openTPM(t)
 	defer rw.Close()
 
-	val := []byte("garmonbozia")
-	got, err := Hash(rw, AlgSHA256, val)
-	if err != nil {
-		t.Fatalf("Hash failed: %v", err)
-	}
-	want := sha256.Sum256(val)
+	run := func(t *testing.T, data []byte, hierarchy tpmutil.Handle, wantValidation bool) {
+		gotDigest, gotValidation, err := Hash(rw, AlgSHA256, data, hierarchy)
+		if err != nil {
+			t.Fatalf("Hash failed: %v", err)
+		}
+		wantDigest := sha256.Sum256(data)
 
-	if !bytes.Equal(got, want[:]) {
-		t.Errorf("Hash(%q) returned %x, want %x", val, got, want)
+		if !bytes.Equal(gotDigest, wantDigest[:]) {
+			t.Errorf("Hash(%q) returned digest %x, want %x", data, gotDigest, wantDigest)
+		}
+		if wantValidation && len(gotValidation.Digest) == 0 {
+			t.Errorf("Hash(%q) unexpectedly returned empty validation ticket", data)
+		}
+		if !wantValidation && len(gotValidation.Digest) != 0 {
+			t.Errorf("Hash(%q) unexpectedly returned non-empty validation ticket", data)
+		}
 	}
+	t.Run("Null hierarchy", func(t *testing.T) {
+		run(t, []byte("foobarbaz"), HandleNull, false)
+	})
+	t.Run("Owner hierarchy", func(t *testing.T) {
+		run(t, []byte("foobarbaz"), HandleOwner, true)
+	})
+	t.Run("Starts with TPM_GENERATED_VALUE", func(t *testing.T) {
+		run(t, []byte("\xffTCGbarbaz"), HandleOwner, false)
+	})
 }
 
 func skipOnUnsupportedAlg(t testing.TB, rw io.ReadWriter, alg Algorithm) {
@@ -623,20 +639,13 @@ func TestSign(t *testing.T) {
 
 		digest := sha256.Sum256([]byte("heyo"))
 
-		var scheme *SigScheme
-		if pub.RSAParameters != nil {
-			scheme = pub.RSAParameters.Sign
-		}
-		if pub.ECCParameters != nil {
-			scheme = pub.ECCParameters.Sign
-		}
-		sig, err := Sign(rw, signerHandle, defaultPassword, digest[:], scheme)
+		sig, err := Sign(rw, signerHandle, defaultPassword, digest[:], nil, nil)
 		if err != nil {
 			t.Fatalf("Sign failed: %s", err)
 		}
 		switch signerPub := signerPub.(type) {
 		case *rsa.PublicKey:
-			switch scheme.Alg {
+			switch pub.RSAParameters.Sign.Alg {
 			case AlgRSASSA:
 				if err := rsa.VerifyPKCS1v15(signerPub, crypto.SHA256, digest[:], sig.RSA.Signature); err != nil {
 					t.Errorf("Signature verification failed: %v", err)
@@ -646,7 +655,7 @@ func TestSign(t *testing.T) {
 					t.Errorf("Signature verification failed: %v", err)
 				}
 			default:
-				t.Errorf("unsupported signature algorithm 0x%x", scheme.Alg)
+				t.Errorf("unsupported signature algorithm 0x%x", pub.RSAParameters.Sign.Alg)
 			}
 		case *ecdsa.PublicKey:
 			if !ecdsa.Verify(signerPub, digest[:], sig.ECC.R, sig.ECC.S) {
@@ -700,6 +709,111 @@ func TestSign(t *testing.T) {
 	})
 }
 
+func TestSignWithAttestationKey(t *testing.T) {
+	rw := openTPM(t)
+	defer rw.Close()
+
+	run := func(t *testing.T, data []byte, wantErr bool, pub Public) {
+		signerHandle, signerPub, err := CreatePrimary(rw, HandleOwner, pcrSelection7, emptyPassword, defaultPassword, pub)
+		if err != nil {
+			t.Fatalf("CreatePrimary failed: %v", err)
+		}
+		defer FlushContext(rw, signerHandle)
+
+		digest, validation, err := Hash(rw, AlgSHA256, data, HandleOwner)
+		if err != nil {
+			t.Fatalf("Hash failed unexpectedly: %v", err)
+		}
+
+		sig, err := Sign(rw, signerHandle, defaultPassword, digest, validation, nil)
+		if err != nil && !wantErr {
+			t.Fatalf("Sign failed unexpectedly: %v", err)
+		}
+		if err == nil && wantErr {
+			t.Fatalf("Sign succeeded unexpectedly: %v", sig)
+		}
+		if !wantErr {
+			switch signerPub := signerPub.(type) {
+			case *rsa.PublicKey:
+				switch pub.RSAParameters.Sign.Alg {
+				case AlgRSASSA:
+					if err := rsa.VerifyPKCS1v15(signerPub, crypto.SHA256, digest[:], sig.RSA.Signature); err != nil {
+						t.Errorf("Signature verification failed: %v", err)
+					}
+				case AlgRSAPSS:
+					if err := rsa.VerifyPSS(signerPub, crypto.SHA256, digest[:], sig.RSA.Signature, nil); err != nil {
+						t.Errorf("Signature verification failed: %v", err)
+					}
+				default:
+					t.Errorf("unsupported signature algorithm 0x%x", pub.RSAParameters.Sign.Alg)
+				}
+			case *ecdsa.PublicKey:
+				if !ecdsa.Verify(signerPub, digest[:], sig.ECC.R, sig.ECC.S) {
+					t.Error("Signature verification failed")
+				}
+			}
+		}
+	}
+
+	rsassa := Public{
+		Type:       AlgRSA,
+		NameAlg:    AlgSHA256,
+		Attributes: FlagSign | FlagRestricted | FlagSensitiveDataOrigin | FlagUserWithAuth,
+		RSAParameters: &RSAParams{
+			Sign: &SigScheme{
+				Alg:  AlgRSASSA,
+				Hash: AlgSHA256,
+			},
+			KeyBits: 2048,
+		},
+	}
+	rsapss := Public{
+		Type:       AlgRSA,
+		NameAlg:    AlgSHA256,
+		Attributes: FlagSign | FlagRestricted | FlagSensitiveDataOrigin | FlagUserWithAuth,
+		RSAParameters: &RSAParams{
+			Sign: &SigScheme{
+				Alg:  AlgRSAPSS,
+				Hash: AlgSHA256,
+			},
+			KeyBits: 2048,
+		},
+	}
+	ecdsa := Public{
+		Type:       AlgECC,
+		NameAlg:    AlgSHA256,
+		Attributes: FlagSign | FlagRestricted | FlagSensitiveDataOrigin | FlagUserWithAuth,
+		ECCParameters: &ECCParams{
+			Sign: &SigScheme{
+				Alg:  AlgECDSA,
+				Hash: AlgSHA256,
+			},
+			CurveID: CurveNISTP256,
+		},
+	}
+
+	t.Run("RSA SSA", func(t *testing.T) {
+		run(t, []byte("test123"), false, rsassa)
+	})
+	t.Run("RSA PSS", func(t *testing.T) {
+		run(t, []byte("test123"), false, rsapss)
+	})
+	t.Run("ECC", func(t *testing.T) {
+		skipOnUnsupportedAlg(t, rw, AlgECC)
+		run(t, []byte("test123"), false, ecdsa)
+	})
+	t.Run("RSA SSA with TPM_GENERATED_VALUE", func(t *testing.T) {
+		run(t, []byte("\xffTCG123"), true, rsassa)
+	})
+	t.Run("RSA PSS with TPM_GENERATED_VALUE", func(t *testing.T) {
+		run(t, []byte("\xffTCG123"), true, rsapss)
+	})
+	t.Run("ECC with TPM_GENERATED_VALUE", func(t *testing.T) {
+		skipOnUnsupportedAlg(t, rw, AlgECC)
+		run(t, []byte("\xffTCG123"), true, ecdsa)
+	})
+}
+
 func TestPCREvent(t *testing.T) {
 	rw := openTPM(t)
 	defer rw.Close()