ADR 007 Transport security and mirror trust - nekrozis/qtogo GitHub Wiki
Status: Accepted (2026-10-09).
- Downloading is the one step where the tool talks to a system it does not control, and
it is where an archive's authenticity is decided. The reference implementation
(
aqt/helper.py) sets that policy loosely:_check_mirroracceptshttpandftp, a fallback mirror is picked at random (secrets.choice), redirect handling differs between the single-hop and multi-hop paths, and--pwis passed on the command line. A tool whose premise is a stricter posture than the reference cannot inherit that. - The plan already states the intended policy (ยง4, ยง9 D6, ยง10
QTOGO-010). This record is where it becomes the decision the code answers to, and the thing a later change has to argue with. - What the repository declares about an archive is weaker than ADR-001 assumed.
Updates.xmlcarries SHA-1 only โ<SHA1>per package and for the metadata โ and the collected corpus holds no SHA-256 anywhere. The stronger declaration is a separate<archive>.sha256file the official endpoint publishes beside an archive, and that is the one worth verifying against.model.Archive.SHA256(ADR-001 ยง4) names an algorithm the metadata never carries. - A mirror is a transport convenience, not a source of truth. Whatever serves the bytes, the answer to "what must these bytes hash to" has to come from somewhere else.
Measured against download.qt.io on 2026-10-09, and against the captured Updates.xml
samples:
| Where | What | Algorithm | Note |
|---|---|---|---|
Updates.xml |
<Checksum> |
โ | a boolean ("this repository provides checksums"), not a digest |
Updates.xml, per package |
<SHA1> |
SHA-1 | the declared digest of the package |
Updates.xml, root |
<SHA1>, beside <MetadataName>
|
SHA-1 | the declared digest of the metadata archive |
| beside any object | <object>.sha1 |
SHA-1 | the file IFW itself requires |
| beside any object | <object>.sha256 |
SHA-256 | what this decision verifies against by default |
| beside any object | <object>.md5 |
MD5 | present, unused |
| redirect recovery | <object>.meta4 |
โ | Metalink v4; not a digest |
The sidecar body is <hex> <file name> โ except an archive's .sha1, which is bare
hex with no file name (40 characters; measured on two archives), while Updates.xml's
.sha1 is the named form. A reader takes the first whitespace-delimited token, so it
accepts both; the default path (.sha256) is always the named form.
Nothing here is version-dependent. The .sha1, .sha256 and .md5 sidecars exist
for Updates.xml in every sampled release from 5.9 to 6.10.1, and beside an archive at
6.10.1, while Updates.xml declares SHA-1 throughout โ no <SHA256> element exists in
the collected or the live samples. What changes by version is which algorithm a client
picks and how the directories nest, not the checksums the repository publishes. This
replaces an earlier research note that dated the .sha256 sidecar to 6.8.1: there is no
such boundary. Only windows_x86/desktop was sampled; the other hosts and the
extensions repository are spot-checked when the transport lands.
The endpoint also redirects the archive itself, and only the archive. Measured with
redirects off: Updates.xml and both checksum sidecars answer 200 directly, while a
.7z returns 302 to a nearby official mirror โ ftp.jaist.ac.jp from here and
qt.mirror.constant.com from GitHub's runners, which is why the community Qt actions
appear to "default" to that mirror. So the metadata and the digests are served by
download.qt.io itself, and the heavy bytes are served by a mirror it names. That split
is the shape the decisions below lean on.
Trust is placed in the content of an archive โ verified against a digest the official endpoint served directly โ and not in the host that served it. Everything below follows from that, and it is why no allowlist of mirrors is kept.
-
HTTPS only.
ftpis refused outright โ not merely off by default.httpis refused unless--allow-insecure-httpis given, which warns on every run it affects. The default source isdownload.qt.io. -
Mirrors are named on the command line, in order, and are never chosen at random. The order is the order they are written and sets the priority; the tool never reorders, measures, or picks, so the same command line reaches the same host on every run. The default is the official endpoint alone, so an invocation has no fallback until one is named. There is no configuration file and no built-in mirror list: the official endpoint already redirects archive bytes to a nearby mirror, so a mirror here is for overriding that choice, and a curated list would be one more set of hosts to keep current and to trust. This replaces the reference tool's
secrets.choice. Every mirror must behttpsunless the insecure switch is set.The flag is
--base, and it is repeatable; the reference tool's is single-valued, so accepting it more than once is a superset of its behaviour.--basenames the whole root โ listings,Updates.xmland archives all come from it โ while the digest still comes from the official endpoint (decision 3): one root for the data, one root for trust. -
The expected digest comes from the official endpoint, not from whatever served the archive. It is fetched over HTTPS from
download.qt.io, keyed by the archive's repository path, whichever mirror delivered the bytes. The digest request may not itself be redirected to another host: the root of trust stays on the official endpoint, so a redirect there is a failure rather than a mirror handing back the expected value. A private--basestill prefers the official digest; where the official endpoint cannot answer for a private path, the digest the base itself publishes is used only under--trust-base-checksum. The same rule covers that fallback: once a base's own digest is trusted it is the root of trust, so its digest request answers to the identical no-cross-host rule. -
The algorithm is SHA-256; SHA-1 is the exception, not the fallback. The expected value is the official
<archive>.sha256. An object for which the official endpoint publishes only SHA-1 (or MD5) is verified only under--allow-weak-checksum, which warns. A missing expected digest is a failure, not a pass: no archive is installed unverified..meta4is not used in the first version.--trust-base-checksumand--allow-weak-checksumare independent โ the first decides whose digest is trusted, the second which algorithm is accepted โ and a private base that publishes only SHA-1 needs both. -
The bytes stream to a
.partfile and are renamed only when whole. The digest is computed as the bytes arrive; on any failure the.partfile is removed. The cache (D5) keys on the archive URL together with this verified digest, so a cache hit re-verifies nothing it has not already proven. -
Redirects are bounded and may not weaken the scheme; a host is not a security property. At most 10 hops; an
httpsURL may not redirect tohttp. No host allowlist is kept: the official endpoint redirects archives to mirrors it names, and the bytes are verified against the digest that endpoint served directly (decision 3), so the host that finally serves them needs no prior approval. A downgrade or a runaway chain is refused; where the bytes came from does not have to be. -
The repository model carries the digest the metadata declares, named for its algorithm. ADR-001 ยง4's
Archive.SHA256becomesArchive.SHA1(jsonsha256โsha1). The SHA-256 the tool verifies against is the transport layer's, obtained from the sidecar as in decision 3 โ not a property of the repository metadata. This amends ADR-001 ยง4. No producer emits the field yet, so the rename costs nothing now and would be a contract change once--jsonoutput exists. -
No new exit codes, and the two scheme refusals split by where they come from. A scheme refused before any request โ an
http/ftp--basegiven without the switch โ is ADR-002's 2 (usage): it is the command line's mistake, it is deterministic, and fixing it means changing the command. A downgrade discovered in transit (anhttpsURL redirecting tohttp), a missing or mismatched digest, a digest request redirected to another host, and a weak digest used without the switch are 5 (integrity or security). An unreachable host, a timeout, a TLS failure, an HTTP error status, and a redirect loop are 4 (network or mirror). A TLS failure is 4, not 5: this model does not rest on TLS, it rests on the digest the official endpoint served (decision 3), and TLS is only how that trust root is reached โ failing to reach it is an arrival failure, not a verdict on the bytes.
- A mirror cannot change what the bytes must hash to (3), cannot win by being chosen at random (2), and cannot be reached over a weaker scheme than the source it replaces (1, 6). Its host is deliberately not a security property, which is what removes the allowlist a stricter reading would have needed.
- The official endpoint has to be reachable even when a mirror is configured. That is the
cost of separating the digest source from the download source, and it is why
--trust-base-checksumexists for a private base the official endpoint does not know. -
model.Archivenames SHA-1, because SHA-1 is what the metadata declares. The stronger digest is a transport fact rather than a repository fact โ the honest split. -
A listing or an
Updates.xmlis not checksum-verified: the digest proves an archive's bytes, not the metadata's. A mirror can therefore serve misleading metadata โ a hidden version, a wrong package list โ but it cannot make unverified bytes land, because every archive is checked against the digest the official endpoint published. Metadata integrity is outside this decision. - A Qt release that publishes only SHA-1 forces
--allow-weak-checksum; the warning says which object and why. -
--basebeing repeatable, where the reference tool's is single-valued, is a strict superset of its behaviour and gets its owndocs/compatibility.mdrow when the flag lands. - These are deliberate departures from the reference tool (schemes, mirror choice, digest
source), so each gets a row in
docs/compatibility.mdwhen the code lands.
The cache's key, location and prune semantics (D5); official-installer support (D7); a
configuration-file layer for mirrors (deferred until a feature such as commercial
licensing needs one โ here mirrors are command-line only); the CLI spelling of any flag
beyond the names used here; _meta.7z package metadata, which follows the same rule when
it lands; and progress events.
-
Accept
http/ftpas the reference tool does. Rejected: it is the clearest security regression the plan set out to fix, and a downloader's whole job is to be trusted about bytes. - Pick a fallback mirror at random. Rejected: it makes a run unreproducible for no benefit, and a fixed order is no worse at spreading load for a single-user CLI.
- Ship a curated mirror list, as the reference tool does. Rejected: the official endpoint already redirects archives to a nearby mirror, so a shipped list buys little while becoming a set of hosts to keep current and to trust. A mirror is named on the command line when one is wanted.
- Keep a host allowlist and refuse every other host. Rejected: the official endpoint redirects archives to mirrors it names, so the allowlist would have to be exactly that mirror set. Content verification (3) makes the host unnecessary, so the list is not kept.
- Trust whichever host served the archive to also state its digest. Rejected: it makes the mirror the authority on the bytes it sent, which is the threat decision 3 removes.
-
Verify the
Updates.xmlSHA-1 by default. Rejected: it is the weaker declaration, and a default that reached for it would erase the difference between the metadata and the sidecar. It stays behind the switch that says it is weaker. -
Keep
Archive.SHA256and addSHA1. Rejected: it leaves a field naming an algorithm the repository never publishes and that no code would ever fill. -
A general-purpose digest type (
{Algo, Value}). Rejected for now: one algorithm is the whole of the repository's vocabulary, and a type that can hold many when one is used is structure without a reason. - Put every refusal on one exit code. Rejected: ADR-002 already separates "could not reach it" (4) from "arrived but untrustworthy" (5), and a caller retries one and not the other.