Initial operating system work

Adds a draft for most of the operating system work, sans external things like EDK2 and kernel build which will be pushed later in a separate diff.

* Sealing/Unsealing of encrypted and integrity-protected data partition using TPM2
* PID1 standard behaviour (mounting minimal filesystems, cleaning up orphans)
* TPM2.0 helper library
* Block device finding and mounting

Test Plan: Manually tested, CI will be dealt with later.

X-Origin-Diff: phab/D157
GitOrigin-RevId: 6fc494f50cab1f081c3d352677158c009f4d7990
diff --git a/cmd/init/main.go b/cmd/init/main.go
new file mode 100644
index 0000000..1477109
--- /dev/null
+++ b/cmd/init/main.go
@@ -0,0 +1,158 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"os/signal"
+	node2 "smalltown/internal/node"
+	"smalltown/internal/storage"
+	"smalltown/pkg/tpm"
+	"syscall"
+
+	"go.uber.org/zap"
+	"golang.org/x/sys/unix"
+)
+
+func main() {
+	logger, err := zap.NewDevelopment()
+	if err != nil {
+		panic(err)
+	}
+	logger.Info("Starting Smalltown Init")
+
+	// Set up bare minimum mounts
+	if err := os.Mkdir("/sys", 0755); err != nil {
+		panic(err)
+	}
+	if err := unix.Mount("sysfs", "/sys", "sysfs", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil {
+		panic(err)
+	}
+
+	if err := os.Mkdir("/proc", 0755); err != nil {
+		panic(err)
+	}
+	if err := unix.Mount("procfs", "/proc", "proc", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil {
+		panic(err)
+	}
+
+	signalChannel := make(chan os.Signal, 2)
+	signal.Notify(signalChannel)
+
+	if err := storage.FindPartitions(); err != nil {
+		logger.Panic("Failed to search for partitions", zap.Error(err))
+	}
+
+	if err := os.Mkdir("/esp", 0755); err != nil {
+		panic(err)
+	}
+
+	if err := unix.Mount(storage.ESPDevicePath, "/esp", "vfat", unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_SYNC, ""); err != nil {
+		logger.Panic("Failed to mount ESP partition", zap.Error(err))
+	}
+
+	if err := tpm.Initialize(logger.With(zap.String("component", "tpm"))); err != nil {
+		logger.Panic("Failed to initialize TPM 2.0", zap.Error(err))
+	}
+
+	// TODO(lorenz): This really doesn't belong here and needs to be asynchronous as well
+	var keyLocation = "/esp/EFI/smalltown/data-key.bin"
+	sealedKeyFile, err := os.Open(keyLocation)
+	if os.IsNotExist(err) {
+		logger.Info("Initializing encrypted storage, this might take a while...")
+		key, err := tpm.GenerateSafeKey(256 / 8)
+		if err != nil {
+			panic(err)
+		}
+		sealedKey, err := tpm.Seal(key, tpm.SecureBootPCRs)
+		if err != nil {
+			panic(err)
+		}
+		if err := storage.InitializeEncryptedBlockDevice("data", storage.SmalltownDataCryptPath, key); err != nil {
+			panic(err)
+		}
+		mkfsCmd := exec.Command("/bin/mkfs.xfs", "-qf", "/dev/data")
+		if _, err := mkfsCmd.Output(); err != nil {
+			panic(err)
+		}
+		// Existence of this file indicates that the encrypted storage has been successfully initialized
+		if err := ioutil.WriteFile(keyLocation, sealedKey, 0600); err != nil {
+			panic(err)
+		}
+		logger.Info("Initialized encrypted storage")
+	} else if err != nil {
+		panic(err)
+	} else {
+		sealedKey, err := ioutil.ReadAll(sealedKeyFile)
+		if err != nil {
+			panic(err)
+		}
+		key, err := tpm.Unseal(sealedKey)
+		if err != nil {
+			panic(err)
+		}
+		if err := storage.MapEncryptedBlockDevice("data", storage.SmalltownDataCryptPath, key); err != nil {
+			panic(err)
+		}
+		logger.Info("Opened encrypted storage")
+	}
+	sealedKeyFile.Close()
+
+	if err := os.Mkdir("/data", 0755); err != nil {
+		panic(err)
+	}
+
+	if err := unix.Mount("/dev/data", "/data", "xfs", unix.MS_NOEXEC|unix.MS_NODEV, ""); err != nil {
+		panic(err)
+	}
+
+	node, err := node2.NewSmalltownNode(logger, "/esp/EFI/smalltown", "/data", 7833, 7834)
+	if err != nil {
+		panic(err)
+	}
+
+	err = node.Start()
+	if err != nil {
+		panic(err)
+	}
+
+	// We're PID1, so orphaned processes get reparented to us to clean up
+	for {
+		sig := <-signalChannel
+		switch sig {
+		case unix.SIGCHLD:
+			var status unix.WaitStatus
+			var rusage unix.Rusage
+			for {
+				res, err := unix.Wait4(-1, &status, syscall.WNOHANG, &rusage)
+				if err != nil {
+					logger.Error("Failed to wait on orphaned child", zap.Error(err))
+					break
+				}
+				if res <= 0 {
+					break
+				}
+			}
+		// TODO(lorenz): We can probably get more than just SIGCHLD as init, but I can't think
+		// of any others right now, just log them in case we hit any of them.
+		default:
+			logger.Warn("Got unexpected signal", zap.String("signal", sig.String()))
+		}
+	}
+}
diff --git a/cmd/mkimage/main.go b/cmd/mkimage/main.go
new file mode 100644
index 0000000..e6c9c58
--- /dev/null
+++ b/cmd/mkimage/main.go
@@ -0,0 +1,128 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"smalltown/generated/common"
+	"smalltown/internal/config"
+
+	"github.com/diskfs/go-diskfs"
+	"github.com/diskfs/go-diskfs/disk"
+	"github.com/diskfs/go-diskfs/filesystem"
+	"github.com/diskfs/go-diskfs/partition/gpt"
+	"github.com/naoina/toml"
+)
+
+var SmalltownDataPartition gpt.Type = gpt.Type("9eeec464-6885-414a-b278-4305c51f7966")
+
+func mibToSectors(size uint64) uint64 {
+	return (size * 1024 * 1024) / 512
+}
+
+var cfg = config.Config{
+	NodeName:      "smalltown-testing",
+	DataDirectory: "/data",
+	ExternalHost:  "",
+	TrustBackend:  common.TrustBackend_DUMMY,
+}
+
+func main() {
+	os.Remove("smalltown.img")
+	diskImg, err := diskfs.Create("smalltown.img", 3*1024*1024*1024, diskfs.Raw)
+	if err != nil {
+		fmt.Printf("Failed to create disk: %v", err)
+		os.Exit(1)
+	}
+
+	table := &gpt.Table{
+		// This is appropriate at least for virtio disks. Might need to be adjusted for real ones.
+		LogicalSectorSize:  512,
+		PhysicalSectorSize: 512,
+		ProtectiveMBR:      true,
+		Partitions: []*gpt.Partition{
+			{
+				Type:  gpt.EFISystemPartition,
+				Name:  "ESP",
+				Start: mibToSectors(1),
+				End:   mibToSectors(128) - 1,
+			},
+			{
+				Type:  SmalltownDataPartition,
+				Name:  "SIGNOS-DATA",
+				Start: mibToSectors(128),
+				End:   mibToSectors(2560) - 1,
+			},
+		},
+	}
+	if err := diskImg.Partition(table); err != nil {
+		fmt.Printf("Failed to apply partition table: %v", err)
+		os.Exit(1)
+	}
+
+	fs, err := diskImg.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32, VolumeLabel: "ESP"})
+	if err != nil {
+		fmt.Printf("Failed to create filesystem: %v", err)
+		os.Exit(1)
+	}
+	if err := fs.Mkdir("/EFI"); err != nil {
+		panic(err)
+	}
+	if err := fs.Mkdir("/EFI/BOOT"); err != nil {
+		panic(err)
+	}
+	if err := fs.Mkdir("/EFI/smalltown"); err != nil {
+		panic(err)
+	}
+	efiPayload, err := fs.OpenFile("/EFI/BOOT/BOOTX64.EFI", os.O_CREATE|os.O_RDWR)
+	if err != nil {
+		fmt.Printf("Failed to open EFI payload for writing: %v", err)
+		os.Exit(1)
+	}
+	efiPayloadSrc, err := os.Open(os.Args[1])
+	if err != nil {
+		fmt.Printf("Failed to open EFI payload for reading: %v", err)
+		os.Exit(1)
+	}
+	defer efiPayloadSrc.Close()
+	// If this is streamed (e.g. using io.Copy) it exposes a bug in diskfs, so do it in one go.
+	efiPayloadFull, err := ioutil.ReadAll(efiPayloadSrc)
+	if err != nil {
+		panic(err)
+	}
+	if _, err := efiPayload.Write(efiPayloadFull); err != nil {
+		fmt.Printf("Failed to write EFI payload: %v", err)
+		os.Exit(1)
+	}
+	configFile, err := fs.OpenFile("/EFI/smalltown/config.toml", os.O_CREATE|os.O_RDWR)
+	if err != nil {
+		fmt.Printf("Failed to open config for writing: %v", err)
+		os.Exit(1)
+	}
+	configData, _ := toml.Marshal(cfg)
+	if _, err := configFile.Write(configData); err != nil {
+		fmt.Printf("Failed to write config: %v", err)
+		os.Exit(1)
+	}
+	if err := diskImg.File.Close(); err != nil {
+		fmt.Printf("Failed to write image: %v", err)
+		os.Exit(1)
+	}
+	fmt.Println("Success! You can now boot smalltown.img")
+}
diff --git a/pkg/devicemapper/devicemapper.go b/pkg/devicemapper/devicemapper.go
index ef101de..dec6260 100644
--- a/pkg/devicemapper/devicemapper.go
+++ b/pkg/devicemapper/devicemapper.go
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package main
+package devicemapper
 
 import (
 	"bytes"
@@ -138,7 +138,16 @@
 func getFd() (uintptr, error) {
 	if fd == 0 {
 		f, err := os.Open("/dev/mapper/control")
-		if err != nil {
+		if os.IsNotExist(err) {
+			os.MkdirAll("/dev/mapper", 0755)
+			if err := unix.Mknod("/dev/mapper/control", unix.S_IFCHR|0600, int(unix.Mkdev(10, 236))); err != nil {
+				return 0, err
+			}
+			f, err = os.Open("/dev/mapper/control")
+			if err != nil {
+				return 0, err
+			}
+		} else if err != nil {
 			return 0, err
 		}
 		fd = f.Fd()
@@ -271,15 +280,15 @@
 func CreateActiveDevice(name string, targets []Target) (uint64, error) {
 	dev, err := CreateDevice(name)
 	if err != nil {
-		return 0, errors.Wrap(err, "DM_DEV_CREATE failed")
+		return 0, fmt.Errorf("DM_DEV_CREATE failed: %w", err)
 	}
 	if err := LoadTable(name, targets); err != nil {
 		RemoveDevice(name)
-		return 0, errors.Wrap(err, "DM_TABLE_LOAD failed")
+		return 0, fmt.Errorf("DM_TABLE_LOAD failed: %w", err)
 	}
 	if err := Resume(name); err != nil {
 		RemoveDevice(name)
-		return 0, errors.Wrap(err, "DM_DEV_SUSPEND failed")
+		return 0, fmt.Errorf("DM_DEV_SUSPEND failed: %w", err)
 	}
 	return dev, nil
 }
diff --git a/pkg/tpm/tpm.go b/pkg/tpm/tpm.go
new file mode 100644
index 0000000..0f5719b
--- /dev/null
+++ b/pkg/tpm/tpm.go
@@ -0,0 +1,197 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tpm
+
+import (
+	"crypto/rand"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"smalltown/pkg/sysfs"
+	"strconv"
+	"sync"
+
+	"github.com/gogo/protobuf/proto"
+	tpmpb "github.com/google/go-tpm-tools/proto"
+	"github.com/google/go-tpm-tools/tpm2tools"
+	"github.com/google/go-tpm/tpm2"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+	"golang.org/x/sys/unix"
+)
+
+var (
+	// SecureBootPCRs are all PCRs that measure the current Secure Boot configuration
+	SecureBootPCRs = []int{7}
+
+	// FirmwarePCRs are alle PCRs that contain the firmware measurements
+	// See https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf
+	FirmwarePCRs = []int{0, 2, 3}
+
+	// FullSystemPCRs are all PCRs that contain any measurements up to the currently running EFI
+	// payload.
+	FullSystemPCRs = []int{0, 1, 2, 3, 4}
+)
+
+var (
+	// ErrNotExists is returned when no TPMs are available in the system
+	ErrNotExists = errors.New("no TPMs found")
+	// ErrNotInitialized is returned when this package was not initialized successfully
+	ErrNotInitialized = errors.New("no TPM was initialized")
+)
+
+// Singleton since the TPM is too
+var tpm *TPM
+
+// We're serializing all TPM operations since it has a limited number of handles and recovering
+// if it runs out is difficult to implement correctly. Might also be marginally more secure.
+var lock sync.Mutex
+
+// TPM represents a high-level interface to a connected TPM 2.0
+type TPM struct {
+	logger *zap.Logger
+	device io.ReadWriteCloser
+}
+
+// Initialize finds and opens the TPM (if any). If there is no TPM available it returns
+// ErrNotExists
+func Initialize(logger *zap.Logger) error {
+	lock.Lock()
+	defer lock.Unlock()
+	tpmDir, err := os.Open("/sys/class/tpm")
+	if err != nil {
+		return errors.Wrap(err, "failed to open sysfs TPM class")
+	}
+	defer tpmDir.Close()
+
+	tpms, err := tpmDir.Readdirnames(2)
+	if err != nil {
+		return errors.Wrap(err, "failed to read TPM device class")
+	}
+
+	if len(tpms) == 0 {
+		return ErrNotExists
+	}
+	if len(tpms) > 1 {
+		logger.Warn("Found more than one TPM, using the first one")
+	}
+	tpmName := tpms[0]
+	ueventData, err := sysfs.ReadUevents(filepath.Join("/sys/class/tpm", tpmName, "uevent"))
+	majorDev, err := strconv.Atoi(ueventData["MAJOR"])
+	if err != nil {
+		return fmt.Errorf("failed to convert uevent: %w", err)
+	}
+	minorDev, err := strconv.Atoi(ueventData["MINOR"])
+	if err != nil {
+		return fmt.Errorf("failed to convert uevent: %w", err)
+	}
+	if err := unix.Mknod("/dev/tpm", 0600|unix.S_IFCHR, int(unix.Mkdev(uint32(majorDev), uint32(minorDev)))); err != nil {
+		return errors.Wrap(err, "failed to create TPM device node")
+	}
+	device, err := tpm2.OpenTPM("/dev/tpm")
+	if err != nil {
+		return errors.Wrap(err, "failed to open TPM")
+	}
+	tpm = &TPM{
+		device: device,
+		logger: logger,
+	}
+	return nil
+}
+
+// GenerateSafeKey uses two sources of randomness (Kernel & TPM) to generate the key
+func GenerateSafeKey(size uint16) ([]byte, error) {
+	lock.Lock()
+	defer lock.Unlock()
+	if tpm == nil {
+		return []byte{}, ErrNotInitialized
+	}
+	encryptionKeyHost := make([]byte, size)
+	if _, err := io.ReadFull(rand.Reader, encryptionKeyHost); err != nil {
+		return []byte{}, errors.Wrap(err, "failed to generate host portion of new key")
+	}
+	var encryptionKeyTPM []byte
+	for i := 48; i > 0; i-- {
+		tpmKeyPart, err := tpm2.GetRandom(tpm.device, size-uint16(len(encryptionKeyTPM)))
+		if err != nil {
+			return []byte{}, errors.Wrap(err, "failed to generate TPM portion of new key")
+		}
+		encryptionKeyTPM = append(encryptionKeyTPM, tpmKeyPart...)
+		if len(encryptionKeyTPM) >= int(size) {
+			break
+		}
+	}
+
+	if len(encryptionKeyTPM) != int(size) {
+		return []byte{}, fmt.Errorf("got incorrect amount of TPM randomess: %v, requested %v", len(encryptionKeyTPM), size)
+	}
+
+	encryptionKey := make([]byte, size)
+	for i := uint16(0); i < size; i++ {
+		encryptionKey[i] = encryptionKeyHost[i] ^ encryptionKeyTPM[i]
+	}
+	return encryptionKey, nil
+}
+
+// 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) {
+	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)
+	if err != nil {
+		return []byte{}, errors.Wrapf(err, "failed to marshal sealed data")
+	}
+	return sealedKeyRaw, nil
+}
+
+// Unseal unseals sensitive data if the current platform configuration allows and sealing constraints
+// allow it.
+func Unseal(data []byte) ([]byte, error) {
+	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()
+
+	var sealedKey tpmpb.SealedBytes
+	if err := proto.Unmarshal(data, &sealedKey); err != nil {
+		return []byte{}, errors.Wrap(err, "failed to decode sealed data")
+	}
+	// Logging this for auditing purposes
+	tpm.logger.Info("Attempting to unseal data protected with PCRs", zap.Int32s("pcrs", sealedKey.Pcrs))
+	unsealedData, err := srk.Unseal(&sealedKey)
+	if err != nil {
+		return []byte{}, errors.Wrap(err, "failed to unseal data")
+	}
+	return unsealedData, nil
+}