diff --git a/opts/opts.go b/opts/opts.go index 92703b66b6f82d2f10a6f96bb1822c3641b90870..b76011208a282d6a3604e521d457671f8709f395 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.Certificate, error) { diff --git a/opts/opts_test.go b/opts/opts_test.go index d34d5b532bd5ce0df46ba58c5e14e503ca7dffdb..d43db8b3794e09330e27110bb71ac575cb32bdee 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/stboot.go b/stboot.go index c8513c635a16f13e3136546cf607eb3b1e733f0b..1a2b251f65ee6341420080ebed387bee4b591763 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,49 +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() - certs, err := opts.ReadCertsFile(signingRootFile, now) - if err != nil { - stlog.Error("signing root certificate: %v", err) - host.Recover() - } - if got := len(certs); got != 1 { - stlog.Error("exactly one root certificate is expected, got %d", got) - host.Recover() - } - signingRootCert := certs[0] - - 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, signingRootCert, 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, signingRootCert, *dryRun) + err := bootProvisionImage(ctx, &trustRoot, *dryRun) if err == nil { return } @@ -272,7 +240,7 @@ func main() { host.Recover() } -func bootConfiguredImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCert *x509.Certificate, httpsRoots []*x509.Certificate, 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 ////////////////// @@ -301,11 +269,11 @@ func bootConfiguredImage(ctx context.Context, trustPolicy *trust.Policy, signing } // System appears provisioned, apply host config. - if trustPolicy.FetchMethod == ospkg.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) && len(httpsRoots) == 0 { + if needsHTTPS(*hostCfg.OSPkgPointer) && len(trustRoot.HTTPSRootCerts) == 0 { return fmt.Errorf("network boot with HTTPS is configured, but HTTPS root certificates are missing") } @@ -314,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, signingRootCert, httpsRoots, decryptionIdentities, hostCfg, deadline, dryRun, timer) + return getAndBootImage(ctx, trustRoot, hostCfg, deadline, dryRun, timer) } -func bootProvisionImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCert *x509.Certificate, dryRun bool) error { - provTrustPolicy := *trustPolicy - provTrustPolicy.FetchMethod = ospkg.FetchFromInitramfs - return getAndBootImage(ctx, &provTrustPolicy, signingRootCert, 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, signingRootCert *x509.Certificate, httpsRoots []*x509.Certificate, decryptionIdentities []age.Identity, hostCfg host.Config, deadline time.Duration, dryRun bool, timer <-chan time.Time) error { - img, err := getImage(ctx, trustPolicy, signingRootCert, 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 } @@ -354,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, signingRootCert *x509.Certificate, httpsRoots []*x509.Certificate, 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) ////////////////// @@ -363,11 +332,11 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCert *x var osp *ospkg.OSPackage - switch trustPolicy.FetchMethod { - case ospkg.FetchFromNetwork: + 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) @@ -381,7 +350,7 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCert *x return nil, errRecover } - case ospkg.FetchFromInitramfs: + case trust.FetchFromInitramfs: stlog.Info("Loading OS package from initramfs") var err error @@ -392,7 +361,7 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCert *x 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 } @@ -403,14 +372,14 @@ func getImage(ctx context.Context, trustPolicy *trust.Policy, signingRootCert *x // TODO: write ospkg.info method for debug output - numSig, valid, err := osp.Verify(signingRootCert, time.Now()) + numSig, valid, err := osp.Verify(trustRoot.SigningRootCert, time.Now()) if err != nil { stlog.Error("Verifying OS package: %v", err) return nil, errRecover } - threshold := trustPolicy.SignatureThreshold + threshold := trustRoot.Policy.SignatureThreshold if valid < threshold { stlog.Error("Not enough valid signatures: %d found, %d valid, %d required", numSig, valid, threshold) diff --git a/stboot_test.go b/stboot_test.go index b2c92a87bfe0afece3575fc6ed082bba1f3c7ee8..3f67b70f2b2f004d1ca683bc98497eef065c06dc 100644 --- a/stboot_test.go +++ b/stboot_test.go @@ -278,7 +278,7 @@ func TestGetImage(t *testing.T) { mockFsys := test.NewMockFS() trustPolicy := trust.Policy{ SignatureThreshold: 1, - FetchMethod: ospkg.FetchFromInitramfs, + FetchMethod: trust.FetchFromInitramfs, } assert.NoError(t, err) err = mockFsys.Add("/ospkg/provision.zip", &fstest.MapFile{Data: ospkgZip}) @@ -294,7 +294,7 @@ func TestGetImage(t *testing.T) { hostCfg := provisionHostConfig() t.Run("no UX identity", func(t *testing.T) { - _, err = getImage(ctx, &trustPolicy, root.Cert, httpsRoots, nil, hostCfg, 20*time.Minute) + _, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCert: root.Cert, HTTPSRootCerts: httpsRoots}, hostCfg, 20*time.Minute) assert.NoError(t, err) }) @@ -306,7 +306,7 @@ func TestGetImage(t *testing.T) { ctx = di.WithEFIVar(ctx, mockEfiVar) - _, err = getImage(ctx, &trustPolicy, root.Cert, httpsRoots, nil, hostCfg, 20*time.Minute) + _, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCert: root.Cert, HTTPSRootCerts: httpsRoots}, hostCfg, 20*time.Minute) assert.NoError(t, err) }) @@ -318,7 +318,7 @@ func TestGetImage(t *testing.T) { assert.NoError(t, err) ctx = di.WithFilesystem(ctx, myMockFs) - _, err = getImage(ctx, &trustPolicy, root.Cert, httpsRoots, nil, hostCfg, 20*time.Minute) + _, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCert: root.Cert, HTTPSRootCerts: httpsRoots}, hostCfg, 20*time.Minute) assert.Error(t, err) }) } @@ -326,7 +326,7 @@ func TestGetImage(t *testing.T) { t.Run("optional https roots", func(t *testing.T) { ctx = di.WithFilesystem(ctx, mockFsys) - _, err = getImage(ctx, &trustPolicy, root.Cert, nil, nil, hostCfg, 20*time.Minute) + _, err = getImage(ctx, &trust.Root{Policy: trustPolicy, SigningRootCert: root.Cert, HTTPSRootCerts: nil}, hostCfg, 20*time.Minute) assert.NoError(t, err) }) @@ -342,19 +342,27 @@ func TestGetImage(t *testing.T) { } ctx = di.WithFilesystem(ctx, myMockFs) - _, err = getImage(ctx, &trust.Policy{ - SignatureThreshold: 1, - FetchMethod: ospkg.FetchFromNetwork, - }, root.Cert, nil, nil, hostCfg, 20*time.Minute) + _, err = getImage(ctx, &trust.Root{ + Policy: trust.Policy{ + SignatureThreshold: 1, + FetchMethod: trust.FetchFromNetwork, + }, + SigningRootCert: 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: ospkg.FetchFromInitramfs, - }, root.Cert, httpsRoots, nil, hostCfg, 20*time.Minute) + _, err = getImage(ctx, &trust.Root{ + Policy: trust.Policy{ + SignatureThreshold: 4, + FetchMethod: trust.FetchFromInitramfs, + }, + SigningRootCert: root.Cert, + HTTPSRootCerts: httpsRoots, + }, hostCfg, 20*time.Minute) assert.Error(t, err) }) } diff --git a/ospkg/fetchmethod.go b/trust/fetchmethod.go similarity index 99% rename from ospkg/fetchmethod.go rename to trust/fetchmethod.go index 7f6cf7af4e0559dbf80944d51515fcb58b53024a..f37e967df50128346cf1710bcf56b9cba60302b8 100644 --- a/ospkg/fetchmethod.go +++ b/trust/fetchmethod.go @@ -1,4 +1,4 @@ -package ospkg +package trust import ( "encoding/json" diff --git a/ospkg/fetchmethod_test.go b/trust/fetchmethod_test.go similarity index 99% rename from ospkg/fetchmethod_test.go rename to trust/fetchmethod_test.go index 84cecb5afae6729ebc37d39c54c8d9974477394a..4cdbe9ecda90cb8c35f346da95d7a16efea545df 100644 --- a/ospkg/fetchmethod_test.go +++ b/trust/fetchmethod_test.go @@ -1,4 +1,4 @@ -package ospkg +package trust import ( "encoding/json" diff --git a/trust/policy.go b/trust/policy.go index fea5e96cfca15ff0af0f108e26ddfa133c61dce1..3238221baa09bca0bbdf548df5e0fef58c16f50a 100644 --- a/trust/policy.go +++ b/trust/policy.go @@ -4,16 +4,15 @@ import ( "encoding/json" "errors" "fmt" - - "system-transparency.org/stboot/ospkg" + "os" ) var ErrInvalidPolicy = errors.New("invalid policy") // Policy holds security configuration. type Policy struct { - SignatureThreshold int `json:"ospkg_signature_threshold"` - FetchMethod ospkg.FetchMethod `json:"ospkg_fetch_method"` + SignatureThreshold int `json:"ospkg_signature_threshold"` + FetchMethod FetchMethod `json:"ospkg_fetch_method"` } // NewPolicy creates a Policy from template. @@ -32,8 +31,8 @@ func NewPolicy(template Policy) (Policy, error) { // policy is used as an alias in Policy.UnmarshalJSON. type policy struct { - SignatureThreshold int `json:"ospkg_signature_threshold"` - FetchMethod ospkg.FetchMethod `json:"ospkg_fetch_method"` + SignatureThreshold int `json:"ospkg_signature_threshold"` + FetchMethod FetchMethod `json:"ospkg_fetch_method"` } // UnmarshalJSON implements json.Unmarshaler. It initializes p from a JSON data @@ -59,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 0169e4d1faf26ac52883ddd2caa4db76ea6f714d..7a8a5657d94366a064931550033936db92f2bd5d 100644 --- a/trust/policy_test.go +++ b/trust/policy_test.go @@ -5,8 +5,6 @@ import ( "errors" "reflect" "testing" - - "system-transparency.org/stboot/ospkg" ) func TestPolicyNew(t *testing.T) { @@ -19,11 +17,11 @@ func TestPolicyNew(t *testing.T) { name: "All set", template: Policy{ SignatureThreshold: 1, - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, want: Policy{ SignatureThreshold: 1, - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, }, } @@ -39,21 +37,21 @@ func TestPolicyNew(t *testing.T) { { name: "SignaturesThreshold missing", template: Policy{ - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, }, { name: "SignaturesThreshold 0", template: Policy{ SignatureThreshold: 0, - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, }, { name: "SignaturesThreshold negative", template: Policy{ SignatureThreshold: -1, - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, }, { @@ -110,7 +108,7 @@ func TestPolicyUnmarshalJSON(t *testing.T) { }`, want: Policy{ SignatureThreshold: 1, - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, }, { @@ -122,7 +120,7 @@ func TestPolicyUnmarshalJSON(t *testing.T) { }`, want: Policy{ SignatureThreshold: 1, - FetchMethod: ospkg.FetchFromNetwork, + FetchMethod: FetchFromNetwork, }, }, } @@ -217,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 0000000000000000000000000000000000000000..499054559ec04bafb47b559039af24ec19df59b5 --- /dev/null +++ b/trust/root.go @@ -0,0 +1,56 @@ +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 + SigningRootCert *x509.Certificate + HTTPSRootCerts []*x509.Certificate + 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 + } + if got := len(signingRootCerts); got != 1 { + return Root{}, fmt.Errorf("exactly one signing root certificate is expected, got %d", got) + } + 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, + SigningRootCert: signingRootCerts[0], + 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