Skip to content

Commit

Permalink
feat: Support signing bundles during copy
Browse files Browse the repository at this point in the history
Make it possible to sign bundles during copy between repositories.
This is handle by creating a new signature, NOT by copying the potential
existing signature. This is done for two reasons:

1. A signature might not be present on the source bundle
1. Repositories might use different digest algorithms or calculate the digest differently

Signed-off-by: Kim Christensen <[email protected]>
  • Loading branch information
kichristensen committed Aug 13, 2024
1 parent c0feb36 commit 1852741
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 9 deletions.
1 change: 1 addition & 0 deletions cmd/porter/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ If the source bundle is a digest reference, destination must be a tagged referen
cmd.Flag("force").Annotations = map[string][]string{
"viper-key": {"force-overwrite"},
}
f.BoolVar(&opts.SignBundle, "sign-bundle", false, "Sign the bundle using the configured signing plugin")

return &cmd
}
1 change: 1 addition & 0 deletions docs/content/docs/references/cli/bundles_copy.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ porter bundles copy [flags]
--force Force push the bundle to overwrite the previously published bundle
-h, --help help for copy
--insecure-registry Don't require TLS for registries
--sign-bundle Sign the bundle using the configured signing plugin
--source string The fully qualified source bundle, including tag or digest.
```

Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/references/cli/copy.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ porter copy [flags]
--force Force push the bundle to overwrite the previously published bundle
-h, --help help for copy
--insecure-registry Don't require TLS for registries
--sign-bundle Sign the bundle using the configured signing plugin
--source string The fully qualified source bundle, including tag or digest.
```

Expand Down
21 changes: 20 additions & 1 deletion pkg/porter/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type CopyOpts struct {
Destination string
InsecureRegistry bool
Force bool
SignBundle bool
}

// Validate performs validation logic on the options specified for a bundle copy
Expand Down Expand Up @@ -98,9 +99,27 @@ func (p *Porter) CopyBundle(ctx context.Context, opts *CopyOpts) error {

bunRef.Reference = destinationRef

_, err = p.Registry.PushBundle(ctx, bunRef, regOpts)
bunRef, err = p.Registry.PushBundle(ctx, bunRef, regOpts)
if err != nil {
return span.Error(fmt.Errorf("unable to copy bundle to new location: %w", err))
}

if opts.SignBundle {
for _, invImage := range bunRef.Definition.InvocationImages {
relocInvImage := bunRef.RelocationMap[invImage.Image]
span.Debugf("Signing invocation image %s...", relocInvImage)
err = p.Signer.Sign(ctx, relocInvImage)
if err != nil {
return span.Errorf("failed to sign image %s: %w", relocInvImage, err)
}
}

span.Debugf("Signing bundle %s", bunRef.Reference.String())
err = p.Signer.Sign(ctx, bunRef.Reference.String())
if err != nil {
return span.Errorf("failed to bundle %s: %w", bunRef.Reference.String(), err)
}
}

return nil
}
80 changes: 72 additions & 8 deletions tests/integration/signing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestCosign(t *testing.T) {
require.NoError(t, err, "Publish failed")

ref = toRefWithDigest(t, ref)
invocationImageRef := resolveInvocationImageDigest(t, output)
invocationImageRef := resolveInvocationImageDigest(t, output, "sign")

_, output = testr.RequirePorter("install", "--verify-bundle", "--reference", ref.String(), "--insecure-registry", "--force")
require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", ref.String()))
Expand Down Expand Up @@ -74,13 +74,46 @@ func TestCosignFromArchive(t *testing.T) {
require.NoError(t, err, "Publish archive failed")

ref = toRefWithDigest(t, ref)
invocationImageRef := getInvocationImageDigest(t, output)
invocationImageRef := getInvocationImageDigest(t, output, "sign-from-archive")

_, output = testr.RequirePorter("install", "--verify-bundle", "--reference", ref.String(), "--insecure-registry", "--force")
require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", ref.String()))
require.Contains(t, output, fmt.Sprintf("invocation image signature verified for %s", invocationImageRef.String()))
}

func TestCosignCopyBundle(t *testing.T) {
testr, err := tester.NewTestWithConfig(t, "tests/integration/testdata/signing/config/config-cosign.yaml")
require.NoError(t, err, "tester.NewTest failed")
defer testr.Close()
reg := testr.StartTestRegistry(tester.TestRegistryOptions{UseTLS: true})
defer reg.Close()
ref := cnab.MustParseOCIReference(fmt.Sprintf("%s/sign:v1.0.0", reg.String()))
secondReg := testr.StartTestRegistry(tester.TestRegistryOptions{UseTLS: true})
defer secondReg.Close()
copiedRef := cnab.MustParseOCIReference(fmt.Sprintf("%s/sign:v1.0.0", secondReg.String()))

setupCosign(t, testr)
_, output, err := testr.RunPorterWith(func(pc *shx.PreparedCommand) {
pc.Args("publish", "--insecure-registry", "-f", "testdata/bundles/signing/porter.yaml", "-r", ref.String())
pc.Env("COSIGN_PASSWORD='test'")
})
require.NoError(t, err, "Publish failed")

_, output, err = testr.RunPorterWith(func(pc *shx.PreparedCommand) {
pc.Args("copy", "--insecure-registry", "--sign-bundle", "--source", ref.String(), "--destination", copiedRef.String())
pc.Env("COSIGN_PASSWORD='test'")
})
fmt.Println(output)
require.NoError(t, err, "Copy failed")

ref = toRefWithDigest(t, ref)
invocationImageRef := getInvocationImageDigest(t, output, "sign")

_, output = testr.RequirePorter("install", "--verify-bundle", "--reference", copiedRef.String(), "--insecure-registry", "--force")
require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", copiedRef.String()))
require.Contains(t, output, fmt.Sprintf("invocation image signature verified for %s", invocationImageRef.String()))
}

func setupCosign(t *testing.T, testr tester.Tester) {
cmd := shx.Command("cosign", "generate-key-pair").Env("COSIGN_PASSWORD='test'").In(testr.PorterHomeDir)
err := cmd.RunE()
Expand All @@ -103,7 +136,7 @@ func TestNotation(t *testing.T) {
require.NoError(t, err, "Publish failed")

ref = toRefWithDigest(t, ref)
invocationImageRef := resolveInvocationImageDigest(t, output)
invocationImageRef := resolveInvocationImageDigest(t, output, "sign")

_, output = testr.RequirePorter("install", "--verify-bundle", "--reference", ref.String(), "--insecure-registry", "--force")
fmt.Println(output)
Expand Down Expand Up @@ -143,13 +176,44 @@ func TestNotationFromArchive(t *testing.T) {
require.NoError(t, err, "Publish archive failed")

ref = toRefWithDigest(t, ref)
invocationImageRef := getInvocationImageDigest(t, output)
invocationImageRef := getInvocationImageDigest(t, output, "sign-from-archive")

_, output = testr.RequirePorter("install", "--verify-bundle", "--reference", ref.String(), "--insecure-registry", "--force")
require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", ref.String()))
require.Contains(t, output, fmt.Sprintf("invocation image signature verified for %s", invocationImageRef.String()))
}

func TestNotationCopyBundle(t *testing.T) {
testr, err := tester.NewTestWithConfig(t, "tests/integration/testdata/signing/config/config-notation.yaml")
require.NoError(t, err, "tester.NewTest failed")
defer testr.Close()
reg := testr.StartTestRegistry(tester.TestRegistryOptions{UseTLS: false})
defer reg.Close()
ref := cnab.MustParseOCIReference(fmt.Sprintf("%s/sign:v1.0.0", reg.String()))
secondReg := testr.StartTestRegistry(tester.TestRegistryOptions{UseTLS: false})
defer secondReg.Close()
copiedRef := cnab.MustParseOCIReference(fmt.Sprintf("%s/sign:v1.0.0", secondReg.String()))

setupNotation(t, testr)
defer cleanupNotation(t)
_, output, err := testr.RunPorterWith(func(pc *shx.PreparedCommand) {
pc.Args("publish", "--insecure-registry", "-f", "testdata/bundles/signing/porter.yaml", "-r", ref.String())
})
require.NoError(t, err, "Publish failed")

_, output, err = testr.RunPorterWith(func(pc *shx.PreparedCommand) {
pc.Args("copy", "--insecure-registry", "--sign-bundle", "--source", ref.String(), "--destination", copiedRef.String())
})
require.NoError(t, err, "Copy failed")

ref = toRefWithDigest(t, ref)
invocationImageRef := getInvocationImageDigest(t, output, "sign")

_, output = testr.RequirePorter("install", "--verify-bundle", "--reference", copiedRef.String(), "--insecure-registry", "--force")
require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", copiedRef.String()))
require.Contains(t, output, fmt.Sprintf("invocation image signature verified for %s", invocationImageRef.String()))
}

func setupNotation(t *testing.T, testr tester.Tester) {
cmd := shx.Command("notation", "cert", "generate-test", "porter-test.org")
err := cmd.RunE()
Expand Down Expand Up @@ -206,8 +270,8 @@ func toRefWithDigest(t *testing.T, ref cnab.OCIReference) cnab.OCIReference {
return ref
}

func resolveInvocationImageDigest(t *testing.T, output string) cnab.OCIReference {
r := regexp.MustCompile(`(?m:^Signing invocation image (localhost:\d+/sign:porter-[0-9a-z]+)\.)`)
func resolveInvocationImageDigest(t *testing.T, output string, imageName string) cnab.OCIReference {
r := regexp.MustCompile(fmt.Sprintf(`(?m:^Signing invocation image (localhost:\d+/%s:porter-[0-9a-z]+)\.)`, imageName))
matches := r.FindAllStringSubmatch(output, -1)
require.Len(t, matches, 1)
invocationImageRefString := matches[0][1]
Expand All @@ -220,8 +284,8 @@ func resolveInvocationImageDigest(t *testing.T, output string) cnab.OCIReference
return ref
}

func getInvocationImageDigest(t *testing.T, output string) cnab.OCIReference {
r := regexp.MustCompile(`(?m:^Signing invocation image (localhost:\d+/sign-from-archive@sha256:[0-9a-z]+)\.)`)
func getInvocationImageDigest(t *testing.T, output string, imageName string) cnab.OCIReference {
r := regexp.MustCompile(fmt.Sprintf(`(?m:^Signing invocation image (localhost:\d+/%s@sha256:[0-9a-z]+)\.)`, imageName))
matches := r.FindAllStringSubmatch(output, -1)
require.Len(t, matches, 1)
invocationImageRefString := matches[0][1]
Expand Down

0 comments on commit 1852741

Please sign in to comment.