Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f1d045c
feat: handle different digests in manifest
upils Sep 2, 2026
a3e7036
docs: refine comments
upils Sep 2, 2026
ef98829
style: cleaning
upils Sep 2, 2026
2013aab
fix: fail loudly on invalid marshalling input
upils Sep 2, 2026
1f8982e
test: improve TestManifestRead
upils Sep 2, 2026
e013b76
docs: cleaning
upils Sep 2, 2026
ace8a36
fix: use sha256 for packages by default
upils Sep 4, 2026
1e5c4d8
docs: refine comments
upils Sep 4, 2026
cb3f498
fet: record multiple hashes in the manifest
upils Sep 16, 2026
5466124
fix: order by strength
upils Sep 17, 2026
7ab32ed
refactor: simplify getting strongest pkg digest kind
upils Sep 17, 2026
ca4f18d
tests: cleaning
upils Sep 17, 2026
104f089
docs: refine comments
upils Sep 17, 2026
8009bb0
tests: rename Hashes field to fit usage
upils Sep 17, 2026
d0d2b9c
docs: remove useless comment
upils Sep 17, 2026
02919e5
style: improve naming and docs
upils Sep 17, 2026
ea1734a
fix: make sure reported digest values are not empty
upils Sep 17, 2026
f7011a8
style: linting
upils Sep 17, 2026
6c03948
fix: revert unrelated change in behavior
upils Sep 17, 2026
e53e543
fix: keep Digest field to avoid breaking change
upils Sep 22, 2026
a73fbdc
feat: add cleaner package creation
upils Sep 22, 2026
811c992
refactor: rework NewPackage
upils Sep 23, 2026
42eed4b
style: clean docs and tests
upils Sep 23, 2026
2e2c407
docs: add deprecation notice for Digest field
upils Sep 24, 2026
886dcd5
style: fix lint
upils Sep 28, 2026
94ddf8c
fix: appy review suggestions
upils Sep 28, 2026
4c5f6e5
fix: apply review comments
upils Sep 28, 2026
9f8ba1c
fix: simplify
upils Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 45 additions & 35 deletions internal/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ type PackageInfo struct {
Name string
Version string
Arch string
SHA256 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 cache.SHA256 }
func (p *PackageInfo) PkgDigest() string { return p.SHA256 }
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
Expand Down Expand Up @@ -146,13 +147,17 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro
return nil, nil, err
}
path := section.Get("Filename")
digest, digestKind := packageDigest(section)
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
}
info := sectionPackageInfo(section)
info := sectionPackageInfo(section, digests)
return reader, info, nil
}

Expand All @@ -161,7 +166,11 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) {
if err != nil {
return nil, err
}
info := sectionPackageInfo(section)
digests, _, ok := packageDigests(section)
if !ok {
return nil, fmt.Errorf("cannot find digest for package %q", pkg)
}
info := sectionPackageInfo(section, digests)
return info, nil
}

Expand Down Expand Up @@ -338,48 +347,49 @@ 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.
// digestFields lists the checksum fields Chisel looks up in archive index
// 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},
}

// 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) {
for _, f := range order {
if d, _, ok := control.ParsePathInfo(release.Get(f.name), path); ok {
return d, f
func findDigest(release control.Section, path string) (digest string, field digestField, ok bool) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Note to reviewer]: This change in signature (returning a bool) addresses a comment of the previous PR that was missed. Same thing for packageDigests below.

for _, f := range digestFields {
if d, _, found := control.ParsePathInfo(release.Get(f.name), path); found {
return d, f, true
}
}
return "", digestField{}
return "", digestField{}, false
}

func packageDigest(section control.Section) (digest string, kind cache.DigestKind) {
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 != "" {
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
}
}
}
// No digest advertised; fall back to SHA256 so the package can still be
// cached and retrieved by its computed digest.
return "", cache.SHA256
return all, strongest, ok
}

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)
if !ok {
return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component)
}

Expand All @@ -396,8 +406,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)
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 {
Expand Down Expand Up @@ -516,12 +526,12 @@ 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, digests map[cache.DigestKind]string) *PackageInfo {
return &PackageInfo{
Name: section.Get("Package"),
Version: section.Get("Version"),
Arch: section.Get("Architecture"),
SHA256: section.Get("SHA256"),
Digests: digests,
}
}

Expand Down
52 changes: 30 additions & 22 deletions internal/archive/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
. "gopkg.in/check.v1"

"crypto/sha256"
"crypto/sha512"
"debug/elf"
"errors"
"flag"
Expand All @@ -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"
)
Expand Down Expand Up @@ -255,7 +255,7 @@ func (s *httpSuite) TestFetchPackage(c *C) {
Name: "mypkg1",
Version: "1.1",
Arch: "amd64",
SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05",
Digests: map[cache.DigestKind]string{cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05"},
})
c.Assert(read(pkg), Equals, "mypkg1 1.1 data")

Expand All @@ -266,20 +266,20 @@ func (s *httpSuite) TestFetchPackage(c *C) {
Name: "mypkg4",
Version: "1.4",
Arch: "amd64",
SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4",
Digests: map[cache.DigestKind]string{cache.SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4"},
})
c.Assert(read(pkg), Equals, "mypkg4 1.4 data")
}

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"},
Expand All @@ -290,22 +290,27 @@ 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",
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, 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.
s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"},
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"},
[]string{"SHA256", "SHA512"}, nil)

options := archive.Options{
Label: "ubuntu",
Version: "25.10",
Version: "26.10",
Arch: "amd64",
Suites: []string{"stonking"},
Components: []string{"main", "universe"},
Expand All @@ -322,14 +327,17 @@ func (s *httpSuite) TestFetchBothDigests(c *C) {
Name: "mypkg1",
Version: "1.1",
Arch: "amd64",
SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05",
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
// Pin the cache key: with multiple 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)
}

Expand Down Expand Up @@ -359,7 +367,7 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) {
Name: "mypkg1",
Version: "1.1",
Arch: "arm64",
SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05",
Digests: map[cache.DigestKind]string{cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05"},
})
c.Assert(read(pkg), Equals, "mypkg1 1.1 data")

Expand All @@ -370,7 +378,7 @@ func (s *httpSuite) TestFetchPortsPackage(c *C) {
Name: "mypkg4",
Version: "1.4",
Arch: "arm64",
SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4",
Digests: map[cache.DigestKind]string{cache.SHA256: "54af70097b30b33cfcbb6911ad3d0df86c2d458928169e348fa7873e4fc678e4"},
})
c.Assert(read(pkg), Equals, "mypkg4 1.4 data")
}
Expand Down Expand Up @@ -410,7 +418,7 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) {
Name: "mypkg1",
Version: "1.1.2.2",
Arch: "amd64",
SHA256: "5448585bdd916e5023eff2bc1bc3b30bcc6ee9db9c03e531375a6a11ddf0913c",
Digests: map[cache.DigestKind]string{cache.SHA256: "5448585bdd916e5023eff2bc1bc3b30bcc6ee9db9c03e531375a6a11ddf0913c"},
})
c.Assert(read(pkg), Equals, "package from jammy-security")

Expand All @@ -420,7 +428,7 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) {
Name: "mypkg2",
Version: "1.2",
Arch: "amd64",
SHA256: "a4b4f3f3a8fa09b69e3ba23c60a41a1f8144691fd371a2455812572fd02e6f79",
Digests: map[cache.DigestKind]string{cache.SHA256: "a4b4f3f3a8fa09b69e3ba23c60a41a1f8144691fd371a2455812572fd02e6f79"},
})
c.Assert(read(pkg), Equals, "mypkg2 1.2 data")
}
Expand Down Expand Up @@ -665,7 +673,7 @@ var packageInfoTests = []struct {
Name: "mypkg1",
Version: "1.1",
Arch: "amd64",
SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05",
Digests: map[cache.DigestKind]string{cache.SHA256: "1f08ef04cfe7a8087ee38a1ea35fa1810246648136c3c42d5a61ad6503d85e05"},
},
}, {
summary: "Package not found in archive",
Expand Down
13 changes: 11 additions & 2 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"time"

"golang.org/x/crypto/sha3"
Expand Down Expand Up @@ -96,11 +97,19 @@ type DigestKind string

const (
SHA256 DigestKind = "sha256"
SHA384 DigestKind = "sha384"
SHA512 DigestKind = "sha512"
SHA384 DigestKind = "sha384"
)

var digestKinds = []DigestKind{SHA256, SHA384, SHA512}
// digestKinds sorted in decreasing order of strength.
var digestKinds = []DigestKind{SHA384, SHA512, SHA256}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
var digestKinds = []DigestKind{SHA384, SHA512, SHA256}
var digestKinds = []DigestKind{SHA512, SHA384, SHA256}

@upils upils Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The existing order is correct. I know this can be debatable but as we are comparing SHA-512 and SHA3-384, I considered SHA3-384 to be "stronger".


func ValidateDigestKind(kind DigestKind) error {
if !slices.Contains(digestKinds, kind) {
return fmt.Errorf("unsupported digest kind: %q", kind)
}
return nil
}

var ErrMiss = fmt.Errorf("not cached")

Expand Down
35 changes: 23 additions & 12 deletions internal/manifestutil/manifestutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -85,16 +84,23 @@ 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",
digests := make(map[string]string, len(info.PkgDigests()))
for kind, digest := range info.PkgDigests() {
digests[string(kind)] = digest
}
pkg, err := manifest.NewPackage(&manifest.PackageOptions{
Name: info.PkgName(),
Version: info.PkgVersion(),
Digest: info.PkgDigest(),
Arch: info.PkgArch(),
Digests: digests,
})
if err != nil {
return err
}
err = dbw.Add(pkg)
if err != nil {
return err
}
}
return nil
}
Expand Down Expand Up @@ -272,13 +278,18 @@ 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)
digests := pkg.PkgDigests()
if len(digests) == 0 {
return fmt.Errorf("package %q missing digests", name)
}
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)
Expand Down
Loading
Loading