Skip to content

Release 0.6.0 - #223

Merged
Gorka Gonzalo (d0ubl3g) merged 51 commits into
mainfrom
dev
Oct 26, 2026
Merged

Gorka Gonzalo (d0ubl3g) merged 51 commits into
mainfrom
dev

Conversation

@d0ubl3g

Copy link
Copy Markdown
Contributor

Added

  • Signed APT and DNF repositories at packages.hodeishield.com for x86_64 and arm64, so apt and
    dnf install and upgrade the CLI. The README has the setup steps for Debian and Ubuntu, and for
    Fedora, RHEL, Rocky Linux and AlmaLinux, with the signing key's fingerprint. It is the only place
    that states the fingerprint and the repository configuration: check the key against it.
    (#161)

Changed

  • The generated /v1 client follows version 2026-10-25 of the API document. alerts list now
    prints each alert's updated_at in --json and --csv output (the table is unchanged), and
    --sort accepts updated_at. The /v1/me answer now also carries beta and user; a null
    user (an API key) is still no user.
    (#210)

Fixed

  • whoami works when the API leaves out the user's email, which it does for a token without the
    email scope: it shows the name, or the id, and no longer fails to read the answer. Browser
    sign-in now also asks for the email scope, but only when the app lists it among its supported
    scopes. The generated client follows version 2026-10-26 of the API document.
    (#217)
  • alerts list no longer depends on page, total and total_pages in the answer, which the API
    now treats as optional. When they are missing, --all stops at a page with a null next_cursor
    or fewer alerts than the page size, and the Page N of M line is not printed.
    (#210)

Security

  • ctutils is updated from 0.4.3 to 0.4.4 and tokio from 1.53.2 to 1.53.3. They fix RUSTSEC-2026-0385
    (ctutils mask generation and u128 comparisons were not constant-time on some platforms) and
    RUSTSEC-2026-0386 (AsyncFd::new and related constructors in tokio violate I/O safety). The CLI
    does not call either affected API directly; both crates are dependencies of its dependencies.

The monitor was inert while packaging/gpg.key was absent, so deleting the
key later would have turned a live repository into a green, unchecked
one. It is now inert only while every APT and DNF entry point answers a
clean HTTP 404. Any other answer (a network or TLS error, 403, 5xx, a
redirect) fails the run, and so does a served index without a readable
key; the existing issue mechanism then opens or reuses the issue.
The decision lives in packaging/repo/live.sh so the CI control job can
test it with a local server.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The control job now also covers the cases that decide whether the
monitor runs at all: a domain that serves nothing is inert, while a
served index with the key missing, empty or unreadable, and any fetch
error that is not a clean 404 (500, 403, a redirect, a refused
connection), are red. A silent monitor must never be the result of a
deleted key or a broken fetch, so each of these is proven to fail.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
CONTRIBUTING said the monitor waits for packaging/gpg.key. It now
waits only for the domain to serve an index, and a served index without
the key is a failure, which maintainers need to know when reading a
repo-freshness issue.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The README and the site page now give the full setup for Debian and
Ubuntu (a keyring file with signed-by, after checking the key's
fingerprint) and for Fedora, RHEL, Rocky Linux and AlmaLinux (the
.repo file, with the DNF 4 and DNF 5 commands), plus upgrading and the
one-line key refresh for when the signing key's expiry is extended.
The README and the changelog carry @@PACKAGES_KEY_FINGERPRINT@@ until
the key is committed; the site page gets the fingerprint from the key
when the repository is built.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
…it expires

gpgv exits 0 for a signature made by an expired or a revoked key and for
an expired signature; it reports EXPKEYSIG, REVKEYSIG or EXPSIG on the
status fd only. build.sh's self-check and freshness.sh looked at the exit
code alone, so such an index would have verified. Both now read the
status lines through one shared helper and require a GOODSIG and a
VALIDSIG made by the repository key's primary key, and fail on EXPKEYSIG,
REVKEYSIG, EXPSIG, BADSIG or ERRSIG.

freshness.sh also reads the expiry of the primary key and of its usable
signing subkeys from packaging/gpg.key and exits 5 when either expires
within 60 days. The weekly workflow turns that into a warning and an
issue, not a red run, so the renewal (which needs a release) is not left
until clients start to fail.

The control job gains throwaway keys made with a faked clock: a signing
subkey that has expired, a revoked one, and keys that expire within 60
days. The first two also check that gpgv itself exits 0 for them, so the
cases keep proving what they claim.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
A plain rpm --import keeps the old copy of a key that has the same
fingerprint (rpm 4.20), so after a renewal dnf could keep checking
against the expired binding. The refresh now removes the old key first.
The APT refresh now includes the fingerprint check that the setup steps
already ask for, so the key is never installed unchecked. Both are
mirrored on the site page, with the reminder to refresh before the old
expiry date.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Documents which parts of the key exist and where, the renewal steps and
what each needs (extending the primary key as well as the subkey,
replacing the CI secret, publishing a release so every index is signed
again), and what to do if the signing subkey leaks or the primary key is
stolen or lost, so none of it depends on one person remembering it.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The CI secret holds the subkey with the binding signature it was
exported with. When the key is renewed, packaging/gpg.key carries the
extended expiry, but the subkey in the keyring would still show the old
one and gpg would refuse to sign with it once that date passed, however
fresh the committed key. Importing the committed public key into the
throwaway keyring first merges the current binding signatures onto the
subkey. A trial with a key renewed from 30 days to 2 years showed the
secret subkey picking up the new expiry, and the keyring still holds
exactly one secret signing subkey, so that check is unaffected.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
CI lints packaging/repo/*.sh from the repository root, where the
relative path of the sourced sigcheck.sh does not resolve. The helper is
linted on its own as one of the inputs, so the source line need not be
followed.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
packaging/gpg.key is the public key of the APT and DNF repositories: an RSA 4096 certify-only
primary key, AA2A F212 85A9 AB99 0833  9089 35D5 F941 71C1 CD0B, with one sign-only subkey,
DC7E2403287CDB08BD84AD7DD7E72E1B635666B0. Both expire on 2028-10-09. The README and the
changelog now show the fingerprint users check before trusting the key.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Part of #161. DNF users write the repository file from the README, with
the key in a local file they have checked, instead of fetching a .repo
file and a key from the package site. The key refresh for DNF imports
the checked local copy. The changelog entry points to the README for
the fingerprint.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
#	README.md
#	packaging/repo/index.html
Part of #161. The DNF test client now takes the .repo file from the
README block that users copy, so the tests install and upgrade with
exactly the documented configuration. The case for another copy of the
same key is renamed P7: P4 was already taken.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Take the published OpenAPI document, regenerate the client, and let the alerts
list work without page, total and total_pages: --all ends on a null cursor or a
short page, and the page footer is skipped.

Closes #210

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Follow /v1 2026-10-25: pagination totals may be absent

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The integrity check derived the files to authenticate from the bundles
attached to the release, so removing a bundle together with replacing
its file went unnoticed as long as one bundle remained. The required
set is now SHA256SUMS, every file it lists and every attached archive
or installer, whichever bundles are present; a missing bundle is
reported.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The signing steps accepted any Developer ID certificate with a
TeamIdentifier, and never read the installer certificate's team. The
expected Team ID is now a constant in the workflows, compared exactly
with the binary's TeamIdentifier and the installer certificate's Team
ID. An empty or malformed value fails the first step. The constant is
empty until a maintainer sets it.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The administrative extraction of the MSI runs AdminExecuteSequence, so
standard actions such as SelfRegModules could load a DLL shipped under
the name of a payload file. The check now also refuses the tables that
make such actions load payload code, requires every action of the
administrative sequences to be a passive standard action, and compares
LICENSE, NOTICE and USER-INFORMATION.md with the repository's copies by
SHA-256. The signing job now checks out the commit for that comparison.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The MSI check now reads _Tables once and refuses any table the package
does not use, reads every row without swallowing errors, allows only the
standard actions seen in each sequence (as a subset) and pins the
Registry and Environment rows to the InstallDir value and the PATH entry.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Mode bits do not cover NFSv4-style ACLs, which a parent folder can pass
on to the sessions directory and the token file. On macOS the CLI now
removes any ACL from the directory and the temporary file it creates,
before a token is written, and refuses an existing directory or file
that has one. On Linux a POSIX ACL is limited by the mask that chmod 600
sets, so nothing changes there.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
It was committed by accident; __pycache__/ is now ignored.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
…-0383

rustls-webpki 0.103.16 rejects certificate paths through the distrusted intermediates, and
webpki-root-certs 1.0.10 drops the root that issued them.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Harden the token store, macOS signing, the release integrity check and the MSI check

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Follow version 2026-10-26 of the API document: GET /v1/me omits
data.user.email for a token without the email scope. The generated
user type now has an optional email, so the answer still decodes, and
whoami falls back to the name, then the id.

Browser sign-in also requests the email scope, but only when the app
lists it in scopes_supported; an app that publishes no list is not
asked for it.

Closes #217

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
ctutils 0.4.4 makes the mask generation and u128 comparisons constant-time, and tokio 1.53.3
fixes the I/O safety violation in AsyncFd::new and related constructors.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Read whoami when the API leaves out the user's email

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Each request used --max-time 60 with up to 3 retries and no overall limit: 4 attempts of 60 s plus
3 delays of 2 s is 246 s per request, 738 s for the three, which is more than the 10 minutes the
job may run. An attempt now lasts at most 20 s and no retry starts after 40 s from the first
attempt, so a request takes at most 60 s and the three of them 180 s.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The check counted every signing subkey that gpg lists, including one whose secret part is absent
(ssb marked #) and a revoked or expired one, so a key rotation that leaves the old subkey in the
public key made it fail. It now counts a subkey only when it can sign, is not revoked, expired or
invalid, and its secret is present.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
…hed it

A clean 404 on every entry point was always only a notice, so the monitor could stay green forever
after go-live. live.sh now pins LAST_RELEASE_WITHOUT_REPOSITORY=0.5.1 and takes the latest stable
release and its age from the workflow: a 404 is accepted for a release up to that version, or for a
newer one inside the grace period (48 hours, as for freshness), and is a failure afterwards. The
workflow reads the release before calling live.sh and fails when it cannot. freshness-test.sh covers
0.5.1, a newer release past the grace period, one inside it, and a malformed version.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
- actions/upload-artifact 7.0.1 -> 7.0.2
- actions/download-artifact 8.0.1 -> 8.0.2
- actions/create-github-app-token 3.2.0 -> 3.3.0
- ossf/scorecard-action 2.4.4 -> 2.5.0
- github/codeql-action/upload-sarif 4.38.2 -> 4.38.3
- github/codeql-action/init 4.38.2 -> 4.38.3
- github/codeql-action/analyze 4.38.2 -> 4.38.3
- google/osv-scanner-action 2.6.0 -> 2.7.0

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The README had users compare the fingerprint by eye, which passes a file that holds the real key and
a second one; the second would then be trusted by signed-by and by the rpm keyring. Each place now
runs a command that fails unless the file has exactly one primary key with the published
fingerprint, and removes the file otherwise so that the install step that follows fails.

freshness.sh also checks that the /gpg.key being served holds exactly the committed primary key and
no other, with the same primaries logic as publish.sh. freshness-test.sh covers another key, an
extra key and a missing file.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
rpm -e with an empty list exits with an error, so the rpm --import after the && never ran. The old
key is now removed through xargs -r, which does nothing for an empty list, and the import is its own
step. The steps also run dnf clean all, with a note that DNF keeps its own copy of the key for
repository metadata checks; it is harmless when nothing was cached.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
packaging/repo/keycheck.sh holds the fingerprint as a constant and fails unless a key file holds
exactly one primary key with it; given the README it also checks that the README states the same
fingerprint. The install-script job runs it on packaging/gpg.key and the README, the repos job of
package-managers.yml runs it before anything is signed or uploaded, and the freshness workflow runs
it before it looks at the repositories, so a mismatch opens the issue.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The note said gpg refuses the old secret whatever packaging/gpg.key says, but the signing job imports
packaging/gpg.key after the secret, which replaces the binding signature. It now says the new key
must be committed before the release that signs.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
On success nothing changes. On failure the status lines go to stderr so the log says why; they hold
no key material.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
shellcheck 0.10.0, the version on the runner image, reports SC2015 for the A && B || die chain;
0.11.0 does not. The check is now an if. The CI command is clean with both versions.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The gate runs in a subshell on the left of ||, where set -e does not apply, so a failing keycheck.sh
was ignored. The subshell no longer relies on set -e: the key check, reading the tag and running
live.sh each end in an explicit exit.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
This reverts commit 4ca77e3.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
The check printed STOP but the install lines after it still ran, so the APT source and the .repo
file were written anyway. Each step now runs only if the check passes, with one STOP handler for
the whole chain, which also removes the downloaded file. The text now says the check needs gpg.
The DNF renewal note no longer claims that dnf clean all makes DNF read the key file again: it is
harmless, and the renewal is still to be tested.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
This reverts commit 2d9bef0.

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Publish the package repository at packages.hodeishield.com

Closes #161

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Update serde_json and GitHub Actions pins

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
Prepare 0.6.0: version and changelog

Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com>
@d0ubl3g
Gorka Gonzalo (d0ubl3g) merged commit 4859e6b into main Oct 26, 2026
49 of 67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant