From 037ebb2f168aa72194c4931c700dcb2fc68b5e77 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niels=20M=C3=B6ller?= <nisse@glasklarteknik.se>
Date: Fri, 6 Dec 2024 10:56:15 +0100
Subject: [PATCH] New type trust.Root, representing all the /etc/trust_policy
 config

---
 opts/opts.go                                  |  16 ---
 opts/opts_test.go                             |  39 ------
 ospkg/descriptor.go                           |  10 +-
 ospkg/descriptor_test.go                      | 126 ++++++++++--------
 ospkg/ospkg.go                                |   5 +-
 ospkg/ospkg_test.go                           |  16 ++-
 stboot.go                                     |  66 +++------
 stboot_test.go                                |  37 +++--
 trust/policy.go                               |  15 +++
 trust/policy_test.go                          |  37 +++++
 trust/root.go                                 |  53 ++++++++
 .../testdata/trust_policy_bad_unset.json      |   0
 .../testdata/trust_policy_good_all_set.json   |   0
 13 files changed, 239 insertions(+), 181 deletions(-)
 create mode 100644 trust/root.go
 rename {opts => trust}/testdata/trust_policy_bad_unset.json (100%)
 rename {opts => trust}/testdata/trust_policy_good_all_set.json (100%)

diff --git a/opts/opts.go b/opts/opts.go
index 08671dca..5699b51b 100644
--- a/opts/opts.go
+++ b/opts/opts.go
@@ -2,7 +2,6 @@ package opts
 
 import (
 	"crypto/x509"
-	"encoding/json"
 	"encoding/pem"
 	"errors"
 	"fmt"
@@ -12,23 +11,8 @@ import (
 	"filippo.io/age"
 
 	"system-transparency.org/stboot/stlog"
-	"system-transparency.org/stboot/trust"
 )
 
-func ReadTrustPolicy(filename string) (*trust.Policy, error) {
-	f, err := os.Open(filename)
-	if err != nil {
-		return nil, fmt.Errorf("opening trust policy failed: %w", err)
-	}
-	defer f.Close()
-
-	trustPolicy := trust.Policy{}
-	if err := json.NewDecoder(f).Decode(&trustPolicy); err != nil {
-		return nil, err
-	}
-	return &trustPolicy, nil
-}
-
 // Read a certificate file, checking and logging validity dates. Skip
 // invalid certs, but return error if no valid certs are found.
 func ReadCertsFile(filename string, now time.Time) (*x509.CertPool, error) {
diff --git a/opts/opts_test.go b/opts/opts_test.go
index 667a0e9e..30de4a94 100644
--- a/opts/opts_test.go
+++ b/opts/opts_test.go
@@ -16,45 +16,6 @@ import (
 	"testing"
 )
 
-func TestReadTrustPolicy(t *testing.T) {
-	tests := []struct {
-		name, file string
-		wantErr    bool
-	}{
-		{
-			name: "Successful loading",
-			file: "testdata/trust_policy_good_all_set.json",
-		},
-		{
-			name:    "Empty",
-			file:    "testdata/empty",
-			wantErr: true,
-		},
-		{
-			name:    "Bad content",
-			file:    "testdata/trust_policy_bad_unset.json",
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			trustPolicy, err := ReadTrustPolicy(tt.file)
-
-			if tt.wantErr {
-				if err != nil {
-					t.Logf("%s: err (expected): %v", tt.name, err)
-				} else {
-					t.Errorf("%s: invalid input, but no error", tt.name)
-				}
-			} else if err != nil {
-				t.Errorf("%s: failed: %v", tt.name, err)
-			} else if trustPolicy == nil {
-				t.Errorf("%s: failed, nil trustPolicy", tt.name)
-			}
-		})
-	}
-}
-
 func TestReadCertsFile(t *testing.T) {
 	log.SetFlags(0)
 
diff --git a/ospkg/descriptor.go b/ospkg/descriptor.go
index 5ed763a7..713f5442 100644
--- a/ospkg/descriptor.go
+++ b/ospkg/descriptor.go
@@ -111,7 +111,7 @@ func (d *Descriptor) AddSignature(certDER []byte, sig []byte) error {
 // to (or is the) root certificate and which also produced signatures
 // on the provided hash, and compare to the policy's signature
 // threshold.
-func (d *Descriptor) Verify(rootCerts *x509.CertPool, policy *trust.Policy, hash []byte, now time.Time) error {
+func (d *Descriptor) Verify(trustRoot *trust.Root, hash []byte, now time.Time) error {
 	found := 0
 	valid := 0
 
@@ -121,7 +121,7 @@ func (d *Descriptor) Verify(rootCerts *x509.CertPool, policy *trust.Policy, hash
 	}
 
 	opts := x509.VerifyOptions{
-		Roots:       rootCerts,
+		Roots:       trustRoot.SigningRootCerts,
 		CurrentTime: now,
 	}
 
@@ -172,10 +172,10 @@ func (d *Descriptor) Verify(rootCerts *x509.CertPool, policy *trust.Policy, hash
 		}
 		valid++
 	}
-	if valid < policy.SignatureThreshold {
-		return fmt.Errorf("not enough valid signatures: %d found, %d valid, %d required", found, valid, policy.SignatureThreshold)
+	if valid < trustRoot.Policy.SignatureThreshold {
+		return fmt.Errorf("not enough valid signatures: %d found, %d valid, %d required", found, valid, trustRoot.Policy.SignatureThreshold)
 	}
-	stlog.Debug("Signatures: %d found, %d valid, %d required", found, valid, policy.SignatureThreshold)
+	stlog.Debug("Signatures: %d found, %d valid, %d required", found, valid, trustRoot.Policy.SignatureThreshold)
 
 	return nil
 }
diff --git a/ospkg/descriptor_test.go b/ospkg/descriptor_test.go
index 5bbb1744..daff8de0 100644
--- a/ospkg/descriptor_test.go
+++ b/ospkg/descriptor_test.go
@@ -135,12 +135,14 @@ func TestDescriptorVerify(t *testing.T) {
 
 	t.Run("No signatures", func(t *testing.T) {
 		desc := Descriptor{}
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 0},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 1},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 0},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 1},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	f := func(t *testing.T, key test.Key) {
@@ -149,12 +151,14 @@ func TestDescriptorVerify(t *testing.T) {
 			Certificates: [][]byte{pem.EncodeToMemory(key.Certpem)},
 			Signatures:   [][]byte{ed25519.Sign(key.Private, archivehash[:])},
 		}
-		require.NoError(t, desc.Verify(certToPool(key.Cert),
-			&trust.Policy{SignatureThreshold: 1},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(key.Cert),
-			&trust.Policy{SignatureThreshold: 2},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 1},
+			SigningRootCerts: certToPool(key.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 2},
+			SigningRootCerts: certToPool(key.Cert),
+		}, archivehash[:], time.Now()))
 	}
 	t.Run("Sign OS package directly with root CA key", func(t *testing.T) { f(t, root1) })
 	t.Run("Sign OS package directly with non-CA root key", func(t *testing.T) { f(t, keys[0]) })
@@ -164,12 +168,14 @@ func TestDescriptorVerify(t *testing.T) {
 			Certificates: [][]byte{pem.EncodeToMemory(keys[0].Certpem)},
 			Signatures:   [][]byte{ed25519.Sign(keys[0].Private, archivehash[:])},
 		}
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 1},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 2},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 1},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 2},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	t.Run("3 of 3 signatures", func(t *testing.T) {
@@ -180,12 +186,14 @@ func TestDescriptorVerify(t *testing.T) {
 			desc.Signatures = append(desc.Signatures, ed25519.Sign(keys[i].Private, archivehash[:]))
 		}
 
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 3},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 4},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 3},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 4},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	t.Run("one dup signature", func(t *testing.T) {
@@ -211,12 +219,14 @@ func TestDescriptorVerify(t *testing.T) {
 		crtPEM := &pem.Block{Type: "CERTIFICATE", Bytes: crt}
 		desc.Certificates = append(desc.Certificates, pem.EncodeToMemory(crtPEM))
 
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 2},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 3},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 2},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 3},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	t.Run("non ca'd signature", func(t *testing.T) {
@@ -227,12 +237,14 @@ func TestDescriptorVerify(t *testing.T) {
 			desc.Signatures = append(desc.Signatures, ed25519.Sign(keys[i].Private, archivehash[:]))
 		}
 
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 3},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 4},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 3},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 4},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	t.Run("invalid signatures", func(t *testing.T) {
@@ -244,12 +256,14 @@ func TestDescriptorVerify(t *testing.T) {
 		}
 		desc.Signatures[0][0] = 0
 
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 2},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 3},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 2},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 3},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	t.Run("invalid certificate", func(t *testing.T) {
@@ -261,12 +275,14 @@ func TestDescriptorVerify(t *testing.T) {
 		}
 		desc.Certificates[0] = pem.EncodeToMemory(keys[2].Certpem)
 
-		require.NoError(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 1},
-			archivehash[:], time.Now()))
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 2},
-			archivehash[:], time.Now()))
+		require.NoError(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 1},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 2},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 
 	t.Run("Check that expired certificate is rejected", func(t *testing.T) {
@@ -287,9 +303,10 @@ func TestDescriptorVerify(t *testing.T) {
 			Certificates: [][]byte{pem.EncodeToMemory(crtPEM)},
 			Signatures:   [][]byte{ed25519.Sign(keys[0].Private, archivehash[:])},
 		}
-		require.Error(t, desc.Verify(certToPool(root1.Cert),
-			&trust.Policy{SignatureThreshold: 1},
-			archivehash[:], time.Now()))
+		require.Error(t, desc.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 1},
+			SigningRootCerts: certToPool(root1.Cert),
+		}, archivehash[:], time.Now()))
 	})
 }
 
@@ -356,9 +373,10 @@ func TestCertLogging(t *testing.T) {
 		desc.Certificates = append(desc.Certificates, pem.EncodeToMemory(key.Certpem))
 		desc.Signatures = append(desc.Signatures, ed25519.Sign(key.Private, archivehash[:]))
 	}
-	require.NoError(t, desc.Verify(certToPool(root.Cert),
-		&trust.Policy{SignatureThreshold: 2},
-		archivehash[:], now.Add(24*time.Hour)))
+	require.NoError(t, desc.Verify(&trust.Root{
+		Policy:           trust.Policy{SignatureThreshold: 2},
+		SigningRootCerts: certToPool(root.Cert),
+	}, archivehash[:], now.Add(24*time.Hour)))
 
 	// Check expectations on logged messages.
 	expectedMsgs := []string{
diff --git a/ospkg/ospkg.go b/ospkg/ospkg.go
index ca1e9026..638ad659 100644
--- a/ospkg/ospkg.go
+++ b/ospkg/ospkg.go
@@ -10,7 +10,6 @@ import (
 	"context"
 	"crypto"
 	"crypto/sha256"
-	"crypto/x509"
 	"fmt"
 	"io/fs"
 	"net/url"
@@ -305,8 +304,8 @@ func (osp *OSPackage) Sign(signer crypto.Signer, certDER []byte) error {
 
 // Verify determines the number of unique certificates that chain up to (or is
 // the) root certificate and which also produced valid OS package signatures.
-func (osp *OSPackage) Verify(rootCerts *x509.CertPool, policy *trust.Policy, now time.Time) error {
-	if err := osp.descriptor.Verify(rootCerts, policy, osp.hash[:], now); err != nil {
+func (osp *OSPackage) Verify(trustRoot *trust.Root, now time.Time) error {
+	if err := osp.descriptor.Verify(trustRoot, osp.hash[:], now); err != nil {
 		return err
 	}
 
diff --git a/ospkg/ospkg_test.go b/ospkg/ospkg_test.go
index 06d511fd..6da65bda 100644
--- a/ospkg/ospkg_test.go
+++ b/ospkg/ospkg_test.go
@@ -58,7 +58,7 @@ func TestCreateOsPkg(t *testing.T) {
 	osp, err = NewOSPackage(archivebuf, descbuf)
 	require.NoError(t, err)
 
-	require.NoError(t, osp.Verify(nil, &trust.Policy{}, time.Now()))
+	require.NoError(t, osp.Verify(&trust.Root{}, time.Now()))
 
 	img, err := osp.LinuxImage()
 	require.NoError(t, err)
@@ -210,7 +210,7 @@ func TestOSPackageArchive(t *testing.T) {
 		osp, err := NewOSPackage(archivebuf, descbuf)
 		require.NoError(t, err)
 
-		require.NoError(t, osp.Verify(nil, &trust.Policy{}, time.Now()))
+		require.NoError(t, osp.Verify(&trust.Root{}, time.Now()))
 
 		_, err = osp.LinuxImage()
 		require.Error(t, err)
@@ -259,7 +259,7 @@ func TestEnforceValidate(t *testing.T) {
 	_, err = osp.LinuxImage()
 	require.Error(t, err)
 
-	require.NoError(t, osp.Verify(nil, &trust.Policy{}, time.Now()))
+	require.NoError(t, osp.Verify(&trust.Root{}, time.Now()))
 
 	_, err = osp.LinuxImage()
 	require.NoError(t, err)
@@ -291,8 +291,14 @@ func TestSigning(t *testing.T) {
 		err = osp.Sign(priv, certDER)
 		require.NoError(t, err)
 		// Check that signature is recognized.
-		require.NoError(t, osp.Verify(certToPool(cert), &trust.Policy{SignatureThreshold: 1}, time.Now()))
-		require.Error(t, osp.Verify(certToPool(cert), &trust.Policy{SignatureThreshold: 2}, time.Now()))
+		require.NoError(t, osp.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 1},
+			SigningRootCerts: certToPool(cert),
+		}, time.Now()))
+		require.Error(t, osp.Verify(&trust.Root{
+			Policy:           trust.Policy{SignatureThreshold: 2},
+			SigningRootCerts: certToPool(cert),
+		}, time.Now()))
 	})
 
 	t.Run("Reuse cert", func(t *testing.T) {
diff --git a/stboot.go b/stboot.go
index 0c43b273..1646339c 100644
--- a/stboot.go
+++ b/stboot.go
@@ -6,7 +6,6 @@ package main
 
 import (
 	"context"
-	"crypto/x509"
 	"errors"
 	"flag"
 	"fmt"
@@ -23,7 +22,6 @@ import (
 
 	"golang.org/x/sys/unix"
 
-	"filippo.io/age"
 	"github.com/u-root/u-root/pkg/boot"
 	"github.com/u-root/u-root/pkg/libinit"
 
@@ -32,7 +30,6 @@ import (
 	di "system-transparency.org/stboot/internal/dependency"
 	"system-transparency.org/stboot/internal/ui"
 	"system-transparency.org/stboot/internal/wctx"
-	"system-transparency.org/stboot/opts"
 	"system-transparency.org/stboot/ospkg"
 	"system-transparency.org/stboot/stlog"
 	"system-transparency.org/stboot/trust"
@@ -46,11 +43,7 @@ const (
 
 // Files at initramfs.
 const (
-	trustPolicyFile = "/etc/trust_policy/trust_policy.json"
-	signingRootFile = "/etc/trust_policy/ospkg_signing_root.pem"
-	httpsRootsFile  = "/etc/trust_policy/tls_roots.pem"
-	// Age decryption identities (optional).
-	decryptionIdentitiesFile = "/etc/trust_policy/decryption_identities"
+	trustPolicyDir = "/etc/trust_policy"
 
 	promptTimeout    = 30 * time.Second
 	interruptTimeout = 5 * time.Second
@@ -220,44 +213,24 @@ func main() {
 	///////////////////////////////////////
 	// Setup of trust policy and cert roots
 	///////////////////////////////////////
-	trustPolicy, err := opts.ReadTrustPolicy(trustPolicyFile)
+	trustRoot, err := trust.ReadTrustRoot(trustPolicyDir, time.Now())
 	if err != nil {
 		stlog.Error("trust policy: %v", err)
 		host.Recover()
 	}
-
-	now := time.Now()
-	signingRootCerts, err := opts.ReadCertsFile(signingRootFile, now)
-	if err != nil {
-		stlog.Error("signing root certificate: %v", err)
-		host.Recover()
-	}
-
-	httpsRoots, err := opts.ReadOptionalCertsFile(httpsRootsFile, now)
-	if err != nil {
-		stlog.Error("https root certificates: %v", err)
-		host.Recover()
-	}
-
-	decryptionIdentities, err := opts.ReadDecryptionIdentities(decryptionIdentitiesFile)
-	if err != nil {
-		stlog.Error("decryption identities: %v", err)
-		host.Recover()
-	}
-
 	deadlineDuration := time.Duration(*deadline) * time.Minute
 
 	// The boot*Image functions are not expected to return,
 	// and they return with a nil error only in dryrun mode,
 	// i.e., getting a nil error is the exception.
-	err = bootConfiguredImage(ctx, trustPolicy, signingRootCerts, httpsRoots, decryptionIdentities, deadlineDuration, *dryRun, hasProvisionImage())
+	err = bootConfiguredImage(ctx, &trustRoot, deadlineDuration, *dryRun, hasProvisionImage())
 	if err == nil {
 		return
 	}
 	stlog.Error("booting configured OS package failed: %v", err)
 	if err == host.ErrConfigNotFound || userWantsProvision() {
 		stlog.Info("PROVISION MODE! Attempting to boot /ospkg/provision.{jzon,zip}")
-		err := bootProvisionImage(ctx, trustPolicy, signingRootCerts, *dryRun)
+		err := bootProvisionImage(ctx, &trustRoot, *dryRun)
 		if err == nil {
 			return
 		}
@@ -267,7 +240,7 @@ func main() {
 	host.Recover()
 }
 
-func bootConfiguredImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *x509.CertPool, httpsRoots *x509.CertPool, decryptionIdentities []age.Identity, deadline time.Duration, dryRun, enableInterrupt bool) error {
+func bootConfiguredImage(ctx context.Context, trustRoot *trust.Root, deadline time.Duration, dryRun, enableInterrupt bool) error {
 	//////////////////
 	// Get host config
 	//////////////////
@@ -296,11 +269,11 @@ func bootConfiguredImage(ctx context.Context, trustPolicy *trust.Policy, signing
 	}
 
 	// System appears provisioned, apply host config.
-	if trustPolicy.FetchMethod == trust.FetchFromNetwork {
+	if trustRoot.Policy.FetchMethod == trust.FetchFromNetwork {
 		// It's possible to have a http url for the ospkg_pointer, but a https
 		// url in the downloaded descriptor. We don't detect that case here, but
 		// it will fail later if no HTTPS roots are configured.
-		if needsHTTPS(*hostCfg.OSPkgPointer) && httpsRoots == nil {
+		if needsHTTPS(*hostCfg.OSPkgPointer) && trustRoot.HTTPSRootCerts == nil {
 			return fmt.Errorf("network boot with HTTPS is configured, but HTTPS root certificates are missing")
 		}
 
@@ -309,17 +282,18 @@ func bootConfiguredImage(ctx context.Context, trustPolicy *trust.Policy, signing
 			return fmt.Errorf("failed to setup network interfaces: %v", err)
 		}
 	}
-	return getAndBootImage(ctx, trustPolicy, signingRootCerts, httpsRoots, decryptionIdentities, hostCfg, deadline, dryRun, timer)
+	return getAndBootImage(ctx, trustRoot, hostCfg, deadline, dryRun, timer)
 }
 
-func bootProvisionImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *x509.CertPool, dryRun bool) error {
-	provTrustPolicy := *trustPolicy
-	provTrustPolicy.FetchMethod = trust.FetchFromInitramfs
-	return getAndBootImage(ctx, &provTrustPolicy, signingRootCerts, nil, nil, provisionHostConfig(), 0, dryRun, nil)
+func bootProvisionImage(ctx context.Context, trustRoot *trust.Root, dryRun bool) error {
+	provTrustRoot := *trustRoot
+	provTrustRoot.Policy.FetchMethod = trust.FetchFromInitramfs
+	provTrustRoot.HTTPSRootCerts = nil
+	return getAndBootImage(ctx, &provTrustRoot, provisionHostConfig(), 0, dryRun, nil)
 }
 
-func getAndBootImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *x509.CertPool, httpsRoots *x509.CertPool, decryptionIdentities []age.Identity, hostCfg host.Config, deadline time.Duration, dryRun bool, timer <-chan time.Time) error {
-	img, err := getImage(ctx, trustPolicy, signingRootCerts, httpsRoots, decryptionIdentities, hostCfg, deadline)
+func getAndBootImage(ctx context.Context, trustRoot *trust.Root, hostCfg host.Config, deadline time.Duration, dryRun bool, timer <-chan time.Time) error {
+	img, err := getImage(ctx, trustRoot, hostCfg, deadline)
 	if err != nil {
 		return err
 	}
@@ -349,7 +323,7 @@ func userWantsProvision() bool {
 }
 
 // Fetch, verify, unpack, and measure an OS package, specified by the host config.
-func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *x509.CertPool, httpsRoots *x509.CertPool, decryptionIdentities []age.Identity, hostCfg host.Config, deadline time.Duration) (*boot.LinuxImage, error) {
+func getImage(ctx context.Context, trustRoot *trust.Root, hostCfg host.Config, deadline time.Duration) (*boot.LinuxImage, error) {
 	fsys := di.DefaultFilesystem(ctx)
 
 	//////////////////
@@ -358,11 +332,11 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *
 
 	var osp *ospkg.OSPackage
 
-	switch trustPolicy.FetchMethod {
+	switch trustRoot.Policy.FetchMethod {
 	case trust.FetchFromNetwork:
 		stlog.Info("Loading OS package via network")
 
-		client := network.NewHTTPClient(httpsRoots, false, network.WithDecryption(decryptionIdentities))
+		client := network.NewHTTPClient(trustRoot.HTTPSRootCerts, false, network.WithDecryption(trustRoot.DecryptionIdentities))
 
 		stlog.Debug("OS package pointer: %s", *hostCfg.OSPkgPointer)
 
@@ -387,7 +361,7 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *
 			return nil, errRecover
 		}
 	default:
-		stlog.Error("unknown OS package fetch method %q", trustPolicy.FetchMethod)
+		stlog.Error("unknown OS package fetch method %q", trustRoot.Policy.FetchMethod)
 
 		return nil, errRecover
 	}
@@ -398,7 +372,7 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCerts *
 
 	// TODO: write ospkg.info method for debug output
 
-	if err := osp.Verify(signingRootCerts, trustPolicy, time.Now()); err != nil {
+	if err := osp.Verify(trustRoot, time.Now()); err != nil {
 		stlog.Error("Verifying OS package: %v", err)
 
 		return nil, errRecover
diff --git a/stboot_test.go b/stboot_test.go
index 6c19fc93..57cf6ece 100644
--- a/stboot_test.go
+++ b/stboot_test.go
@@ -293,7 +293,7 @@ func TestGetImage(t *testing.T) {
 	hostCfg := provisionHostConfig()
 
 	t.Run("no UX identity", func(t *testing.T) {
-		_, err = getImage(ctx, &trustPolicy, certsToPool(root.Cert), httpsRoots, nil, hostCfg, 20*time.Minute)
+		_, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCerts: certsToPool(root.Cert), HTTPSRootCerts: httpsRoots}, hostCfg, 20*time.Minute)
 		assert.NoError(t, err)
 	})
 
@@ -305,7 +305,7 @@ func TestGetImage(t *testing.T) {
 
 		ctx = di.WithEFIVar(ctx, mockEfiVar)
 
-		_, err = getImage(ctx, &trustPolicy, certsToPool(root.Cert), httpsRoots, nil, hostCfg, 20*time.Minute)
+		_, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCerts: certsToPool(root.Cert), HTTPSRootCerts: httpsRoots}, hostCfg, 20*time.Minute)
 		assert.NoError(t, err)
 	})
 
@@ -317,7 +317,7 @@ func TestGetImage(t *testing.T) {
 			assert.NoError(t, err)
 			ctx = di.WithFilesystem(ctx, myMockFs)
 
-			_, err = getImage(ctx, &trustPolicy, certsToPool(root.Cert), httpsRoots, nil, hostCfg, 20*time.Minute)
+			_, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCerts: certsToPool(root.Cert), HTTPSRootCerts: httpsRoots}, hostCfg, 20*time.Minute)
 			assert.Error(t, err)
 		})
 	}
@@ -325,7 +325,7 @@ func TestGetImage(t *testing.T) {
 	t.Run("optional https roots", func(t *testing.T) {
 		ctx = di.WithFilesystem(ctx, mockFsys)
 
-		_, err = getImage(ctx, &trustPolicy, certsToPool(root.Cert), nil, nil, hostCfg, 20*time.Minute)
+		_, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCerts: certsToPool(root.Cert), HTTPSRootCerts: nil}, hostCfg, 20*time.Minute)
 		assert.NoError(t, err)
 	})
 
@@ -341,26 +341,37 @@ func TestGetImage(t *testing.T) {
 		}
 		ctx = di.WithFilesystem(ctx, myMockFs)
 
-		_, err = getImage(ctx, &trust.Policy{
-			SignatureThreshold: 1,
-			FetchMethod:        trust.FetchFromNetwork,
-		}, certsToPool(root.Cert), nil, nil, hostCfg, 20*time.Minute)
+		_, err = getImage(ctx, &trust.Root{
+			Policy: trust.Policy{
+				SignatureThreshold: 1,
+				FetchMethod:        trust.FetchFromNetwork,
+			},
+			SigningRootCerts: certsToPool(root.Cert),
+			HTTPSRootCerts:   nil,
+		}, hostCfg, 20*time.Minute)
 		assert.Error(t, err)
 	})
 	t.Run("invalid signature threshold", func(t *testing.T) {
 		ctx = di.WithFilesystem(ctx, mockFsys)
 
-		_, err = getImage(ctx, &trust.Policy{
-			SignatureThreshold: 4,
-			FetchMethod:        trust.FetchFromInitramfs,
-		}, certsToPool(root.Cert), httpsRoots, nil, hostCfg, 20*time.Minute)
+		_, err = getImage(ctx, &trust.Root{
+			Policy: trust.Policy{
+				SignatureThreshold: 4,
+				FetchMethod:        trust.FetchFromInitramfs,
+			},
+			SigningRootCerts: certsToPool(root.Cert),
+			HTTPSRootCerts:   httpsRoots,
+		}, hostCfg, 20*time.Minute)
 		assert.Error(t, err)
 	})
 	t.Run("multiple signature roots", func(t *testing.T) {
 		ctx = di.WithFilesystem(ctx, mockFsys)
 		r1 := test.MkKey(t, nil)
 		r2 := test.MkKey(t, nil)
-		_, err = getImage(ctx, &trustPolicy, certsToPool(r1.Cert, root.Cert, r2.Cert), nil, nil, hostCfg, 20*time.Minute)
+		_, err = getImage(ctx, &trust.Root{
+			Policy:           trustPolicy,
+			SigningRootCerts: certsToPool(r1.Cert, root.Cert, r2.Cert),
+		}, hostCfg, 20*time.Minute)
 		assert.NoError(t, err)
 	})
 }
diff --git a/trust/policy.go b/trust/policy.go
index c47c66e2..3238221b 100644
--- a/trust/policy.go
+++ b/trust/policy.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"os"
 )
 
 var ErrInvalidPolicy = errors.New("invalid policy")
@@ -57,6 +58,20 @@ func (p *Policy) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
+func ReadTrustPolicy(filename string) (Policy, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return Policy{}, fmt.Errorf("opening trust policy failed: %w", err)
+	}
+	defer f.Close()
+
+	trustPolicy := Policy{}
+	if err := json.NewDecoder(f).Decode(&trustPolicy); err != nil {
+		return Policy{}, err
+	}
+	return trustPolicy, nil
+}
+
 func (p *Policy) validate() error {
 	var validationSet = []func() error{
 		p.checkOSPKGSignatureThreshold,
diff --git a/trust/policy_test.go b/trust/policy_test.go
index a96645cc..7a8a5657 100644
--- a/trust/policy_test.go
+++ b/trust/policy_test.go
@@ -215,3 +215,40 @@ func TestPolicyUnmarshalJSON(t *testing.T) {
 		})
 	}
 }
+
+func TestReadTrustPolicy(t *testing.T) {
+	tests := []struct {
+		name, file string
+		wantErr    bool
+	}{
+		{
+			name: "Successful loading",
+			file: "testdata/trust_policy_good_all_set.json",
+		},
+		{
+			name:    "Empty",
+			file:    "testdata/empty",
+			wantErr: true,
+		},
+		{
+			name:    "Bad content",
+			file:    "testdata/trust_policy_bad_unset.json",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := ReadTrustPolicy(tt.file)
+
+			if tt.wantErr {
+				if err != nil {
+					t.Logf("%s: err (expected): %v", tt.name, err)
+				} else {
+					t.Errorf("%s: invalid input, but no error", tt.name)
+				}
+			} else if err != nil {
+				t.Errorf("%s: failed: %v", tt.name, err)
+			}
+		})
+	}
+}
diff --git a/trust/root.go b/trust/root.go
new file mode 100644
index 00000000..3195b444
--- /dev/null
+++ b/trust/root.go
@@ -0,0 +1,53 @@
+package trust
+
+import (
+	"crypto/x509"
+	"fmt"
+	"path/filepath"
+	"time"
+
+	"filippo.io/age"
+
+	"system-transparency.org/stboot/opts"
+)
+
+const (
+	trustPolicyFile = "trust_policy.json"
+	signingRootFile = "ospkg_signing_root.pem"
+	httpsRootsFile  = "tls_roots.pem"
+	// Age decryption identities (optional).
+	decryptionIdentitiesFile = "decryption_identities"
+)
+
+type Root struct {
+	Policy               Policy
+	SigningRootCerts     *x509.CertPool
+	HTTPSRootCerts       *x509.CertPool
+	DecryptionIdentities []age.Identity
+}
+
+func ReadTrustRoot(dir string, now time.Time) (Root, error) {
+	trustPolicy, err := ReadTrustPolicy(filepath.Join(dir, trustPolicyFile))
+	if err != nil {
+		return Root{}, err
+	}
+
+	signingRootCerts, err := opts.ReadCertsFile(filepath.Join(dir, signingRootFile), now)
+	if err != nil {
+		return Root{}, err
+	}
+	httpsRoots, err := opts.ReadOptionalCertsFile(filepath.Join(dir, httpsRootsFile), now)
+	if err != nil {
+		return Root{}, err
+	}
+	decryptionIdentities, err := opts.ReadDecryptionIdentities(filepath.Join(dir, decryptionIdentitiesFile))
+	if err != nil {
+		return Root{}, fmt.Errorf("decryption identities: %v", err)
+	}
+	return Root{
+		Policy:               trustPolicy,
+		SigningRootCerts:     signingRootCerts,
+		HTTPSRootCerts:       httpsRoots,
+		DecryptionIdentities: decryptionIdentities,
+	}, nil
+}
diff --git a/opts/testdata/trust_policy_bad_unset.json b/trust/testdata/trust_policy_bad_unset.json
similarity index 100%
rename from opts/testdata/trust_policy_bad_unset.json
rename to trust/testdata/trust_policy_bad_unset.json
diff --git a/opts/testdata/trust_policy_good_all_set.json b/trust/testdata/trust_policy_good_all_set.json
similarity index 100%
rename from opts/testdata/trust_policy_good_all_set.json
rename to trust/testdata/trust_policy_good_all_set.json
-- 
GitLab