Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

offline-update: Add multi-target bundle support #387

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions client/foundries.go
Original file line number Diff line number Diff line change
Expand Up @@ -1172,20 +1172,24 @@ func (a *Api) TufMetadataGet(factory string, metadata string, tag string, prod b
return a.Get(url)
}

func (a *Api) TufTargetMetadataRefresh(factory string, target string, tag string, expiresIn int, prod bool, wave string) (map[string]tuf.Signed, error) {
func (a *Api) TufTargetMetadataRefresh(
factory string, target string, tag string, expiresIn int, prod bool, wave string, bundleTargets *tuf.Signed,
) (map[string]tuf.Signed, error) {
url := a.serverUrl + "/ota/factories/" + factory + "/targets/" + target + "/meta/"
type targetMeta struct {
Tag string `json:"tag"`
ExpiresIn int `json:"expires-in-days"`
Prod bool `json:"production"`
Wave string `json:"wave"`
Tag string `json:"tag"`
ExpiresIn int `json:"expires-in-days"`
Prod bool `json:"production"`
Wave string `json:"wave"`
BundleTargets *tuf.Signed `json:"bundle-targets,omitempty"`
}

b, err := json.Marshal(targetMeta{
Tag: tag,
ExpiresIn: expiresIn,
Prod: prod,
Wave: wave,
Tag: tag,
ExpiresIn: expiresIn,
Prod: prod,
Wave: wave,
BundleTargets: bundleTargets,
})
if err != nil {
return nil, err
Expand Down
186 changes: 182 additions & 4 deletions subcommands/targets/offline-update.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
"strings"
"time"

canonical "github.com/docker/go/canonical/json"
tuf "github.com/theupdateframework/notary/tuf/data"

"github.com/foundriesio/fioctl/client"
"github.com/foundriesio/fioctl/subcommands"
"github.com/foundriesio/fioctl/subcommands/keys"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -30,6 +32,16 @@ type (
buildTag string
fetchedApps *client.FetchedApps
}

ouBundleMeta struct {
Type string `json:"type"`
Tag string `json:"tag"`
Targets []string `json:"targets"`
}
ouBundleTufMeta struct {
tuf.SignedCommon
ouBundleMeta `json:"x-fio-offline-bundle"`
}
)

var (
Expand Down Expand Up @@ -77,6 +89,43 @@ func init() {
"Allow multiple targets to be stored in the same <dst> directory")
offlineUpdateCmd.MarkFlagsMutuallyExclusive("tag", "wave")
offlineUpdateCmd.MarkFlagsMutuallyExclusive("prod", "wave")
initSignCmd(offlineUpdateCmd)
initShowCmd(offlineUpdateCmd)
}

func initSignCmd(parentCmd *cobra.Command) {
signCmd := &cobra.Command{
Use: "sign <path to an offline bundle>",
Short: "Sign an offline bundle with a targets role offline key",
Long: `Sign an offline bundle with a targets role offline key.

Run this command if your offline update bundle contains production/wave targets.
In this case, the bundle has to be signed by one or more targets role offline keys.
The number of required signatures depends on the threshold number set in the current TUF root role metadata,
and is printed by this command.`,
Run: doSignBundle,
Args: cobra.ExactArgs(1),
}
signCmd.Flags().StringP("keys", "k", "",
"Path to the <tuf-targets-keys.tgz> key to sign the bundle metadata with. "+
"This is the same key used to sign prod & wave TUF targets.")
_ = signCmd.MarkFlagRequired("keys")
parentCmd.AddCommand(signCmd)
}

func initShowCmd(parentCmd *cobra.Command) {
showCmd := &cobra.Command{
Use: "show <path to an offline bundle>",
Short: "Parse and print the specified bundle metadata",
Long: `Parse and print the specified bundle metadata.

Run this command if you would like to get information about an offline bundle.
Specifically, what targets it includes, what the type of the targets (CI or production),
a bundle's expiration time', etc.`,
Run: doShowBundle,
Args: cobra.ExactArgs(1),
}
parentCmd.AddCommand(showCmd)
}

func doOfflineUpdate(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -149,6 +198,7 @@ Notice that multiple targets in the same directory is only supported in LmP >= v
}
fmt.Println("Successfully downloaded offline update content")
}
doShowBundle(cmd, []string{dstDir})
}

func getTargetInfo(targetFile *tuf.FileMeta) (*ouTargetInfo, error) {
Expand Down Expand Up @@ -286,16 +336,19 @@ func downloadTufRepo(factory string, target string, tag string, prod bool, wave
}
ver += 1
}

meta, err := api.TufTargetMetadataRefresh(factory, target, tag, expiresIn, prod, wave)
bundleTargets, err := getBundleTargetsMeta(dstDir, false)
if err != nil {
return err
}
meta, err := api.TufTargetMetadataRefresh(factory, target, tag, expiresIn, prod, wave, bundleTargets)
if err != nil {
return err
}
metadataNames := []string{
"timestamp", "snapshot", "targets",
"timestamp", "snapshot", "targets", "bundle-targets",
}
for _, metaName := range metadataNames {
b, err := json.Marshal(meta[metaName])
b, err := canonical.MarshalCanonical(meta[metaName])
if err != nil {
return err
}
Expand Down Expand Up @@ -396,3 +449,128 @@ func untar(r io.Reader, dstDir string) error {

return nil
}

func getBundleTargetsMeta(bundleTufPath string, errIfNotExist bool) (bundleTargets *tuf.Signed, err error) {
if b, readErr := os.ReadFile(path.Join(bundleTufPath, "bundle-targets.json")); readErr == nil {
var foundBundleTargets tuf.Signed
if err = canonical.Unmarshal(b, &foundBundleTargets); err == nil {
bundleTargets = &foundBundleTargets
}
mike-sul marked this conversation as resolved.
Show resolved Hide resolved
} else if errIfNotExist || !errors.Is(readErr, os.ErrNotExist) {
err = readErr
}
return
}

func doSignBundle(cmd *cobra.Command, args []string) {
offlineKeysFile, _ := cmd.Flags().GetString("keys")
offlineKeys, err := keys.GetOfflineCreds(offlineKeysFile)
subcommands.DieNotNil(err, "Failed to open offline keys file")

bundleTufPath := path.Join(args[0], "tuf")
bundleTargets, err := getBundleTargetsMeta(bundleTufPath, true)
subcommands.DieNotNil(err)

rootMeta, err := getLatestRoot(bundleTufPath)
subcommands.DieNotNil(err)

err = signBundleTargets(rootMeta, bundleTargets, offlineKeys)
subcommands.DieNotNil(err)

if b, err := canonical.MarshalCanonical(bundleTargets); err == nil {
subcommands.DieNotNil(os.WriteFile(path.Join(bundleTufPath, "bundle-targets.json"), b, 0666))
numberOfMoreRequiredSignatures := rootMeta.Signed.Roles["targets"].Threshold - len(bundleTargets.Signatures)
if numberOfMoreRequiredSignatures > 0 {
fmt.Printf("%d more signature(s) is/are required to meet the required threshold (%d)\n",
numberOfMoreRequiredSignatures, rootMeta.Signed.Roles["targets"].Threshold)
} else {
fmt.Printf("The bundle is signed with enough number of signatures (%d) to meet the required threshold (%d)\n",
len(bundleTargets.Signatures), rootMeta.Signed.Roles["targets"].Threshold)
}
} else {
subcommands.DieNotNil(err)
}
doShowBundle(cmd, args)
}

func getLatestRoot(bundleTufPath string) (*client.AtsTufRoot, error) {
var latestVersionBytes []byte
var readErr error

curVer := 3
for {

latestVersionPath := path.Join(bundleTufPath, fmt.Sprintf("%d.root.json", curVer))
if b, err := os.ReadFile(latestVersionPath); err != nil {
readErr = err
break
} else {
latestVersionBytes = b
curVer += 1
}
}
if !errors.Is(readErr, os.ErrNotExist) {
return nil, readErr
}

rootMeta := client.AtsTufRoot{}
if err := json.Unmarshal(latestVersionBytes, &rootMeta); err != nil {
return nil, err
}
return &rootMeta, nil
}

func signBundleTargets(rootMeta *client.AtsTufRoot, bundleTargetsMeta *tuf.Signed, offlineKeys keys.OfflineCreds) error {
signer, err := keys.FindOneTufSigner(rootMeta, offlineKeys, rootMeta.Signed.Roles["targets"].KeyIDs)
if err != nil {
return fmt.Errorf("%s %w", keys.ErrMsgReadingTufKey("targets", "current"), err)
}
for _, signature := range bundleTargetsMeta.Signatures {
if signature.KeyID == signer.Id {
return fmt.Errorf("the bundle is already signed by the provided key: %s", signer.Id)
}
}
if bundleTargetsMeta.Signed == nil {
panic(fmt.Errorf("the input bundle metadata to sign is nil"))
}
fmt.Printf("Signing the bundle with a new key; ID: %s, type: %s\n", signer.Id, signer.Type.Name())
signatures, err := keys.SignTufMeta(*bundleTargetsMeta.Signed, signer)
if err != nil {
return err
}
bundleTargetsMeta.Signatures = append(bundleTargetsMeta.Signatures, signatures[0])
return nil
}

func doShowBundle(cmd *cobra.Command, args []string) {
tufMetaPath := path.Join(args[0], "tuf")
bundleTufMeta, err := getBundleTargetsMeta(tufMetaPath, true)
subcommands.DieNotNil(err)
bundleMeta := ouBundleTufMeta{}
subcommands.DieNotNil(json.Unmarshal(*bundleTufMeta.Signed, &bundleMeta))
fmt.Println("Bundle targets info:")
fmt.Printf("\tType:\t\t%s\n", bundleMeta.ouBundleMeta.Type)
fmt.Printf("\tTag:\t\t%s\n", bundleMeta.Tag)
fmt.Printf("\tExpires:\t%s\n", bundleMeta.SignedCommon.Expires)
fmt.Println("\tTargets:")
for _, target := range bundleMeta.Targets {
fmt.Printf("\t\t\t%s\n", target)
}
fmt.Println("\tSignatures:")
for _, sig := range bundleTufMeta.Signatures {
fmt.Printf("\t\t\t- %s\n", sig.KeyID)
}

rootMeta, err := getLatestRoot(tufMetaPath)
subcommands.DieNotNil(err)
fmt.Println("\tAllowed keys:")
for _, key := range rootMeta.Signed.Roles["targets"].KeyIDs {
fmt.Printf("\t\t\t- %s\n", key)
}
fmt.Printf("\tThreshold:\t%d\n", rootMeta.Signed.Roles["targets"].Threshold)
numberOfMissingSignatures := rootMeta.Signed.Roles["targets"].Threshold - len(bundleTufMeta.Signatures)
if numberOfMissingSignatures > 0 {
fmt.Printf("\tMissing:\t%d (the number of required additional signatures;"+
" run the `sign` sub-command to sign the bundle)\n", numberOfMissingSignatures)
mike-sul marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading