From f1d045ced9089d0285af8143b43eee09ca29c062 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 2 Sep 2026 12:18:57 +0200 Subject: [PATCH 01/24] feat: handle different digests in manifest --- internal/archive/archive.go | 29 +-- internal/archive/archive_test.go | 90 ++++---- internal/cache/cache.go | 10 + internal/manifestutil/manifestutil.go | 48 +++-- internal/manifestutil/manifestutil_test.go | 236 +++++++++++++++++---- internal/slicer/slicer_test.go | 47 +++- internal/testutil/archive.go | 35 ++- public/manifest/manifest.go | 88 +++++++- public/manifest/manifest_test.go | 51 ++++- 9 files changed, 500 insertions(+), 134 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index d301494b5..49cb03da5 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -26,18 +26,19 @@ type Archive interface { } type PackageInfo struct { - Name string - Version string - Arch string - SHA256 string + Name string + Version string + Arch string + Digest string + DigestKind cache.DigestKind } func (p *PackageInfo) PkgName() string { return p.Name } func (p *PackageInfo) PkgVersion() string { return p.Version } func (p *PackageInfo) PkgRevision() int { return 0 } func (p *PackageInfo) PkgArch() string { return p.Arch } -func (p *PackageInfo) PkgDigestKind() cache.DigestKind { return cache.SHA256 } -func (p *PackageInfo) PkgDigest() string { return p.SHA256 } +func (p *PackageInfo) PkgDigestKind() cache.DigestKind { return p.DigestKind } +func (p *PackageInfo) PkgDigest() string { return p.Digest } type Options struct { Label string @@ -152,7 +153,7 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro if err != nil { return nil, nil, err } - info := sectionPackageInfo(section) + info := sectionPackageInfo(section, digest, digestKind) return reader, info, nil } @@ -161,7 +162,8 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) { if err != nil { return nil, err } - info := sectionPackageInfo(section) + digest, digestKind := packageDigest(section) + info := sectionPackageInfo(section, digest, digestKind) return info, nil } @@ -516,12 +518,13 @@ func (index *ubuntuIndex) fetch(path, digest string, digestKind cache.DigestKind return index.archive.cache.Open(digestKind, writer.Digest()) } -func sectionPackageInfo(section control.Section) *PackageInfo { +func sectionPackageInfo(section control.Section, digest string, digestKind cache.DigestKind) *PackageInfo { return &PackageInfo{ - Name: section.Get("Package"), - Version: section.Get("Version"), - Arch: section.Get("Architecture"), - SHA256: section.Get("SHA256"), + Name: section.Get("Package"), + Version: section.Get("Version"), + Arch: section.Get("Architecture"), + Digest: digest, + DigestKind: digestKind, } } diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index b89a8d71d..d0f368734 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -5,7 +5,6 @@ import ( . "gopkg.in/check.v1" "crypto/sha256" - "crypto/sha512" "debug/elf" "errors" "flag" @@ -20,6 +19,7 @@ import ( "github.com/canonical/chisel/internal/archive" "github.com/canonical/chisel/internal/archive/testarchive" + "github.com/canonical/chisel/internal/cache" "github.com/canonical/chisel/internal/tarball" "github.com/canonical/chisel/internal/testutil" ) @@ -252,10 +252,11 @@ func (s *httpSuite) TestFetchPackage(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") @@ -263,10 +264,11 @@ func (s *httpSuite) TestFetchPackage(c *C) { pkg, info, err = testArchive.Fetch("mypkg4") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg4", - Version: "1.4", - Arch: "amd64", - SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4", + Name: "mypkg4", + Version: "1.4", + Arch: "amd64", + Digest: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "mypkg4 1.4 data") } @@ -290,16 +292,22 @@ func (s *httpSuite) TestFetchSHA512Digests(c *C) { testArchive, err := archive.Open(&options) c.Assert(err, IsNil) - pkg, _, err := testArchive.Fetch("mypkg1") + pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) + c.Assert(info, DeepEquals, &archive.PackageInfo{ + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digest: "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb", + DigestKind: cache.SHA512, + }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") } func (s *httpSuite) TestFetchBothDigests(c *C) { // An archive publishing both SHA256 and SHA512 sections (index table and // package fields) must be handled, with the strongest digest preferred - // for verification and caching. PackageInfo.SHA256 still surfaces: it is - // read from the package section directly, not from the preference order. + // for verification, caching and the manifest. s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, []string{"SHA256", "SHA512"}, nil) @@ -319,17 +327,18 @@ func (s *httpSuite) TestFetchBothDigests(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digest: "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb", + DigestKind: cache.SHA512, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") // Pin the cache key: with both digests advertised, the package is cached // under its strongest digest. - sha512Digest := fmt.Sprintf("%x", sha512.Sum512([]byte("mypkg1 1.1 data"))) - _, err = os.Stat(filepath.Join(options.CacheDir, "sha512", sha512Digest)) + _, err = os.Stat(filepath.Join(options.CacheDir, "sha512", + "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb")) c.Assert(err, IsNil) } @@ -356,10 +365,11 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "arm64", - SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + Name: "mypkg1", + Version: "1.1", + Arch: "arm64", + Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") @@ -367,10 +377,11 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) { pkg, info, err = testArchive.Fetch("mypkg4") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg4", - Version: "1.4", - Arch: "arm64", - SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4", + Name: "mypkg4", + Version: "1.4", + Arch: "arm64", + Digest: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "mypkg4 1.4 data") } @@ -407,20 +418,22 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1.2.2", - Arch: "amd64", - SHA256: "5448585bdd916e5023eff2bc1bc3b30bcc6ee9db9c03e531375a6a11ddf0913c", + Name: "mypkg1", + Version: "1.1.2.2", + Arch: "amd64", + Digest: "5448585bdd916e5023eff2bc1bc3b30bcc6ee9db9c03e531375a6a11ddf0913c", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "package from jammy-security") pkg, info, err = testArchive.Fetch("mypkg2") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg2", - Version: "1.2", - Arch: "amd64", - SHA256: "a4b4f3f3a8fa09b69e3ba23c60a41a1f8144691fd371a2455812572fd02e6f79", + Name: "mypkg2", + Version: "1.2", + Arch: "amd64", + Digest: "a4b4f3f3a8fa09b69e3ba23c60a41a1f8144691fd371a2455812572fd02e6f79", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "mypkg2 1.2 data") } @@ -662,10 +675,11 @@ var packageInfoTests = []struct { summary: "Basic", pkg: "mypkg1", info: &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + DigestKind: cache.SHA256, }, }, { summary: "Package not found in archive", diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 3b5621ecc..2f0c876c4 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -9,6 +9,7 @@ import ( "io" "os" "path/filepath" + "slices" "time" "golang.org/x/crypto/sha3" @@ -102,6 +103,15 @@ const ( var digestKinds = []DigestKind{SHA256, SHA384, SHA512} +// ValidateKind returns an error unless kind is a digest kind Chisel +// supports. +func ValidateKind(kind DigestKind) error { + if !slices.Contains(digestKinds, kind) { + return fmt.Errorf("unsupported digest kind: %q", kind) + } + return nil +} + var ErrMiss = fmt.Errorf("not cached") func (c *Cache) filePath(digestKind DigestKind, digest string) string { diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 3cf4db75d..8bed79a0a 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -86,11 +86,12 @@ func Write(options *WriteOptions, writer io.Writer) error { func manifestAddPackages(dbw *jsonwall.DBWriter, infos []PackageInfo) error { for _, info := range infos { err := dbw.Add(&manifest.Package{ - Kind: "package", - Name: info.PkgName(), - Version: info.PkgVersion(), - Digest: info.PkgDigest(), - Arch: info.PkgArch(), + Kind: "package", + Name: info.PkgName(), + Version: info.PkgVersion(), + Digest: info.PkgDigest(), + DigestKind: string(info.PkgDigestKind()), + Arch: info.PkgArch(), }) if err != nil { return err @@ -272,13 +273,13 @@ func validatePackage(pkg PackageInfo) (err error) { if pkg.PkgArch() == "" { return fmt.Errorf("package %q missing arch", name) } - // The manifest records the package digest as a SHA256 one. Fail rather - // than recording a digest of another kind under that name. - // TODO: record packages whose digest is not a SHA256 one, such as the - // ones coming from a store. This requires recording the digest kind in - // the manifest as well. - if pkg.PkgDigestKind() != cache.SHA256 || pkg.PkgDigest() == "" { - return fmt.Errorf("package %q missing sha256", name) + kind := pkg.PkgDigestKind() + err = cache.ValidateKind(kind) + if err != nil { + return fmt.Errorf("package %q: %w", name, err) + } + if pkg.PkgDigest() == "" { + return fmt.Errorf("package %q missing %s", name, kind) } if pkg.PkgVersion() == "" { return fmt.Errorf("package %q missing version", name) @@ -298,7 +299,28 @@ func Validate(mfest *manifest.Manifest) (err error) { pkgExist := map[string]bool{} err = mfest.IteratePackages(func(pkg *manifest.Package) error { - pkgExist[pkg.Name] = true + // Same rules as validatePackage applies on the write side. + name := pkg.Name + if name == "" { + return fmt.Errorf("package name not set") + } + if pkg.Arch == "" { + return fmt.Errorf("package %q missing arch", name) + } + kind := cache.DigestKind(pkg.DigestKind) + if kind == "" { + return fmt.Errorf("package %q missing digest", name) + } + if err := cache.ValidateKind(kind); err != nil { + return fmt.Errorf("package %q: %w", name, err) + } + if pkg.Digest == "" { + return fmt.Errorf("package %q missing %s", name, kind) + } + if pkg.Version == "" { + return fmt.Errorf("package %q missing version", name) + } + pkgExist[name] = true return nil }) if err != nil { diff --git a/internal/manifestutil/manifestutil_test.go b/internal/manifestutil/manifestutil_test.go index e7e68752d..494a3c9e2 100644 --- a/internal/manifestutil/manifestutil_test.go +++ b/internal/manifestutil/manifestutil_test.go @@ -13,6 +13,7 @@ import ( "github.com/canonical/chisel/internal/apachetestutil" "github.com/canonical/chisel/internal/archive" + "github.com/canonical/chisel/internal/cache" "github.com/canonical/chisel/internal/manifestutil" "github.com/canonical/chisel/internal/setup" "github.com/canonical/chisel/public/manifest" @@ -150,16 +151,18 @@ var generateManifestTests = []struct { }, packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - SHA256: "s1", + Name: "package1", + Version: "v1", + Arch: "a1", + Digest: "s1", + DigestKind: cache.SHA256, }, &archive.PackageInfo{ - Name: "package2", - Version: "v2", - Arch: "a2", - SHA256: "s2", + Name: "package2", + Version: "v2", + Arch: "a2", + Digest: "s2", + DigestKind: cache.SHA256, }, }, expected: &apachetestutil.ManifestContents{ @@ -179,17 +182,19 @@ var generateManifestTests = []struct { Slices: []string{"package1_slice1", "package2_slice2"}, }}, Packages: []*manifest.Package{{ - Kind: "package", - Name: "package1", - Version: "v1", - Digest: "s1", - Arch: "a1", + Kind: "package", + Name: "package1", + Version: "v1", + Digest: "s1", + DigestKind: "sha256", + Arch: "a1", }, { - Kind: "package", - Name: "package2", - Version: "v2", - Digest: "s2", - Arch: "a2", + Kind: "package", + Name: "package2", + Version: "v2", + Digest: "s2", + DigestKind: "sha256", + Arch: "a2", }}, Slices: []*manifest.Slice{{ Kind: "slice", @@ -212,6 +217,100 @@ var generateManifestTests = []struct { Path: "/link", }}, }, +}, { + summary: "SHA512 package digest", + selection: []*setup.Slice{slice1}, + report: &manifestutil.Report{ + Root: "/", + Entries: map[string]manifestutil.ReportEntry{ + "/file": { + Path: "/file", + Mode: 0o456, + Slices: map[*setup.Slice]bool{slice1: true}, + }, + }, + }, + packageInfo: []manifestutil.PackageInfo{ + &archive.PackageInfo{ + Name: "package1", + Version: "v1", + Arch: "a1", + Digest: "s512", + DigestKind: cache.SHA512, + }, + }, + expected: &apachetestutil.ManifestContents{ + Paths: []*manifest.Path{{ + Kind: "path", + Path: "/file", + Mode: "0456", + Slices: []string{"package1_slice1"}, + }}, + Packages: []*manifest.Package{{ + Kind: "package", + Name: "package1", + Version: "v1", + Digest: "s512", + DigestKind: "sha512", + Arch: "a1", + }}, + Slices: []*manifest.Slice{{ + Kind: "slice", + Name: "package1_slice1", + }}, + Contents: []*manifest.Content{{ + Kind: "content", + Slice: "package1_slice1", + Path: "/file", + }}, + }, +}, { + summary: "SHA384 package digest", + selection: []*setup.Slice{slice1}, + report: &manifestutil.Report{ + Root: "/", + Entries: map[string]manifestutil.ReportEntry{ + "/file": { + Path: "/file", + Mode: 0o456, + Slices: map[*setup.Slice]bool{slice1: true}, + }, + }, + }, + packageInfo: []manifestutil.PackageInfo{ + &archive.PackageInfo{ + Name: "package1", + Version: "v1", + Arch: "a1", + Digest: "s384", + DigestKind: cache.SHA384, + }, + }, + expected: &apachetestutil.ManifestContents{ + Paths: []*manifest.Path{{ + Kind: "path", + Path: "/file", + Mode: "0456", + Slices: []string{"package1_slice1"}, + }}, + Packages: []*manifest.Package{{ + Kind: "package", + Name: "package1", + Version: "v1", + Digest: "s384", + DigestKind: "sha384", + Arch: "a1", + }}, + Slices: []*manifest.Slice{{ + Kind: "slice", + Name: "package1_slice1", + }}, + Contents: []*manifest.Content{{ + Kind: "content", + Slice: "package1_slice1", + Path: "/file", + }}, + }, }, { summary: "Missing slice", report: &manifestutil.Report{ @@ -400,10 +499,11 @@ var generateManifestTests = []struct { }, packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - SHA256: "s1", + Name: "package1", + Version: "v1", + Arch: "a1", + Digest: "s1", + DigestKind: cache.SHA256, }, }, expected: &apachetestutil.ManifestContents{ @@ -427,11 +527,12 @@ var generateManifestTests = []struct { Inode: 1, }}, Packages: []*manifest.Package{{ - Kind: "package", - Name: "package1", - Version: "v1", - Digest: "s1", - Arch: "a1", + Kind: "package", + Name: "package1", + Version: "v1", + Digest: "s1", + DigestKind: "sha256", + Arch: "a1", }}, Slices: []*manifest.Slice{{ Kind: "slice", @@ -501,9 +602,10 @@ var generateManifestTests = []struct { summary: "Invalid package: missing name", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Version: "v1", - Arch: "a1", - SHA256: "s1", + Version: "v1", + Arch: "a1", + Digest: "s1", + DigestKind: cache.SHA256, }, }, error: `internal error: invalid manifest: package name not set`, @@ -511,9 +613,10 @@ var generateManifestTests = []struct { summary: "Invalid package: missing version", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Arch: "a1", - SHA256: "s1", + Name: "package-1", + Arch: "a1", + Digest: "s1", + DigestKind: cache.SHA256, }, }, error: `internal error: invalid manifest: package "package-1" missing version`, @@ -521,22 +624,36 @@ var generateManifestTests = []struct { summary: "Invalid package: missing arch", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Version: "v1", - SHA256: "s1", + Name: "package-1", + Version: "v1", + Digest: "s1", + DigestKind: cache.SHA256, }, }, error: `internal error: invalid manifest: package "package-1" missing arch`, }, { - summary: "Invalid package: missing sha256", + summary: "Invalid package: missing digest", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Version: "v1", - Arch: "a1", + Name: "package-1", + Version: "v1", + Arch: "a1", + DigestKind: cache.SHA256, }, }, error: `internal error: invalid manifest: package "package-1" missing sha256`, +}, { + summary: "Invalid package: unsupported digest kind", + packageInfo: []manifestutil.PackageInfo{ + &archive.PackageInfo{ + Name: "package-1", + Version: "v1", + Arch: "a1", + Digest: "s1", + DigestKind: cache.DigestKind("md5"), + }, + }, + error: `internal error: invalid manifest: package "package-1": unsupported digest kind: "md5"`, }} func (s *S) TestGenerateManifests(c *C) { @@ -548,10 +665,11 @@ func (s *S) TestGenerateManifests(c *C) { if test.packageInfo == nil { test.packageInfo = []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - SHA256: "s1", + Name: "package1", + Version: "v1", + Arch: "a1", + Digest: "s1", + DigestKind: cache.SHA256, }, } } @@ -639,6 +757,36 @@ var validateManifestTests = []struct { {"kind":"slice","name":"pkg1_myslice"} `, error: `invalid manifest: content path /dir/ has no matching entry in paths`, +}, { + summary: "Package with sha512 digest", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":2} + {"kind":"package","name":"pkg1","version":"v1","sha512":"hash1","arch":"arch1"} + {"kind":"slice","name":"pkg1_myslice"} + `, +}, { + summary: "Package with sha384 digest", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":2} + {"kind":"package","name":"pkg1","version":"v1","sha384":"hash1","arch":"arch1"} + {"kind":"slice","name":"pkg1_myslice"} + `, +}, { + summary: "Package with missing digest", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":2} + {"kind":"package","name":"pkg1","version":"v1","arch":"arch1"} + {"kind":"slice","name":"pkg1_myslice"} + `, + error: `invalid manifest: package "pkg1" missing digest`, +}, { + summary: "Package with multiple digests", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":2} + {"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","sha512":"hash2","arch":"arch1"} + {"kind":"slice","name":"pkg1_myslice"} + `, + error: `invalid manifest: cannot read manifest: package "pkg1" has multiple digests recorded`, }, { summary: "Malformed jsonwall", input: ` diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index d6ef9ca0d..57e0b06ff 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -859,8 +859,8 @@ var slicerTests = []slicerTest{{ "/other-file": "file 0644 fa0c9cdb {other-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 h1", - "other-package": "other-package v3 a3 h3", + "test-package": "test-package v1 a1 sha256 h1", + "other-package": "other-package v3 a3 sha256 h3", }, }, { summary: "Pinned archive bypasses higher priority", @@ -928,7 +928,7 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 fa0c9cdb {test-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v2 a2 h2", + "test-package": "test-package v2 a2 sha256 h2", }, }, { summary: "Pinned archive does not have the package", @@ -1097,7 +1097,38 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 7a3e00f5 {test-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 h1", + "test-package": "test-package v1 a1 sha256 h1", + }, +}, { + summary: "Package with sha512 digest is recorded in the manifest", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Hash: "h1", + HashKind: "sha512", + Version: "v1", + Arch: "a1", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0o644, "./file", "from foo"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + `, + }, + filesystem: map[string]string{ + "/file": "file 0644 7a3e00f5", + }, + manifestPaths: map[string]string{ + "/file": "file 0644 7a3e00f5 {test-package_myslice}", + }, + manifestPkgs: map[string]string{ + "test-package": "test-package v1 a1 sha512 h1", }, }, { summary: "Multiple slices of same package", @@ -1386,8 +1417,8 @@ var slicerTests = []slicerTest{{ `, }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 h1", - "other-package": "other-package v2 a2 h2", + "test-package": "test-package v1 a1 sha256 h1", + "other-package": "other-package v2 a2 sha256 h2", }, }, { summary: "Two packages, only one is selected and recorded", @@ -1422,7 +1453,7 @@ var slicerTests = []slicerTest{{ `, }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 h1", + "test-package": "test-package v1 a1 sha256 h1", }, }, { summary: "Relative paths are properly trimmed during extraction", @@ -2220,7 +2251,7 @@ func treeDumpManifestPaths(mfest *manifest.Manifest) (map[string]string, error) func dumpManifestPkgs(mfest *manifest.Manifest) (map[string]string, error) { result := map[string]string{} err := mfest.IteratePackages(func(pkg *manifest.Package) error { - result[pkg.Name] = fmt.Sprintf("%s %s %s %s", pkg.Name, pkg.Version, pkg.Arch, pkg.Digest) + result[pkg.Name] = fmt.Sprintf("%s %s %s %s %s", pkg.Name, pkg.Version, pkg.Arch, pkg.DigestKind, pkg.Digest) return nil }) if err != nil { diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index d06fd1b0c..7a26e8636 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -6,6 +6,7 @@ import ( "io" "github.com/canonical/chisel/internal/archive" + "github.com/canonical/chisel/internal/cache" ) type TestArchive struct { @@ -14,14 +15,24 @@ type TestArchive struct { } type TestPackage struct { - Name string - Version string - Hash string + Name string + Version string + Hash string + // HashKind is the digest kind of Hash. When unset, Hash is treated as + // a sha256 digest. + HashKind string Arch string Data []byte Archives []string } +func (p *TestPackage) digestKind() cache.DigestKind { + if p.HashKind == "" { + return cache.SHA256 + } + return cache.DigestKind(p.HashKind) +} + func (a *TestArchive) Options() *archive.Options { return &a.Opts } @@ -32,10 +43,11 @@ func (a *TestArchive) Fetch(pkgName string) (io.ReadSeekCloser, *archive.Package return nil, nil, fmt.Errorf("cannot find package %q in archive", pkgName) } info := &archive.PackageInfo{ - Name: pkg.Name, - Version: pkg.Version, - SHA256: pkg.Hash, - Arch: pkg.Arch, + Name: pkg.Name, + Version: pkg.Version, + Digest: pkg.Hash, + DigestKind: pkg.digestKind(), + Arch: pkg.Arch, } return ReadSeekNopCloser(bytes.NewReader(pkg.Data)), info, nil } @@ -51,9 +63,10 @@ func (a *TestArchive) Info(pkgName string) (*archive.PackageInfo, error) { return nil, fmt.Errorf("cannot find package %q in archive", pkgName) } return &archive.PackageInfo{ - Name: pkg.Name, - Version: pkg.Version, - SHA256: pkg.Hash, - Arch: pkg.Arch, + Name: pkg.Name, + Version: pkg.Version, + Digest: pkg.Hash, + DigestKind: pkg.digestKind(), + Arch: pkg.Arch, }, nil } diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index 1e4809b8b..183d6cb12 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -3,6 +3,7 @@ package manifest import ( + "encoding/json" "fmt" "io" @@ -11,14 +12,99 @@ import ( const Schema = "1.0" +// Package describes a package installed in the target filesystem. DigestKind +// names the algorithm used to compute Digest, and is one of "sha256", "sha384" +// or "sha512", or empty when no digest is recorded. In the manifest the digest +// is recorded under the JSON field named after its kind, so that it always +// holds the same position in the entry regardless of the algorithm used. type Package struct { + Kind string + Name string + Version string + Digest string + DigestKind string + Arch string +} + +// packageJSON is the JSON encoding of a Package, with the digest recorded +// under the field named after its kind. At most one of the digest fields may +// be set. +type packageJSON struct { Kind string `json:"kind"` Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` - Digest string `json:"sha256,omitempty"` + SHA256 string `json:"sha256,omitempty"` + SHA384 string `json:"sha384,omitempty"` + SHA512 string `json:"sha512,omitempty"` Arch string `json:"arch,omitempty"` } +func (p *Package) MarshalJSON() ([]byte, error) { + pj := packageJSON{ + Kind: p.Kind, + Name: p.Name, + Version: p.Version, + Arch: p.Arch, + } + switch p.DigestKind { + case "": + // No digest recorded. + case "sha256": + pj.SHA256 = p.Digest + case "sha384": + pj.SHA384 = p.Digest + case "sha512": + pj.SHA512 = p.Digest + default: + return nil, fmt.Errorf("cannot marshal package %q: unsupported digest kind %q", p.Name, p.DigestKind) + } + return json.Marshal(pj) +} + +func (p *Package) UnmarshalJSON(data []byte) error { + var pj packageJSON + err := json.Unmarshal(data, &pj) + if err != nil { + return err + } + digest, kind, err := pj.digest() + if err != nil { + return err + } + *p = Package{ + Kind: pj.Kind, + Name: pj.Name, + Version: pj.Version, + Digest: digest, + DigestKind: kind, + Arch: pj.Arch, + } + return nil +} + +// digest returns the package digest and its kind, as recorded in the wire +// representation. At most one digest field may be set. +func (pj *packageJSON) digest() (digest, kind string, err error) { + set := 0 + for _, entry := range []struct { + kind string + digest string + }{ + {"sha256", pj.SHA256}, + {"sha384", pj.SHA384}, + {"sha512", pj.SHA512}, + } { + if entry.digest != "" { + set++ + digest, kind = entry.digest, entry.kind + } + } + if set > 1 { + return "", "", fmt.Errorf("package %q has multiple digests recorded", pj.Name) + } + return digest, kind, nil +} + type Slice struct { Kind string `json:"kind"` Name string `json:"name,omitempty"` diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index d710e121d..1bb21e0bf 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -52,8 +52,8 @@ var readManifestTests = []struct { {Kind: "path", Path: "/manifest/manifest.wall", Mode: "0644", Slices: []string{"pkg1_manifest"}, SHA256: "", FinalSHA256: "", Size: 0x0, Link: ""}, }, Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", Arch: "arch1"}, - {Kind: "package", Name: "pkg2", Version: "v2", Digest: "hash2", Arch: "arch2"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", DigestKind: "sha256", Arch: "arch1"}, + {Kind: "package", Name: "pkg2", Version: "v2", Digest: "hash2", DigestKind: "sha256", Arch: "arch2"}, }, Slices: []*manifest.Slice{ {Kind: "slice", Name: "pkg1_manifest"}, @@ -70,6 +70,35 @@ var readManifestTests = []struct { {Kind: "content", Slice: "pkg2_myotherslice", Path: "/dir/foo/bar/"}, }, }, +}, { + summary: "SHA512 package digest", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":1} + {"kind":"package","name":"pkg1","version":"v1","sha512":"hash1","arch":"arch1"} + `, + mfest: &apachetestutil.ManifestContents{ + Packages: []*manifest.Package{ + {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", DigestKind: "sha512", Arch: "arch1"}, + }, + }, +}, { + summary: "SHA384 package digest", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":1} + {"kind":"package","name":"pkg1","version":"v1","sha384":"hash1","arch":"arch1"} + `, + mfest: &apachetestutil.ManifestContents{ + Packages: []*manifest.Package{ + {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", DigestKind: "sha384", Arch: "arch1"}, + }, + }, +}, { + summary: "Multiple digests recorded", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":1} + {"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","sha512":"hash2","arch":"arch1"} + `, + error: `cannot read manifest: package "pkg1" has multiple digests recorded`, }, { summary: "Unknown schema", input: ` @@ -97,24 +126,34 @@ func (s *S) TestManifestRead(c *C) { tmpDir := c.MkDir() manifestPath := path.Join(tmpDir, "manifest.wall") - w, err := os.OpenFile(manifestPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + w, err := os.OpenFile(manifestPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) c.Assert(err, IsNil) _, err = w.Write([]byte(test.input)) c.Assert(err, IsNil) w.Close() - r, err := os.OpenFile(manifestPath, os.O_RDONLY, 0644) + r, err := os.OpenFile(manifestPath, os.O_RDONLY, 0o644) c.Assert(err, IsNil) defer r.Close() mfest, err := manifest.Read(r) - if test.error != "" { + if err != nil { + // Reading itself may fail (e.g. on an unknown schema version). + c.Assert(test.error, Not(Equals), "", Commentf("unexpected error: %s", err)) c.Assert(err, ErrorMatches, test.error) continue } - c.Assert(err, IsNil) if test.mfest != nil { c.Assert(apachetestutil.DumpManifestContents(c, mfest), DeepEquals, test.mfest) } + if test.error != "" { + // Entry-level errors surface while iterating, as the manifest + // is not fully decoded on read. + err := mfest.IteratePackages(func(pkg *manifest.Package) error { + return nil + }) + c.Assert(err, ErrorMatches, test.error) + continue + } } } From a3e703623fea0fa136f874b06ad820d8546b3ce1 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 2 Sep 2026 13:37:24 +0200 Subject: [PATCH 02/24] docs: refine comments --- internal/manifestutil/manifestutil.go | 2 +- public/manifest/manifest.go | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 8bed79a0a..cb2bce2fd 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -299,7 +299,7 @@ func Validate(mfest *manifest.Manifest) (err error) { pkgExist := map[string]bool{} err = mfest.IteratePackages(func(pkg *manifest.Package) error { - // Same rules as validatePackage applies on the write side. + // Same rules as validatePackage apply on the write side. name := pkg.Name if name == "" { return fmt.Errorf("package name not set") diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index 183d6cb12..076cd96c0 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -12,16 +12,14 @@ import ( const Schema = "1.0" -// Package describes a package installed in the target filesystem. DigestKind -// names the algorithm used to compute Digest, and is one of "sha256", "sha384" -// or "sha512", or empty when no digest is recorded. In the manifest the digest -// is recorded under the JSON field named after its kind, so that it always -// holds the same position in the entry regardless of the algorithm used. +// Package describes a package installed in the target filesystem. type Package struct { - Kind string - Name string - Version string - Digest string + Kind string + Name string + Version string + Digest string + // DigestKind is the algorithm used to compute Digest, and is empty when + // no digest is recorded. DigestKind string Arch string } From ef988296f0b6a3424868615a3155eebe58a2c562 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 2 Sep 2026 14:11:57 +0200 Subject: [PATCH 03/24] style: cleaning --- internal/manifestutil/manifestutil.go | 4 ++-- public/manifest/manifest_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index cb2bce2fd..02433ef7f 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -276,7 +276,7 @@ func validatePackage(pkg PackageInfo) (err error) { kind := pkg.PkgDigestKind() err = cache.ValidateKind(kind) if err != nil { - return fmt.Errorf("package %q: %w", name, err) + return fmt.Errorf("package %q: %s", name, err) } if pkg.PkgDigest() == "" { return fmt.Errorf("package %q missing %s", name, kind) @@ -312,7 +312,7 @@ func Validate(mfest *manifest.Manifest) (err error) { return fmt.Errorf("package %q missing digest", name) } if err := cache.ValidateKind(kind); err != nil { - return fmt.Errorf("package %q: %w", name, err) + return fmt.Errorf("package %q: %s", name, err) } if pkg.Digest == "" { return fmt.Errorf("package %q missing %s", name, kind) diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index 1bb21e0bf..cc74306f8 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -126,13 +126,13 @@ func (s *S) TestManifestRead(c *C) { tmpDir := c.MkDir() manifestPath := path.Join(tmpDir, "manifest.wall") - w, err := os.OpenFile(manifestPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) + w, err := os.OpenFile(manifestPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) c.Assert(err, IsNil) _, err = w.Write([]byte(test.input)) c.Assert(err, IsNil) w.Close() - r, err := os.OpenFile(manifestPath, os.O_RDONLY, 0o644) + r, err := os.OpenFile(manifestPath, os.O_RDONLY, 0644) c.Assert(err, IsNil) defer r.Close() From 2013aab811e1f81a3ada787dcc448d4bd44261c8 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 2 Sep 2026 14:32:22 +0200 Subject: [PATCH 04/24] fix: fail loudly on invalid marshalling input --- public/manifest/manifest.go | 4 +- public/manifest/manifest_test.go | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index 076cd96c0..0a46b23cc 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -46,7 +46,9 @@ func (p *Package) MarshalJSON() ([]byte, error) { } switch p.DigestKind { case "": - // No digest recorded. + if p.Digest != "" { + return nil, fmt.Errorf("cannot marshal package %q: digest set without a digest kind", p.Name) + } case "sha256": pj.SHA256 = p.Digest case "sha384": diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index cc74306f8..c4aee0e42 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -3,6 +3,7 @@ package manifest_test import ( + "encoding/json" "os" "path" "slices" @@ -157,3 +158,75 @@ func (s *S) TestManifestRead(c *C) { } } } + +var marshalPackageTests = []struct { + summary string + pkg *manifest.Package + expected string + error string +}{{ + summary: "SHA256 digest", + pkg: &manifest.Package{ + Kind: "package", + Name: "pkg1", + Version: "v1", + Digest: "hash1", + DigestKind: "sha256", + Arch: "arch1", + }, + expected: `{"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","arch":"arch1"}`, +}, { + summary: "SHA512 digest", + pkg: &manifest.Package{ + Kind: "package", + Name: "pkg1", + Version: "v1", + Digest: "hash1", + DigestKind: "sha512", + Arch: "arch1", + }, + expected: `{"kind":"package","name":"pkg1","version":"v1","sha512":"hash1","arch":"arch1"}`, +}, { + summary: "No digest recorded", + pkg: &manifest.Package{ + Kind: "package", + Name: "pkg1", + Version: "v1", + Arch: "arch1", + }, + expected: `{"kind":"package","name":"pkg1","version":"v1","arch":"arch1"}`, +}, { + summary: "Digest set without a digest kind", + pkg: &manifest.Package{ + Kind: "package", + Name: "pkg1", + Version: "v1", + Digest: "hash1", + Arch: "arch1", + }, + error: `json: error calling MarshalJSON for type \*manifest\.Package: cannot marshal package "pkg1": digest set without a digest kind`, +}, { + summary: "Unsupported digest kind", + pkg: &manifest.Package{ + Kind: "package", + Name: "pkg1", + Version: "v1", + Digest: "hash1", + DigestKind: "md5", + Arch: "arch1", + }, + error: `json: error calling MarshalJSON for type \*manifest\.Package: cannot marshal package "pkg1": unsupported digest kind "md5"`, +}} + +func (s *S) TestMarshalPackage(c *C) { + for _, test := range marshalPackageTests { + c.Logf("Summary: %s", test.summary) + data, err := json.Marshal(test.pkg) + if test.error != "" { + c.Assert(err, ErrorMatches, test.error) + continue + } + c.Assert(err, IsNil) + c.Assert(string(data), Equals, test.expected) + } +} From 1f8982ef89fb476af775252cb455444381e1c4e4 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 2 Sep 2026 14:37:30 +0200 Subject: [PATCH 05/24] test: improve TestManifestRead --- public/manifest/manifest_test.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index c4aee0e42..f236fc511 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -138,24 +138,21 @@ func (s *S) TestManifestRead(c *C) { defer r.Close() mfest, err := manifest.Read(r) - if err != nil { - // Reading itself may fail (e.g. on an unknown schema version). - c.Assert(test.error, Not(Equals), "", Commentf("unexpected error: %s", err)) - c.Assert(err, ErrorMatches, test.error) - continue - } - if test.mfest != nil { - c.Assert(apachetestutil.DumpManifestContents(c, mfest), DeepEquals, test.mfest) - } - if test.error != "" { + if err == nil { // Entry-level errors surface while iterating, as the manifest // is not fully decoded on read. - err := mfest.IteratePackages(func(pkg *manifest.Package) error { + err = mfest.IteratePackages(func(pkg *manifest.Package) error { return nil }) + } + if test.error != "" { c.Assert(err, ErrorMatches, test.error) continue } + c.Assert(err, IsNil) + if test.mfest != nil { + c.Assert(apachetestutil.DumpManifestContents(c, mfest), DeepEquals, test.mfest) + } } } From e013b76a0840585863dbd630d3e8825729f264ba Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 2 Sep 2026 14:44:13 +0200 Subject: [PATCH 06/24] docs: cleaning --- internal/manifestutil/manifestutil.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 02433ef7f..e47f84a8e 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -299,7 +299,6 @@ func Validate(mfest *manifest.Manifest) (err error) { pkgExist := map[string]bool{} err = mfest.IteratePackages(func(pkg *manifest.Package) error { - // Same rules as validatePackage apply on the write side. name := pkg.Name if name == "" { return fmt.Errorf("package name not set") From ace8a36d43ff56ee9fa2b573d627eb37302a27cd Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 4 Sep 2026 12:09:29 +0200 Subject: [PATCH 07/24] fix: use sha256 for packages by default --- internal/archive/archive.go | 13 ++++++++++++- internal/archive/archive_test.go | 17 ++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 49cb03da5..17d8dcaf1 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -367,8 +367,19 @@ func findDigest(release control.Section, path string, order []digestField) (dige return "", digestField{} } +// packageDigestFields lists the checksum fields Chisel looks up in a package +// section, in order of preference. Unlike index files, packages are fetched +// by their named pool path, so the by-hash layout does not constrain this +// order. SHA256 is preferred so that the digest recorded in the manifest +// keeps matching the one consumers expect for as long as archives publish +// it; the strongest available digest is used only when they do not. +var packageDigestFields = []digestField{ + {"SHA256", cache.SHA256}, + {"SHA512", cache.SHA512}, +} + func packageDigest(section control.Section) (digest string, kind cache.DigestKind) { - for _, f := range digestFields { + for _, f := range packageDigestFields { if d := section.Get(f.name); d != "" { return d, f.kind } diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index d0f368734..660c2b6a1 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -306,8 +306,11 @@ func (s *httpSuite) TestFetchSHA512Digests(c *C) { func (s *httpSuite) TestFetchBothDigests(c *C) { // An archive publishing both SHA256 and SHA512 sections (index table and - // package fields) must be handled, with the strongest digest preferred - // for verification, caching and the manifest. + // package fields) must be handled. For packages, SHA256 is preferred for + // verification, caching and the manifest, so the recorded digest keeps + // matching the one consumers expect; the strongest digest is used only + // when the archive does not publish SHA256. Index files still use the + // strongest digest, as required by the by-hash layout. s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, []string{"SHA256", "SHA512"}, nil) @@ -330,15 +333,15 @@ func (s *httpSuite) TestFetchBothDigests(c *C) { Name: "mypkg1", Version: "1.1", Arch: "amd64", - Digest: "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb", - DigestKind: cache.SHA512, + Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + DigestKind: cache.SHA256, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") // Pin the cache key: with both digests advertised, the package is cached - // under its strongest digest. - _, err = os.Stat(filepath.Join(options.CacheDir, "sha512", - "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb")) + // under its SHA256 digest. + _, err = os.Stat(filepath.Join(options.CacheDir, "sha256", + "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05")) c.Assert(err, IsNil) } From 1e5c4d8e87ee0eeaec4cea328005ca5ff88618a9 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 4 Sep 2026 13:27:31 +0200 Subject: [PATCH 08/24] docs: refine comments --- internal/archive/archive.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 17d8dcaf1..74605dfbd 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -368,16 +368,18 @@ func findDigest(release control.Section, path string, order []digestField) (dige } // packageDigestFields lists the checksum fields Chisel looks up in a package -// section, in order of preference. Unlike index files, packages are fetched -// by their named pool path, so the by-hash layout does not constrain this -// order. SHA256 is preferred so that the digest recorded in the manifest -// keeps matching the one consumers expect for as long as archives publish -// it; the strongest available digest is used only when they do not. +// section, in order of preference: sha256 first. var packageDigestFields = []digestField{ {"SHA256", cache.SHA256}, {"SHA512", cache.SHA512}, } +// packageDigest returns the digest recorded for the package in the section, +// along with its kind. SHA256 is preferred over stronger digests so that +// the digest used for verification, caching and the manifest keeps matching +// the one consumers expect for as long as archives publish it. Unlike index +// files, packages are fetched by their named pool path, so the by-hash +// layout does not constrain the preference order. func packageDigest(section control.Section) (digest string, kind cache.DigestKind) { for _, f := range packageDigestFields { if d := section.Get(f.name); d != "" { From cb3f49841ea1d9b4abea682595d0e5447c084305 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 16 Sep 2026 16:19:30 +0200 Subject: [PATCH 09/24] fet: record multiple hashes in the manifest --- internal/archive/archive.go | 111 ++++++++------- internal/archive/archive_test.go | 97 ++++++-------- internal/cache/cache.go | 6 +- internal/manifestutil/manifestutil.go | 44 +++--- internal/manifestutil/manifestutil_test.go | 149 +++++++++------------ internal/slicer/slicer_test.go | 70 ++++++++-- internal/testutil/archive.go | 34 +++-- public/manifest/manifest.go | 82 +++++------- public/manifest/manifest_test.go | 53 ++++---- 9 files changed, 329 insertions(+), 317 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 74605dfbd..5eb06c016 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -26,19 +26,19 @@ type Archive interface { } type PackageInfo struct { - Name string - Version string - Arch string - Digest string - DigestKind cache.DigestKind + Name string + Version string + Arch string + Digests map[cache.DigestKind]string } -func (p *PackageInfo) PkgName() string { return p.Name } -func (p *PackageInfo) PkgVersion() string { return p.Version } -func (p *PackageInfo) PkgRevision() int { return 0 } -func (p *PackageInfo) PkgArch() string { return p.Arch } -func (p *PackageInfo) PkgDigestKind() cache.DigestKind { return p.DigestKind } -func (p *PackageInfo) PkgDigest() string { return p.Digest } +func (p *PackageInfo) PkgName() string { return p.Name } +func (p *PackageInfo) PkgVersion() string { return p.Version } +func (p *PackageInfo) PkgRevision() int { return 0 } +func (p *PackageInfo) PkgArch() string { return p.Arch } +func (p *PackageInfo) PkgDigests() map[cache.DigestKind]string { + return p.Digests +} type Options struct { Label string @@ -147,13 +147,20 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro return nil, nil, err } path := section.Get("Filename") - digest, digestKind := packageDigest(section) + digests := packageDigests(section) + // The strongest published digest is used for verification and caching. + digest, digestKind := strongestDigest(digests) + if digest == "" { + // No digest advertised; fetch unverified and cache the package under + // its computed SHA256 digest. + digestKind = cache.SHA256 + } logf("Fetching %s...", path) reader, err := index.fetch(path, digest, digestKind, fetchBulk) if err != nil { return nil, nil, err } - info := sectionPackageInfo(section, digest, digestKind) + info := sectionPackageInfo(section, digests) return reader, info, nil } @@ -162,8 +169,11 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) { if err != nil { return nil, err } - digest, digestKind := packageDigest(section) - info := sectionPackageInfo(section, digest, digestKind) + digests := packageDigests(section) + if len(digests) == 0 { + return nil, fmt.Errorf("cannot find digest for package %q", pkg) + } + info := sectionPackageInfo(section, digests) return info, nil } @@ -340,61 +350,67 @@ func (index *ubuntuIndex) fetchRelease() error { return nil } -// digestField is an archive checksum field Chisel can verify. Its name -// doubles as the by-hash directory name in the archive layout. +// digestField is an archive checksum field Chisel can verify, along with +// the digest kind it carries. type digestField struct { name string kind cache.DigestKind } // digestFields lists the checksum fields Chisel can verify, in order of -// preference: strongest first. The order also matches the by-hash archive -// layout, where only the by-hash directory of the strongest advertised hash -// is guaranteed to exist. +// preference: strongest first. var digestFields = []digestField{ {"SHA512", cache.SHA512}, {"SHA256", cache.SHA256}, } -// findDigest returns the digest recorded for path in the release, along with -// the field it was found in, trying the given fields in order. -func findDigest(release control.Section, path string, order []digestField) (digest string, field digestField) { +func findDigest(release control.Section, path string, order []digestField) (digest string, field digestField, ok bool) { for _, f := range order { - if d, _, ok := control.ParsePathInfo(release.Get(f.name), path); ok { - return d, f + if d, _, found := control.ParsePathInfo(release.Get(f.name), path); found { + return d, f, true } } - return "", digestField{} + return "", digestField{}, false } // packageDigestFields lists the checksum fields Chisel looks up in a package -// section, in order of preference: sha256 first. +// section, in order of preference: strongest first. The first field found is +// used for verification and caching; all fields found are recorded in the +// manifest. Only kinds Ubuntu archives publish are listed; digest kinds +// weaker than SHA256 (e.g. MD5) and kinds Chisel supports for other sources +// (e.g. SHA384, used by stores) are not looked up here. var packageDigestFields = []digestField{ - {"SHA256", cache.SHA256}, {"SHA512", cache.SHA512}, + {"SHA256", cache.SHA256}, } -// packageDigest returns the digest recorded for the package in the section, -// along with its kind. SHA256 is preferred over stronger digests so that -// the digest used for verification, caching and the manifest keeps matching -// the one consumers expect for as long as archives publish it. Unlike index -// files, packages are fetched by their named pool path, so the by-hash -// layout does not constrain the preference order. -func packageDigest(section control.Section) (digest string, kind cache.DigestKind) { +// packageDigests returns every digest the section advertises, keyed by +// digest kind, following packageDigestFields. +func packageDigests(section control.Section) map[cache.DigestKind]string { + digests := make(map[cache.DigestKind]string) for _, f := range packageDigestFields { if d := section.Get(f.name); d != "" { + digests[f.kind] = d + } + } + return digests +} + +// strongestDigest returns the strongest digest in digests, following +// packageDigestFields, or the zero values when digests is empty. +func strongestDigest(digests map[cache.DigestKind]string) (digest string, kind cache.DigestKind) { + for _, f := range packageDigestFields { + if d, ok := digests[f.kind]; ok { return d, f.kind } } - // No digest advertised; fall back to SHA256 so the package can still be - // cached and retrieved by its computed digest. - return "", cache.SHA256 + return "", "" } func (index *ubuntuIndex) fetchIndex() error { packagesPath := fmt.Sprintf("%s/binary-%s/Packages", index.component, index.arch) - packagesDigest, field := findDigest(index.release, packagesPath, digestFields) - if packagesDigest == "" { + packagesDigest, field, ok := findDigest(index.release, packagesPath, digestFields) + if !ok { return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component) } @@ -411,8 +427,8 @@ func (index *ubuntuIndex) fetchIndex() error { // hash the archive advertises, which is what findDigest prefers. If // the archive advertises a hash stronger than any Chisel knows, the // URL may 404 and the named-path fallback below applies. - packagesGzDigest, byHashField := findDigest(index.release, packagesGzPath, digestFields) - if packagesGzDigest != "" { + packagesGzDigest, byHashField, ok := findDigest(index.release, packagesGzPath, digestFields) + if ok { packagesByHashPath := fmt.Sprintf("%s/binary-%s/by-hash/%s/%s", index.component, index.arch, byHashField.name, packagesGzDigest) r, err := index.fetch(index.distPath(packagesByHashPath), packagesDigest, field.kind, fetchBulk|fetchGzip) if err != nil && err != errNotFound { @@ -531,13 +547,12 @@ func (index *ubuntuIndex) fetch(path, digest string, digestKind cache.DigestKind return index.archive.cache.Open(digestKind, writer.Digest()) } -func sectionPackageInfo(section control.Section, digest string, digestKind cache.DigestKind) *PackageInfo { +func sectionPackageInfo(section control.Section, digests map[cache.DigestKind]string) *PackageInfo { return &PackageInfo{ - Name: section.Get("Package"), - Version: section.Get("Version"), - Arch: section.Get("Architecture"), - Digest: digest, - DigestKind: digestKind, + Name: section.Get("Package"), + Version: section.Get("Version"), + Arch: section.Get("Architecture"), + Digests: digests, } } diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 660c2b6a1..bcd4d93a0 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -252,11 +252,10 @@ func (s *httpSuite) TestFetchPackage(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", - DigestKind: cache.SHA256, + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digests: map[cache.DigestKind]string{cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05"}, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") @@ -264,11 +263,10 @@ func (s *httpSuite) TestFetchPackage(c *C) { pkg, info, err = testArchive.Fetch("mypkg4") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg4", - Version: "1.4", - Arch: "amd64", - Digest: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4", - DigestKind: cache.SHA256, + Name: "mypkg4", + Version: "1.4", + Arch: "amd64", + Digests: map[cache.DigestKind]string{cache.SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4"}, }) c.Assert(read(pkg), Equals, "mypkg4 1.4 data") } @@ -295,22 +293,18 @@ func (s *httpSuite) TestFetchSHA512Digests(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - Digest: "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb", - DigestKind: cache.SHA512, + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digests: map[cache.DigestKind]string{cache.SHA512: "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb"}, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") } func (s *httpSuite) TestFetchBothDigests(c *C) { // An archive publishing both SHA256 and SHA512 sections (index table and - // package fields) must be handled. For packages, SHA256 is preferred for - // verification, caching and the manifest, so the recorded digest keeps - // matching the one consumers expect; the strongest digest is used only - // when the archive does not publish SHA256. Index files still use the - // strongest digest, as required by the by-hash layout. + // package fields) must be handled. All published digests are recorded in + // the manifest; the strongest one is used for verification and caching. s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, []string{"SHA256", "SHA512"}, nil) @@ -330,18 +324,20 @@ func (s *httpSuite) TestFetchBothDigests(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", - DigestKind: cache.SHA256, + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digests: map[cache.DigestKind]string{ + cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", + cache.SHA512: "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb", + }, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") // Pin the cache key: with both digests advertised, the package is cached - // under its SHA256 digest. - _, err = os.Stat(filepath.Join(options.CacheDir, "sha256", - "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05")) + // under its strongest digest. + _, err = os.Stat(filepath.Join(options.CacheDir, "sha512", + "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb")) c.Assert(err, IsNil) } @@ -368,11 +364,10 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "arm64", - Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", - DigestKind: cache.SHA256, + Name: "mypkg1", + Version: "1.1", + Arch: "arm64", + Digests: map[cache.DigestKind]string{cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05"}, }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") @@ -380,11 +375,10 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) { pkg, info, err = testArchive.Fetch("mypkg4") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg4", - Version: "1.4", - Arch: "arm64", - Digest: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4", - DigestKind: cache.SHA256, + Name: "mypkg4", + Version: "1.4", + Arch: "arm64", + Digests: map[cache.DigestKind]string{cache.SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4"}, }) c.Assert(read(pkg), Equals, "mypkg4 1.4 data") } @@ -421,22 +415,20 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) { pkg, info, err := testArchive.Fetch("mypkg1") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1.2.2", - Arch: "amd64", - Digest: "5448585bdd916e5023eff2bc1bc3b30bcc6ee9db9c03e531375a6a11ddf0913c", - DigestKind: cache.SHA256, + Name: "mypkg1", + Version: "1.1.2.2", + Arch: "amd64", + Digests: map[cache.DigestKind]string{cache.SHA256: "5448585bdd916e5023eff2bc1bc3b30bcc6ee9db9c03e531375a6a11ddf0913c"}, }) c.Assert(read(pkg), Equals, "package from jammy-security") pkg, info, err = testArchive.Fetch("mypkg2") c.Assert(err, IsNil) c.Assert(info, DeepEquals, &archive.PackageInfo{ - Name: "mypkg2", - Version: "1.2", - Arch: "amd64", - Digest: "a4b4f3f3a8fa09b69e3ba23c60a41a1f8144691fd371a2455812572fd02e6f79", - DigestKind: cache.SHA256, + Name: "mypkg2", + Version: "1.2", + Arch: "amd64", + Digests: map[cache.DigestKind]string{cache.SHA256: "a4b4f3f3a8fa09b69e3ba23c60a41a1f8144691fd371a2455812572fd02e6f79"}, }) c.Assert(read(pkg), Equals, "mypkg2 1.2 data") } @@ -678,11 +670,10 @@ var packageInfoTests = []struct { summary: "Basic", pkg: "mypkg1", info: &archive.PackageInfo{ - Name: "mypkg1", - Version: "1.1", - Arch: "amd64", - Digest: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05", - DigestKind: cache.SHA256, + Name: "mypkg1", + Version: "1.1", + Arch: "amd64", + Digests: map[cache.DigestKind]string{cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05"}, }, }, { summary: "Package not found in archive", diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 2f0c876c4..5fbacba63 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -97,11 +97,13 @@ type DigestKind string const ( SHA256 DigestKind = "sha256" - SHA384 DigestKind = "sha384" SHA512 DigestKind = "sha512" + SHA384 DigestKind = "sha384" ) -var digestKinds = []DigestKind{SHA256, SHA384, SHA512} +// digestKinds lists the digest kinds Chisel supports, in order of strength: +// strongest first. +var digestKinds = []DigestKind{SHA384, SHA512, SHA256} // ValidateKind returns an error unless kind is a digest kind Chisel // supports. diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index e47f84a8e..1fd548a4c 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -26,8 +26,7 @@ type PackageInfo interface { // revisions. PkgRevision() int PkgArch() string - PkgDigestKind() cache.DigestKind - PkgDigest() string + PkgDigests() map[cache.DigestKind]string } const DefaultFilename = "manifest.wall" @@ -85,13 +84,16 @@ func Write(options *WriteOptions, writer io.Writer) error { func manifestAddPackages(dbw *jsonwall.DBWriter, infos []PackageInfo) error { for _, info := range infos { + digests := make(map[string]string, len(info.PkgDigests())) + for kind, digest := range info.PkgDigests() { + digests[string(kind)] = digest + } err := dbw.Add(&manifest.Package{ - Kind: "package", - Name: info.PkgName(), - Version: info.PkgVersion(), - Digest: info.PkgDigest(), - DigestKind: string(info.PkgDigestKind()), - Arch: info.PkgArch(), + Kind: "package", + Name: info.PkgName(), + Version: info.PkgVersion(), + Digests: digests, + Arch: info.PkgArch(), }) if err != nil { return err @@ -273,13 +275,15 @@ func validatePackage(pkg PackageInfo) (err error) { if pkg.PkgArch() == "" { return fmt.Errorf("package %q missing arch", name) } - kind := pkg.PkgDigestKind() - err = cache.ValidateKind(kind) - if err != nil { - return fmt.Errorf("package %q: %s", name, err) + digests := pkg.PkgDigests() + if len(digests) == 0 { + return fmt.Errorf("package %q missing digest", name) } - if pkg.PkgDigest() == "" { - return fmt.Errorf("package %q missing %s", name, kind) + for kind := range digests { + err = cache.ValidateKind(kind) + if err != nil { + return fmt.Errorf("package %q: %s", name, err) + } } if pkg.PkgVersion() == "" { return fmt.Errorf("package %q missing version", name) @@ -306,15 +310,13 @@ func Validate(mfest *manifest.Manifest) (err error) { if pkg.Arch == "" { return fmt.Errorf("package %q missing arch", name) } - kind := cache.DigestKind(pkg.DigestKind) - if kind == "" { + if len(pkg.Digests) == 0 { return fmt.Errorf("package %q missing digest", name) } - if err := cache.ValidateKind(kind); err != nil { - return fmt.Errorf("package %q: %s", name, err) - } - if pkg.Digest == "" { - return fmt.Errorf("package %q missing %s", name, kind) + for kind := range pkg.Digests { + if err := cache.ValidateKind(cache.DigestKind(kind)); err != nil { + return fmt.Errorf("package %q: %s", name, err) + } } if pkg.Version == "" { return fmt.Errorf("package %q missing version", name) diff --git a/internal/manifestutil/manifestutil_test.go b/internal/manifestutil/manifestutil_test.go index 494a3c9e2..18bd46543 100644 --- a/internal/manifestutil/manifestutil_test.go +++ b/internal/manifestutil/manifestutil_test.go @@ -151,18 +151,16 @@ var generateManifestTests = []struct { }, packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - Digest: "s1", - DigestKind: cache.SHA256, + Name: "package1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA256: "s1"}, }, &archive.PackageInfo{ - Name: "package2", - Version: "v2", - Arch: "a2", - Digest: "s2", - DigestKind: cache.SHA256, + Name: "package2", + Version: "v2", + Arch: "a2", + Digests: map[cache.DigestKind]string{cache.SHA256: "s2"}, }, }, expected: &apachetestutil.ManifestContents{ @@ -182,19 +180,17 @@ var generateManifestTests = []struct { Slices: []string{"package1_slice1", "package2_slice2"}, }}, Packages: []*manifest.Package{{ - Kind: "package", - Name: "package1", - Version: "v1", - Digest: "s1", - DigestKind: "sha256", - Arch: "a1", + Kind: "package", + Name: "package1", + Version: "v1", + Digests: map[string]string{"sha256": "s1"}, + Arch: "a1", }, { - Kind: "package", - Name: "package2", - Version: "v2", - Digest: "s2", - DigestKind: "sha256", - Arch: "a2", + Kind: "package", + Name: "package2", + Version: "v2", + Digests: map[string]string{"sha256": "s2"}, + Arch: "a2", }}, Slices: []*manifest.Slice{{ Kind: "slice", @@ -232,11 +228,10 @@ var generateManifestTests = []struct { }, packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - Digest: "s512", - DigestKind: cache.SHA512, + Name: "package1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA512: "s512"}, }, }, expected: &apachetestutil.ManifestContents{ @@ -247,12 +242,11 @@ var generateManifestTests = []struct { Slices: []string{"package1_slice1"}, }}, Packages: []*manifest.Package{{ - Kind: "package", - Name: "package1", - Version: "v1", - Digest: "s512", - DigestKind: "sha512", - Arch: "a1", + Kind: "package", + Name: "package1", + Version: "v1", + Digests: map[string]string{"sha512": "s512"}, + Arch: "a1", }}, Slices: []*manifest.Slice{{ Kind: "slice", @@ -279,11 +273,10 @@ var generateManifestTests = []struct { }, packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - Digest: "s384", - DigestKind: cache.SHA384, + Name: "package1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA384: "s384"}, }, }, expected: &apachetestutil.ManifestContents{ @@ -294,12 +287,11 @@ var generateManifestTests = []struct { Slices: []string{"package1_slice1"}, }}, Packages: []*manifest.Package{{ - Kind: "package", - Name: "package1", - Version: "v1", - Digest: "s384", - DigestKind: "sha384", - Arch: "a1", + Kind: "package", + Name: "package1", + Version: "v1", + Digests: map[string]string{"sha384": "s384"}, + Arch: "a1", }}, Slices: []*manifest.Slice{{ Kind: "slice", @@ -499,11 +491,10 @@ var generateManifestTests = []struct { }, packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - Digest: "s1", - DigestKind: cache.SHA256, + Name: "package1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA256: "s1"}, }, }, expected: &apachetestutil.ManifestContents{ @@ -527,12 +518,11 @@ var generateManifestTests = []struct { Inode: 1, }}, Packages: []*manifest.Package{{ - Kind: "package", - Name: "package1", - Version: "v1", - Digest: "s1", - DigestKind: "sha256", - Arch: "a1", + Kind: "package", + Name: "package1", + Version: "v1", + Digests: map[string]string{"sha256": "s1"}, + Arch: "a1", }}, Slices: []*manifest.Slice{{ Kind: "slice", @@ -602,10 +592,9 @@ var generateManifestTests = []struct { summary: "Invalid package: missing name", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Version: "v1", - Arch: "a1", - Digest: "s1", - DigestKind: cache.SHA256, + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA256: "s1"}, }, }, error: `internal error: invalid manifest: package name not set`, @@ -613,10 +602,9 @@ var generateManifestTests = []struct { summary: "Invalid package: missing version", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Arch: "a1", - Digest: "s1", - DigestKind: cache.SHA256, + Name: "package-1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA256: "s1"}, }, }, error: `internal error: invalid manifest: package "package-1" missing version`, @@ -624,10 +612,9 @@ var generateManifestTests = []struct { summary: "Invalid package: missing arch", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Version: "v1", - Digest: "s1", - DigestKind: cache.SHA256, + Name: "package-1", + Version: "v1", + Digests: map[cache.DigestKind]string{cache.SHA256: "s1"}, }, }, error: `internal error: invalid manifest: package "package-1" missing arch`, @@ -635,22 +622,20 @@ var generateManifestTests = []struct { summary: "Invalid package: missing digest", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Version: "v1", - Arch: "a1", - DigestKind: cache.SHA256, + Name: "package-1", + Version: "v1", + Arch: "a1", }, }, - error: `internal error: invalid manifest: package "package-1" missing sha256`, + error: `internal error: invalid manifest: package "package-1" missing digest`, }, { summary: "Invalid package: unsupported digest kind", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package-1", - Version: "v1", - Arch: "a1", - Digest: "s1", - DigestKind: cache.DigestKind("md5"), + Name: "package-1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.DigestKind("md5"): "s1"}, }, }, error: `internal error: invalid manifest: package "package-1": unsupported digest kind: "md5"`, @@ -665,11 +650,10 @@ func (s *S) TestGenerateManifests(c *C) { if test.packageInfo == nil { test.packageInfo = []manifestutil.PackageInfo{ &archive.PackageInfo{ - Name: "package1", - Version: "v1", - Arch: "a1", - Digest: "s1", - DigestKind: cache.SHA256, + Name: "package1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA256: "s1"}, }, } } @@ -786,7 +770,6 @@ var validateManifestTests = []struct { {"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","sha512":"hash2","arch":"arch1"} {"kind":"slice","name":"pkg1_myslice"} `, - error: `invalid manifest: cannot read manifest: package "pkg1" has multiple digests recorded`, }, { summary: "Malformed jsonwall", input: ` diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index 57e0b06ff..0ffdc3e3d 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -15,6 +15,7 @@ import ( . "gopkg.in/check.v1" "github.com/canonical/chisel/internal/archive" + "github.com/canonical/chisel/internal/cache" "github.com/canonical/chisel/internal/manifestutil" "github.com/canonical/chisel/internal/setup" "github.com/canonical/chisel/internal/slicer" @@ -859,8 +860,8 @@ var slicerTests = []slicerTest{{ "/other-file": "file 0644 fa0c9cdb {other-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 sha256 h1", - "other-package": "other-package v3 a3 sha256 h3", + "test-package": "test-package v1 a1 sha256=h1", + "other-package": "other-package v3 a3 sha256=h3", }, }, { summary: "Pinned archive bypasses higher priority", @@ -928,7 +929,7 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 fa0c9cdb {test-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v2 a2 sha256 h2", + "test-package": "test-package v2 a2 sha256=h2", }, }, { summary: "Pinned archive does not have the package", @@ -1097,17 +1098,49 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 7a3e00f5 {test-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 sha256 h1", + "test-package": "test-package v1 a1 sha256=h1", }, }, { summary: "Package with sha512 digest is recorded in the manifest", slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ - Name: "test-package", - Hash: "h1", - HashKind: "sha512", - Version: "v1", - Arch: "a1", + Name: "test-package", + Hashes: map[cache.DigestKind]string{cache.SHA512: "h1"}, + Version: "v1", + Arch: "a1", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Reg(0o644, "./file", "from foo"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: + `, + }, + filesystem: map[string]string{ + "/file": "file 0644 7a3e00f5", + }, + manifestPaths: map[string]string{ + "/file": "file 0644 7a3e00f5 {test-package_myslice}", + }, + manifestPkgs: map[string]string{ + "test-package": "test-package v1 a1 sha512=h1", + }, +}, { + summary: "Package with multiple digests is recorded in the manifest", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Hashes: map[cache.DigestKind]string{ + cache.SHA256: "h256", + cache.SHA512: "h512", + }, + Version: "v1", + Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ testutil.Reg(0o644, "./file", "from foo"), }), @@ -1128,7 +1161,7 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 7a3e00f5 {test-package_myslice}", }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 sha512 h1", + "test-package": "test-package v1 a1 sha256=h256,sha512=h512", }, }, { summary: "Multiple slices of same package", @@ -1417,8 +1450,8 @@ var slicerTests = []slicerTest{{ `, }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 sha256 h1", - "other-package": "other-package v2 a2 sha256 h2", + "test-package": "test-package v1 a1 sha256=h1", + "other-package": "other-package v2 a2 sha256=h2", }, }, { summary: "Two packages, only one is selected and recorded", @@ -1453,7 +1486,7 @@ var slicerTests = []slicerTest{{ `, }, manifestPkgs: map[string]string{ - "test-package": "test-package v1 a1 sha256 h1", + "test-package": "test-package v1 a1 sha256=h1", }, }, { summary: "Relative paths are properly trimmed during extraction", @@ -2251,7 +2284,16 @@ func treeDumpManifestPaths(mfest *manifest.Manifest) (map[string]string, error) func dumpManifestPkgs(mfest *manifest.Manifest) (map[string]string, error) { result := map[string]string{} err := mfest.IteratePackages(func(pkg *manifest.Package) error { - result[pkg.Name] = fmt.Sprintf("%s %s %s %s %s", pkg.Name, pkg.Version, pkg.Arch, pkg.DigestKind, pkg.Digest) + kinds := make([]string, 0, len(pkg.Digests)) + for kind := range pkg.Digests { + kinds = append(kinds, kind) + } + sort.Strings(kinds) + digests := make([]string, 0, len(kinds)) + for _, kind := range kinds { + digests = append(digests, kind+"="+pkg.Digests[kind]) + } + result[pkg.Name] = fmt.Sprintf("%s %s %s %s", pkg.Name, pkg.Version, pkg.Arch, strings.Join(digests, ",")) return nil }) if err != nil { diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index 7a26e8636..735159758 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -17,20 +17,20 @@ type TestArchive struct { type TestPackage struct { Name string Version string - Hash string - // HashKind is the digest kind of Hash. When unset, Hash is treated as - // a sha256 digest. - HashKind string + // Hashes holds the digests of the package, keyed by digest kind. When + // unset, a sha256 digest of Hash is used. + Hashes map[cache.DigestKind]string + Hash string Arch string Data []byte Archives []string } -func (p *TestPackage) digestKind() cache.DigestKind { - if p.HashKind == "" { - return cache.SHA256 +func (p *TestPackage) digests() map[cache.DigestKind]string { + if p.Hashes == nil { + return map[cache.DigestKind]string{cache.SHA256: p.Hash} } - return cache.DigestKind(p.HashKind) + return p.Hashes } func (a *TestArchive) Options() *archive.Options { @@ -43,11 +43,10 @@ func (a *TestArchive) Fetch(pkgName string) (io.ReadSeekCloser, *archive.Package return nil, nil, fmt.Errorf("cannot find package %q in archive", pkgName) } info := &archive.PackageInfo{ - Name: pkg.Name, - Version: pkg.Version, - Digest: pkg.Hash, - DigestKind: pkg.digestKind(), - Arch: pkg.Arch, + Name: pkg.Name, + Version: pkg.Version, + Digests: pkg.digests(), + Arch: pkg.Arch, } return ReadSeekNopCloser(bytes.NewReader(pkg.Data)), info, nil } @@ -63,10 +62,9 @@ func (a *TestArchive) Info(pkgName string) (*archive.PackageInfo, error) { return nil, fmt.Errorf("cannot find package %q in archive", pkgName) } return &archive.PackageInfo{ - Name: pkg.Name, - Version: pkg.Version, - Digest: pkg.Hash, - DigestKind: pkg.digestKind(), - Arch: pkg.Arch, + Name: pkg.Name, + Version: pkg.Version, + Digests: pkg.digests(), + Arch: pkg.Arch, }, nil } diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index 0a46b23cc..dd8ce1f3e 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -17,16 +17,14 @@ type Package struct { Kind string Name string Version string - Digest string - // DigestKind is the algorithm used to compute Digest, and is empty when - // no digest is recorded. - DigestKind string - Arch string + // Digests holds the digests of the package, keyed by digest kind + // (e.g. "sha256"). It is empty when no digest is recorded. + Digests map[string]string + Arch string } -// packageJSON is the JSON encoding of a Package, with the digest recorded -// under the field named after its kind. At most one of the digest fields may -// be set. +// packageJSON is the JSON encoding of a Package, with each digest recorded +// under the field named after its kind. type packageJSON struct { Kind string `json:"kind"` Name string `json:"name,omitempty"` @@ -44,19 +42,17 @@ func (p *Package) MarshalJSON() ([]byte, error) { Version: p.Version, Arch: p.Arch, } - switch p.DigestKind { - case "": - if p.Digest != "" { - return nil, fmt.Errorf("cannot marshal package %q: digest set without a digest kind", p.Name) + for kind, digest := range p.Digests { + switch kind { + case "sha256": + pj.SHA256 = digest + case "sha384": + pj.SHA384 = digest + case "sha512": + pj.SHA512 = digest + default: + return nil, fmt.Errorf("cannot marshal package %q: unsupported digest kind %q", p.Name, kind) } - case "sha256": - pj.SHA256 = p.Digest - case "sha384": - pj.SHA384 = p.Digest - case "sha512": - pj.SHA512 = p.Digest - default: - return nil, fmt.Errorf("cannot marshal package %q: unsupported digest kind %q", p.Name, p.DigestKind) } return json.Marshal(pj) } @@ -67,42 +63,24 @@ func (p *Package) UnmarshalJSON(data []byte) error { if err != nil { return err } - digest, kind, err := pj.digest() - if err != nil { - return err - } - *p = Package{ - Kind: pj.Kind, - Name: pj.Name, - Version: pj.Version, - Digest: digest, - DigestKind: kind, - Arch: pj.Arch, - } - return nil -} - -// digest returns the package digest and its kind, as recorded in the wire -// representation. At most one digest field may be set. -func (pj *packageJSON) digest() (digest, kind string, err error) { - set := 0 - for _, entry := range []struct { - kind string - digest string - }{ - {"sha256", pj.SHA256}, - {"sha384", pj.SHA384}, - {"sha512", pj.SHA512}, + digests := make(map[string]string) + for kind, digest := range map[string]string{ + "sha256": pj.SHA256, + "sha384": pj.SHA384, + "sha512": pj.SHA512, } { - if entry.digest != "" { - set++ - digest, kind = entry.digest, entry.kind + if digest != "" { + digests[kind] = digest } } - if set > 1 { - return "", "", fmt.Errorf("package %q has multiple digests recorded", pj.Name) + *p = Package{ + Kind: pj.Kind, + Name: pj.Name, + Version: pj.Version, + Digests: digests, + Arch: pj.Arch, } - return digest, kind, nil + return nil } type Slice struct { diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index f236fc511..9f17d5fde 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -53,8 +53,8 @@ var readManifestTests = []struct { {Kind: "path", Path: "/manifest/manifest.wall", Mode: "0644", Slices: []string{"pkg1_manifest"}, SHA256: "", FinalSHA256: "", Size: 0x0, Link: ""}, }, Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", DigestKind: "sha256", Arch: "arch1"}, - {Kind: "package", Name: "pkg2", Version: "v2", Digest: "hash2", DigestKind: "sha256", Arch: "arch2"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1"}, Arch: "arch1"}, + {Kind: "package", Name: "pkg2", Version: "v2", Digests: map[string]string{"sha256": "hash2"}, Arch: "arch2"}, }, Slices: []*manifest.Slice{ {Kind: "slice", Name: "pkg1_manifest"}, @@ -79,7 +79,7 @@ var readManifestTests = []struct { `, mfest: &apachetestutil.ManifestContents{ Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", DigestKind: "sha512", Arch: "arch1"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha512": "hash1"}, Arch: "arch1"}, }, }, }, { @@ -90,7 +90,7 @@ var readManifestTests = []struct { `, mfest: &apachetestutil.ManifestContents{ Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digest: "hash1", DigestKind: "sha384", Arch: "arch1"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha384": "hash1"}, Arch: "arch1"}, }, }, }, { @@ -99,7 +99,11 @@ var readManifestTests = []struct { {"jsonwall":"1.0","schema":"1.0","count":1} {"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","sha512":"hash2","arch":"arch1"} `, - error: `cannot read manifest: package "pkg1" has multiple digests recorded`, + mfest: &apachetestutil.ManifestContents{ + Packages: []*manifest.Package{ + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1", "sha512": "hash2"}, Arch: "arch1"}, + }, + }, }, { summary: "Unknown schema", input: ` @@ -164,23 +168,21 @@ var marshalPackageTests = []struct { }{{ summary: "SHA256 digest", pkg: &manifest.Package{ - Kind: "package", - Name: "pkg1", - Version: "v1", - Digest: "hash1", - DigestKind: "sha256", - Arch: "arch1", + Kind: "package", + Name: "pkg1", + Version: "v1", + Digests: map[string]string{"sha256": "hash1"}, + Arch: "arch1", }, expected: `{"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","arch":"arch1"}`, }, { summary: "SHA512 digest", pkg: &manifest.Package{ - Kind: "package", - Name: "pkg1", - Version: "v1", - Digest: "hash1", - DigestKind: "sha512", - Arch: "arch1", + Kind: "package", + Name: "pkg1", + Version: "v1", + Digests: map[string]string{"sha512": "hash1"}, + Arch: "arch1", }, expected: `{"kind":"package","name":"pkg1","version":"v1","sha512":"hash1","arch":"arch1"}`, }, { @@ -193,24 +195,23 @@ var marshalPackageTests = []struct { }, expected: `{"kind":"package","name":"pkg1","version":"v1","arch":"arch1"}`, }, { - summary: "Digest set without a digest kind", + summary: "Multiple digests recorded", pkg: &manifest.Package{ Kind: "package", Name: "pkg1", Version: "v1", - Digest: "hash1", + Digests: map[string]string{"sha256": "hash1", "sha512": "hash2"}, Arch: "arch1", }, - error: `json: error calling MarshalJSON for type \*manifest\.Package: cannot marshal package "pkg1": digest set without a digest kind`, + expected: `{"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","sha512":"hash2","arch":"arch1"}`, }, { summary: "Unsupported digest kind", pkg: &manifest.Package{ - Kind: "package", - Name: "pkg1", - Version: "v1", - Digest: "hash1", - DigestKind: "md5", - Arch: "arch1", + Kind: "package", + Name: "pkg1", + Version: "v1", + Digests: map[string]string{"md5": "hash1"}, + Arch: "arch1", }, error: `json: error calling MarshalJSON for type \*manifest\.Package: cannot marshal package "pkg1": unsupported digest kind "md5"`, }} From 5466124efc53300b90eba690f3b097e979e06953 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 10:37:33 +0200 Subject: [PATCH 10/24] fix: order by strength --- public/manifest/manifest.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index dd8ce1f3e..588ff4723 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -30,8 +30,8 @@ type packageJSON struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` SHA256 string `json:"sha256,omitempty"` - SHA384 string `json:"sha384,omitempty"` SHA512 string `json:"sha512,omitempty"` + SHA384 string `json:"sha384,omitempty"` Arch string `json:"arch,omitempty"` } @@ -46,10 +46,10 @@ func (p *Package) MarshalJSON() ([]byte, error) { switch kind { case "sha256": pj.SHA256 = digest - case "sha384": - pj.SHA384 = digest case "sha512": pj.SHA512 = digest + case "sha384": + pj.SHA384 = digest default: return nil, fmt.Errorf("cannot marshal package %q: unsupported digest kind %q", p.Name, kind) } @@ -66,8 +66,8 @@ func (p *Package) UnmarshalJSON(data []byte) error { digests := make(map[string]string) for kind, digest := range map[string]string{ "sha256": pj.SHA256, - "sha384": pj.SHA384, "sha512": pj.SHA512, + "sha384": pj.SHA384, } { if digest != "" { digests[kind] = digest From 7ab32ed3b8518396b4de3b616e3a9275f3a1abaf Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 11:39:08 +0200 Subject: [PATCH 11/24] refactor: simplify getting strongest pkg digest kind --- internal/archive/archive.go | 70 +++++++++++++------------------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 5eb06c016..89b968a2f 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -147,16 +147,13 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro return nil, nil, err } path := section.Get("Filename") - digests := packageDigests(section) - // The strongest published digest is used for verification and caching. - digest, digestKind := strongestDigest(digests) - if digest == "" { - // No digest advertised; fetch unverified and cache the package under - // its computed SHA256 digest. - digestKind = cache.SHA256 + digests, strongestKind, ok := packageDigests(section) + if !ok { + return nil, nil, fmt.Errorf("cannot find digest for package %q", pkg) } + digest := digests[strongestKind] logf("Fetching %s...", path) - reader, err := index.fetch(path, digest, digestKind, fetchBulk) + reader, err := index.fetch(path, digest, strongestKind, fetchBulk) if err != nil { return nil, nil, err } @@ -169,8 +166,8 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) { if err != nil { return nil, err } - digests := packageDigests(section) - if len(digests) == 0 { + digests, _, ok := packageDigests(section) + if !ok { return nil, fmt.Errorf("cannot find digest for package %q", pkg) } info := sectionPackageInfo(section, digests) @@ -357,15 +354,17 @@ type digestField struct { kind cache.DigestKind } -// digestFields lists the checksum fields Chisel can verify, in order of -// preference: strongest first. +// digestFields lists the checksum fields Chisel looks up in archive index +// and package files, in order of preference: strongest first. Only kinds +// Ubuntu archives publish are listed; digest kinds weaker than SHA256 +// (e.g. MD5) are not looked up. var digestFields = []digestField{ {"SHA512", cache.SHA512}, {"SHA256", cache.SHA256}, } -func findDigest(release control.Section, path string, order []digestField) (digest string, field digestField, ok bool) { - for _, f := range order { +func findDigest(release control.Section, path string) (digest string, field digestField, ok bool) { + for _, f := range digestFields { if d, _, found := control.ParsePathInfo(release.Get(f.name), path); found { return d, f, true } @@ -373,43 +372,24 @@ func findDigest(release control.Section, path string, order []digestField) (dige return "", digestField{}, false } -// packageDigestFields lists the checksum fields Chisel looks up in a package -// section, in order of preference: strongest first. The first field found is -// used for verification and caching; all fields found are recorded in the -// manifest. Only kinds Ubuntu archives publish are listed; digest kinds -// weaker than SHA256 (e.g. MD5) and kinds Chisel supports for other sources -// (e.g. SHA384, used by stores) are not looked up here. -var packageDigestFields = []digestField{ - {"SHA512", cache.SHA512}, - {"SHA256", cache.SHA256}, -} - -// packageDigests returns every digest the section advertises, keyed by -// digest kind, following packageDigestFields. -func packageDigests(section control.Section) map[cache.DigestKind]string { - digests := make(map[cache.DigestKind]string) - for _, f := range packageDigestFields { +func packageDigests(section control.Section) (all map[cache.DigestKind]string, strongest cache.DigestKind, ok bool) { + all = make(map[cache.DigestKind]string) + for _, f := range digestFields { if d := section.Get(f.name); d != "" { - digests[f.kind] = d - } - } - return digests -} - -// strongestDigest returns the strongest digest in digests, following -// packageDigestFields, or the zero values when digests is empty. -func strongestDigest(digests map[cache.DigestKind]string) (digest string, kind cache.DigestKind) { - for _, f := range packageDigestFields { - if d, ok := digests[f.kind]; ok { - return d, f.kind + all[f.kind] = d + if !ok { + // digestFields is ordered strongest first, so the first + // digest found is the strongest. + strongest, ok = f.kind, true + } } } - return "", "" + return all, strongest, ok } func (index *ubuntuIndex) fetchIndex() error { packagesPath := fmt.Sprintf("%s/binary-%s/Packages", index.component, index.arch) - packagesDigest, field, ok := findDigest(index.release, packagesPath, digestFields) + packagesDigest, field, ok := findDigest(index.release, packagesPath) if !ok { return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component) } @@ -427,7 +407,7 @@ func (index *ubuntuIndex) fetchIndex() error { // hash the archive advertises, which is what findDigest prefers. If // the archive advertises a hash stronger than any Chisel knows, the // URL may 404 and the named-path fallback below applies. - packagesGzDigest, byHashField, ok := findDigest(index.release, packagesGzPath, digestFields) + packagesGzDigest, byHashField, ok := findDigest(index.release, packagesGzPath) if ok { packagesByHashPath := fmt.Sprintf("%s/binary-%s/by-hash/%s/%s", index.component, index.arch, byHashField.name, packagesGzDigest) r, err := index.fetch(index.distPath(packagesByHashPath), packagesDigest, field.kind, fetchBulk|fetchGzip) From ca4f18dffb8b9bd407dd5c02156714013bbaeb97 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 12:03:16 +0200 Subject: [PATCH 12/24] tests: cleaning --- internal/slicer/slicer_test.go | 30 +++++++++++++++--------------- internal/testutil/archive.go | 15 +++------------ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index 0ffdc3e3d..ceff0fcbd 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -784,25 +784,25 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}, {"other-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hash: "h1", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ - testutil.Reg(0644, "./file", "from foo"), + testutil.Reg(0o644, "./file", "from foo"), }), Archives: []string{"foo"}, }, { Name: "test-package", - Hash: "h2", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.MustMakeDeb([]testutil.TarEntry{ - testutil.Reg(0644, "./file", "from bar"), + testutil.Reg(0o644, "./file", "from bar"), }), Archives: []string{"bar"}, }, { Name: "other-package", - Hash: "h3", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h3"}, Version: "v3", Arch: "a3", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -868,16 +868,16 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hash: "h1", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ - testutil.Reg(0644, "./file", "from foo"), + testutil.Reg(0o644, "./file", "from foo"), }), Archives: []string{"foo"}, }, { Name: "test-package", - Hash: "h2", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -1056,7 +1056,7 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hash: "h1", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -1424,13 +1424,13 @@ var slicerTests = []slicerTest{{ }, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hash: "h1", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.PackageData["test-package"], }, { Name: "other-package", - Hash: "h2", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.PackageData["other-package"], @@ -1460,13 +1460,13 @@ var slicerTests = []slicerTest{{ }, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hash: "h1", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.PackageData["test-package"], }, { Name: "other-package", - Hash: "h2", + Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.PackageData["other-package"], @@ -2118,8 +2118,8 @@ func runSlicerTests(s *S, c *C, tests []slicerTest) { if pkg.Arch == "" { pkg.Arch = "arch" } - if pkg.Hash == "" { - pkg.Hash = "hash" + if pkg.Hashes == nil { + pkg.Hashes = map[cache.DigestKind]string{cache.SHA256: "hash"} } if pkg.Version == "" { pkg.Version = "version" diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index 735159758..03b57e39f 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -17,22 +17,13 @@ type TestArchive struct { type TestPackage struct { Name string Version string - // Hashes holds the digests of the package, keyed by digest kind. When - // unset, a sha256 digest of Hash is used. + // Hashes holds the digests of the package, keyed by digest kind. Hashes map[cache.DigestKind]string - Hash string Arch string Data []byte Archives []string } -func (p *TestPackage) digests() map[cache.DigestKind]string { - if p.Hashes == nil { - return map[cache.DigestKind]string{cache.SHA256: p.Hash} - } - return p.Hashes -} - func (a *TestArchive) Options() *archive.Options { return &a.Opts } @@ -45,7 +36,7 @@ func (a *TestArchive) Fetch(pkgName string) (io.ReadSeekCloser, *archive.Package info := &archive.PackageInfo{ Name: pkg.Name, Version: pkg.Version, - Digests: pkg.digests(), + Digests: pkg.Hashes, Arch: pkg.Arch, } return ReadSeekNopCloser(bytes.NewReader(pkg.Data)), info, nil @@ -64,7 +55,7 @@ func (a *TestArchive) Info(pkgName string) (*archive.PackageInfo, error) { return &archive.PackageInfo{ Name: pkg.Name, Version: pkg.Version, - Digests: pkg.digests(), + Digests: pkg.Hashes, Arch: pkg.Arch, }, nil } From 104f08928b5f92596e83fba01d1bf39f4f79b41b Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 12:05:48 +0200 Subject: [PATCH 13/24] docs: refine comments --- internal/cache/cache.go | 4 ++-- public/manifest/manifest.go | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 5fbacba63..7d721efc3 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -101,8 +101,8 @@ const ( SHA384 DigestKind = "sha384" ) -// digestKinds lists the digest kinds Chisel supports, in order of strength: -// strongest first. +// digestKinds lists the digest kinds the cache supports, in order of +// strength: strongest first. var digestKinds = []DigestKind{SHA384, SHA512, SHA256} // ValidateKind returns an error unless kind is a digest kind Chisel diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index 588ff4723..71e228132 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -12,19 +12,14 @@ import ( const Schema = "1.0" -// Package describes a package installed in the target filesystem. type Package struct { Kind string Name string Version string - // Digests holds the digests of the package, keyed by digest kind - // (e.g. "sha256"). It is empty when no digest is recorded. Digests map[string]string Arch string } -// packageJSON is the JSON encoding of a Package, with each digest recorded -// under the field named after its kind. type packageJSON struct { Kind string `json:"kind"` Name string `json:"name,omitempty"` From 8009bb02b00ea71a94e4b4fb581ae142dbe2e1e8 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 13:39:26 +0200 Subject: [PATCH 14/24] tests: rename Hashes field to fit usage --- internal/slicer/slicer_test.go | 28 ++++++++++++++-------------- internal/testutil/archive.go | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index ceff0fcbd..d4b226aa0 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -784,7 +784,7 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}, {"other-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -793,7 +793,7 @@ var slicerTests = []slicerTest{{ Archives: []string{"foo"}, }, { Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -802,7 +802,7 @@ var slicerTests = []slicerTest{{ Archives: []string{"bar"}, }, { Name: "other-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h3"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h3"}, Version: "v3", Arch: "a3", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -868,7 +868,7 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -877,7 +877,7 @@ var slicerTests = []slicerTest{{ Archives: []string{"foo"}, }, { Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -1056,7 +1056,7 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -1105,7 +1105,7 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA512: "h1"}, + Digests: map[cache.DigestKind]string{cache.SHA512: "h1"}, Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ @@ -1135,7 +1135,7 @@ var slicerTests = []slicerTest{{ slices: []setup.SliceKey{{"test-package", "myslice"}}, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{ + Digests: map[cache.DigestKind]string{ cache.SHA256: "h256", cache.SHA512: "h512", }, @@ -1424,13 +1424,13 @@ var slicerTests = []slicerTest{{ }, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.PackageData["test-package"], }, { Name: "other-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.PackageData["other-package"], @@ -1460,13 +1460,13 @@ var slicerTests = []slicerTest{{ }, pkgs: []*testutil.TestPackage{{ Name: "test-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h1"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h1"}, Version: "v1", Arch: "a1", Data: testutil.PackageData["test-package"], }, { Name: "other-package", - Hashes: map[cache.DigestKind]string{cache.SHA256: "h2"}, + Digests: map[cache.DigestKind]string{cache.SHA256: "h2"}, Version: "v2", Arch: "a2", Data: testutil.PackageData["other-package"], @@ -2118,8 +2118,8 @@ func runSlicerTests(s *S, c *C, tests []slicerTest) { if pkg.Arch == "" { pkg.Arch = "arch" } - if pkg.Hashes == nil { - pkg.Hashes = map[cache.DigestKind]string{cache.SHA256: "hash"} + if pkg.Digests == nil { + pkg.Digests = map[cache.DigestKind]string{cache.SHA256: "hash"} } if pkg.Version == "" { pkg.Version = "version" diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index 03b57e39f..eed682d8c 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -17,8 +17,8 @@ type TestArchive struct { type TestPackage struct { Name string Version string - // Hashes holds the digests of the package, keyed by digest kind. - Hashes map[cache.DigestKind]string + // Digests holds the digests of the package, keyed by digest kind. + Digests map[cache.DigestKind]string Arch string Data []byte Archives []string @@ -36,7 +36,7 @@ func (a *TestArchive) Fetch(pkgName string) (io.ReadSeekCloser, *archive.Package info := &archive.PackageInfo{ Name: pkg.Name, Version: pkg.Version, - Digests: pkg.Hashes, + Digests: pkg.Digests, Arch: pkg.Arch, } return ReadSeekNopCloser(bytes.NewReader(pkg.Data)), info, nil @@ -55,7 +55,7 @@ func (a *TestArchive) Info(pkgName string) (*archive.PackageInfo, error) { return &archive.PackageInfo{ Name: pkg.Name, Version: pkg.Version, - Digests: pkg.Hashes, + Digests: pkg.Digests, Arch: pkg.Arch, }, nil } From d0d2b9cb9061238128244aa54843fee8ebe2b924 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 13:40:30 +0200 Subject: [PATCH 15/24] docs: remove useless comment --- internal/testutil/archive.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index eed682d8c..a881f314b 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -17,7 +17,6 @@ type TestArchive struct { type TestPackage struct { Name string Version string - // Digests holds the digests of the package, keyed by digest kind. Digests map[cache.DigestKind]string Arch string Data []byte From 02919e55db1c32c3198461e113c9a5ca52d04574 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 13:58:27 +0200 Subject: [PATCH 16/24] style: improve naming and docs --- internal/archive/archive.go | 5 ++--- internal/archive/archive_test.go | 8 ++++---- internal/cache/cache.go | 4 +--- internal/manifestutil/manifestutil.go | 4 ++-- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 89b968a2f..97c5c5644 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -355,9 +355,8 @@ type digestField struct { } // digestFields lists the checksum fields Chisel looks up in archive index -// and package files, in order of preference: strongest first. Only kinds -// Ubuntu archives publish are listed; digest kinds weaker than SHA256 -// (e.g. MD5) are not looked up. +// and package files, in order of preference: strongest first. Digest kinds +// weaker than SHA256 (e.g. MD5) are not looked up. var digestFields = []digestField{ {"SHA512", cache.SHA512}, {"SHA256", cache.SHA256}, diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index bcd4d93a0..cb721f989 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -274,12 +274,12 @@ func (s *httpSuite) TestFetchPackage(c *C) { func (s *httpSuite) TestFetchSHA512Digests(c *C) { // Ubuntu 26.10+ publishes SHA512-only indices (no SHA256 section), so both // the index digest and the package digest must be read from SHA512. - s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, + s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main", "universe"}, []string{"SHA512"}, nil) options := archive.Options{ Label: "ubuntu", - Version: "25.10", + Version: "26.10", Arch: "amd64", Suites: []string{"stonking"}, Components: []string{"main", "universe"}, @@ -305,12 +305,12 @@ func (s *httpSuite) TestFetchBothDigests(c *C) { // An archive publishing both SHA256 and SHA512 sections (index table and // package fields) must be handled. All published digests are recorded in // the manifest; the strongest one is used for verification and caching. - s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"}, + s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main", "universe"}, []string{"SHA256", "SHA512"}, nil) options := archive.Options{ Label: "ubuntu", - Version: "25.10", + Version: "26.10", Arch: "amd64", Suites: []string{"stonking"}, Components: []string{"main", "universe"}, diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 7d721efc3..10a2e3a0f 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -105,9 +105,7 @@ const ( // strength: strongest first. var digestKinds = []DigestKind{SHA384, SHA512, SHA256} -// ValidateKind returns an error unless kind is a digest kind Chisel -// supports. -func ValidateKind(kind DigestKind) error { +func ValidateDigestKind(kind DigestKind) error { if !slices.Contains(digestKinds, kind) { return fmt.Errorf("unsupported digest kind: %q", kind) } diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 1fd548a4c..47f03733a 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -280,7 +280,7 @@ func validatePackage(pkg PackageInfo) (err error) { return fmt.Errorf("package %q missing digest", name) } for kind := range digests { - err = cache.ValidateKind(kind) + err = cache.ValidateDigestKind(kind) if err != nil { return fmt.Errorf("package %q: %s", name, err) } @@ -314,7 +314,7 @@ func Validate(mfest *manifest.Manifest) (err error) { return fmt.Errorf("package %q missing digest", name) } for kind := range pkg.Digests { - if err := cache.ValidateKind(cache.DigestKind(kind)); err != nil { + if err := cache.ValidateDigestKind(cache.DigestKind(kind)); err != nil { return fmt.Errorf("package %q: %s", name, err) } } From ea1734ad602747098f4457aa69491385ae6346ef Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 14:29:52 +0200 Subject: [PATCH 17/24] fix: make sure reported digest values are not empty --- internal/archive/archive_test.go | 6 +++--- internal/manifestutil/manifestutil.go | 9 ++++++--- internal/manifestutil/manifestutil_test.go | 19 +++++++++++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index cb721f989..f30303ea7 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -301,8 +301,8 @@ func (s *httpSuite) TestFetchSHA512Digests(c *C) { c.Assert(read(pkg), Equals, "mypkg1 1.1 data") } -func (s *httpSuite) TestFetchBothDigests(c *C) { - // An archive publishing both SHA256 and SHA512 sections (index table and +func (s *httpSuite) TestFetchMultipleDigests(c *C) { + // An archive publishing SHA256 and SHA512 sections (index table and // package fields) must be handled. All published digests are recorded in // the manifest; the strongest one is used for verification and caching. s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main", "universe"}, @@ -334,7 +334,7 @@ func (s *httpSuite) TestFetchBothDigests(c *C) { }) c.Assert(read(pkg), Equals, "mypkg1 1.1 data") - // Pin the cache key: with both digests advertised, the package is cached + // Pin the cache key: with multiple digests advertised, the package is cached // under its strongest digest. _, err = os.Stat(filepath.Join(options.CacheDir, "sha512", "27c6e88def3d3848f4a068040bddbf908ab90e33bf93fc24fd02af7ed6a1953151302f2c59306313f065163143b51f1000cd22d102b7a58d7efd6430f5e162fb")) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 47f03733a..b664cdbcb 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -277,13 +277,16 @@ func validatePackage(pkg PackageInfo) (err error) { } digests := pkg.PkgDigests() if len(digests) == 0 { - return fmt.Errorf("package %q missing digest", name) + return fmt.Errorf("package %q missing digests", name) } - for kind := range digests { + for kind, digest := range digests { err = cache.ValidateDigestKind(kind) if err != nil { return fmt.Errorf("package %q: %s", name, err) } + if digest == "" { + return fmt.Errorf("package %q has empty %s digest", name, kind) + } } if pkg.PkgVersion() == "" { return fmt.Errorf("package %q missing version", name) @@ -311,7 +314,7 @@ func Validate(mfest *manifest.Manifest) (err error) { return fmt.Errorf("package %q missing arch", name) } if len(pkg.Digests) == 0 { - return fmt.Errorf("package %q missing digest", name) + return fmt.Errorf("package %q missing digests", name) } for kind := range pkg.Digests { if err := cache.ValidateDigestKind(cache.DigestKind(kind)); err != nil { diff --git a/internal/manifestutil/manifestutil_test.go b/internal/manifestutil/manifestutil_test.go index 18bd46543..0e61cf054 100644 --- a/internal/manifestutil/manifestutil_test.go +++ b/internal/manifestutil/manifestutil_test.go @@ -619,7 +619,7 @@ var generateManifestTests = []struct { }, error: `internal error: invalid manifest: package "package-1" missing arch`, }, { - summary: "Invalid package: missing digest", + summary: "Invalid package: missing digests", packageInfo: []manifestutil.PackageInfo{ &archive.PackageInfo{ Name: "package-1", @@ -627,7 +627,7 @@ var generateManifestTests = []struct { Arch: "a1", }, }, - error: `internal error: invalid manifest: package "package-1" missing digest`, + error: `internal error: invalid manifest: package "package-1" missing digests`, }, { summary: "Invalid package: unsupported digest kind", packageInfo: []manifestutil.PackageInfo{ @@ -639,6 +639,17 @@ var generateManifestTests = []struct { }, }, error: `internal error: invalid manifest: package "package-1": unsupported digest kind: "md5"`, +}, { + summary: "Invalid package: empty digest", + packageInfo: []manifestutil.PackageInfo{ + &archive.PackageInfo{ + Name: "package-1", + Version: "v1", + Arch: "a1", + Digests: map[cache.DigestKind]string{cache.SHA256: ""}, + }, + }, + error: `internal error: invalid manifest: package "package-1" has empty sha256 digest`, }} func (s *S) TestGenerateManifests(c *C) { @@ -756,13 +767,13 @@ var validateManifestTests = []struct { {"kind":"slice","name":"pkg1_myslice"} `, }, { - summary: "Package with missing digest", + summary: "Package with missing digests", input: ` {"jsonwall":"1.0","schema":"1.0","count":2} {"kind":"package","name":"pkg1","version":"v1","arch":"arch1"} {"kind":"slice","name":"pkg1_myslice"} `, - error: `invalid manifest: package "pkg1" missing digest`, + error: `invalid manifest: package "pkg1" missing digests`, }, { summary: "Package with multiple digests", input: ` From f7011a8b40aa2ffba29d15786667d6c5b59b22d2 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 14:30:55 +0200 Subject: [PATCH 18/24] style: linting --- internal/testutil/archive.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index a881f314b..7c6e57c4c 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -15,8 +15,8 @@ type TestArchive struct { } type TestPackage struct { - Name string - Version string + Name string + Version string Digests map[cache.DigestKind]string Arch string Data []byte From 6c039485e7e9b169e25d2ac085a04b7170687800 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 17 Sep 2026 14:53:49 +0200 Subject: [PATCH 19/24] fix: revert unrelated change in behavior --- internal/manifestutil/manifestutil.go | 20 +------------------- internal/manifestutil/manifestutil_test.go | 8 -------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index b664cdbcb..5544b4398 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -306,25 +306,7 @@ func Validate(mfest *manifest.Manifest) (err error) { pkgExist := map[string]bool{} err = mfest.IteratePackages(func(pkg *manifest.Package) error { - name := pkg.Name - if name == "" { - return fmt.Errorf("package name not set") - } - if pkg.Arch == "" { - return fmt.Errorf("package %q missing arch", name) - } - if len(pkg.Digests) == 0 { - return fmt.Errorf("package %q missing digests", name) - } - for kind := range pkg.Digests { - if err := cache.ValidateDigestKind(cache.DigestKind(kind)); err != nil { - return fmt.Errorf("package %q: %s", name, err) - } - } - if pkg.Version == "" { - return fmt.Errorf("package %q missing version", name) - } - pkgExist[name] = true + pkgExist[pkg.Name] = true return nil }) if err != nil { diff --git a/internal/manifestutil/manifestutil_test.go b/internal/manifestutil/manifestutil_test.go index 0e61cf054..bab518922 100644 --- a/internal/manifestutil/manifestutil_test.go +++ b/internal/manifestutil/manifestutil_test.go @@ -766,14 +766,6 @@ var validateManifestTests = []struct { {"kind":"package","name":"pkg1","version":"v1","sha384":"hash1","arch":"arch1"} {"kind":"slice","name":"pkg1_myslice"} `, -}, { - summary: "Package with missing digests", - input: ` - {"jsonwall":"1.0","schema":"1.0","count":2} - {"kind":"package","name":"pkg1","version":"v1","arch":"arch1"} - {"kind":"slice","name":"pkg1_myslice"} - `, - error: `invalid manifest: package "pkg1" missing digests`, }, { summary: "Package with multiple digests", input: ` From e53e543fa48ae17388d93757aae46942d7213b5c Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 22 Sep 2026 15:18:55 +0200 Subject: [PATCH 20/24] fix: keep Digest field to avoid breaking change --- internal/manifestutil/manifestutil_test.go | 4 ++++ public/manifest/manifest.go | 8 ++++++++ public/manifest/manifest_test.go | 8 ++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/manifestutil/manifestutil_test.go b/internal/manifestutil/manifestutil_test.go index bab518922..19eaaf43b 100644 --- a/internal/manifestutil/manifestutil_test.go +++ b/internal/manifestutil/manifestutil_test.go @@ -183,12 +183,14 @@ var generateManifestTests = []struct { Kind: "package", Name: "package1", Version: "v1", + Digest: "s1", Digests: map[string]string{"sha256": "s1"}, Arch: "a1", }, { Kind: "package", Name: "package2", Version: "v2", + Digest: "s2", Digests: map[string]string{"sha256": "s2"}, Arch: "a2", }}, @@ -245,6 +247,7 @@ var generateManifestTests = []struct { Kind: "package", Name: "package1", Version: "v1", + Digest: "s512", Digests: map[string]string{"sha512": "s512"}, Arch: "a1", }}, @@ -521,6 +524,7 @@ var generateManifestTests = []struct { Kind: "package", Name: "package1", Version: "v1", + Digest: "s1", Digests: map[string]string{"sha256": "s1"}, Arch: "a1", }}, diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index 71e228132..d0b88b6b5 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -16,6 +16,9 @@ type Package struct { Kind string Name string Version string + // Digest holds the sha256 digest when present, and the sha512 digest + // otherwise. It is empty when neither is recorded. + Digest string Digests map[string]string Arch string } @@ -68,10 +71,15 @@ func (p *Package) UnmarshalJSON(data []byte) error { digests[kind] = digest } } + digest := pj.SHA256 + if digest == "" { + digest = pj.SHA512 + } *p = Package{ Kind: pj.Kind, Name: pj.Name, Version: pj.Version, + Digest: digest, Digests: digests, Arch: pj.Arch, } diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index 9f17d5fde..5e85d9c6a 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -53,8 +53,8 @@ var readManifestTests = []struct { {Kind: "path", Path: "/manifest/manifest.wall", Mode: "0644", Slices: []string{"pkg1_manifest"}, SHA256: "", FinalSHA256: "", Size: 0x0, Link: ""}, }, Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1"}, Arch: "arch1"}, - {Kind: "package", Name: "pkg2", Version: "v2", Digests: map[string]string{"sha256": "hash2"}, Arch: "arch2"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1"}, Digest: "hash1", Arch: "arch1"}, + {Kind: "package", Name: "pkg2", Version: "v2", Digests: map[string]string{"sha256": "hash2"}, Digest: "hash2", Arch: "arch2"}, }, Slices: []*manifest.Slice{ {Kind: "slice", Name: "pkg1_manifest"}, @@ -79,7 +79,7 @@ var readManifestTests = []struct { `, mfest: &apachetestutil.ManifestContents{ Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha512": "hash1"}, Arch: "arch1"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha512": "hash1"}, Digest: "hash1", Arch: "arch1"}, }, }, }, { @@ -101,7 +101,7 @@ var readManifestTests = []struct { `, mfest: &apachetestutil.ManifestContents{ Packages: []*manifest.Package{ - {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1", "sha512": "hash2"}, Arch: "arch1"}, + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1", "sha512": "hash2"}, Digest: "hash1", Arch: "arch1"}, }, }, }, { From a73fbdcc3bba0143bd497b2481aabb346caba2a7 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 22 Sep 2026 17:23:54 +0200 Subject: [PATCH 21/24] feat: add cleaner package creation --- internal/manifestutil/manifestutil.go | 6 ++--- public/manifest/manifest.go | 39 ++++++++++++++++++++------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 5544b4398..932425646 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -88,13 +88,13 @@ func manifestAddPackages(dbw *jsonwall.DBWriter, infos []PackageInfo) error { for kind, digest := range info.PkgDigests() { digests[string(kind)] = digest } - err := dbw.Add(&manifest.Package{ - Kind: "package", + pkg := manifest.NewPackage(&manifest.PackageOptions{ Name: info.PkgName(), Version: info.PkgVersion(), - Digests: digests, Arch: info.PkgArch(), + Digests: digests, }) + err := dbw.Add(pkg) if err != nil { return err } diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index d0b88b6b5..fe6a225c0 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "maps" "github.com/canonical/chisel/public/jsonwall" ) @@ -19,6 +20,8 @@ type Package struct { // Digest holds the sha256 digest when present, and the sha512 digest // otherwise. It is empty when neither is recorded. Digest string + // Digests holds the digests of the package, keyed by digest kind + // (e.g. "sha256"). Digests map[string]string Arch string } @@ -33,6 +36,30 @@ type packageJSON struct { Arch string `json:"arch,omitempty"` } +type PackageOptions struct { + Name string + Version string + Arch string + Digests map[string]string +} + +func NewPackage(opts *PackageOptions) *Package { + digests := make(map[string]string, len(opts.Digests)) + maps.Copy(digests, opts.Digests) + digest := digests["sha256"] + if digest == "" { + digest = digests["sha512"] + } + return &Package{ + Kind: "package", + Name: opts.Name, + Version: opts.Version, + Digest: digest, + Digests: digests, + Arch: opts.Arch, + } +} + func (p *Package) MarshalJSON() ([]byte, error) { pj := packageJSON{ Kind: p.Kind, @@ -71,18 +98,12 @@ func (p *Package) UnmarshalJSON(data []byte) error { digests[kind] = digest } } - digest := pj.SHA256 - if digest == "" { - digest = pj.SHA512 - } - *p = Package{ - Kind: pj.Kind, + *p = *NewPackage(&PackageOptions{ Name: pj.Name, Version: pj.Version, - Digest: digest, - Digests: digests, Arch: pj.Arch, - } + Digests: digests, + }) return nil } From 811c9922f4bbac11880b00e5b3afe5b4e535923a Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 23 Sep 2026 10:33:21 +0200 Subject: [PATCH 22/24] refactor: rework NewPackage --- internal/manifestutil/manifestutil.go | 7 +++-- public/manifest/manifest.go | 43 ++++++++++++++++++++------- public/manifest/manifest_test.go | 17 +++++++++-- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/internal/manifestutil/manifestutil.go b/internal/manifestutil/manifestutil.go index 932425646..b6ede40f5 100644 --- a/internal/manifestutil/manifestutil.go +++ b/internal/manifestutil/manifestutil.go @@ -88,13 +88,16 @@ func manifestAddPackages(dbw *jsonwall.DBWriter, infos []PackageInfo) error { for kind, digest := range info.PkgDigests() { digests[string(kind)] = digest } - pkg := manifest.NewPackage(&manifest.PackageOptions{ + pkg, err := manifest.NewPackage(&manifest.PackageOptions{ Name: info.PkgName(), Version: info.PkgVersion(), Arch: info.PkgArch(), Digests: digests, }) - err := dbw.Add(pkg) + if err != nil { + return err + } + err = dbw.Add(pkg) if err != nil { return err } diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index fe6a225c0..e14283292 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "maps" "github.com/canonical/chisel/public/jsonwall" ) @@ -43,21 +42,39 @@ type PackageOptions struct { Digests map[string]string } -func NewPackage(opts *PackageOptions) *Package { - digests := make(map[string]string, len(opts.Digests)) - maps.Copy(digests, opts.Digests) - digest := digests["sha256"] +func NewPackage(opts *PackageOptions) (*Package, error) { + o, err := getValidOptions(opts) + if err != nil { + return nil, err + } + digest := o.Digests["sha256"] if digest == "" { - digest = digests["sha512"] + digest = o.Digests["sha512"] } return &Package{ Kind: "package", - Name: opts.Name, - Version: opts.Version, + Name: o.Name, + Version: o.Version, Digest: digest, - Digests: digests, - Arch: opts.Arch, + Digests: o.Digests, + Arch: o.Arch, + }, nil +} + +func getValidOptions(options *PackageOptions) (*PackageOptions, error) { + optsCopy := *options + o := &optsCopy + digests := make(map[string]string, len(options.Digests)) + for kind, digest := range options.Digests { + switch kind { + case "sha256", "sha512", "sha384": + digests[kind] = digest + default: + return nil, fmt.Errorf("cannot create package %q: unsupported digest kind %q", options.Name, kind) + } } + o.Digests = digests + return o, nil } func (p *Package) MarshalJSON() ([]byte, error) { @@ -98,12 +115,16 @@ func (p *Package) UnmarshalJSON(data []byte) error { digests[kind] = digest } } - *p = *NewPackage(&PackageOptions{ + pkg, err := NewPackage(&PackageOptions{ Name: pj.Name, Version: pj.Version, Arch: pj.Arch, Digests: digests, }) + if err != nil { + return err + } + *p = *pkg return nil } diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index 5e85d9c6a..d6c3f1f1d 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -104,6 +104,17 @@ var readManifestTests = []struct { {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1", "sha512": "hash2"}, Digest: "hash1", Arch: "arch1"}, }, }, +}, { + summary: "Uppercase digest kind fields", + input: ` + {"jsonwall":"1.0","schema":"1.0","count":1} + {"kind":"package","name":"pkg1","version":"v1","SHA256":"hash1","SHA512":"hash2","arch":"arch1"} + `, + mfest: &apachetestutil.ManifestContents{ + Packages: []*manifest.Package{ + {Kind: "package", Name: "pkg1", Version: "v1", Digests: map[string]string{"sha256": "hash1", "sha512": "hash2"}, Digest: "hash1", Arch: "arch1"}, + }, + }, }, { summary: "Unknown schema", input: ` @@ -205,15 +216,15 @@ var marshalPackageTests = []struct { }, expected: `{"kind":"package","name":"pkg1","version":"v1","sha256":"hash1","sha512":"hash2","arch":"arch1"}`, }, { - summary: "Unsupported digest kind", + summary: "Invalid digest kind", pkg: &manifest.Package{ Kind: "package", Name: "pkg1", Version: "v1", - Digests: map[string]string{"md5": "hash1"}, + Digests: map[string]string{"invalid": "hash1"}, Arch: "arch1", }, - error: `json: error calling MarshalJSON for type \*manifest\.Package: cannot marshal package "pkg1": unsupported digest kind "md5"`, + error: `json: error calling MarshalJSON for type \*manifest\.Package: cannot marshal package "pkg1": unsupported digest kind "invalid"`, }} func (s *S) TestMarshalPackage(c *C) { From 42eed4b7f3d0b1da662a65fc359ed5a72e754948 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 23 Sep 2026 11:19:11 +0200 Subject: [PATCH 23/24] style: clean docs and tests --- internal/cache/cache.go | 3 +-- internal/slicer/slicer_test.go | 18 +++++++----------- public/manifest/manifest_test.go | 7 ------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 10a2e3a0f..1450c316c 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -101,8 +101,7 @@ const ( SHA384 DigestKind = "sha384" ) -// digestKinds lists the digest kinds the cache supports, in order of -// strength: strongest first. +// digestKinds sorted in decreasing order of strength. var digestKinds = []DigestKind{SHA384, SHA512, SHA256} func ValidateDigestKind(kind DigestKind) error { diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index d4b226aa0..e939719c6 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -788,7 +788,7 @@ var slicerTests = []slicerTest{{ Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ - testutil.Reg(0o644, "./file", "from foo"), + testutil.Reg(0644, "./file", "from foo"), }), Archives: []string{"foo"}, }, { @@ -797,7 +797,7 @@ var slicerTests = []slicerTest{{ Version: "v2", Arch: "a2", Data: testutil.MustMakeDeb([]testutil.TarEntry{ - testutil.Reg(0o644, "./file", "from bar"), + testutil.Reg(0644, "./file", "from bar"), }), Archives: []string{"bar"}, }, { @@ -872,7 +872,7 @@ var slicerTests = []slicerTest{{ Version: "v1", Arch: "a1", Data: testutil.MustMakeDeb([]testutil.TarEntry{ - testutil.Reg(0o644, "./file", "from foo"), + testutil.Reg(0644, "./file", "from foo"), }), Archives: []string{"foo"}, }, { @@ -2284,15 +2284,11 @@ func treeDumpManifestPaths(mfest *manifest.Manifest) (map[string]string, error) func dumpManifestPkgs(mfest *manifest.Manifest) (map[string]string, error) { result := map[string]string{} err := mfest.IteratePackages(func(pkg *manifest.Package) error { - kinds := make([]string, 0, len(pkg.Digests)) - for kind := range pkg.Digests { - kinds = append(kinds, kind) - } - sort.Strings(kinds) - digests := make([]string, 0, len(kinds)) - for _, kind := range kinds { - digests = append(digests, kind+"="+pkg.Digests[kind]) + digests := make([]string, 0, len(pkg.Digests)) + for kind, digest := range pkg.Digests { + digests = append(digests, kind+"="+digest) } + sort.Strings(digests) result[pkg.Name] = fmt.Sprintf("%s %s %s %s", pkg.Name, pkg.Version, pkg.Arch, strings.Join(digests, ",")) return nil }) diff --git a/public/manifest/manifest_test.go b/public/manifest/manifest_test.go index d6c3f1f1d..831a8a887 100644 --- a/public/manifest/manifest_test.go +++ b/public/manifest/manifest_test.go @@ -153,13 +153,6 @@ func (s *S) TestManifestRead(c *C) { defer r.Close() mfest, err := manifest.Read(r) - if err == nil { - // Entry-level errors surface while iterating, as the manifest - // is not fully decoded on read. - err = mfest.IteratePackages(func(pkg *manifest.Package) error { - return nil - }) - } if test.error != "" { c.Assert(err, ErrorMatches, test.error) continue From 2e2c40767123cc68342796fd64aa4d39bc5883cd Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Thu, 24 Sep 2026 08:36:19 +0200 Subject: [PATCH 24/24] docs: add deprecation notice for Digest field --- public/manifest/manifest.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/manifest/manifest.go b/public/manifest/manifest.go index e14283292..3eb78a3e6 100644 --- a/public/manifest/manifest.go +++ b/public/manifest/manifest.go @@ -18,6 +18,9 @@ type Package struct { Version string // Digest holds the sha256 digest when present, and the sha512 digest // otherwise. It is empty when neither is recorded. + // + // Deprecated: Digest exists for historical compatibility. More than + // one digest can be recorded for a package. Use Digests instead. Digest string // Digests holds the digests of the package, keyed by digest kind // (e.g. "sha256").