diff --git a/README.md b/README.md index 5518af3..e09b24f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ initializing console (text) go-boot • tamago/amd64 (go1.24.1) • UEFI x64 . # load and start EFI image +bt (none|offline|online)? # show/set boot-transparency status build # build information cat # show file contents clear # clear screen @@ -240,6 +241,16 @@ for Google Compute Engine: * [Google Compute Engine](https://github.com/usbarmory/go-boot/wiki/Google-Compute-Engine) +Boot transparency +================= + +The interaction with a transparency ecosystem for boot loading operations is provided +by the [boot-transparency](https://github.com/usbarmory/boot-transparency) Go library. + +The following example demonstrates how to enable, and configure, the boot transparency support: + +* [Boot transparency](https://github.com/usbarmory/go-boot/wiki/Boot-Transparency) + License ======= diff --git a/cmd/auth.go b/cmd/auth.go new file mode 100644 index 0000000..86cf52c --- /dev/null +++ b/cmd/auth.go @@ -0,0 +1,79 @@ +// Copyright (c) The go-boot authors. All Rights Reserved. +// +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +package cmd + +import ( + "errors" + "fmt" + "io/fs" + "net" + "regexp" + "strings" + + "github.com/usbarmory/boot-transparency/artifact" + "github.com/usbarmory/go-boot/shell" + "github.com/usbarmory/go-boot/transparency" + "github.com/usbarmory/go-boot/uapi" +) + +var btConfig transparency.Config + +func init() { + shell.Add(shell.Cmd{ + Name: "bt", + Args: 1, + Pattern: regexp.MustCompile(`^(?:bt)( none| offline| online)?$`), + Syntax: "(none|offline|online)?", + Help: "show/change boot-transparency status", + Fn: btCmd, + }) +} + +func btCmd(_ *shell.Interface, arg []string) (res string, err error) { + if len(arg[0]) > 0 { + switch strings.TrimSpace(arg[0]) { + case "none": + btConfig.Status = transparency.None + case "offline": + btConfig.Status = transparency.Offline + case "online": + if net.SocketFunc == nil { + return "", errors.New("network unavailable") + } + btConfig.Status = transparency.Online + } + } + + switch btConfig.Status { + case transparency.None: + res = fmt.Sprintf("boot-transparency is disabled\n") + case transparency.Offline, transparency.Online: + res = fmt.Sprintf("boot-transparency is enabled in %s mode\n", btConfig.Status.Resolve()) + } + + return +} + +func btValidateLinux(entry *uapi.Entry, root fs.FS) (err error) { + if entry == nil || len(entry.Linux) == 0 { + return errors.New("invalid kernel entry") + } + + btConfig.UefiRoot = root + + btEntry := transparency.BootEntry{ + transparency.Artifact{ + Category: artifact.LinuxKernel, + Hash: transparency.Hash(&entry.Linux), + }, + transparency.Artifact{ + Category: artifact.Initrd, + Hash: transparency.Hash(&entry.Initrd), + }, + } + + return btEntry.Validate(&btConfig) +} diff --git a/cmd/linux.go b/cmd/linux.go index 580ca41..b5dd357 100644 --- a/cmd/linux.go +++ b/cmd/linux.go @@ -15,6 +15,7 @@ import ( "github.com/usbarmory/armory-boot/exec" "github.com/usbarmory/go-boot/shell" + "github.com/usbarmory/go-boot/transparency" "github.com/usbarmory/go-boot/uapi" "github.com/usbarmory/go-boot/uefi" "github.com/usbarmory/go-boot/uefi/x64" @@ -245,6 +246,13 @@ func linuxCmd(_ *shell.Interface, arg []string) (res string, err error) { return "", errors.New("empty kernel entry") } + // boot transparency validation (if enabled) + if btConfig.Status != transparency.None { + if err = btValidateLinux(entry, root); err != nil { + return "", fmt.Errorf("boot transparency validation failed, %v", err) + } + } + image := &exec.LinuxImage{ Kernel: entry.Linux, InitialRamDisk: entry.Initrd, diff --git a/cmd/net.go b/cmd/net.go index e1372cf..66c4b60 100644 --- a/cmd/net.go +++ b/cmd/net.go @@ -21,6 +21,9 @@ import ( "github.com/usbarmory/go-boot/shell" "github.com/usbarmory/go-boot/uefi" "github.com/usbarmory/go-boot/uefi/x64" + + // maintained set of TLD roots for any potential TLS client request + _ "golang.org/x/crypto/x509roots/fallback" ) // Resolver represents the default name server diff --git a/go.mod b/go.mod index 42c4236..6ebac00 100644 --- a/go.mod +++ b/go.mod @@ -23,9 +23,15 @@ require ( github.com/therootcompany/xz v1.0.1 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/ulikunitz/xz v0.5.15 // indirect + github.com/usbarmory/boot-transparency v0.0.0-20251214154413-cc17620555b5 // indirect golang.org/x/crypto v0.42.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20251208183426-19acf81bd7bc // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.44.0 // indirect golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.7.0 // indirect gvisor.dev/gvisor v0.0.0-20250911055229-61a46406f068 // indirect + sigsum.org/sigsum-go v0.11.2 // indirect ) diff --git a/go.sum b/go.sum index e5f105f..61fa573 100644 --- a/go.sum +++ b/go.sum @@ -28,21 +28,35 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/usbarmory/armory-boot v0.0.0-20251106084515-0ad568ddb5ff h1:ei2XuYJXp8gJ4Ezte4EtJQOleFMs5d2Ppehm/NyNRWc= github.com/usbarmory/armory-boot v0.0.0-20251106084515-0ad568ddb5ff/go.mod h1:2cCdG4eUnVtrKyfbCc2A+0SHJl62Cgf4jEXTw/OvlW4= +github.com/usbarmory/boot-transparency v0.0.0-20251209141545-d49bc8b83d3e h1:06J4nBtMyqUhYwNigymqipQIXDezoytYJCaZb2WAYCM= +github.com/usbarmory/boot-transparency v0.0.0-20251209141545-d49bc8b83d3e/go.mod h1:sRG+Jvx0W7gw7ATXOGPgF93DhjHYZ0I9CnT0myhtdKo= +github.com/usbarmory/boot-transparency v0.0.0-20251214154413-cc17620555b5 h1:n/gGzxy3Y7QfEDCgANybJGiKjRFVxJPza6y7z/18FoQ= +github.com/usbarmory/boot-transparency v0.0.0-20251214154413-cc17620555b5/go.mod h1:sRG+Jvx0W7gw7ATXOGPgF93DhjHYZ0I9CnT0myhtdKo= github.com/usbarmory/go-net v0.0.0-20251003201608-93d9ffe808de h1:5O20CXXbFwjrqyDFtCyFUlxiRLCdc+ksT8MUihbjIDg= github.com/usbarmory/go-net v0.0.0-20251003201608-93d9ffe808de/go.mod h1:+6WiKCFJtJQZdNM2VpwQsYGo/aBJ39pN7nWx6Td3Z8s= github.com/usbarmory/tamago v1.25.5 h1:9iOrTuQnS/MtBTIhiI/YSJE3eWLZNiwxANUZtb19d9c= github.com/usbarmory/tamago v1.25.5/go.mod h1:CySGMX26pVXp1CMBxA5bq52eXEWqXqr+mWcZQW2e54c= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto/x509roots/fallback v0.0.0-20251208183426-19acf81bd7bc h1:jKhXqlxjiWmODO7bW0ihd0EGOzSDgQ1YVarUldQI/Wk= +golang.org/x/crypto/x509roots/fallback v0.0.0-20251208183426-19acf81bd7bc/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gvisor.dev/gvisor v0.0.0-20250911055229-61a46406f068 h1:95kdltF/maTDk/Wulj7V81cSLgjB/Mg/6eJmOKsey4U= gvisor.dev/gvisor v0.0.0-20250911055229-61a46406f068/go.mod h1:K16uJjZ+hSqDVsXhU2Rg2FpMN7kBvjZp/Ibt5BYZJjw= +sigsum.org/sigsum-go v0.11.2 h1:7HhDPC8gVJzl3wB3gAg3j6gTpO2t0UPHC0ogwhKuNRc= +sigsum.org/sigsum-go v0.11.2/go.mod h1:pGa/r4QsNYom+RqRMkdhcG5E00ty1nlTmALEizdRWPk= diff --git a/transparency/boot_entry.go b/transparency/boot_entry.go new file mode 100644 index 0000000..6314621 --- /dev/null +++ b/transparency/boot_entry.go @@ -0,0 +1,219 @@ +// Copyright (c) The go-boot authors. All Rights Reserved. +// +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +// Package transparency implements an interface to the +// boot-transparency library functions to ease boot bundle +// validation. +package transparency + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/usbarmory/boot-transparency/artifact" + "github.com/usbarmory/boot-transparency/engine/sigsum" + "github.com/usbarmory/boot-transparency/policy" + "github.com/usbarmory/boot-transparency/transparency" +) + +// Artifact represents a boot artifact. +type Artifact struct { + // Category represents the artifact category as defined + // in the boot-transparency library. + Category uint + + // Hash represents the SHA-256 hash of the artifact. + Hash string +} + +// BootEntry represent a boot entry as a set of artifacts. +type BootEntry []Artifact + +// Validate applies boot-transparency validation (e.g. inclusion proof, +// boot policy and claims consistency) for the argument [Config] representing +// the boot artifacts. +// Returns error if the boot artifacts are not passing the validation. +func (b BootEntry) Validate(c *Config) (err error) { + if c.Status == None { + return + } + + if len(b) == 0 { + return fmt.Errorf("invalid boot entry") + } + + // Automatically load the configuration from the UEFI partition + // when the function is used within the UEFI boot loader. + if c.UefiRoot != nil { + entryPath, err := c.Path(b) + if err != nil { + return fmt.Errorf("cannot load boot-transparency configuration, %v", err) + } + + if err = c.loadFromUefiRoot(entryPath); err != nil { + return fmt.Errorf("cannot load boot-transparency configuration, %v", err) + } + } + + te, err := transparency.GetEngine(transparency.Sigsum) + if err != nil { + return fmt.Errorf("unable to configure the transparency engine, %w", err) + } + + if err = te.SetKey([]string{string(c.LogKey)}, []string{string(c.SubmitKey)}); err != nil { + return fmt.Errorf("unable to set log and submitter keys, %v", err) + } + + wp, err := te.ParseWitnessPolicy(c.WitnessPolicy) + if err != nil { + return fmt.Errorf("unable to parse witness policy, %v", err) + } + + if err = te.SetWitnessPolicy(wp); err != nil { + return fmt.Errorf("unable to set witness policy, %v", err) + } + + pb, _, err := te.ParseProof(c.ProofBundle) + if err != nil { + return fmt.Errorf("unable to parse proof bundle, %v", err) + } + + // If network access is available the inclusion proof verification + // is performed using the proof fetched from the log. + if c.Status == Online { + pr, err := te.GetProof(pb) + if err != nil { + return err + } + + freshBundle := pb.(*sigsum.ProofBundle) + freshBundle.Proof = string(pr) + + if err = te.VerifyProof(freshBundle); err != nil { + return err + } + } else { + // If network access is not available the inclusion proof verification + // is performed using the proof included in the proof bundle. + if err = te.VerifyProof(pb); err != nil { + return err + } + } + + requirements, err := policy.ParseRequirements(c.BootPolicy) + if err != nil { + return + } + + proof := pb.(*sigsum.ProofBundle) + + claims, err := policy.ParseStatement(proof.Statement) + if err != nil { + return + } + + // Validate the matching between loaded artifact hashes and + // the ones included in the proof bundle. + if err = b.validateProofHashes(claims); err != nil { + return + } + + // Validate the matching between the logged claims and the policy requirements. + if err = policy.Validate(requirements, claims); err != nil { + // The boot bundle is NOT authorized. + return + } + + // boot-transparency validation passed, boot bundle is authorized. + return +} + +// Hash performs data hashing using SHA-256, which is +// the same algorithm used by boot-transparency library. +// Returns the computed hash as hex string. +func Hash(data *[]byte) (hexHash string) { + h := sha256.New() + h.Write(*data) + hash := h.Sum(nil) + + return hex.EncodeToString(hash) +} + +// Validate the matching between loaded artifact hashes and +// the ones included in the proof bundle. +// This step is vital to ensure the correspondence between the artifacts +// loaded in memory during the boot and the claims that will be validated +// by the boot-transparency policy function. +func (b BootEntry) validateProofHashes(s *policy.Statement) (err error) { + for _, a := range b { + if err = a.validateProofHash(s); err != nil { + return err + } + } + + return +} + +func (a Artifact) validateProofHash(s *policy.Statement) (err error) { + var h artifact.Handler + var found bool + + if err = a.validHash(); err != nil { + return + } + + for _, claimedArtifact := range s.Artifacts { + // The claims are referring to a different artifact + // category, try with next block of claims in the statement. + if a.Category != claimedArtifact.Category { + continue + } + + if h, err = artifact.GetHandler(a.Category); err != nil { + return + } + + // boot-transparency expect to parse requirements in JSON format. + requirements, _ := json.Marshal(map[string]string{"file_hash": a.Hash}) + + r, err := h.ParseRequirements([]byte(requirements)) + if err != nil { + return err + } + + c, err := h.ParseClaims([]byte(claimedArtifact.Claims)) + if err != nil { + return err + } + + // The validation logic is safe in the sense that error is returned + // if a file hash requested by the boot loader is not present in the + // statement for a given artifact category. + if err = h.Validate(r, c); err != nil { + return fmt.Errorf("loaded boot artifacts do not correspond to the proof bundle ones, file hash mismatch") + } + + found = true + break + } + + if !found { + return fmt.Errorf("loaded boot artifacts do not correspond to the proof bundle ones, one or more artifacts are not present in the proof bundle") + } + + return +} + +func (a Artifact) validHash() (err error) { + h, err := hex.DecodeString(a.Hash) + + if err != nil || len(h) != sha256.Size { + return fmt.Errorf("invalid artifact hash") + } + + return +} diff --git a/transparency/boot_entry_test.go b/transparency/boot_entry_test.go new file mode 100644 index 0000000..d5e72c0 --- /dev/null +++ b/transparency/boot_entry_test.go @@ -0,0 +1,172 @@ +// Copyright (c) The go-boot authors. All Rights Reserved. +// +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +// Package transparency implements an interface to the +// boot-transparency library functions to ease boot bundle +// validation. +package transparency + +import ( + "regexp" + "testing" + + "github.com/usbarmory/boot-transparency/artifact" +) + +func TestOfflineValidate(t *testing.T) { + c := Config{ + Status: Offline, + + BootPolicy: []byte(testBootPolicy), + WitnessPolicy: []byte(testWitnessPolicy), + SubmitKey: []byte(testSubmitKey), + LogKey: []byte(testLogKey), + ProofBundle: []byte(testProofBundle), + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: kernelHash, + }, + Artifact{ + Category: artifact.Initrd, + Hash: initrdHash, + }, + } + + if err := b.Validate(&c); err != nil { + t.Fatal(err) + } +} + +func TestOnlineValidate(t *testing.T) { + c := Config{ + Status: Online, + + BootPolicy: []byte(testBootPolicy), + WitnessPolicy: []byte(testWitnessPolicy), + SubmitKey: []byte(testSubmitKey), + LogKey: []byte(testLogKey), + ProofBundle: []byte(testProofBundle), + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: kernelHash, + }, + Artifact{ + Category: artifact.Initrd, + Hash: initrdHash, + }, + } + + if err := b.Validate(&c); err != nil { + t.Fatal(err) + } +} + +func TestOfflineValidateInvalidBootEntry(t *testing.T) { + c := Config{ + Status: Offline, + + BootPolicy: []byte(testBootPolicy), + WitnessPolicy: []byte(testWitnessPolicy), + SubmitKey: []byte(testSubmitKey), + LogKey: []byte(testLogKey), + ProofBundle: []byte(testProofBundle), + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: kernelHash, + }, + Artifact{ + Category: artifact.Initrd, + // missing Hash + }, + } + + err := b.Validate(&c) + + // Error expected: missing required Hash. + if err == nil { + t.Fatal(err) + } + + if !regexp.MustCompile(`invalid artifact hash`).MatchString(err.Error()) { + t.Fatal(err) + } +} + +func TestOfflineValidateHashMismatch(t *testing.T) { + c := Config{ + Status: Offline, + + BootPolicy: []byte(testBootPolicy), + WitnessPolicy: []byte(testWitnessPolicy), + SubmitKey: []byte(testSubmitKey), + LogKey: []byte(testLogKey), + ProofBundle: []byte(testProofBundle), + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: incorrectKernelHash, + }, + Artifact{ + Category: artifact.Initrd, + Hash: initrdHash, + }, + } + + err := b.Validate(&c) + + // Error expected: incorrect hash. + if err == nil { + t.Fatal(err) + } + + if !regexp.MustCompile(`file hash mismatch`).MatchString(err.Error()) { + t.Fatal(err) + } +} + +func TestOfflineValidatePolicyNotMet(t *testing.T) { + c := Config{ + Status: Offline, + + BootPolicy: []byte(testBootPolicyUnauthorized), + WitnessPolicy: []byte(testWitnessPolicy), + SubmitKey: []byte(testSubmitKey), + LogKey: []byte(testLogKey), + ProofBundle: []byte(testProofBundle), + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: kernelHash, + }, + Artifact{ + Category: artifact.Initrd, + Hash: initrdHash, + }, + } + + // Error expected: requirement not met. + err := b.Validate(&c) + + if err == nil { + t.Fatal(err) + } + + if !regexp.MustCompile(`build args requirement .+ not met`).MatchString(err.Error()) { + t.Fatal(err) + } +} diff --git a/transparency/config.go b/transparency/config.go new file mode 100644 index 0000000..017d865 --- /dev/null +++ b/transparency/config.go @@ -0,0 +1,146 @@ +// Copyright (c) The go-boot authors. All Rights Reserved. +// +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +// Package transparency implements an interface to the +// boot-transparency library functions to ease boot bundle +// validation. +package transparency + +import ( + "fmt" + "io/fs" + "path" + "sort" + "strings" +) + +// Represents the status of the boot-transparency functionality. +type Status int + +// Represents boot-transparency status codes. +const ( + // Boot-transparency disabled. + None Status = iota + + // Boot-transparency enabled in offline mode. + Offline + + // Boot-transparency enabled in online mode. + Online +) + +// Resolve resolves Status codes into a human-readable strings. +func (s Status) Resolve() string { + statusName := map[Status]string{ + None: "none", + Offline: "offline", + Online: "online", + } + + return statusName[s] +} + +// Boot-transparency configuration root directory and filenames. +const ( + transparencyRoot = `/transparency` + + bootPolicy = `policy.json` + witnessPolicy = `trust_policy` + proofBundle = `proof-bundle.json` + submitKey = `submit-key.pub` + logKey = `log-key.pub` +) + +// Config represents the configuration for the boot-transparency functionality. +type Config struct { + // Status represents the status of the boot-transparency functionality. + Status Status + + // UefiRoot represents the UEFI filesystem to "automatically" load the + // configuration files when running within the boot loader context. + // If the transparency pkg is imported "externally" by user-space tools + // this field is not set. + UefiRoot fs.FS + + // BootPolicy represents the boot policy in JSON format + // following the policy syntax supported by boot-transparency library. + BootPolicy []byte + + // WitnessPolicy represents the witness policy following + // the Sigsum plaintext witness policy format. + WitnessPolicy []byte + + // SubmitKey represents the log submitter public key in·OpenSSH·format. + SubmitKey []byte + + // LogKey represents the log public key in OpenSSH format. + LogKey []byte + + // ProofBundle represents the proof bundle in JSON format + // following the proof bundle format supported by boot-transparency library. + ProofBundle []byte +} + +// Path returns a unique configuration path for a given set of +// artifacts (i.e. boot entry). +// Returns error if one of the artifacts does not include a valid +// SHA-256 hash. +func (c *Config) Path(b BootEntry) (entryPath string, err error) { + if len(b) == 0 { + return "", fmt.Errorf("cannot build configuration path, got an invalid boot entry") + } + + artifacts := b + + // Sort the passed artifacts, by their Category, to ensure + // consistency in the way the entry path is build. + sort.Slice(artifacts, func(i, j int) bool { + return artifacts[i].Category < artifacts[j].Category + }) + + entryPath = transparencyRoot + for _, artifact := range artifacts { + if err = artifact.validHash(); err != nil { + return "", fmt.Errorf("cannot build configuration path, %v", err) + } + + entryPath = path.Join(entryPath, artifact.Hash) + } + + // Rewrite paths only when the pkg is used in the context + // of the UEFI boot loader. + if c.UefiRoot != nil { + entryPath = strings.ReplaceAll(entryPath, `/`, `\`) + } + + return +} + +// loadFromUefiRoot reads the transparency configuration files from +// the UEFI partition. The entry argument allows per-bundle configurations. +func (c *Config) loadFromUefiRoot(entryPath string) (err error) { + assets := map[string]*[]byte{ + bootPolicy: &c.BootPolicy, + witnessPolicy: &c.WitnessPolicy, + submitKey: &c.SubmitKey, + logKey: &c.LogKey, + proofBundle: &c.ProofBundle, + } + + if c.UefiRoot == nil { + return fmt.Errorf("cannot open uefi root filesystem") + } + + for filename, dst := range assets { + p := path.Join(entryPath, filename) + p = strings.ReplaceAll(p, `/`, `\`) + + if *dst, err = fs.ReadFile(c.UefiRoot, p); err != nil { + return fmt.Errorf("cannot load configuration file: %v", filename) + } + } + + return +} diff --git a/transparency/config_test.go b/transparency/config_test.go new file mode 100644 index 0000000..f032ec6 --- /dev/null +++ b/transparency/config_test.go @@ -0,0 +1,74 @@ +// Copyright (c) The go-boot authors. All Rights Reserved. +// +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +// Package transparency implements an interface to the +// boot-transparency library functions to ease boot bundle +// validation. +package transparency + +import ( + "testing" + + "github.com/usbarmory/boot-transparency/artifact" +) + +func TestPath(t *testing.T) { + c := Config{ + Status: Offline, + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: kernelHash, + }, + Artifact{ + Category: artifact.Initrd, + Hash: initrdHash, + }, + } + + p, err := c.Path(b) + if err != nil { + t.Fatal(err) + } + + if p != entryPath { + t.Fatal("got an invalid path.") + } +} + +func TestPathInvalidHash(t *testing.T) { + c := Config{ + Status: Offline, + } + + b := BootEntry{ + Artifact{ + Category: artifact.LinuxKernel, + Hash: invalidKernelHash, + }, + Artifact{ + Category: artifact.Initrd, + Hash: initrdHash, + }, + } + + // Error expected due to the invalid hash in the test entry. + if _, err := c.Path(b); err == nil { + t.Fatal(err) + } +} + +func TestLoadFromUefiRoot(t *testing.T) { + c := Config{ + Status: Offline, + UefiRoot: testUefiRoot, + } + + if err := c.loadFromUefiRoot(""); err != nil { + t.Fatal(err) + } +} diff --git a/transparency/data_test.go b/transparency/data_test.go new file mode 100644 index 0000000..3023ffc --- /dev/null +++ b/transparency/data_test.go @@ -0,0 +1,170 @@ +// Copyright (c) The go-boot authors. All Rights Reserved. +// +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +// Package transparency implements an interface to the +// boot-transparency library functions to ease boot bundle +// validation. +package transparency + +import ( + "testing/fstest" +) + +const ( + kernelHash = "4551848b4ab43cb4321c4d6ba98e1d215f950cee21bfd82c8c82ab64e34ec9a6" + initrdHash = "337630b74e55eae241f460faadf5a2f9a2157d6de2853d4106c35769e4acf538" + invalidKernelHash = "0672136965536be27980489b0388d864c96c74efd73d21432d0bcf10f9269f3" + incorrectKernelHash = "5551848b4ab43cb4321c4d6ba98e1d215f950cee21bfd82c8c82ab64e34ec9a6" + entryPath = "/transparency/4551848b4ab43cb4321c4d6ba98e1d215f950cee21bfd82c8c82ab64e34ec9a6/337630b74e55eae241f460faadf5a2f9a2157d6de2853d4106c35769e4acf538" + + testBootPolicy = `[ + { + "artifacts": [ + { + "category": 1, + "requirements": { + "min_version": "v6.14.0-29", + "tainted": false, + "build_args": { + "CONFIG_STACKPROTECTOR_STRONG": "y" + } + } + }, + { + "category": 2, + "requirements": { + "tainted": false + } + } + ], + "signatures": { + "signers": [ + { + "name": "dist signatory I", + "pub_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP5rbNcIOcwqBHzLOhJEfdKFHa+pIs10idfTm8c+HDnK" + }, + { + "name": "dist signatory II", + "pub_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0zV5fSWzzXa4R7Kpk6RAXkvWsJGpvkQ+9/xxpHC49J" + } + ], + "quorum": 2 + } + } +]` + + testWitnessPolicy = `log 4644af2abd40f4895a003bca350f9d5912ab301a49c77f13e5b6d905c20a5fe6 https://test.sigsum.org/barreleye + +witness poc.sigsum.org/nisse 1c25f8a44c635457e2e391d1efbca7d4c2951a0aef06225a881e46b98962ac6c +witness rgdd.se/poc-witness 28c92a5a3a054d317c86fc2eeb6a7ab2054d6217100d0be67ded5b74323c5806 + +group demo-quorum-rule any poc.sigsum.org/nisse rgdd.se/poc-witness +quorum demo-quorum-rule +` + + testProofBundle = `{ + "format": 1, + "statement": { + "header": { + "description": "Linux bundle", + "revision": "v1" + }, + "artifacts": [ + { + "category": 1, + "claims": { + "file_name": "vmlinuz-6.14.0-36-generic", + "file_hash": "4551848b4ab43cb4321c4d6ba98e1d215f950cee21bfd82c8c82ab64e34ec9a6", + "version": "v6.14.0-36-generic", + "tainted": false, + "build_args": { + "CONFIG_STACKPROTECTOR_STRONG": "y" + } + } + }, + { + "category": 2, + "claims": { + "file_name": "initrd.img-6.14.0-36-generic", + "file_hash": "337630b74e55eae241f460faadf5a2f9a2157d6de2853d4106c35769e4acf538", + "tainted": false + } + } + ], + "signatures": [ + { + "pub_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP5rbNcIOcwqBHzLOhJEfdKFHa+pIs10idfTm8c+HDnK", + "signature": "d29fe25fb7b0f8e977662bd50adac19ce0fdf3a51d8c2d8142d3fdd8a856200c7c4f1bf9ac8bb0bb011070869e585ebc8237d1e863e108b35788404de40bb40a" + }, + { + "pub_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0zV5fSWzzXa4R7Kpk6RAXkvWsJGpvkQ+9/xxpHC49J", + "signature": "fac959414a3032f93490790193460e3ab014eaae674dd2fef1211edaf0630d47eac774065e6f9897b1a49b6e66c89ec7c3cc4387068dc2261bd4115be096aa0d" + } + ] + }, + "probe": { + "origin": "https://test.sigsum.org/barreleye", + "leaf_signature": "b380a0b0df0ae1710c1a51a708d7f45c0df840047a91c820dac04fe3f2c03b40b10b3f0cbcb189de410f83ad735ccf0d8146fe0b9c1408eda2b421d1daea8101", + "log_public_key_hash": "4e89cc51651f0d95f3c6127c15e1a42e3ddf7046c5b17b752689c402e773bb4d", + "submit_public_key_hash": "302928c2e0e01da52e3b161c54906de9b55ce250f0f47e80e022d04036e2765c" + }, + "proof": "version=2\nlog=4e89cc51651f0d95f3c6127c15e1a42e3ddf7046c5b17b752689c402e773bb4d\nleaf=302928c2e0e01da52e3b161c54906de9b55ce250f0f47e80e022d04036e2765c b380a0b0df0ae1710c1a51a708d7f45c0df840047a91c820dac04fe3f2c03b40b10b3f0cbcb189de410f83ad735ccf0d8146fe0b9c1408eda2b421d1daea8101\n\nsize=17460\nroot_hash=4b07f20245d211b6fe058dd396baf2821d260d4180da47b561833ec3611cdfcd\nsignature=016235c8ce22d377028da78453b8b9d30e664c36f1892b436fca2a29aab182ff6251c14ae0c3d453fddc28fb3f5a73ea932517ba2657716a7d251ab2c5b77b0a\ncosignature=70b861a010f25030de6ff6a5267e0b951e70c04b20ba4a3ce41e7fba7b9b7dfc 1765901875 d713b5ed21fc156e942d735808586d91f27c494f61a59414ecc19048866d46a57a265e82eaa2c80acbcbb7cf90a57d9e11f1a5c22ecdd880264246d6dac7580e\ncosignature=26983895bdf491838dd4885848670562e7b728b6efa15fd9047b5b97a9a0618f 1765901875 ff48b36e74670e2e74910bca4b0bc1d9ab0c62f162c9ccd1ad86aca05f2b1a31e1920aa0aa029973866174780d3ee6f480c8337d9927feeb118a40202a07cf01\ncosignature=d960fcff859a34d677343e4789c6843e897c9ff195ea7140a6ef382566df3b65 1765901875 9019fccf24c91a5dc535c5f73b6b8bc85da8bfa97136704f9c13733808ba52b7213ac664a2cef4aa67e2512736a77853524cbb0ee1ace6d3451b47351475a20e\ncosignature=e4a6a1e4657d8d7a187cc0c20ed51055d88c72f340d29534939aee32d86b4021 1765901875 2ccd5af89a0d25d94679c86e7f782a868f1b61da6fb3365f59558b355be583d588acd11c57c1a33e2f5683c82cb4237eb62d6d8275d25a51014e42f06a2e110e\ncosignature=42351ad474b29c04187fd0c8c7670656386f323f02e9a4ef0a0055ec061ecac8 1765901875 f679eb4fb0bf40b516b30eb6e74c6d6293e8797be0cb8ab6335698f547e1aae1397f91b4ee60ceecf02e02daf202e65b26117a0c86676c956d72eaaa8e829a07\ncosignature=c1d2d6935c2fb43bef395792b1f3c1dfe4072d4c6cadd05e0cc90b28d7141ed3 1765901875 8faa05703639bc15bd5f783214393e50e57a3f0eb61d9f79de042b2428c613586261f357f31d57a81c8793fabb50d889f5b80c6e9c9da8a94b675e3485ccbd01\ncosignature=1c997261f16e6e81d13f420900a2542a4b6a049c2d996324ee5d82a90ca3360c 1765901875 580be37fda6053b5fe1442b174e97b4ea5d49404855b1a5f3de034ee16ba988c2d694b6bc56683b707f640507271f78b367691dcebb7ee53f6e2f797e3653101\ncosignature=49c4cd6124b7c572f3354d854d50b2a4b057a750f786cf03103c09de339c4ea3 1765901875 1be97c08da21bf8adfccfbf4b69166fb8529ec13123f84bebef3c5a582d7e5a5c0f579e96f813410c8ca1f966f15944c4e48272570cd670d62b9a6321fa4f500\ncosignature=86b5414ae57f45c2953a074640bb5bedebad023925d4dc91a31de1350b710089 1765901875 d4daf5a7be9482af90bcb665c6bc75dd05421b7fcb5f053b3bcb9a6994b9e516339e2736e03c831cd273947cda964898f92c4288fba8001bf84e699be0b7300e\n\nleaf_index=17459\nnode_hash=83deb0c1a1b1d1468883321dc1ce686c437142c23eba4cf842dd0520ac6d5025\nnode_hash=503d30009fc38c5179225883ada9a9cb1d27cfdd9b2d730218ee95ec31c83d7c\nnode_hash=5d1d426d73f0a178a3c6385e2cc8811e18c81edee8dee566c03d82c7f4404626\nnode_hash=4f1e71bd71a62ab4255e6eb2d739b1f28b21fb0cbda97434c76da71399031a74\nnode_hash=9ad205a7b91cfbdcbe22a13e77d877509f2eb90c4b19355247781ef32c8446bb\nnode_hash=297c6cc8145fd585a9e009057191d91c9ce013957ee48ae26982ac96d52b578b\n" +}` + + testSubmitKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMqym9S/tFn6B/Eri5hGJiEV8BpGumEPcm65uxC+FG6K sigsum key` + testLogKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZEryq9QPSJWgA7yjUPnVkSqzAaScd/E+W22QXCCl/m` + + testBootPolicyUnauthorized = `[ + { + "artifacts": [ + { + "category": 1, + "requirements": { + "build_args": { + "I_WANT_CANDY": "y" + } + } + }, + { + "category": 2, + "requirements": { + "tainted": false + } + } + ], + "signatures": { + "signers": [ + { + "name": "dist signatory I", + "pub_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP5rbNcIOcwqBHzLOhJEfdKFHa+pIs10idfTm8c+HDnK" + }, + { + "name": "dist signatory II", + "pub_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0zV5fSWzzXa4R7Kpk6RAXkvWsJGpvkQ+9/xxpHC49J" + } + ], + "quorum": 2 + } + } +]` +) + +var testUefiRoot = fstest.MapFS{ + bootPolicy: { + Data: []byte(testBootPolicy), + }, + witnessPolicy: { + Data: []byte(testWitnessPolicy), + }, + submitKey: { + Data: []byte(testSubmitKey), + }, + logKey: { + Data: []byte(testLogKey), + }, + proofBundle: { + Data: []byte(testProofBundle), + }, +}