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

feat(docker): add verification of image digest(s) #227

Merged
merged 4 commits into from
Oct 1, 2020

Conversation

vdice
Copy link
Member

@vdice vdice commented Aug 5, 2020

  • Adds invocation image digest verification per the CNAB Spec to the Docker driver.

The specific part of the spec that this PR currently implements is the following line:

If a contentDigest field is present, a runtime MUST validate the image digest prior to executing an action.

I didn't see how this would be done generically, e.g. outside of specific driver implementations, hence implementing it on the Docker driver only. Am I right here? If so, perhaps a follow-up could implement similar in the kubernetes driver.

Note some remaining TODOs/Qs inline. Feel free to respond/answer in the form of code review comments.

@vdice vdice force-pushed the feat/verify-digest branch 3 times, most recently from 473d629 to 55b2bab Compare August 5, 2020 23:45
@vdice vdice force-pushed the feat/verify-digest branch 3 times, most recently from 4971735 to 78117b5 Compare August 6, 2020 00:32
Signed-off-by: Vaughn Dice <vadice@microsoft.com>
@vdice vdice force-pushed the feat/verify-digest branch from 78117b5 to 8ad516f Compare August 6, 2020 00:40
driver/docker/docker.go Outdated Show resolved Hide resolved
Signed-off-by: Vaughn Dice <vadice@microsoft.com>
@vdice
Copy link
Member Author

vdice commented Sep 22, 2020

@carolynvs @radu-matei @silvin-lubecki if any of y'all have a spare moment, it would be great to get feedback on this PR, as it addresses an area where cnab-go isn't in adherence to the spec.

Copy link
Contributor

@carolynvs carolynvs left a comment

Choose a reason for hiding this comment

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

Looks good, I've provided some suggestions on how to use the docker library for the parsing.

./e2e-kind.sh

.PHONY: compile-integration-tests
compile-integration-tests:
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! I've had this bite me a few times. Now I just need to remember to run coverage before pushing the PR. 😊

// validateImageDigest validates the operation image digest, if exists, against
// the supplied repoDigests
func (d *Driver) validateImageDigest(image bundle.InvocationImage, repoDigests []string) error {
if image.Digest != "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: swap the check and do an immediate return at the top instead. It's a bit easier to follow and the majority of the code in the function isn't nested in an if statement, e.g.

if image.Digest == "" {
  return nil
}

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 I feel like I always miss this one; please keep reminding me! Definitely like the clarity/readability improvement.


repoDigest := repoDigests[0]
// RepoDigests are of the form 'imageName@sha256:<sha256>'; we parse out the digest itself for comparison
// TODO: is there a better (i.e. via docker api) way to extract the digest?
Copy link
Contributor

Choose a reason for hiding this comment

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

Here's something

// RepoDigests are of the form 'imageName@sha256:<sha256>'; we parse out the digest itself for comparison
repoDigest := repoDigests[0]
ref, err := reference.ParseNormalizedNamed(repoDigest)
if err != nil {
	return err
}
digestRef, ok := ref.(reference.Digested)
if !ok {
	return fmt.Errorf("unable to parse repo digest %s", repoDigest)
}
digest := digestRef.Digest().String()

This is the updated unit test I used to verify

func TestDriver_ValidateImageDigest(t *testing.T) {
	repoDigests := []string{
		"myreg/myimg@sha256:9d4178a3b6059e46a02e4c5b39ac26d51d91046faeb7bb9f3c3825f92adafc5a",
	}

	t.Run("no image digest", func(t *testing.T) {
		d := &Driver{}

		image := bundle.InvocationImage{}
		image.Image = "myreg/myimg"

		err := d.validateImageDigest(image, repoDigests)
		assert.NoError(t, err)
	})

	t.Run("image digest exists - no match exists", func(t *testing.T) {
		d := &Driver{}

		image := bundle.InvocationImage{}
		image.Image = "myreg/myimg"
		image.Digest = "sha256:8b92b7269f59e3ed824e811a1ff1ee64f0d44c0218efefada57a4bebc2d7ef6f"

		err := d.validateImageDigest(image, repoDigests)
		assert.NotNil(t, err, "expected an error")
		assert.Contains(t, err.Error(), "content digest mismatch")
	})

	t.Run("image digest exists - a match exists", func(t *testing.T) {
		d := &Driver{}

		image := bundle.InvocationImage{}
		image.Image = "myreg/myimg"
		image.Digest = "sha256:9d4178a3b6059e46a02e4c5b39ac26d51d91046faeb7bb9f3c3825f92adafc5a"

		err := d.validateImageDigest(image, repoDigests)
		assert.NoError(t, err)
	})
}

I had to switch to full digests because the docker library doesn't parse short digests.

Copy link
Member Author

@vdice vdice Sep 28, 2020

Choose a reason for hiding this comment

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

Thank you, I've incorporated these changes. Changing to full digests in the tests is also an improvement.

Also added a test case around an image having multiple repo digests.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was sad to see livebeef/deadbeef go as test values. 😂

}
// TODO: if digest empty, do we want to provide a warning somehow? Spec says:
// If the contentDigest is not present, the runtime SHOULD report an error so the user is aware that there is no contentDigest provided.
return nil
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is something that the the tool should do. In developer loops, we fully expect the digest to be left off until it has stabilized. So they (tools like porter or duffle) are in a better position to bubble up and make that warning proper.

For example, allowing it to be empty when running from a local bundle (and only displaying a warning) but perhaps displaying a stronger error if there is no digest in a published bundle.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. I've removed this TODO and we'll leave implementation up to tools.

Co-authored-by: Carolyn Van Slyck <me@carolynvanslyck.com>
Signed-off-by: Vaughn Dice <vadice@microsoft.com>
Copy link
Contributor

@carolynvs carolynvs left a comment

Choose a reason for hiding this comment

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

LGTM just one nit to resolve if you don't mind

repoDigest := repoDigests[0]
ref, err := reference.ParseNormalizedNamed(repoDigest)
if err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I gave you informal code. We should probably wrap this error and provide a better error message with context. Otherwise it won't be clear what reference is invalid.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated! And deadbeef returns in a test for this scenario! 🎉

Signed-off-by: Vaughn Dice <vadice@microsoft.com>
Copy link
Contributor

@carolynvs carolynvs left a comment

Choose a reason for hiding this comment

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

LGTM for reals this time


err := d.validateImageDigest(image, repoDigests)
assert.NoError(t, err)
badRepoDigests := []string{"myreg/myimg@sha256:deadbeef"}
Copy link
Contributor

Choose a reason for hiding this comment

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

THE RETURN OF DEADBEEFS 👍

Copy link
Contributor

@carolynvs carolynvs left a comment

Choose a reason for hiding this comment

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

github is not liking my approvals

/force LGTM dagnabbit

@vdice vdice merged commit 02823c6 into cnabio:master Oct 1, 2020
@vdice vdice deleted the feat/verify-digest branch October 1, 2020 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants