diff --git a/internal/controller/storage.go b/internal/controller/storage.go index 98fb7359..63e2abfa 100644 --- a/internal/controller/storage.go +++ b/internal/controller/storage.go @@ -21,6 +21,7 @@ import ( "compress/gzip" "context" "fmt" + "github.com/opencontainers/go-digest" "io" "io/fs" "net/url" @@ -325,6 +326,35 @@ func (s *Storage) ArtifactExist(artifact v1.Artifact) bool { return fi.Mode().IsRegular() } +// VerifyArtifact verifies if the Digest of the v1.Artifact matches the digest +// of the file in Storage. It returns an error if the digests don't match, or +// if it can't be verified. +func (s *Storage) VerifyArtifact(artifact v1.Artifact) error { + if artifact.Digest == "" { + return fmt.Errorf("artifact has no digest") + } + + d, err := digest.Parse(artifact.Digest) + if err != nil { + return fmt.Errorf("failed to parse artifact digest '%s': %w", artifact.Digest, err) + } + + f, err := os.Open(s.LocalPath(artifact)) + if err != nil { + return err + } + defer f.Close() + + verifier := d.Verifier() + if _, err = io.Copy(verifier, f); err != nil { + return err + } + if !verifier.Verified() { + return fmt.Errorf("computed digest doesn't match '%s'", d.String()) + } + return nil +} + // ArchiveFileFilter must return true if a file should not be included in the archive after inspecting the given path // and/or os.FileInfo. type ArchiveFileFilter func(p string, fi os.FileInfo) bool diff --git a/internal/controller/storage_test.go b/internal/controller/storage_test.go index bdf21b53..00e9bb1e 100644 --- a/internal/controller/storage_test.go +++ b/internal/controller/storage_test.go @@ -20,6 +20,7 @@ import ( "archive/tar" "compress/gzip" "context" + "errors" "fmt" "io" "os" @@ -718,3 +719,61 @@ func TestStorage_GarbageCollect(t *testing.T) { }) } } + +func TestStorage_VerifyArtifact(t *testing.T) { + g := NewWithT(t) + + dir := t.TempDir() + s, err := NewStorage(dir, "", 0, 0) + g.Expect(err).ToNot(HaveOccurred(), "failed to create new storage") + + g.Expect(os.WriteFile(filepath.Join(dir, "artifact"), []byte("test"), 0o600)).To(Succeed()) + + t.Run("artifact without digest", func(t *testing.T) { + g := NewWithT(t) + + err := s.VerifyArtifact(sourcev1.Artifact{}) + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("artifact has no digest")) + }) + + t.Run("artifact with invalid digest", func(t *testing.T) { + g := NewWithT(t) + + err := s.VerifyArtifact(sourcev1.Artifact{Digest: "invalid"}) + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("failed to parse artifact digest 'invalid': invalid checksum digest format")) + }) + + t.Run("artifact with invalid path", func(t *testing.T) { + g := NewWithT(t) + + err := s.VerifyArtifact(sourcev1.Artifact{ + Digest: "sha256:9ba7a35ce8acd3557fe30680ef193ca7a36bb5dc62788f30de7122a0a5beab69", + Path: "invalid", + }) + g.Expect(err).To(HaveOccurred()) + g.Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue()) + }) + + t.Run("artifact with digest mismatch", func(t *testing.T) { + g := NewWithT(t) + + err := s.VerifyArtifact(sourcev1.Artifact{ + Digest: "sha256:9ba7a35ce8acd3557fe30680ef193ca7a36bb5dc62788f30de7122a0a5beab69", + Path: "artifact", + }) + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError("computed digest doesn't match 'sha256:9ba7a35ce8acd3557fe30680ef193ca7a36bb5dc62788f30de7122a0a5beab69'")) + }) + + t.Run("artifact with digest match", func(t *testing.T) { + g := NewWithT(t) + + err := s.VerifyArtifact(sourcev1.Artifact{ + Digest: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + Path: "artifact", + }) + g.Expect(err).ToNot(HaveOccurred()) + }) +}