Skip to content

Commit

Permalink
add image index support (#286)
Browse files Browse the repository at this point in the history
* add image index support

* add basic test case

* use nil instead of empy byte slice

* add test case for index signed with image hash
  • Loading branch information
halamix2 authored Oct 9, 2024
1 parent 49c19cd commit 767ce0d
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 17 deletions.
73 changes: 56 additions & 17 deletions internal/validate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (s *notaryService) Validate(ctx context.Context, image string) error {
return err
}

shaImageBytes, shaManifestBytes, err := s.loggedGetImageDigestHash(ctx, image)
shaImageBytes, shaManifestBytes, err := s.loggedGetRepositoryDigestHash(ctx, image)
if err != nil {
return err
}
Expand All @@ -89,7 +89,7 @@ func (s *notaryService) Validate(ctx context.Context, image string) error {
return nil
}

if subtle.ConstantTimeCompare(shaManifestBytes, expectedShaBytes) == 1 {
if shaManifestBytes != nil && subtle.ConstantTimeCompare(shaManifestBytes, expectedShaBytes) == 1 {
logger.Warn("deprecated: manifest hash was used for verification")
return nil
}
Expand All @@ -107,47 +107,86 @@ func (s *notaryService) isImageAllowed(imgRepo string) bool {
return false
}

func (s *notaryService) loggedGetImageDigestHash(ctx context.Context, image string) ([]byte, []byte, error) {
func (s *notaryService) loggedGetRepositoryDigestHash(ctx context.Context, image string) ([]byte, []byte, error) {
const message = "request to image registry"
closeLog := helpers.LogStartTime(ctx, message)
defer closeLog()
return s.getImageDigestHash(image)
return s.getRepositoryDigestHash(image)
}

func (s *notaryService) getImageDigestHash(image string) ([]byte, []byte, error) {
func (s *notaryService) getRepositoryDigestHash(image string) ([]byte, []byte, error) {
if len(image) == 0 {
return []byte{}, []byte{}, pkg.NewValidationFailedErr(errors.New("empty image provided"))
return nil, nil, pkg.NewValidationFailedErr(errors.New("empty image provided"))
}

ref, err := name.ParseReference(image)
if err != nil {
return []byte{}, []byte{}, pkg.NewValidationFailedErr(errors.Wrap(err, "ref parse"))
return nil, nil, pkg.NewValidationFailedErr(errors.Wrap(err, "ref parse"))
}

descriptor, err := remote.Get(ref)
if err != nil {
return nil, nil, pkg.NewUnknownResultErr(errors.Wrap(err, "get image descriptor"))
}

if descriptor.MediaType.IsIndex() {
digest, err := getIndexDigestHash(ref)
if err != nil {
return nil, nil, err
}
return digest, nil, nil
} else if descriptor.MediaType.IsImage() {
digest, manifest, err := getImageDigestHash(ref)
if err != nil {
return nil, nil, err
}
return digest, manifest, nil
}
return nil, nil, pkg.NewValidationFailedErr(errors.New("not an image or image list"))
}

func getIndexDigestHash(ref name.Reference) ([]byte, error) {
i, err := remote.Index(ref)
if err != nil {
return nil, pkg.NewUnknownResultErr(errors.Wrap(err, "get image"))
}
digest, err := i.Digest()
if err != nil {
return nil, pkg.NewUnknownResultErr(errors.Wrap(err, "image digest"))
}
digestBytes, err := hex.DecodeString(digest.Hex)
if err != nil {
return nil, pkg.NewUnknownResultErr(errors.Wrap(err, "checksum error: %w"))
}
return digestBytes, nil
}

func getImageDigestHash(ref name.Reference) ([]byte, []byte, error) {
i, err := remote.Image(ref)
if err != nil {
return []byte{}, []byte{}, pkg.NewUnknownResultErr(errors.Wrap(err, "get image"))
return nil, nil, pkg.NewUnknownResultErr(errors.Wrap(err, "get image"))
}

// Deprecated: Remove manifest hash verification after all images has been signed using the new method
m, err := i.Manifest()
if err != nil {
return []byte{}, []byte{}, pkg.NewUnknownResultErr(errors.Wrap(err, "image manifest"))
return nil, nil, pkg.NewUnknownResultErr(errors.Wrap(err, "image manifest"))
}

manifestBytes, err := hex.DecodeString(m.Config.Digest.Hex)
if err != nil {
return []byte{}, []byte{}, pkg.NewUnknownResultErr(errors.Wrap(err, "manifest checksum error: %w"))
return nil, nil, pkg.NewUnknownResultErr(errors.Wrap(err, "manifest checksum error: %w"))
}

digest, err := i.Digest()
if err != nil {
return []byte{}, []byte{}, pkg.NewUnknownResultErr(errors.Wrap(err, "image digest"))
return nil, nil, pkg.NewUnknownResultErr(errors.Wrap(err, "image digest"))
}

digestBytes, err := hex.DecodeString(digest.Hex)

if err != nil {
return []byte{}, []byte{}, pkg.NewUnknownResultErr(errors.Wrap(err, "checksum error: %w"))
return nil, nil, pkg.NewUnknownResultErr(errors.Wrap(err, "checksum error: %w"))
}

return digestBytes, manifestBytes, nil
Expand All @@ -163,31 +202,31 @@ func (s *notaryService) loggedGetNotaryImageDigestHash(ctx context.Context, imgR

func (s *notaryService) getNotaryImageDigestHash(ctx context.Context, imgRepo, imgTag string) ([]byte, error) {
if len(imgRepo) == 0 || len(imgTag) == 0 {
return []byte{}, pkg.NewValidationFailedErr(errors.New("empty arguments provided"))
return nil, pkg.NewValidationFailedErr(errors.New("empty arguments provided"))
}

const messageNewRepoClient = "request to notary (NewRepoClient)"
closeLog := helpers.LogStartTime(ctx, messageNewRepoClient)
c, err := s.RepoFactory.NewRepoClient(imgRepo, s.NotaryConfig)
closeLog()
if err != nil {
return []byte{}, pkg.NewUnknownResultErr(err)
return nil, pkg.NewUnknownResultErr(err)
}

const messageGetTargetByName = "request to notary (GetTargetByName)"
closeLog = helpers.LogStartTime(ctx, messageGetTargetByName)
target, err := c.GetTargetByName(imgTag)
closeLog()
if err != nil {
return []byte{}, parseNotaryErr(err)
return nil, parseNotaryErr(err)
}

if len(target.Hashes) == 0 {
return []byte{}, pkg.NewValidationFailedErr(errors.New("image hash is missing"))
return nil, pkg.NewValidationFailedErr(errors.New("image hash is missing"))
}

if len(target.Hashes) > 1 {
return []byte{}, pkg.NewValidationFailedErr(errors.New("more than one hash for image"))
return nil, pkg.NewValidationFailedErr(errors.New("more than one hash for image"))
}

key := ""
Expand Down
44 changes: 44 additions & 0 deletions internal/validate/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ var (
// manifest hash
hash: []byte{157, 125, 211, 253, 79, 175, 129, 184, 184, 72, 163, 165, 92, 251, 19, 70, 92, 162, 125, 90, 135, 102, 39, 28, 194, 201, 221, 188, 72, 73, 136, 239},
}
trustedIndex = image{
name: "europe-docker.pkg.dev/kyma-project/prod/external/golang",
tag: "1.22.2-alpine3.19",
// index hash
hash: []byte{205, 200, 109, 159, 54, 62, 135, 134, 132, 91, 234, 32, 64, 49, 43, 78, 250, 50, 27, 130, 138, 205, 235, 38, 243, 147, 250, 168, 100, 216, 135, 176},
}
differentHashIndex = image{
name: "europe-docker.pkg.dev/kyma-project/prod/external/alpine",
tag: "3.20.0",
// image hash instead of index hash
hash: []byte{33, 98, 102, 200, 111, 196, 220, 239, 86, 25, 147, 11, 211, 148, 36, 88, 36, 194, 175, 82, 253, 33, 186, 124, 111, 160, 230, 24, 101, 125, 76, 59},
}
differentHashImage = image{
name: "nginx",
tag: "latest",
Expand Down Expand Up @@ -74,6 +86,15 @@ func Test_Validate_ProperImageLegacy_ShouldPass(t *testing.T) {
require.NoError(t, err)
}

func Test_Validate_ProperIndex_ShouldPass(t *testing.T) {
cfg := validate.ServiceConfig{NotaryConfig: validate.NotaryConfig{}}
f := setupMockFactory()

s := validate.NewImageValidator(&cfg, f)
err := s.Validate(context.TODO(), trustedIndex.image())
require.NoError(t, err)
}

func Test_Validate_InvalidImageName_ShouldReturnError(t *testing.T) {
cfg := validate.ServiceConfig{NotaryConfig: validate.NotaryConfig{}}
f := setupMockFactory()
Expand Down Expand Up @@ -110,6 +131,19 @@ func Test_Validate_InvalidImageName_ShouldReturnError(t *testing.T) {
}
}

func Test_Validate_IndexWithDifferentHashInNotary_ShouldReturnError(t *testing.T) {
//GIVEN
cfg := validate.ServiceConfig{NotaryConfig: validate.NotaryConfig{}}
f := setupMockFactory()
s := validate.NewImageValidator(&cfg, f)
//WHEN
err := s.Validate(context.TODO(), differentHashIndex.image())

//THEN
require.ErrorContains(t, err, "unexpected image hash value")
require.Equal(t, pkg.ValidationError, pkg.ErrorCode(err))
}

func Test_Validate_ImageWithDifferentHashInNotary_ShouldReturnError(t *testing.T) {
//GIVEN
cfg := validate.ServiceConfig{NotaryConfig: validate.NotaryConfig{}}
Expand Down Expand Up @@ -317,15 +351,25 @@ func setupMockFactory() validate.RepoFactory {
Hashes: map[string][]byte{"ignored": trustedImageLegacy.hash},
Length: 1}}

trustedImageIndex := client.TargetWithRole{Target: client.Target{Name: "ignored",
Hashes: map[string][]byte{"ignored": trustedIndex.hash},
Length: 1}}

unknown := client.TargetWithRole{Target: client.Target{Name: "ignored",
Hashes: map[string][]byte{"ignored": unknownImage.hash},
Length: 1}}

different := client.TargetWithRole{Target: client.Target{Name: "ignored",
Hashes: map[string][]byte{"ignored": differentHashImage.hash}}}

differentIndex := client.TargetWithRole{Target: client.Target{Name: "ignored",
Hashes: map[string][]byte{"ignored": differentHashIndex.hash},
Length: 1}}

notaryClient.On("GetTargetByName", trustedImage.tag).Return(&trusted, nil)
notaryClient.On("GetTargetByName", trustedImageLegacy.tag).Return(&trustedLegacy, nil)
notaryClient.On("GetTargetByName", trustedIndex.tag).Return(&trustedImageIndex, nil)
notaryClient.On("GetTargetByName", differentHashIndex.tag).Return(&differentIndex, nil)
notaryClient.On("GetTargetByName", differentHashImage.tag).Return(&different, nil)
notaryClient.On("GetTargetByName", unknownImage.tag).Return(&unknown, nil)
notaryClient.On("GetTargetByName", untrustedImage.tag).
Expand Down

0 comments on commit 767ce0d

Please sign in to comment.