m/p/tpm: use secretbox with seal/unseal for larger payloads

Natively the Seal/Unseal operation in the TPM 2.0 specification only
supports up to 128 bytes of payload. If you need to seal more than that
the specification tells you to generate and seal a key and use that to
encrypt and authenticate the rest of the data. This CL implements said
mechanism transparently as part of the Seal and Unseal functions using
a nacl-compatible secretbox as the authenticated encryption primitive.

Change-Id: I0a724b12aae5e5151d103b52ed13b71c864076ab
Reviewed-on: https://review.monogon.dev/c/monogon/+/626
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/tpm/BUILD.bazel b/metropolis/pkg/tpm/BUILD.bazel
index 1cafbf3..da2154a 100644
--- a/metropolis/pkg/tpm/BUILD.bazel
+++ b/metropolis/pkg/tpm/BUILD.bazel
@@ -11,12 +11,13 @@
     deps = [
         "//metropolis/pkg/logtree:go_default_library",
         "//metropolis/pkg/sysfs:go_default_library",
-        "@com_github_gogo_protobuf//proto:go_default_library",
+        "//metropolis/pkg/tpm/proto:go_default_library",
+        "@com_github_golang_protobuf//proto:go_default_library",
         "@com_github_google_go_tpm//tpm2:go_default_library",
         "@com_github_google_go_tpm//tpmutil:go_default_library",
-        "@com_github_google_go_tpm_tools//proto:go_default_library",
         "@com_github_google_go_tpm_tools//tpm2tools:go_default_library",
         "@com_github_pkg_errors//:go_default_library",
+        "@org_golang_x_crypto//nacl/secretbox:go_default_library",
         "@org_golang_x_sys//unix:go_default_library",
     ],
 )
diff --git a/metropolis/pkg/tpm/proto/BUILD.bazel b/metropolis/pkg/tpm/proto/BUILD.bazel
new file mode 100644
index 0000000..7d7ee86
--- /dev/null
+++ b/metropolis/pkg/tpm/proto/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "proto_proto",
+    srcs = ["tpm.proto"],
+    visibility = ["//visibility:public"],
+    deps = ["@com_github_google_go_tpm_tools//proto:proto_proto"],  #keep
+)
+
+go_proto_library(
+    name = "proto_go_proto",
+    importpath = "source.monogon.dev/metropolis/pkg/tpm/proto",
+    proto = ":proto_proto",
+    visibility = ["//visibility:public"],
+    deps = ["@com_github_google_go_tpm_tools//proto:go_default_library"],  #keep
+)
+
+go_library(
+    name = "go_default_library",
+    embed = [":proto_go_proto"],
+    importpath = "source.monogon.dev/metropolis/pkg/tpm/proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/pkg/tpm/proto/tpm.proto b/metropolis/pkg/tpm/proto/tpm.proto
new file mode 100644
index 0000000..29b74aa
--- /dev/null
+++ b/metropolis/pkg/tpm/proto/tpm.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+option go_package = "source.monogon.dev/metropolis/pkg/tpm/proto";
+package metropolis.pkg.tpm;
+
+import "proto/tpm.proto";
+
+// ExtendedSealedBytes contains data sealed by a TPM2 via an indirection to
+// allow for more than 128 bytes of payload. It seals an ephemeral key for
+// a nacl secretbox in the TPM and stores the encrypted box next to the sealed
+// key.
+message ExtendedSealedBytes {
+  // The secretbox key, as sealed by the TPM.
+  .proto.SealedBytes sealed_key = 1;
+  // The encrypted box contents.
+  bytes encrypted_payload = 2;
+}
\ No newline at end of file
diff --git a/metropolis/pkg/tpm/tpm.go b/metropolis/pkg/tpm/tpm.go
index de9b0d8..fe7c698 100644
--- a/metropolis/pkg/tpm/tpm.go
+++ b/metropolis/pkg/tpm/tpm.go
@@ -31,16 +31,17 @@
 	"sync"
 	"time"
 
-	"github.com/gogo/protobuf/proto"
-	tpmpb "github.com/google/go-tpm-tools/proto"
+	"github.com/golang/protobuf/proto"
 	"github.com/google/go-tpm-tools/tpm2tools"
 	"github.com/google/go-tpm/tpm2"
 	"github.com/google/go-tpm/tpmutil"
 	"github.com/pkg/errors"
+	"golang.org/x/crypto/nacl/secretbox"
 	"golang.org/x/sys/unix"
 
 	"source.monogon.dev/metropolis/pkg/logtree"
 	"source.monogon.dev/metropolis/pkg/sysfs"
+	tpmpb "source.monogon.dev/metropolis/pkg/tpm/proto"
 )
 
 var (
@@ -226,22 +227,39 @@
 // Seal seals sensitive data and only allows access if the current platform
 // configuration in matches the one the data was sealed on.
 func Seal(data []byte, pcrs []int) ([]byte, error) {
+	// Generate a key and use secretbox to encrypt and authenticate the actual
+	// payload as go-tpm2 uses a raw seal operation limiting payload size to
+	// 128 bytes which is insufficient.
+	boxKey, err := GenerateSafeKey(32)
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to generate boxKey: %w", err)
+	}
 	lock.Lock()
 	defer lock.Unlock()
-	if tpm == nil {
-		return []byte{}, ErrNotInitialized
-	}
 	srk, err := tpm2tools.StorageRootKeyRSA(tpm.device)
 	if err != nil {
 		return []byte{}, errors.Wrap(err, "failed to load TPM SRK")
 	}
 	defer srk.Close()
-	sealedKey, err := srk.Seal(pcrs, data)
-	sealedKeyRaw, err := proto.Marshal(sealedKey)
+	var boxKeyArr [32]byte
+	copy(boxKeyArr[:], boxKey)
+	// Nonce is not used as we're generating a new boxKey for every operation,
+	// therefore we can just leave it all-zero.
+	var unusedNonce [24]byte
+	encryptedData := secretbox.Seal(nil, data, &unusedNonce, &boxKeyArr)
+	sealedKey, err := srk.Seal(pcrs, boxKey)
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to seal boxKey: %w", err)
+	}
+	sealedBytes := tpmpb.ExtendedSealedBytes{
+		SealedKey:        sealedKey,
+		EncryptedPayload: encryptedData,
+	}
+	rawSealedBytes, err := proto.Marshal(&sealedBytes)
 	if err != nil {
 		return []byte{}, errors.Wrapf(err, "failed to marshal sealed data")
 	}
-	return sealedKeyRaw, nil
+	return rawSealedBytes, nil
 }
 
 // Unseal unseals sensitive data if the current platform configuration allows
@@ -258,21 +276,31 @@
 	}
 	defer srk.Close()
 
-	var sealedKey tpmpb.SealedBytes
-	if err := proto.Unmarshal(data, &sealedKey); err != nil {
-		return []byte{}, errors.Wrap(err, "failed to decode sealed data")
+	var sealedBytes tpmpb.ExtendedSealedBytes
+	if err := proto.Unmarshal(data, &sealedBytes); err != nil {
+		return []byte{}, errors.Wrap(err, "failed to unmarshal sealed data")
 	}
 	// Logging this for auditing purposes
 	pcrList := []string{}
-	for _, pcr := range sealedKey.Pcrs {
+	for _, pcr := range sealedBytes.SealedKey.Pcrs {
 		pcrList = append(pcrList, string(pcr))
 	}
-	tpm.logger.Infof("Attempting to unseal data protected with PCRs %s", strings.Join(pcrList, ","))
-	unsealedData, err := srk.Unseal(&sealedKey)
+	tpm.logger.Infof("Attempting to unseal key protected with PCRs %s", strings.Join(pcrList, ","))
+	unsealedKey, err := srk.Unseal(sealedBytes.SealedKey)
 	if err != nil {
-		return []byte{}, errors.Wrap(err, "failed to unseal data")
+		return []byte{}, errors.Wrap(err, "failed to unseal key")
 	}
-	return unsealedData, nil
+	var key [32]byte
+	if len(unsealedKey) != len(key) {
+		return []byte{}, fmt.Errorf("unsealed key has wrong length: expected %v bytes, got %v", len(key), len(unsealedKey))
+	}
+	copy(key[:], unsealedKey)
+	var unusedNonce [24]byte
+	payload, ok := secretbox.Open(nil, sealedBytes.EncryptedPayload, &unusedNonce, &key)
+	if !ok {
+		return []byte{}, errors.New("payload box cannot be opened")
+	}
+	return payload, nil
 }
 
 // Standard AK template for RSA2048 non-duplicatable restricted signing for