Skip to content

fix(contract): pin gpgkey= against [repositories.gpgkeys] - #619

Closed
dtg01100 wants to merge 8 commits into
projectbluefin:fix/validation-gatesfrom
dtg01100:fix/issue-617-gpgkey-pin-check
Closed

dtg01100 wants to merge 8 commits into
projectbluefin:fix/validation-gatesfrom
dtg01100:fix/issue-617-gpgkey-pin-check

Conversation

@dtg01100

@dtg01100 dtg01100 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Closes #617.

Pin declared RPM GPG keys to [repositories.gpgkeys]. Reject unpinned keys in ordinary repositories and named partial or disabled DNF overrides. Reject wildcard key replacement before override shortcuts and propagate pins to both runtime override directories. Key URL paths remain exact, including trailing slashes.

Stacked on #623 for the static/pre-commit fixes; retarget to main after #623 merges. #527 is already merged. Detailed authenticity policy is preserved in a linked skill reference.

Validation: full just check and pre-commit passed on ae0fdd3. Regression tests cover partial, disabled and wildcard key substitutions. Installed-guest checks are in the pending isolated VM run: https://github.com/projectbluefin/utah/actions/runs/37568822950 .

…efin#617)

The repo_security_option_errors gate attested proxy, sslverify, gpgcheck,
pkg_gpgcheck, and repo_gpgcheck on allowlisted repository sections but
left `gpgkey=` entirely unchecked. An override drop-in setting
`gpgkey=https://attacker.example.com/key` on an allowlisted id passed:
the gate had no record of which key the section was supposed to serve.

Add `[repositories.gpgkeys]` to the overlay manifest, mapping repo_id to
the URLs/file paths the section is approved to use. Mirror the
`[repositories.baseurls]` shape: a `gpgkey=` value in any allowlisted
section must match a pinned entry, an entry for a repo not in
`[repositories.allowed]` is rejected as approving nothing, and a
non-list value in the manifest fails loudly. utah-packages needs no
entry because its bind-mounted OCI repo authenticates RPMs by image
provenance rather than an RPM GPG key.

Wire the new table through check_repo_sections and verify_repository_policy
so both the `--check` path and the runtime reposdir scan gate gpgkey=.

The shipped manifest pins the keys hummingbird.repo (file path and
Hummingbird-published URL) and nvidia-container.repo (NVIDIA Open
Source Projects gpgkey) declare.

Hive-Run: projectbluefin#617
Hive-Plan: issue-617-gpgkey-pin-check
Hive-Spec: package-contract#repo-policy
Signed-off-by: dtg01100 <dtg01100@users.noreply.github.com>

@hivecommons-hive hivecommons-hive Bot 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.

style

  • scripts/verify-rpm-contract.py:717-718 — in the enabled branch, repo_gpgkey_pin_errors runs for every section, not just allowlisted ones (unlike repo_pin_errors at L715, which is under elif section_name in allowed_repos). A non-allowlisted section with gpgkey= therefore emits a second, false diagnostic: Allowlisted repository 'evil' is enabled in f.repo with gpgkey=…; the manifest declares no pinned key… alongside the correct Unapproved repository 'evil' error (reproduced at head). Fails closed, so not a gate hole, but the message states the opposite of the truth. Gate L717 on section_name in allowed_repos (the disabled branch at L689-697 already does).
  • scripts/verify-rpm-contract.py:97-109 and :548-556 — docstrings contain garbled prose ("whitespace-trimmed, trailing-whitespace sensitive only where libdnf5 would be…"; "(https://, issue #617)"). Worth a tidy since these explain the pin semantics.
  • tests/test_verify_rpm_contract.py — test name test_repo_gpgkey_pin_errors_passes_when_every_sever_keys_is_pinned typo (sever).

No findings from: correctness, security, intent-alignment, docs-currency.

Confidence: 3/5 (needs attention) — 1 perspective requested changes

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

Only run repo_gpgkey_pin_errors for allowlisted sections, matching the
disabled-section branch, so unapproved repos get a single accurate
diagnostic. Clean up normalize_gpgkey/repo_gpgkey_pin_errors docstrings
and fix a test name typo.

Hive-Fix-Attempt: 1/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <scanner@hive.kubestellar.io>

@hivecommons-hive hivecommons-hive Bot 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.

Reviewed at head 7c23810. Ran tests/test_verify_rpm_contract.py (175 passed) and verify-rpm-contract.py --check packages/bluefin.toml packages/utah.toml (valid).

intent-alignment

  • (low) packages/utah.toml:585 pins https://packages.redhat.com/api/pulp-content/public-hummingbird/x86_64/RPM-GPG-KEY-redhat-release-2 as a second approved key for public-hummingbird-x86_64-rpms. The PR body says packages/hummingbird.repo declares "the file path and the Hummingbird-published URL", but packages/hummingbird.repo:17 declares only gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release-2, no other file in the tree references that URL, and it returns HTTP 404 today. The entry approves a key location nothing uses and nothing serves; suggest dropping it (and the "both spellings are pinned" comment at packages/utah.toml:579-581) so the pin set stays exactly what the shipped .repo files declare.

docs-currency

  • (low) docs/skills/package-contract.md:141-143 says utah-packages "is exempt" from the gpgkey pin. Code has no exemption: scripts/verify-rpm-contract.py:552-563 rejects any gpgkey= on an allowlisted id without a [repositories.gpgkeys] entry, so a drop-in adding gpgkey= to utah-packages fails (correctly). Reword to "has no entry because its .repo declares no gpgkey=; any gpgkey= added to it is rejected", matching the accurate wording at packages/utah.toml:575-577.

No findings from: correctness, security, style.

Confidence: 3/5 (needs attention) — 1 perspective requested changes

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

Reword package-contract.md: utah-packages has no [repositories.gpgkeys]
entry because it declares no gpgkey=, not because it is exempt; an added
gpgkey= is rejected. Drop the packages.redhat.com key URL pin, which no
shipped .repo references, so the pin set matches hummingbird.repo.

Hive-Fix-Attempt: 2/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <scanner@hive.kubestellar.io>

@hivecommons-hive hivecommons-hive Bot 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.

Read at head 930ff97.

docs-currency

  • docs/skills/package-contract.md:168-169 — "the shipped [repositories.gpgkeys] pins the key URL each publishes" refers to the two signature-check exceptions (utah-packages, nvidia-container-toolkit), but utah-packages has no pin (as the same paragraph says at L145 and packages/utah.toml:572-574). Only NVIDIA's URL is pinned; the sentence contradicts itself.

intent-alignment

  • PR body says the Hummingbird pin covers "file path and the Hummingbird-published URL"; packages/utah.toml:583 pins only file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release-2. The diff is tighter than the description (good) — body should be corrected so the record matches.

style

  • scripts/verify-rpm-contract.py:528-534 split_gpgkeys is byte-identical to split_baseurls (L524-525); alias or reuse rather than duplicate.

No findings from: correctness, security.

Confidence: 3/5 (needs attention) — 1 perspective requested changes

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

Hive-Fix-Attempt: 3/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <scanner@hive.kubestellar.io>

@hivecommons-hive hivecommons-hive Bot 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.

docs-currency

  • docs/skills/package-contract.md:168-170 says "Only nvidia-container-toolkit has a [repositories.gpgkeys] pin", but the manifest this PR ships pins two repos: packages/utah.toml:583 (public-hummingbird-x86_64-rpms → file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release-2) and :586 (nvidia-container-toolkit). The PR body gets it right; the prose contradicts the manifest it describes. Drop the "Only …" sentence or list both pins.

correctness (low, non-blocking)

  • scripts/verify-rpm-contract.py:97-115 normalize_gpgkey is documented as mirroring normalize_baseurl, but omits the ${var}→$var fold at :516. A gpgkey=…/$basearch/key pin spelled ${basearch} in a .repo file would be reported as unpinned. No shipped .repo uses variables in gpgkey=, so the gate holds today; worth aligning since the two helpers are meant to share semantics.

Verified at 9fc319c: python3 scripts/verify-rpm-contract.py --check packages/bluefin.toml passes; pytest tests/test_verify_rpm_contract.py → 175 passed.

No findings from: security, intent-alignment, style.

Confidence: 3/5 (needs attention) — 1 medium finding; 1 perspective requested changes

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

…n docs

normalize_gpgkey now applies the same ${var} -> $var fold as
normalize_baseurl, so braced and bare variable spellings compare equal.
package-contract.md now names both [repositories.gpgkeys] pins
(public-hummingbird-x86_64-rpms and nvidia-container-toolkit).

Hive-Fix-Attempt: 4/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <scanner@hive.kubestellar.io>

@hivecommons-hive hivecommons-hive Bot 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.

Reviewed — no findings from correctness, security, intent-alignment, style, docs-currency.

Confidence: 5/5 (safe)

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

Stack on the validated static-gate fixes and preserve exact key URL paths. Check partial, disabled, and wildcard overrides before early returns, and pass key pins to every runtime scan.

Assisted-by: GPT-6 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hanthor
hanthor changed the base branch from main to fix/validation-gates October 7, 2026 03:56

@hivecommons-hive hivecommons-hive Bot 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.

Read scripts/verify-rpm-contract.py at ae0fdd3 end to end, plus the four packages/*.repo files. The gate change looks correct to me and matches #617.

Verified

  • [repositories.gpgkeys] in packages/utah.toml pins exactly what the shipped sections declare: packages/hummingbird.repo:17 → file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release-2, packages/nvidia-container.repo:27 → https://nvidia.github.io/libnvidia-container/gpgkey. packages/utah-packages.repo has no gpgkey=, so its absence from the table is right and a drop-in adding one is rejected. fedora-44.repo is builder-only and not allowlisted, so its key is out of scope as intended.
  • check_repo_sections (:770-774) runs the key check before every continue, so a partial or enabled=0 named override that carries gpgkey= is still rejected, and a wildcard override with gpgkey= is refused outright before glob_override_errors. That matches the "checked before their early returns" note in the new reference doc.
  • expected_gpgkeys reaches all three verify_repository_policy call sites (:1240 source check, :1312 on-image repos.d, :1341 override dirs) and the one check_repo_sections call (:891). Nothing is left unpinned.
  • normalize_gpgkey lowercases only scheme and host and keeps the path exact, including trailing slashes — appropriate for file identities; ${basearch} → $basearch folding matches how dnf5 expands either spelling.
  • Manifest validation rejects a [repositories.gpgkeys] id outside [repositories.allowed] (:1136-1143), mirroring [repositories.security].

One low-severity wording issue (repo_gpgkey_pin_errors, :584-589): for override scans the check runs on every section (is_override or section_name in allowed_repos), so a non-allowlisted override section with an unpinned gpgkey= is reported as "Allowlisted repository '…' is enabled … the manifest declares no pinned key". The rejection itself is correct; the message just mislabels the id as allowlisted (and as enabled, even when enabled=0). Worth a small tweak so the operator isn't sent to the allowlist for a repo that isn't on it.

Stacked on #623 (fix/validation-gates); retarget to main after it lands.

Confidence: 5/5 (safe)

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

Report a drop-in override whose id is not allowlisted as a repository
override rather than an allowlisted repository when it sets an unpinned
gpgkey=, split the gpgkey branch in check_repo_sections for readability,
describe override key pinning declaratively in the authenticity
reference, and correct a test docstring that contradicted its assertion.

Hive-Fix-Attempt: 5/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <scanner@hive.kubestellar.io>

@hivecommons-hive hivecommons-hive Bot 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.

docs-currency

  • (low) docs/skills/package-contract.md:176-189 — the on-image override bullets still give the exhaustive rule for partial and wildcard drop-ins ("passes for any id unless it sets a proxy= or disables sslverify= or an unapproved signature check"; "a wildcard override passes only when it … sets no proxy=, and disables neither sslverify= nor any signature check"). After scripts/verify-rpm-contract.py:776-790 a partial/disabled override with gpgkey= is pinned and any wildcard override setting gpgkey= is rejected, but only the new reference (references/repository-authenticity.md:60-64) says so. Add gpgkey= to both bullets so the summary doc does not promise a pass the gate no longer gives.

No findings from: correctness, security, intent-alignment, style.

Confidence: 3/5 (needs attention) — 1 perspective requested changes

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hanthor

hanthor commented Oct 7, 2026

Copy link
Copy Markdown
Member

Installed KVM evidence at b32c2b3: baseline RPM contract and all four malicious GPG-key overrides were checked in the booted system, including disabled and wildcard sections and the trailing-slash key resource. All were rejected, then the restored baseline passed. Run 37574083892 retained review-checks.json. The broader gate stopped later on a Flatpak ref formatter issue, so full VM success remains pending.

Hive-Fix-Attempt: 6/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <scanner@hive.kubestellar.io>

@hivecommons-hive hivecommons-hive Bot 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.

Reviewed — no findings from correctness, security, intent-alignment, style, docs-currency.

Confidence: 5/5 (safe)

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Hive filed out-of-scope review findings as backlog issues so they do not block this PR:

Cap: 3 backlog issue(s) per PR.

@castrojo
castrojo deleted the branch projectbluefin:fix/validation-gates October 9, 2026 17:29
@castrojo castrojo closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent/scanner Filed or owned by the scanner agent.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Review backlog (security): gpgkey= not gated in repo override/reposdir scans

3 participants