feat: plugin add <url> — fetch a package over HTTP, with the digest pin mandatory #236
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#236
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Today
plugin add [SOURCE]takes a.cippath, ansha256:identity already in the store, or the single.cipunder<root>/.code-index/plugins/. Getting a package from a release meanscurltwice — the.cipand its.cips— and knowing that the second is not optional. That is the first thing an operator does and the only step with no support.What to build
SOURCEaccepts anhttp(s)://URL alongside the existing forms..cipsis fetched too, from<url>.cipsby default, overridable with--signature-url. A missing signature is a REFUSAL naming the URL it tried — a.cipwithout its.cipsis not undocumented, it is uninstallable, which is what v0.24.0 shipped.--sha256is REQUIRED when SOURCE is a URL. See the threat note below; this is the clause that matters.Why the digest pin is mandatory for a URL and not for a file
The signature already gives authenticity: a hostile URL or a MITM cannot forge a package without the publisher key, and verification happens before anything is rendered. So HTTP does not weaken that.
What a URL adds is substitution. The bytes at a URL are chosen by whoever controls the URL and the network, not by the operator — so a validly signed but different package can be served: an older version with a known defect, or a different package by the same publisher. With a local file the operator at least chose the bytes off disk.
--sha256closes exactly that gap and nothing else. It should be stated in those terms in the help, not as generic caution.What a fix must prove
--sha256REFUSES, naming both digests. Must go RED against a build that skips the check..cipsbeside it REFUSES naming the signature URL it tried.signature_untrusted— i.e. the existing trust check is reached, not bypassed by the fetch path.--sha256omitted with a URL SOURCE is a refusal, not a warning. Mutation: make it a warning → that test goes RED.--sha256still installs. Otherwise "require the pin everywhere" passes and the distinction this issue rests on is untested.Not in scope
Registry protocols, search, version resolution. This is one URL to one file. Version resolution is where auto-update lives — filed separately.
Implemented in
4b02fb5, onmaster.18 integration arms, all green. Every item in "what a fix must prove" is implemented and tested, including the anti-vacuity one —
a_local_file_without_the_pin_still_installs, without which "require the pin everywhere" would pass and the distinction this issue rests on would go untested.The change is step 1 only, with one deliberate exception
locate_packagegrew a URL arm that ends with a.cipand its.cipson local disk in a staging directory. Everything from parse onward is reached by identical code with identical arguments.The exception is the pin comparison, and it cannot live in step 1: the install identity is
package_digestof the parsed container — domain-separated and structured — not a hash of the file. So "are these the bytes you named" is a question only a parsed package can answer. It sits wherecmd_installalready puts it, before the anchor, before the grant, before anything is rendered or written.Verified independently before merge, not taken from the report: inverting that comparison to
if &entry.digest == want && falsemakes both pin arms fail —i.e. the substituted package installs. Restored by
cp, md5 identical.Locating the signature
<url>plus one appended byte, derived fromSIGNATURE_EXT.strip_prefix(PACKAGE_EXT)— the URL spelling ofpackages::signature_path's append, never replace rule, held against the file rule by a unit test over four names.--signature-urloverrides and is refused for a non-URL source.Note
…/download/latest→…/download/latests, which is correct and surprising, so it is covered. That case is also why one of the survivors below mattered.Both size ceilings are derived, and enforced twice
Declared
Content-Lengthand bytes that actually arrived — the argumentread_boundedalready makes about lying metadata, applied to a server:.cip—MAX_FETCH_BYTESisMAX_PACKAGE_BYTES: a byte past whatread_packageaccepts off disk could never be installed anyway..cips—SIG_HEADER_LEN + SIG_RECORD_LEN * MAX_SIGNATURES= 16 + 8×96 = 784 bytes exactly, the largest fileSigFile::parsecan accept.MAX_REDIRECTS = 5is not derivable and its doc says so. What makes it reviewable instead is that the hops used and the final URL are disclosed on every fetch line.Redirects are followed, and the issue was wrong to imply otherwise
The issue listed redirects among the things that must fail. They are followed, up to the bound, and disclosed — because refusing the first hop would refuse the release-download case this issue exists for.
--proto/--proto-redirare pinned tohttp,https.The transport is the operator's
curl, and that follows from this issue's own threat modelThe transport is trusted for nothing: the signature gives authenticity, the pin gives exactness. Adding TLS to get confidentiality of which package is fetched would mean a crypto crate, a cert path, ~30 crates in a binary that executes untrusted wasm, a
cargo denylicence question (webpki-rootsis MPL-2.0), and C builds on four release targets.Verified rather than asserted: zero TLS crates in
Cargo.lock, and shelling out is already this tree's pattern —gitfor diffs,ps/killfor process facts. A missingcurlrefuses by name and points at the pre-existing workflow, which performs the same two checks over the same bytes.Three mutations SURVIVED and are recorded in the tests rather than hidden
cmd_addsurvived — the trust check has two doors;Store::installre-verifies. The mutation that does grade it (disablingverify_detachedinsideStore::verifying_anchor) went RED: the unanchored package installed over HTTP.--max-filesize(exit 63). Kept anyway, because that is a property of the fetcher's version, not of the contract — and removing--max-filesizereddens the declared-size arm while leaving this one green, which is how the two were told apart.default_signature_urlreplacing the extension survived its first test, because for a name ending in.cipappend and replace agree. Strengthened with non-.cipnames; then RED.Also worth recording: making
fetch_packagereturnanyhow::Resultsurvived until the result was bound aslet e: FetchError—?still converts, so the type was lost with the compiler silent. It is now a compile error.Reuse for #237, and a door deliberately left shut
fetch_packagereturns a typedFetchError { what, url, failure }, so auto-update can report unreachable rather than up to date without matching on prose. The fetch report goes to stderr, becauseplugin add's stdout must still begin with the confirmation (nothing_is_written_before_the_answerasserts it).No URL door over MCP.
plugin_addstill takesdigest/fileonly — an agent must not choose which bytes the machine fetches.no_url_door_exists_over_mcpnow grades that, because threeNotAgentVisibleregistry rows rest on it.Not established
curlon Windows. It ships in Windows 10 1803+/Server 2019+ andCommand::new("curl")gets the binary rather than a shell alias, but the native CI job is the only place this gets proven. If it is absent there the new suite goes RED rather than skipping — deliberate.One more: this repo's own
prose_spacing_gatecaught a real defect in the work — a lost\continuation was shipping 18 spaces of indent inside an operator-facing refusal._prdoc/guides/80-operator-recovery.mdclaimed "plugin addhas no--sha256". Now false; corrected.Gates, isolated: fmt, clippy (host and windows-gnu),
cargo test --workspace --no-fail-fast(327 suites, 0 FAILED, counted over the whole log), rustdoc-D warnings— all exit 0.plugin add <url>holds the id AND the source URL but offers no[update]stanza — the one moment "easy update" is free, and it is dropped #257