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