Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a948b10
feat: add sha256
lczyk Jul 6, 2026
e1b7e40
docs: comment
lczyk Jul 6, 2026
daf5864
tests: test SHA512 cache entries
upils Jul 7, 2026
46d5658
fix: refine comments
upils Jul 7, 2026
1511187
tests: use stonking for SHA512 tests
upils Jul 7, 2026
bbdf2fe
fix: rely on ok to check digest
upils Jul 7, 2026
7501ca0
Merge remote-tracking branch 'canonical/main' into fix/issue-305-acce…
lczyk Jul 7, 2026
c973b90
test: cover by-hash over sha512
lczyk Jul 7, 2026
9fa6d53
test: propagate digest kind to packages in test helper
lczyk Jul 7, 2026
46fefb3
test: cover release publishing both digests
lczyk Jul 7, 2026
1e1a0d1
refactor: rename digestField to digestSection
lczyk Jul 7, 2026
58c0e97
test: inline makeSha256
lczyk Jul 7, 2026
82dd7e7
test: make digest kinds an archive-wide property
lczyk Jul 7, 2026
f8eb118
test: drop implicit SHA256 default for digest kinds
lczyk Jul 7, 2026
bc4b7f6
fix: check Walk return in inheritDigestKinds
lczyk Jul 7, 2026
c179489
fix: build by-hash URLs from strongest digest
lczyk Jul 8, 2026
744fae9
refactor: name digest fields, not sections
lczyk Jul 8, 2026
8c5cbc1
fix: panic on unknown digest kind in test archive
lczyk Jul 8, 2026
7a308a3
refactor: drop redundant inheritDigestKinds call
lczyk Jul 8, 2026
2ec8ab9
refactor: propagate Walk errors in test archive
lczyk Jul 8, 2026
c5ce9e3
docs: describe digestKinds field, not its writer
lczyk Jul 8, 2026
1397709
refactor: packages read digest kinds through the release
lczyk Jul 8, 2026
7cf965b
refactor: adopt packages through Walk
lczyk Jul 8, 2026
abfc91f
refactor: rename adoptPackages to wirePackages
lczyk Jul 8, 2026
0ebadf2
docs: keep findDigest comment above the table format
lczyk Jul 8, 2026
606e7a1
test: cover by-hash dirs beyond the strongest digest
lczyk Jul 8, 2026
86024cd
docs: by-hash dirs beyond the strongest are legal
lczyk Jul 8, 2026
b70e1b4
Update internal/archive/testarchive/testarchive.go
lczyk Jul 9, 2026
311052e
refactor: single strongest-first digest preference
lczyk Jul 9, 2026
0c9e9e3
test: pass DigestKinds instead of release pointer
upils Jul 15, 2026
b9da3c9
refactor: strongest kind at the release level
upils Jul 15, 2026
8caa89f
fix: respect adjustRelease intent
upils Aug 19, 2026
90c46ba
fix(test): refine DigestKinds handling
upils Aug 20, 2026
ce36ce4
fix: revert adjustRelease call move
upils Aug 20, 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
74 changes: 64 additions & 10 deletions internal/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro
return nil, nil, err
}
path := section.Get("Filename")
digest, digestKind := packageDigest(section)
logf("Fetching %s...", path)
reader, err := index.fetch(path, section.Get("SHA256"), fetchBulk)
reader, err := index.fetch(path, digest, digestKind, fetchBulk)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -280,7 +281,9 @@ func openUbuntu(options *Options) (Archive, error) {

func (index *ubuntuIndex) fetchRelease() error {
logf("Fetching %s %s %s suite details...", index.displayName(), index.version, index.suite)
reader, err := index.fetch(index.distPath("InRelease"), "", fetchDefault)
// InRelease has no digest to check against (it is verified by its PGP
// signature below), so the digest kind here is arbitrary.
reader, err := index.fetch(index.distPath("InRelease"), "", cache.SHA256, fetchDefault)
if err != nil {
return err
}
Expand Down Expand Up @@ -328,10 +331,57 @@ 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.
type digestField struct {
name string
kind cache.DigestKind
}

// digestFields lists the checksum fields Chisel can verify, strongest first.
// This order matches the by-hash archive layout: an archive is only guaranteed
// to publish a by-hash directory for the strongest hash it advertises.
var digestFields = []digestField{
Comment thread
lczyk marked this conversation as resolved.
{"SHA512", cache.SHA512},
{"SHA256", cache.SHA256},
}

// cacheDigestFields is digestFields reordered to prefer SHA256, so that
Comment thread
lczyk marked this conversation as resolved.
Outdated
// existing caches keep their keys. It picks the digest used to verify content
// and key the cache, and nothing else.
// TODO Drop this in favour of digestFields. That changes cache keys for
// archives publishing both digests, so it must wait for a minor release.
var cacheDigestFields = []digestField{
{"SHA256", cache.SHA256},
{"SHA512", cache.SHA512},
}

// findDigest returns the checksum recorded for path in a Release "<hash>
// <size> <path>" table, along with the field it was found in, trying the
// fields in order.
Comment thread
lczyk marked this conversation as resolved.
Outdated
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
}
}
return "", digestField{}
}

func packageDigest(section control.Section) (digest string, kind cache.DigestKind) {
for _, f := range cacheDigestFields {
if d := section.Get(f.name); d != "" {
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
}

func (index *ubuntuIndex) fetchIndex() error {
releaseDigests := index.release.Get("SHA256")
packagesPath := fmt.Sprintf("%s/binary-%s/Packages", index.component, index.arch)
packagesDigest, _, _ := control.ParsePathInfo(releaseDigests, packagesPath)
packagesDigest, cacheField := findDigest(index.release, packagesPath, cacheDigestFields)
if packagesDigest == "" {
return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component)
}
Expand All @@ -345,10 +395,15 @@ func (index *ubuntuIndex) fetchIndex() error {
packagesGzPath := packagesPath + ".gz"
var reader io.ReadSeekCloser
if index.release.Get("Acquire-By-Hash") == "yes" {
packagesGzDigest, _, _ := control.ParsePathInfo(releaseDigests, packagesGzPath)
// By-hash directories are only guaranteed to exist for the strongest
// hash the archive advertises, so the URL is built from the strongest
// field, independently of the digest used for verification and the
// cache. 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 != "" {
packagesByHashPath := fmt.Sprintf("%s/binary-%s/by-hash/SHA256/%s", index.component, index.arch, packagesGzDigest)
r, err := index.fetch(index.distPath(packagesByHashPath), packagesDigest, fetchBulk|fetchGzip)
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, cacheField.kind, fetchBulk|fetchGzip)
if err != nil && err != errNotFound {
return err
}
Expand All @@ -358,7 +413,7 @@ func (index *ubuntuIndex) fetchIndex() error {
}
}
if reader == nil {
r, err := index.fetch(index.distPath(packagesGzPath), packagesDigest, fetchBulk|fetchGzip)
r, err := index.fetch(index.distPath(packagesGzPath), packagesDigest, cacheField.kind, fetchBulk|fetchGzip)
if err != nil {
return err
}
Expand Down Expand Up @@ -399,8 +454,7 @@ func (index *ubuntuIndex) distPath(suffix string) string {
return "dists/" + index.suite + "/" + suffix
}

func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadSeekCloser, error) {
const digestKind = cache.SHA256
func (index *ubuntuIndex) fetch(path, digest string, digestKind cache.DigestKind, flags fetchFlags) (io.ReadSeekCloser, error) {
reader, err := index.archive.cache.Open(digestKind, digest)
if err == nil {
return reader, nil
Expand Down
167 changes: 160 additions & 7 deletions internal/archive/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,11 @@ func (s *httpSuite) prepareArchive(suite, version, arch string, components []str

func (s *httpSuite) prepareArchiveAdjustRelease(suite, version, arch string, components []string, adjustRelease func(*testarchive.Release)) *testarchive.Release {
release := &testarchive.Release{
Suite: suite,
Version: version,
Label: "Ubuntu",
PrivKey: s.privKey,
Suite: suite,
Version: version,
Label: "Ubuntu",
PrivKey: s.privKey,
DigestKinds: []string{"SHA256"},
}
for i, component := range components {
index := &testarchive.PackageIndex{
Expand All @@ -158,7 +159,10 @@ func (s *httpSuite) prepareArchiveAdjustRelease(suite, version, arch string, com
if adjustRelease != nil {
adjustRelease(release)
}
release.Render(base.Path, s.responses)
err = release.Render(base.Path, s.responses)
if err != nil {
panic(err)

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.

This wasn't here before. It's probably a good thing to add it, but please just confirm that this is okay given the context I just mentioned.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As mentioned in the other comment #306 (comment) this should not affect real archive test so this should be okay and should not risk hiding failures.

}
return release
}

Expand Down Expand Up @@ -264,6 +268,70 @@ func (s *httpSuite) TestFetchPackage(c *C) {
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"},
func(release *testarchive.Release) {
release.DigestKinds = []string{"SHA512"}
})

options := archive.Options{
Label: "ubuntu",
Version: "25.10",
Arch: "amd64",
Suites: []string{"stonking"},
Components: []string{"main", "universe"},
CacheDir: c.MkDir(),
PubKeys: []*packet.PublicKey{s.pubKey},
}

testArchive, err := archive.Open(&options)
c.Assert(err, IsNil)

pkg, _, err := testArchive.Fetch("mypkg1")
c.Assert(err, IsNil)
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 SHA256 preferred per the cache
// ordering -- so PackageInfo.SHA256 is the field that surfaces.
s.prepareArchiveAdjustRelease("stonking", "25.10", "amd64", []string{"main", "universe"},
func(release *testarchive.Release) {
release.DigestKinds = []string{"SHA256", "SHA512"}
})

options := archive.Options{
Label: "ubuntu",
Version: "25.10",
Arch: "amd64",
Suites: []string{"stonking"},
Components: []string{"main", "universe"},
CacheDir: c.MkDir(),
PubKeys: []*packet.PublicKey{s.pubKey},
}

testArchive, err := archive.Open(&options)
c.Assert(err, IsNil)

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",
})
c.Assert(read(pkg), Equals, "mypkg1 1.1 data")

// Pin the cache key: with both digests advertised, packages must stay
// cached under their SHA256 so existing caches keep their entries.
_, err = os.Stat(filepath.Join(options.CacheDir, "sha256", info.SHA256))
c.Assert(err, IsNil)
}

func (s *httpSuite) TestFetchPortsPackage(c *C) {

s.base = "http://ports.ubuntu.com/ubuntu-ports/"
Expand Down Expand Up @@ -310,14 +378,16 @@ func (s *httpSuite) TestFetchSecurityPackage(c *C) {

for i, suite := range []string{"jammy", "jammy-updates", "jammy-security"} {
release := s.prepareArchive(suite, "22.04", "amd64", []string{"main", "universe"})
release.Walk(func(item testarchive.Item) error {
err := release.Walk(func(item testarchive.Item) error {
if p, ok := item.(*testarchive.Package); ok && p.Name == "mypkg1" {
p.Version = fmt.Sprintf("%s.%d", p.Version, i)
p.Data = []byte("package from " + suite)
}
return nil
})
release.Render("/ubuntu", s.responses)
c.Assert(err, IsNil)
err = release.Render("/ubuntu", s.responses)
c.Assert(err, IsNil)
}

options := archive.Options{
Expand Down Expand Up @@ -687,6 +757,89 @@ func (s *httpSuite) TestFetchByHashSucceedsWhenNamedPathIsStale(c *C) {
c.Assert(status, Equals, 200)
}

func (s *httpSuite) TestFetchByHashSHA512(c *C) {
// Ubuntu 26.10+ advertises Acquire-By-Hash with SHA512-only indices, so
// the by-hash URL must be built under the SHA512 directory.
s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main"}, func(release *testarchive.Release) {
release.ByHash = true
release.DigestKinds = []string{"SHA512"}
})

// Stale content at the named Packages.gz path, so a fallback would fail
// the digest check -- only the by-hash path serves the correct bytes.
for p := range s.responses {
if strings.Contains(p, "Packages.gz") && !strings.Contains(p, "/by-hash/") {
s.responses[p] = testarchive.MakeGzip([]byte("stale Packages from previous publication"))
}
}

options := archive.Options{
Label: "ubuntu",
Version: "26.10",
Arch: "amd64",
Suites: []string{"stonking"},
Components: []string{"main"},
CacheDir: c.MkDir(),
PubKeys: []*packet.PublicKey{s.pubKey},
}

testArchive, err := archive.Open(&options)
c.Assert(err, IsNil)

pkg, _, err := testArchive.Fetch("mypkg1")
c.Assert(err, IsNil)
c.Assert(read(pkg), Equals, "mypkg1 1.1 data")

// The SHA512 by-hash request must have been attempted and succeeded;
// the named path only has stale content.
attempted, status := s.fetchRequestStatus("/by-hash/SHA512/")
c.Assert(attempted, Equals, true)
c.Assert(status, Equals, 200)
}

func (s *httpSuite) TestFetchByHashBothDigests(c *C) {
// When a by-hash archive publishes both digests, the by-hash URL must be
// built under SHA512: archives only guarantee a by-hash directory for the
// strongest hash they advertise. SHA256 by-hash must not be requested.
s.prepareArchiveAdjustRelease("stonking", "26.10", "amd64", []string{"main"}, func(release *testarchive.Release) {
release.ByHash = true
release.DigestKinds = []string{"SHA256", "SHA512"}
})

// Stale content at the named Packages.gz path, so a fallback would fail
// the digest check -- only the by-hash path serves the correct bytes.
for p := range s.responses {
if strings.Contains(p, "Packages.gz") && !strings.Contains(p, "/by-hash/") {
s.responses[p] = testarchive.MakeGzip([]byte("stale Packages from previous publication"))
}
}

options := archive.Options{
Label: "ubuntu",
Version: "26.10",
Arch: "amd64",
Suites: []string{"stonking"},
Components: []string{"main"},
CacheDir: c.MkDir(),
PubKeys: []*packet.PublicKey{s.pubKey},
}

testArchive, err := archive.Open(&options)
c.Assert(err, IsNil)

pkg, _, err := testArchive.Fetch("mypkg1")
c.Assert(err, IsNil)
c.Assert(read(pkg), Equals, "mypkg1 1.1 data")

// The SHA512 by-hash request must have been made and succeeded; the SHA256
// by-hash directory must never be touched.
attempted, status := s.fetchRequestStatus("/by-hash/SHA512/")
c.Assert(attempted, Equals, true)
c.Assert(status, Equals, 200)
attempted, _ = s.fetchRequestStatus("/by-hash/SHA256/")
c.Assert(attempted, Equals, false)
}

func (s *httpSuite) TestFetchByHashFallsBackOnNotFound(c *C) {
s.prepareArchiveAdjustRelease("jammy", "22.04", "amd64", []string{"main"}, func(r *testarchive.Release) {
r.ByHash = true
Expand Down
Loading
Loading