Attestation & Identity & Global Unlock & Enrolment

This changes the node startup sequence significantly. Now the following three startup procedures replace the old setup/join mechanic:
* If no enrolment config is present, automatically bootstrap a new cluster and become master for it.
* If an enrolment config with an enrolment token is present, register with the NodeManagementService.
* If an enrolment config without an enrolment token is present, attempt a normal cluster unlock.

It also completely revamps the GRPC management services:
* NodeManagementService is a master-only service that deals with other nodes and has a cluster-wide identity
* NodeService is only available in unlocked state and keyed with the node identity
* ClusterManagement is now a master-only service that's been spun out of the main NMS since they have very different authentication models and also deals with EnrolmentConfigs

The TPM support library has also been extended by:
* Lots of integrity attestation and verification functions
* Built-in AK management
* Some advanced policy-based authentication stuff

Also contains various enhancements to the network service to make everything work in a proper multi-node environment.

Lots of old code has been thrown out.

Test Plan: Passed a full manual test of all three startup modes (bootstrap, enrolment and normal unlock) including automated EnrolmentConfig generation and consumption in a dual-node configuration on swtpm / OVMF.

Bug: T499

X-Origin-Diff: phab/D291
GitOrigin-RevId: d53755c828218b1df83a1d7ad252c7b3231abca8
diff --git a/core/internal/storage/data.go b/core/internal/storage/data.go
index 80af4c9..2b2251a 100644
--- a/core/internal/storage/data.go
+++ b/core/internal/storage/data.go
@@ -18,13 +18,13 @@
 
 import (
 	"fmt"
-	"git.monogon.dev/source/nexantic.git/core/pkg/tpm"
 	"io/ioutil"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"sync"
 
+	"git.monogon.dev/source/nexantic.git/core/pkg/tpm"
 	"go.uber.org/zap"
 	"golang.org/x/sys/unix"
 )
@@ -65,72 +65,99 @@
 	manager.mutex.Lock()
 	defer manager.mutex.Unlock()
 
-	sealedKeyFile, err := os.Open(etcdSealedKeyLocation)
-	if os.IsNotExist(err) {
-		logger.Info("Initializing encrypted storage, this might take a while...")
-		go manager.initializeData()
-	} else if err != nil {
-		return nil, err
-	} else {
-		sealedKey, err := ioutil.ReadAll(sealedKeyFile)
-		sealedKeyFile.Close()
-		if err != nil {
-			return nil, err
-		}
-		key, err := tpm.Unseal(sealedKey)
-		if err != nil {
-			return nil, err
-		}
-		if err := MapEncryptedBlockDevice("data", SmalltownDataCryptPath, key); err != nil {
-			return nil, err
-		}
-		if err := manager.mountData(); err != nil {
-			return nil, err
-		}
-		logger.Info("Mounted encrypted storage")
-	}
 	return manager, nil
 }
 
-func (s *Manager) initializeData() {
-	key, err := tpm.GenerateSafeKey(256 / 8)
+var keySize uint16 = 256 / 8
+
+// MountData mounts the Smalltown data partition with the given global unlock key. It automatically
+// unseals the local unlock key from the TPM.
+func (s *Manager) MountData(globalUnlockKey []byte) error {
+	localPath, err := s.GetPathInPlace(PlaceESP, "local_unlock.bin")
 	if err != nil {
-		s.logger.Error("Failed to generate master key", zap.Error(err))
-		s.initializationError = fmt.Errorf("Failed to generate master key: %w", err)
-		return
+		return fmt.Errorf("failed to find ESP mount: %w", err)
 	}
-	sealedKey, err := tpm.Seal(key, tpm.FullSystemPCRs)
+	localUnlockBlob, err := ioutil.ReadFile(localPath)
 	if err != nil {
-		s.logger.Error("Failed to seal master key", zap.Error(err))
-		s.initializationError = fmt.Errorf("Failed to seal master key: %w", err)
-		return
+		return fmt.Errorf("failed to read local unlock file from ESP: %w", err)
 	}
+	localUnlockKey, err := tpm.Unseal(localUnlockBlob)
+	if err != nil {
+		return fmt.Errorf("failed to unseal local unlock key: %w", err)
+	}
+
+	key := make([]byte, keySize)
+	for i := uint16(0); i < keySize; i++ {
+		key[i] = localUnlockKey[i] ^ globalUnlockKey[i]
+	}
+
+	if err := MapEncryptedBlockDevice("data", SmalltownDataCryptPath, key); err != nil {
+		return err
+	}
+	if err := s.mountData(); err != nil {
+		return err
+	}
+	s.mutex.Lock()
+	s.dataReady = true
+	s.mutex.Unlock()
+	s.logger.Info("Mounted encrypted storage")
+	return nil
+}
+
+// InitializeData initializes the Smalltown data partition and returns the global unlock key. It seals
+// the local portion into the TPM and stores the blob on the ESP. This is a potentially slow
+// operation since it touches the whole partition.
+func (s *Manager) InitializeData() ([]byte, error) {
+	localUnlockKey, err := tpm.GenerateSafeKey(keySize)
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to generate safe key: %w", err)
+	}
+	globalUnlockKey, err := tpm.GenerateSafeKey(keySize)
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to generate safe key: %w", err)
+	}
+
+	localUnlockBlob, err := tpm.Seal(localUnlockKey, tpm.SecureBootPCRs)
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to seal local unlock key: %w", err)
+	}
+
+	// The actual key is generated by XORing together the localUnlockKey and the globalUnlockKey
+	// This provides us with a mathematical guarantee that the resulting key cannot be recovered
+	// whithout knowledge of both parts.
+	key := make([]byte, keySize)
+	for i := uint16(0); i < keySize; i++ {
+		key[i] = localUnlockKey[i] ^ globalUnlockKey[i]
+	}
+
 	if err := InitializeEncryptedBlockDevice("data", SmalltownDataCryptPath, key); err != nil {
 		s.logger.Error("Failed to initialize encrypted block device", zap.Error(err))
-		s.initializationError = fmt.Errorf("Failed to initialize encrypted block device: %w", err)
-		return
+		return []byte{}, fmt.Errorf("failed to initialize encrypted block device: %w", err)
 	}
 	mkfsCmd := exec.Command("/bin/mkfs.xfs", "-qf", "/dev/data")
 	if _, err := mkfsCmd.Output(); err != nil {
 		s.logger.Error("Failed to format encrypted block device", zap.Error(err))
-		s.initializationError = fmt.Errorf("Failed to format encrypted block device: %w", err)
-		return
-	}
-	// This file is the marker if the partition has
-	if err := ioutil.WriteFile(etcdSealedKeyLocation, sealedKey, 0600); err != nil {
-		panic(err)
+		return []byte{}, fmt.Errorf("failed to format encrypted block device: %w", err)
 	}
 
 	if err := s.mountData(); err != nil {
-		s.initializationError = err
-		return
+		return []byte{}, err
 	}
 
 	s.mutex.Lock()
 	s.dataReady = true
 	s.mutex.Unlock()
 
+	localPath, err := s.GetPathInPlace(PlaceESP, "local_unlock.bin")
+	if err != nil {
+		return []byte{}, fmt.Errorf("failed to find ESP mount: %w", err)
+	}
+	if err := ioutil.WriteFile(localPath, localUnlockBlob, 0600); err != nil {
+		return []byte{}, fmt.Errorf("failed to write local unlock file to ESP: %w", err)
+	}
+
 	s.logger.Info("Initialized encrypted storage")
+	return globalUnlockKey, nil
 }
 
 func (s *Manager) mountData() error {