Skip to content

fix: add support for sha512-only archives#306

Open
lczyk wants to merge 31 commits into
canonical:mainfrom
lczyk:fix/issue-305-accept-sha512
Open

fix: add support for sha512-only archives#306
lczyk wants to merge 31 commits into
canonical:mainfrom
lczyk:fix/issue-305-accept-sha512

Conversation

@lczyk

@lczyk lczyk commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
  • Have you signed the CLA?

Attempted fix for #305.

Resolve digests through a small preference list (strongest first: SHA512, then SHA256) instead of hardcoding one field, and carry the hash kind alongside the digest so downloads are verified and cached with the matching algorithm. This covers:

  • the Release index table (Packages digest),
  • the per-package digest (the .deb), and
  • the Acquire-By-Hash path merged from main, which now builds by-hash/<algo>/<digest> URLs from the resolved field rather than assuming by-hash/SHA256/.

Archives only guarantee a by-hash directory for the strongest hash they advertise, so the by-hash URL must be built from that one -- thanks @FinnRG for catching this. Since the preference list is strongest-first, the resolved field is already the right one, and a single ordering serves both purposes.

As of this writing Stonking publishes SHA512 only, in the base suite and in -security/-updates alike, so the whole suite resolves via SHA512. Resolute and earlier remain SHA256-only

An archive advertising both digests, content is now verified and cached under its SHA512 digest rather than its SHA256 one. Cache entries written by an older chisel are keyed by SHA256, so they go cold and are fetched once more, then expire normally (which, at the moment means they don't expire #261, but that's a separate issue). Nothing gets corrupted. Importantly, at present no archive Chisel targets advertises both digests (Resolute is SHA256-only, Stonking is SHA512-only), so in practice this is not a behaviour change.

Testing

Added unit tests for the new behaviour and tested manually against the Stonking archive. The by-hash tests cover the four archive shapes: a by-hash directory for the strongest hash only, directories for several hashes (as Stonking actually publishes), a directory for a weaker hash only (a buggy archive, falls back to the named path), and no by-hash directories at all.

Out of scope

Something i've deliberately decided is out of scope but really should be fixed in a followup: The manifest's per-package digest still reads SHA256, so on a SHA512-only archive it still errors. Most chisel cut's still work unless the user requests the manifest to be cut too.

@lczyk lczyk marked this pull request as ready for review July 6, 2026 22:24
@upils upils added the Bug An undesired feature ;-) label Jul 7, 2026
upils added 4 commits July 7, 2026 09:21
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
Signed-off-by: Paul Mars <paul.mars@canonical.com>
@lczyk lczyk changed the title feat: add sha256 feat: add support for sha256-only archives Jul 7, 2026
@lczyk lczyk changed the title feat: add support for sha256-only archives fix: add support for sha256-only archives Jul 7, 2026
…pt-sha512

# Conflicts:
#	internal/archive/archive.go
#	internal/archive/testarchive/testarchive.go
@lczyk lczyk requested a review from upils July 7, 2026 07:51
@lczyk lczyk changed the title fix: add support for sha256-only archives fix: add support for sha512-only archives Jul 7, 2026
Comment thread internal/archive/testarchive/testarchive.go Outdated

@upils upils left a comment

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.

Thanks @lczyk! This already looks very good.

I agree that, as you pointed out in the PR description, a follow-up PR must be created to deal with the manifest generation to consider this bug completely fixed. I can do it once this PR is merged, as we need to make sure it will work nicely with the current work on supporting bins.

Comment thread internal/archive/archive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
Comment thread internal/archive/archive_test.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
lczyk added 6 commits July 7, 2026 13:33
Change the testarchive digest field from a single kind to a list so a
package/release can publish several checksum fields at once. Add tests
asserting SHA256 is preferred when both are published, for the named and
by-hash paths.
Use "section" for the checksum groups, matching the control.Section
terminology used across the archive package.
Its only caller is makeDigest; fold the sha256 sum in and drop the
one-line helper.
Digest kinds are a per-archive choice, not per-package, so model them on
the Release alone. The package copy becomes an unexported render cache,
filled from the release at render time, so tests can only set kinds in
one place and the post-adjustRelease propagation walk goes away.
Digest kinds now render exactly as set on the Release: an empty list
publishes no digest sections rather than silently falling back to
SHA256. The single construction site sets the SHA256 default
explicitly, so tests still get it without hidden behaviour.
golangci-lint errcheck flagged the unchecked item.Walk. The callback
only sets a field and always returns nil, so Walk cannot error here;
ignore the return explicitly to satisfy the linter.
@lczyk

lczyk commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@upils ok, ready for another review round. +302 -29 is on the bigger side but vast majority is tests, so i hope this is still ok

@upils upils left a comment

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.

Thanks @lczyk. I have more comments, and sorry for the back-and-forth on the section/field naming (see my comment below).

Comment thread internal/archive/archive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go
Comment thread internal/archive/testarchive/testarchive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
lczyk added 5 commits July 8, 2026 10:04
Archives only guarantee a by-hash directory for the strongest hash they
advertise, so a by-hash URL must never be built from a weaker one. The
digest used to verify content and key the cache is a separate concern,
and still prefers SHA256 to keep existing caches valid.

The test archive now publishes by-hash directories only for the
strongest kind, matching ftpmaster, so building a by-hash URL from a
weaker hash fails the tests instead of passing silently.
makeDigest silently fell back to SHA256 for any kind it did not know, so
makeDigest("MD5", ...) returned a SHA256 sum. Panic instead, as the other
helpers in this package do.
Render calls it before walking the items, and Walk visits the Release
first, so Content always runs after the packages have inherited.
inheritDigestKinds assumed its callback could never error, so neither
could Walk. Return the error instead, and handle it at the call sites,
so a future Walk with a real error path cannot fail silently.
lczyk added 3 commits July 8, 2026 10:26
Replace the render-time copy of the release's digest kinds with a
backpointer set when the release adopts the package. Sections always
see the release's current choice, and rendering a section before any
release adopted the package panics instead of silently rendering
without digests.
adoptPackages hand-rolled the recursion and error propagation that Walk
already provides.

@upils upils left a comment

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.

Thanks again @lczyk. This is indeed getting a bit bigger than we wanted but I do not see a sensible way to split this work further.

Comment thread internal/archive/archive.go Outdated
Comment thread internal/archive/archive.go
Comment thread internal/archive/archive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
// release is set when a release adopts this package at render time.
// Section reads the archive-wide digest kinds through it, so the package
// always sees the release's current choice.
release *Release

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.

I wonder why we now need to wire the whole release object here when we are only interested in the digest kinds. I saw your commit message but it is unclear why a copy at render time is not enough. Is the digest kinds slice changing over time in some cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i thought a backpointer to Release was a cleaner way to implement the if p.release == nil guard.

with DigestKinds []string, the empty/nil slice is ambiguous: it means both "archive publishes no digests" and "not rendered yet", so we'd need another field (like rendered bool). with release *Release this is only one field sincep.release == nil` always means "not rendered yet".

happy to change back to copy but then we grow one more field. if that's fine then no problem. 👍

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.

I agree with you, let's keep it.

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 indeed does not feel so great. We are improving Chisel to generalize the digest logic where we assumed a constraint in the past, and on the way we are again establishing a new constraint that is not a ground truth for how archives are organized.

Just to make the point more clear, which release of Ubuntu is this:

https://www.archive.ubuntu.com/ubuntu/pool/main/l/linux/

Also, there is a question about the fact this is being done per release instead of per package. How did we transition between the hashes? Was the entire release rebuilt at once, and has apt survived the transition phase without failures? How?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The ftpmaster publisher always rebuilds the entire Release (and associated index files) all at once, so there was no transition phase.

Just to be clear, the hashes inside the Packages and Sources index files may be different from the hash(es) used in the Release file and therefore the by-hash lookup.

Comment thread internal/archive/testarchive/testarchive.go Outdated
Comment thread internal/archive/testarchive/testarchive.go Outdated
@lczyk

lczyk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

This is indeed getting a bit bigger than we wanted but I do not see a sensible way to split this work further.

yeah, alas neither. very open for ideas though, and trying to keep scope small and focused.

@lczyk lczyk requested a review from upils July 8, 2026 22:25

@upils upils left a comment

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.

Thanks @lczyk. Let's do the simplification you proposed and this should be good for the core review.

Comment thread internal/archive/archive.go Outdated
Comment thread internal/archive/archive.go
Comment thread internal/archive/testarchive/testarchive.go Outdated
// release is set when a release adopts this package at render time.
// Section reads the archive-wide digest kinds through it, so the package
// always sees the release's current choice.
release *Release

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.

I agree with you, let's keep it.

Comment thread internal/archive/testarchive/testarchive.go Outdated
lczyk and others added 2 commits July 9, 2026 11:01
Co-authored-by: Upils <5464641+upils@users.noreply.github.com>
Drop the SHA256-first cache ordering: content is verified and cached
under the strongest advertised digest. Worst case, entries cached by an
older chisel under SHA256 go cold and are fetched once more. No current
archive advertises both digests, so today this changes nothing.
@lczyk lczyk requested a review from upils July 9, 2026 10:31
@lczyk

lczyk commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@upils changes made and desc updated

@upils upils left a comment

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.

Thanks!

@niemeyer niemeyer left a comment

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.

Thanks a lot for this. The code looks clean and simple, but some of the wiring choices might do with some improve.

Given this was already reviewed repeatedly, I suggest you pick up from here @upils.

// release is set when a release adopts this package at render time.
// Section reads the archive-wide digest kinds through it, so the package
// always sees the release's current choice.
release *Release

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 indeed does not feel so great. We are improving Chisel to generalize the digest logic where we assumed a constraint in the past, and on the way we are again establishing a new constraint that is not a ground truth for how archives are organized.

Just to make the point more clear, which release of Ubuntu is this:

https://www.archive.ubuntu.com/ubuntu/pool/main/l/linux/

Also, there is a question about the fact this is being done per release instead of per package. How did we transition between the hashes? Was the entire release rebuilt at once, and has apt survived the transition phase without failures? How?


func (p *Package) Section() []byte {
if p.release == nil {
panic("package section rendered before a release adopted the package; render via Release.Render")

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.

As another consequence, and I know this is clear by now, but just to state it: this used to pure a pure data type and with this change we cannot render a Package without a private field being set, and it panics instead of failing. If nothing else, it's more evidence that it seems to be making things less nice.

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.

I changed it back to setting a slice of DigestKinds in Package to avoid all these drawbacks. This is consistent with how we deal with other fields (especially arch) . Only prepareArchiveAdjustRelease considers that both Release.DigestKinds and Package.DigestKinds should be in sync by default, but callers are free to tweak that If not using prepareArchiveAdjustRelease, callers are also responsible to properly fill Release and Package. I think this is a better separation of concerns and is more consistent with the existing code.

byHashPath := path.Join(prefix, "dists", r.Suite, path.Dir(itemPath), "by-hash", "SHA256", makeSha256(itemContent))
content[byHashPath] = itemContent
// Archives only guarantee a by-hash directory for the strongest
// hash they advertise, though they may publish more. Render the

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.

Is this algorithm how apt determines those paths as well? Given the history of hash changes in several applications, I don't feel very safe to be making another guess that can easily break when we start to have multiple hash kinds for the same archives in the near future.

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.

The aim here is to be consistent with the promise made by a real valid archive: when supporting by-hash URL, publish them at least for the strongest supported hash in the release. This piece of code is unrelated to how the client should behave when consuming the archive.

Regarding the actual behavior of Chisel: when trying to fetch the package index, findDigest is called to get the strongest hash associated to the file, and this hash is then used to try to get the file "by-hash". In the scenario where the archive published multiple hashes for files, Chisel only tries to use the "by-hash" URL with the strongest hash it supports and falls back to the regular URLs if encounters a 404 error. This is consistent with apt's behavior.

// guaranteed one only, so tests catch clients building by-hash
// URLs from a weaker hash; tests wanting extra directories add
// them on top of the rendered content.
if kind := strongestKind(r.DigestKinds); kind != "" {

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.

It's at least worth of note that we are doing this for every individual element instead of the overall release. Is that how it works for the archives as well? What happened during the transition between the the hash kinds, did we have half the release with one hash and half with the other? Was apt making the right choices during that time?

I moved half of this question to earlier on in the review, so feel free to ignore the points already replied to.

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.

@FinnRG answer to these questions: #306 (comment). I do not really trust GH linking so I am pasting the answer here:

The ftpmaster publisher always rebuilds the entire Release (and associated index files) all at once, so there was no transition phase.

Just to be clear, the hashes inside the Packages and Sources index files may be different from the hash(es) used in the Release file and therefore the by-hash lookup.

As the used hash is a release-level decision, I am moving the call strongestKind before the call to Walk to make it obvious.

Signed-off-by: Paul Mars <paul.mars@canonical.com>
Comment on lines +140 to +141
if adjustRelease != nil {
adjustRelease(release)

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.

[Note to reviewer]: This was moved before preparing the packages so they pick up the adjusted DigestKinds.

Signed-off-by: Paul Mars <paul.mars@canonical.com>
@lczyk

lczyk commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

i agree with @upils changes 👍 have reached roughly the same conclusion / direction when looking at this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug An undesired feature ;-)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants