Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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) {
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
14 changes: 12 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,20 @@ 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 the cache supports, in order of
// strength: strongest first.
var digestKinds = []DigestKind{SHA384, SHA512, SHA256}

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
32 changes: 20 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,13 +84,17 @@ 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 := manifest.NewPackage(&manifest.PackageOptions{
Name: info.PkgName(),
Version: info.PkgVersion(),
Digest: info.PkgDigest(),
Arch: info.PkgArch(),
Digests: digests,
})
err := dbw.Add(pkg)
if err != nil {
return err
}
Expand Down Expand Up @@ -272,13 +275,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