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

chore: bundle signing #843

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions docs/command-reference/uds_deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ uds deploy [BUNDLE_TARBALL|OCI_REF] [flags]
```
-c, --confirm Confirms bundle deployment without prompting. ONLY use with bundles you trust
-h, --help help for deploy
-k, --key string Path to a public key file that will be used to validate a signed bundle
-p, --packages stringArray Specify which zarf packages you would like to deploy from the bundle. By default all zarf packages in the bundle are deployed.
-r, --resume Only deploys packages from the bundle which haven't already been deployed
--retries int Specify the number of retries for package deployments (applies to all pkgs in a bundle) (default 3)
Expand Down
1 change: 1 addition & 0 deletions src/cmd/uds.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func init() {
rootCmd.AddCommand(deployCmd)
deployCmd.Flags().StringToStringVar(&bundleCfg.DeployOpts.SetVariables, "set", nil, lang.CmdBundleDeployFlagSet)
deployCmd.Flags().BoolVarP(&config.CommonOptions.Confirm, "confirm", "c", false, lang.CmdBundleDeployFlagConfirm)
deployCmd.Flags().StringVarP(&bundleCfg.DeployOpts.PublicKeyPath, "key", "k", v.GetString(V_BNDL_DEPLOY_KEY), lang.CmdBundleInspectFlagKey)
deployCmd.Flags().StringArrayVarP(&bundleCfg.DeployOpts.Packages, "packages", "p", []string{}, lang.CmdBundleDeployFlagPackages)
deployCmd.Flags().BoolVarP(&bundleCfg.DeployOpts.Resume, "resume", "r", false, lang.CmdBundleDeployFlagResume)
deployCmd.Flags().IntVar(&bundleCfg.DeployOpts.Retries, "retries", 3, lang.CmdBundleDeployFlagRetries)
Expand Down
3 changes: 3 additions & 0 deletions src/cmd/viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const (
// Bundle pull config keys
V_BNDL_PULL_OUTPUT = "bundle.pull.output"
V_BNDL_PULL_KEY = "bundle.pull.key"

// Bundle deploy config keys
V_BNDL_DEPLOY_KEY = "bundle.deploy.key"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of bundle.deployKey? Just thinking of ways to reduce YAML layers

)

var (
Expand Down
1 change: 1 addition & 0 deletions src/config/lang/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const (
CmdPackageInspectFlagExtractSBOM = "Create a folder of SBOMs contained in the bundle"
CmdBundleInspectFlagFindImages = "Derive images from a uds-bundle.yaml file and list them"
CmdBundleInspectFlagListVariables = "List all configurable variables in a bundle (including zarf variables)"
CmdBundleInspectSignedNoPublicKey = "The package was signed but no public key was provided, skipping signature validation"

// bundle remove
CmdBundleRemoveShort = "Remove a bundle that has been deployed already"
Expand Down
25 changes: 1 addition & 24 deletions src/pkg/bundle/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/defenseunicorns/uds-cli/src/pkg/utils"
"github.com/defenseunicorns/uds-cli/src/types"
zarfConfig "github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/pkg/interactive"
"github.com/defenseunicorns/zarf/src/pkg/message"
zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/pterm/pterm"
Expand Down Expand Up @@ -66,28 +65,6 @@ func (b *Bundle) Create() error {
validateSpinner.Successf("Bundle Validated")
pterm.Print()

// sign the bundle if a signing key was provided
if b.cfg.CreateOpts.SigningKeyPath != "" {
// write the bundle to disk so we can sign it
bundlePath := filepath.Join(b.tmp, config.BundleYAML)
if err := zarfUtils.WriteYaml(bundlePath, &b.bundle, 0600); err != nil {
return err
}

getSigCreatePassword := func(_ bool) ([]byte, error) {
if b.cfg.CreateOpts.SigningKeyPassword != "" {
return []byte(b.cfg.CreateOpts.SigningKeyPassword), nil
}
return interactive.PromptSigPassword()
}
// sign the bundle
signaturePath := filepath.Join(b.tmp, config.BundleYAMLSignature)
_, err := zarfUtils.CosignSignBlob(bundlePath, signaturePath, b.cfg.CreateOpts.SigningKeyPath, getSigCreatePassword)
if err != nil {
return err
}
}

// for dev mode update package ref for local bundles, refs for remote bundles updated on deploy
if config.Dev && len(b.cfg.DevDeployOpts.Ref) != 0 {
for i, pkg := range b.bundle.Packages {
Expand All @@ -104,7 +81,7 @@ func (b *Bundle) Create() error {
}
bundlerClient := bundler.NewBundler(&opts)

return bundlerClient.Create()
return bundlerClient.Create(b.cfg.CreateOpts)
}

// confirmBundleCreation prompts the user to confirm bundle creation
Expand Down
7 changes: 6 additions & 1 deletion src/pkg/bundle/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
"path/filepath"
"strings"

"github.com/defenseunicorns/pkg/helpers/v2"
"github.com/defenseunicorns/pkg/oci"
"github.com/defenseunicorns/uds-cli/src/config"
"github.com/defenseunicorns/uds-cli/src/config/lang"
"github.com/defenseunicorns/uds-cli/src/pkg/sources"
"github.com/defenseunicorns/uds-cli/src/pkg/utils"
"github.com/defenseunicorns/uds-cli/src/types"
Expand Down Expand Up @@ -61,7 +63,10 @@ func (b *Bundle) Inspect() error {
}

// validate the sig (if present)
if err := ValidateBundleSignature(filepaths[config.BundleYAML], filepaths[config.BundleYAMLSignature], b.cfg.InspectOpts.PublicKeyPath); err != nil {
// The package is signed, but no public key was provided
if !helpers.InvalidPath(filepaths[config.BundleYAMLSignature]) && helpers.InvalidPath(b.cfg.InspectOpts.PublicKeyPath) {
message.Warn(lang.CmdBundleInspectSignedNoPublicKey)
} else if err := ValidateBundleSignature(filepaths[config.BundleYAML], filepaths[config.BundleYAMLSignature], b.cfg.InspectOpts.PublicKeyPath); err != nil {
return err
}

Expand Down
8 changes: 4 additions & 4 deletions src/pkg/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ func NewBundler(opts *Options) *Bundler {
}

// Create creates a bundle
func (b *Bundler) Create() error {
func (b *Bundler) Create(createOpts types.BundleCreateOptions) error {
if utils.IsRegistryURL(b.output) {
remoteBundle := NewRemoteBundle(&RemoteBundleOpts{Bundle: b.bundle, Output: b.output})
err := remoteBundle.create(nil)
remoteBundle := NewRemoteBundle(&RemoteBundleOpts{Bundle: b.bundle, TmpDstDir: b.tmpDstDir, Output: b.output})
err := remoteBundle.create(createOpts)
if err != nil {
return err
}
} else {
localBundle := NewLocalBundle(&LocalBundleOpts{Bundle: b.bundle, TmpDstDir: b.tmpDstDir, SourceDir: b.sourceDir, OutputDir: b.output})
err := localBundle.create(nil)
err := localBundle.create(createOpts)
if err != nil {
return err
}
Expand Down
72 changes: 54 additions & 18 deletions src/pkg/bundler/localbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/defenseunicorns/uds-cli/src/pkg/utils"
"github.com/defenseunicorns/uds-cli/src/pkg/utils/boci"
"github.com/defenseunicorns/uds-cli/src/types"
"github.com/defenseunicorns/zarf/src/pkg/interactive"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/zoci"
goyaml "github.com/goccy/go-yaml"
Expand All @@ -27,6 +28,8 @@ import (
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2/content"
ocistore "oras.land/oras-go/v2/content/oci"

zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
)

// LocalBundleOpts are the options for creating a local bundle
Expand Down Expand Up @@ -56,7 +59,7 @@ func NewLocalBundle(opts *LocalBundleOpts) *LocalBundle {
}

// create creates the bundle and outputs to a local tarball
func (lo *LocalBundle) create(signature []byte) error {
func (lo *LocalBundle) create(createOpts types.BundleCreateOptions) error {
bundle := lo.bundle
if bundle.Metadata.Architecture == "" {
return fmt.Errorf("architecture is required for bundling")
Expand Down Expand Up @@ -118,6 +121,14 @@ func (lo *LocalBundle) create(signature []byte) error {
digest := bundleYAMLDesc.Digest.Encoded()
artifactPathMap[filepath.Join(lo.tmpDstDir, config.BlobsDir, digest)] = filepath.Join(config.BlobsDir, digest)

// sign bundle.yaml layer
if createOpts.SigningKeyPath != "" {
rootManifest, artifactPathMap, err = lo.signBundle(createOpts, digest, store, artifactPathMap, rootManifest)
if err != nil {
return err
}
}

// create and push bundle manifest config
manifestConfigDesc, err := pushManifestConfig(store, bundle.Metadata, bundle.Build)
if err != nil {
Expand All @@ -142,20 +153,6 @@ func (lo *LocalBundle) create(signature []byte) error {
// grab oci-layout
artifactPathMap[filepath.Join(lo.tmpDstDir, "oci-layout")] = "oci-layout"

// push the bundle's signature todo: need to understand functionality and add tests
if len(signature) > 0 {
signatureDesc, err := pushBundleSignature(store, signature)
if err != nil {
return err
}
rootManifest.Layers = append(rootManifest.Layers, signatureDesc)
jsonValue, err := utils.JSONValue(signatureDesc)
if err != nil {
return err
}
message.Debug("Pushed", config.BundleYAMLSignature+":", jsonValue)
}

// tag the local bundle artifact
// todo: no need to tag the local artifact
err = store.Tag(ctx, rootManifestDesc, bundle.Metadata.Version)
Expand Down Expand Up @@ -297,10 +294,49 @@ jobLoop:
return nil
}

func pushBundleSignature(store *ocistore.Store, signature []byte) (ocispec.Descriptor, error) {
// signBundle signs the bundle layer
func (lo *LocalBundle) signBundle(createOpts types.BundleCreateOptions, digest string, store *ocistore.Store, artifactPathMap types.PathMap, rootManifest ocispec.Manifest) (ocispec.Manifest, types.PathMap, error) {
getSigCreatePassword := func(_ bool) ([]byte, error) {
if createOpts.SigningKeyPassword != "" {
return []byte(createOpts.SigningKeyPassword), nil
}
if config.CommonOptions.Confirm {
return nil, nil
}
return interactive.PromptSigPassword()
}
// sign the bundle layer
signaturePath := filepath.Join(lo.tmpDstDir, config.BundleYAMLSignature)
_, err := zarfUtils.CosignSignBlob(filepath.Join(lo.tmpDstDir, config.BlobsDir, digest), signaturePath, createOpts.SigningKeyPath, getSigCreatePassword)
if err != nil {
return ocispec.Manifest{}, nil, err
}

// append uds-bundle.yaml.sig layer to rootManifest and grab path for archiving
signatureDesc, err := pushBundleSignature(store, lo.tmpDstDir)
if err != nil {
return ocispec.Manifest{}, nil, err
}
rootManifest.Layers = append(rootManifest.Layers, signatureDesc)
digest = signatureDesc.Digest.Encoded()
artifactPathMap[filepath.Join(lo.tmpDstDir, config.BlobsDir, digest)] = filepath.Join(config.BlobsDir, digest)

jsonValue, err := utils.JSONValue(signatureDesc)
if err != nil {
return ocispec.Manifest{}, nil, err
}
message.Debug("Pushed", config.BundleYAMLSignature+":", jsonValue)
return rootManifest, artifactPathMap, nil
}

func pushBundleSignature(store *ocistore.Store, tmpDstDir string) (ocispec.Descriptor, error) {
ctx := context.TODO()
signatureDesc := content.NewDescriptorFromBytes(zoci.ZarfLayerMediaTypeBlob, signature)
err := store.Push(ctx, signatureDesc, bytes.NewReader(signature))
signatureBytes, err := os.ReadFile(filepath.Join(tmpDstDir, config.BundleYAMLSignature))
if err != nil {
return ocispec.Descriptor{}, err
}
signatureDesc := content.NewDescriptorFromBytes(zoci.ZarfLayerMediaTypeBlob, signatureBytes)
err = store.Push(ctx, signatureDesc, bytes.NewReader(signatureBytes))
if err != nil {
return ocispec.Descriptor{}, err
}
Expand Down
99 changes: 80 additions & 19 deletions src/pkg/bundler/remotebundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ package bundler
import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/defenseunicorns/pkg/helpers/v2"
"github.com/defenseunicorns/pkg/oci"
"github.com/defenseunicorns/uds-cli/src/config"
"github.com/defenseunicorns/uds-cli/src/pkg/bundler/pusher"
Expand All @@ -18,6 +21,9 @@ import (
"github.com/defenseunicorns/zarf/src/pkg/zoci"
goyaml "github.com/goccy/go-yaml"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"

"github.com/defenseunicorns/zarf/src/pkg/interactive"
zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
)

// RemoteBundleOpts are the options for creating a remote bundle
Expand All @@ -44,9 +50,8 @@ func NewRemoteBundle(opts *RemoteBundleOpts) *RemoteBundle {
}

// create creates the bundle in a remote OCI registry publishes w/ optional signature to the remote repository.
func (r *RemoteBundle) create(signature []byte) error {
func (r *RemoteBundle) create(createOpts types.BundleCreateOptions) error {
ctx := context.TODO()

// set the bundle remote's reference from metadata
r.output = boci.EnsureOCIPrefix(r.output)
ref, err := referenceFromMetadata(r.output, &r.bundle.Metadata)
Expand Down Expand Up @@ -120,23 +125,6 @@ func (r *RemoteBundle) create(signature []byte) error {
message.Debug("Pushed", config.BundleYAML+":", jsonValue)
rootManifest.Layers = append(rootManifest.Layers, *bundleYamlDesc)

// push the bundle's signature
if len(signature) > 0 {
bundleYamlSigDesc, err := bundleRemote.PushLayer(ctx, signature, zoci.ZarfLayerMediaTypeBlob)
if err != nil {
return err
}
bundleYamlSigDesc.Annotations = map[string]string{
ocispec.AnnotationTitle: config.BundleYAMLSignature,
}
rootManifest.Layers = append(rootManifest.Layers, *bundleYamlSigDesc)
jsonValue, err := utils.JSONValue(bundleYamlSigDesc)
if err != nil {
return err
}
message.Debug("Pushed", config.BundleYAMLSignature+":", jsonValue)
}

// push the bundle manifest config
configDesc, err := pushManifestConfigFromMetadata(bundleRemote.OrasRemote, &bundle.Metadata, &bundle.Build)
if err != nil {
Expand Down Expand Up @@ -164,6 +152,14 @@ func (r *RemoteBundle) create(signature []byte) error {
return err
}

// Pull the bundle.yaml, sign it, and repush it to the remote along with the signature
if createOpts.SigningKeyPath != "" {
rootManifestDesc, err = r.signBundle(ctx, bundleRemote, createOpts, rootManifest)
if err != nil {
return err
}
}

// create or update, then push index.json
err = boci.UpdateIndex(index, bundleRemote.OrasRemote, bundle, *rootManifestDesc)
if err != nil {
Expand All @@ -182,3 +178,68 @@ func (r *RemoteBundle) create(signature []byte) error {

return nil
}

// signBundle signs the bundle.yaml layer and pushes the signature to the remote
func (r *RemoteBundle) signBundle(ctx context.Context, bundleRemote *zoci.Remote, createOpts types.BundleCreateOptions, rootManifest ocispec.Manifest) (*ocispec.Descriptor, error) {
// pull the bundle.yaml
if err := helpers.CreateDirectory(filepath.Join(r.tmpDstDir, config.BlobsDir), 0700); err != nil {
return nil, err
}
layers, err := bundleRemote.PullPaths(context.TODO(), filepath.Join(r.tmpDstDir, config.BlobsDir), config.BundleAlwaysPull)
if err != nil {
return nil, err
}
filepaths := make(types.PathMap)
for _, layer := range layers {
rel := layer.Annotations[ocispec.AnnotationTitle]
abs := filepath.Join(r.tmpDstDir, config.BlobsDir, rel)
absSha := filepath.Join(r.tmpDstDir, config.BlobsDir, layer.Digest.Encoded())
if err := os.Rename(abs, absSha); err != nil {
return nil, err
}
filepaths[rel] = absSha
}
// sign the bundle.yaml layer
getSigCreatePassword := func(_ bool) ([]byte, error) {
if createOpts.SigningKeyPassword != "" {
return []byte(createOpts.SigningKeyPassword), nil
}
if config.CommonOptions.Confirm {
return nil, nil
}
return interactive.PromptSigPassword()
}
// sign the bundle layer
signaturePath := filepath.Join(r.tmpDstDir, config.BundleYAMLSignature)
_, err = zarfUtils.CosignSignBlob(filepaths[config.BundleYAML], signaturePath, createOpts.SigningKeyPath, getSigCreatePassword)
if err != nil {
return nil, err
}
// push the bundle's signature
signatureBytes, err := os.ReadFile(signaturePath)
if err != nil {
// Handle the error
fmt.Println("Error reading file:", err)
return nil, err
}

bundleYamlSigDesc, err := bundleRemote.PushLayer(ctx, signatureBytes, zoci.ZarfLayerMediaTypeBlob)
if err != nil {
return nil, err
}
bundleYamlSigDesc.Annotations = map[string]string{
ocispec.AnnotationTitle: config.BundleYAMLSignature,
}
rootManifest.Layers = append(rootManifest.Layers, *bundleYamlSigDesc)
jsonValue, err := utils.JSONValue(bundleYamlSigDesc)
if err != nil {
return nil, err
}
message.Debug("Pushed", config.BundleYAMLSignature+":", jsonValue)

rootManifestDesc, err := boci.ToOCIRemote(rootManifest, ocispec.MediaTypeImageManifest, bundleRemote.OrasRemote)
if err != nil {
return nil, err
}
return rootManifestDesc, nil
}
11 changes: 11 additions & 0 deletions src/test/e2e/bundle-test.prv-key
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
OCwicCI6MX0sInNhbHQiOiJTbDhUaGJNOERCKzZwUTNWNFprN1lSWEQ3bU9yemhK
ODMrMmJNY25lZGlzPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
Iiwibm9uY2UiOiJsODR0M0JTa1JMa0F4UVU2SkpDVHRKV3FyNW1QU1QxUyJ9LCJj
aXBoZXJ0ZXh0IjoiZjhpMm5ObThhZTdySk80YUx0SHhtRTc2VzlUd0plYjhoNUw0
Z2p4eFM2dWg0bjhsVHlTTXVQSkc0Q3hldFhPQ0J3ajViaWwvSzZwMlpTY1VIbGNX
M2FsR3JEdEYyQjNXT2NTd2pqdGJ6TWNJY2c4L1ZnWkRGRWdXS0E2aFdmWThRa2c4
R3JLKzNyZGZYWGt6RkhYdGI3UkJtUWNKQm4zUjlHU21TUkxBYThrNGpDN0hwRDRF
ZDJFQ2lIS29HNnFkb0pYRHdKVE1FbXhnTXc9PSJ9
-----END ENCRYPTED COSIGN PRIVATE KEY-----
4 changes: 4 additions & 0 deletions src/test/e2e/bundle-test.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGXxUhVYpuKyWNXFwjaRKiNHQcKyI
wjoIQCI8Th5WS/Bkbmxxbxa7v20c+w9DgyeB450qsGJoaFh+uMhdbSwlCA==
-----END PUBLIC KEY-----
Loading
Loading