Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1d7a2fb
Run the freshness monitor whenever the domain serves an index
d0ubl3g Oct 10, 2026
b8c916b
Test the live switch of the freshness monitor in CI
d0ubl3g Oct 10, 2026
e6508b7
Document when the freshness monitor is inert
d0ubl3g Oct 10, 2026
fa0491b
Document installing from the APT and DNF repositories
d0ubl3g Oct 10, 2026
80dfbbf
Accept only good signatures from the repository key, and warn before …
d0ubl3g Oct 10, 2026
aeb78b6
Make the documented key refresh replace the old key
d0ubl3g Oct 10, 2026
3d1b19d
Describe how the repository signing key is kept and renewed
d0ubl3g Oct 10, 2026
44953ff
Import the public key beside the signing subkey before publishing
d0ubl3g Oct 10, 2026
d2e0857
Keep the shellcheck run clean for the shared signature helper
d0ubl3g Oct 10, 2026
f168a85
Publish the repository signing key and its fingerprint
d0ubl3g Oct 10, 2026
02552bc
State the key fingerprint and the DNF configuration in the README only
d0ubl3g Oct 12, 2026
63c828d
Merge branch 'dev' into feat/packages-repo-live
d0ubl3g Oct 12, 2026
fe8ec3d
Repository tests: configure DNF with the README's own repository block
d0ubl3g Oct 12, 2026
9027971
Merge branch 'main' into dev
d0ubl3g Oct 19, 2026
f60bad5
Follow /v1 2026-10-25: pagination totals may be absent
d0ubl3g Oct 19, 2026
4af3a9c
Merge pull request #213 from Hodeitek/fix/openapi-2026-10-25
d0ubl3g Oct 19, 2026
8b958d7
Require a Sigstore bundle for every signed release file
d0ubl3g Oct 21, 2026
5b9ae75
Bind macOS signing to an expected Apple Team ID
d0ubl3g Oct 21, 2026
35eec12
Restrict what the MSI check lets msiexec run
d0ubl3g Oct 21, 2026
3aae80b
Allow only the tables and actions of the published MSI
d0ubl3g Oct 21, 2026
446cd53
Refuse extended ACLs on the plain-text token store on macOS
d0ubl3g Oct 21, 2026
71af291
Stop tracking a compiled Python cache file
d0ubl3g Oct 21, 2026
ceaf715
Update rustls-webpki and webpki-root-certs for RUSTSEC-2026-0382 and …
d0ubl3g Oct 21, 2026
82ae954
Merge pull request #216 from Hodeitek/fix/codex-2026-10-21
d0ubl3g Oct 21, 2026
49ba134
Read whoami when the API leaves out the user's email
d0ubl3g Oct 26, 2026
8d39685
Update ctutils and tokio for RUSTSEC-2026-0385 and -0386
d0ubl3g Oct 26, 2026
bd915dd
Merge branch 'dev' into feat/packages-repo-live
d0ubl3g Oct 26, 2026
27b0862
Fix the shellcheck warning in the repository liveness check
d0ubl3g Oct 26, 2026
7a765c8
Merge pull request #218 from Hodeitek/fix/whoami-email-scope
d0ubl3g Oct 26, 2026
5bb1059
Keep the liveness probes inside the job timeout
d0ubl3g Oct 26, 2026
d7a0f5b
Count only usable signing subkeys with a secret in the signing key check
d0ubl3g Oct 26, 2026
dad1f74
Make a missing repository a failure once a release should have publis…
d0ubl3g Oct 26, 2026
4ca77e3
Update serde_json to 1.0.154
d0ubl3g Oct 26, 2026
f6752e4
Update GitHub Actions pins
d0ubl3g Oct 26, 2026
ccf30dc
Check that the repository key file holds exactly one key
d0ubl3g Oct 26, 2026
4a404c2
Let the DNF key renewal run when no old key is installed
d0ubl3g Oct 26, 2026
98f726d
Pin the repository key fingerprint outside the README
d0ubl3g Oct 26, 2026
254cea6
Fix the key renewal note that contradicted the signing job
d0ubl3g Oct 26, 2026
293f764
Refuse to build the repositories when the key fingerprint cannot be read
d0ubl3g Oct 26, 2026
89777bf
Show gpgv's status lines when a signature check fails
d0ubl3g Oct 26, 2026
76afb9a
Merge branch 'dev' into feat/packages-repo-live
d0ubl3g Oct 26, 2026
24a566a
Fix the shellcheck warning in the key check
d0ubl3g Oct 26, 2026
026e305
Make the key check in the liveness gate fail the run
d0ubl3g Oct 26, 2026
2d9bef0
Revert "Update serde_json to 1.0.154"
d0ubl3g Oct 26, 2026
635bf2b
Stop the README key steps when the key check fails
d0ubl3g Oct 26, 2026
4231b86
Allow the Zlib licence for foldhash
d0ubl3g Oct 26, 2026
4fa8264
Reapply "Update serde_json to 1.0.154"
d0ubl3g Oct 26, 2026
5b0ed2b
Merge pull request #219 from Hodeitek/feat/packages-repo-live
d0ubl3g Oct 26, 2026
a5962a5
Merge pull request #220 from Hodeitek/chore/deps-2026-10-26
d0ubl3g Oct 26, 2026
c26b726
Prepare 0.6.0: version and changelog
d0ubl3g Oct 26, 2026
3062dd9
Merge pull request #222 from Hodeitek/release/0.6.0-prep
d0ubl3g Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
19 changes: 19 additions & 0 deletions .github/scripts/release-integrity-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,25 @@ def test_negative_a_certificate_whose_signature_does_not_verify_does_not_chain(s
self.assertFalse(self.trust.certificate_chains(changed, self.entry["integratedTime"]))


class Bundles(unittest.TestCase):
SUMS = {"hodeishield-1.0.0-x86_64.tar.gz": "a" * 64, "hodeishield-1.0.0.pkg": "b" * 64}

def test_the_required_set_ignores_which_bundles_are_attached(self):
assets = {"SHA256SUMS", "install.sh", "hodeishield-1.0.0.msi", *self.SUMS}
self.assertEqual(ri.must_be_signed(assets, self.SUMS), {"SHA256SUMS", "install.sh", "hodeishield-1.0.0.msi", *self.SUMS})

def test_negative_a_deleted_bundle_does_not_shrink_the_required_set(self):
assets = {"SHA256SUMS", "SHA256SUMS.sigstore.json", *self.SUMS}
self.assertEqual(ri.must_be_signed(assets, self.SUMS), {"SHA256SUMS", *self.SUMS})

def test_a_file_in_sha256sums_is_required_even_if_its_name_matches_no_pattern(self):
self.assertIn("other.bin", ri.must_be_signed({"SHA256SUMS"}, {"other.bin": "c" * 64}))

def test_bundles_and_provenance_are_not_required_to_have_bundles(self):
names = {"x.tar.gz.sigstore.json", "multiple.intoto.jsonl"}
self.assertEqual(ri.must_be_signed(names, {}), {"SHA256SUMS"})


class Parsing(unittest.TestCase):
def test_sha256sums_in_text_and_binary_mode(self):
text = "a" * 64 + " one.tar.gz\n" + "b" * 64 + " *two.zip\n\n"
Expand Down
25 changes: 20 additions & 5 deletions .github/scripts/release-integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
commit, and from 0.4.0 a successful container-image.yml run;
files every release file matches its line in the release's SHA256SUMS, and nothing
else is attached;
Rekor every file the release signs (it has a `.sigstore.json` bundle) is looked up in
Rekor by its SHA-256. An entry counts only if its signed entry timestamp
bundles every archive and installer attached, every file in SHA256SUMS and SHA256SUMS
itself must carry a `.sigstore.json` bundle; this set does not depend on which
bundles are attached, so deleting a bundle is reported;
Rekor every one of those files is looked up in Rekor by its SHA-256. An entry counts only if its signed entry timestamp
verifies with Rekor's key and its certificate chains to Sigstore's
certificate authority at the time it was logged: Rekor accepts any
certificate, so one that does not chain could claim any identity. At least
Expand Down Expand Up @@ -70,6 +72,10 @@
ACCEPTED_RUNS: dict[int, str] = {}

VERSION_TAG = re.compile(r"^v(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$")
# The release assets release.yml signs, by name (the "Sign every archive and SHA256SUMS" step).
SIGNED_ASSET = re.compile(
r"hodeishield-.*\.(tar\.gz|zip|msi|pkg|rpm|cdx\.json)|hodeishield[-_].*\.deb|install\.sh"
)
IMAGE_SIGNATURE_TAG = re.compile(r"^sha256-[0-9a-f]{64}(\.sig|\.att|\.sbom)?$")


Expand Down Expand Up @@ -232,6 +238,12 @@ def download(url: str) -> bytes:
return body


def must_be_signed(assets, sums) -> set[str]:
"""The files that must carry a Sigstore bundle, whichever bundles are attached: SHA256SUMS,
every file it lists and every attached file that release.yml signs by name."""
return {"SHA256SUMS", *sums} | {name for name in assets if SIGNED_ASSET.fullmatch(name)}


def parse_sha256sums(text: str) -> dict[str, str]:
sums = {}
for line in text.splitlines():
Expand Down Expand Up @@ -575,9 +587,12 @@ def problem(text: str):
):
problem(f"{tag}: {name} is attached but not in SHA256SUMS")

signed = [name for name in digests if f"{name}.sigstore.json" in assets]
if not signed:
problem(f"{tag}: no file has a Sigstore bundle")
signed = []
for name in sorted(must_be_signed(assets, sums)):
if f"{name}.sigstore.json" not in assets:
problem(f"{tag}: {name} has no Sigstore bundle")
elif name in digests:
signed.append(name)
for name in signed:
matched, wrong = judge_rekor(
rekor_entries(digests[name], trust), repository=repository, tag=tag, releases=signers
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ jobs:
- name: shellcheck
run: shellcheck -s sh install.sh packaging/repo/*.sh packaging/build/*.sh

# packaging/gpg.key, the README and the constant in keycheck.sh must all name the same key, and
# the file must hold that one key and no other.
- name: The repository key is the pinned one
run: sh packaging/repo/keycheck.sh packaging/gpg.key README.md

- name: Every curl call in install.sh is HTTPS-only
run: |
awk '
Expand Down Expand Up @@ -302,7 +307,9 @@ jobs:

# The repository freshness monitor must go red: its control builds small repositories with a
# throwaway key, serves them from localhost and checks that every stale, altered, unsigned or
# missing case fails and the current one passes.
# missing case fails and the current one passes. It also checks the live switch: inert only when
# the domain serves nothing (all 404); a live repository without the key, or a fetch error that is
# not a 404, is red.
repo-freshness:
name: repo-freshness
runs-on: ubuntu-latest
Expand Down
26 changes: 22 additions & 4 deletions .github/workflows/macos-signing-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ on:

permissions: {}

# The Apple Team ID the Developer ID certificates must belong to: repository policy, changed only
# through a reviewed pull request. Empty until a maintainer sets it, so signing fails closed. The
# same value is in release.yml; keep the two in step.
env:
MACOS_TEAM_ID: ""

concurrency:
group: macos-signing-check
cancel-in-progress: false
Expand All @@ -42,6 +48,12 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
run: |
set -euo pipefail
# The expected Apple Team ID is repository policy (MACOS_TEAM_ID above), not a secret: the
# certificates in the environment must belong to it. Empty or malformed fails closed.
if ! [[ "$MACOS_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]]; then
echo "::error::MACOS_TEAM_ID is not set to a 10-character Apple Team ID in this workflow."
exit 1
fi
missing=""
for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PASSWORD MACOS_SIGNING_IDENTITY \
MACOS_INSTALLER_CERTIFICATE MACOS_INSTALLER_CERTIFICATE_PASSWORD \
Expand Down Expand Up @@ -116,8 +128,8 @@ jobs:
exit 1
fi
done
if [ -z "$team" ] || [ "$team" = "not set" ]; then
echo "::error::The signature carries no TeamIdentifier."
if [ "$team" != "$MACOS_TEAM_ID" ]; then
echo "::error::The signature's TeamIdentifier is not the expected MACOS_TEAM_ID."
exit 1
fi

Expand Down Expand Up @@ -171,8 +183,8 @@ jobs:
bin="$RUNNER_TEMP/check/signing-check"
pkg="$RUNNER_TEMP/signing-check.pkg"
team="$(codesign --display -vv "$bin" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
if [ -z "$team" ] || [ "$team" = "not set" ]; then
echo "::error::The signature carries no TeamIdentifier."
if [ "$team" != "$MACOS_TEAM_ID" ]; then
echo "::error::The signature's TeamIdentifier is not the expected MACOS_TEAM_ID."
exit 1
fi
accepted=false
Expand All @@ -192,6 +204,12 @@ jobs:
echo "::error::The package is not signed with a Developer ID Installer certificate."
exit 1
fi
# The installer certificate's Team ID is the last parenthesis of its name.
pkg_team="$(sed -n 's/^ *[0-9]*\. Developer ID Installer: .* (\([A-Z0-9]\{10\}\))$/\1/p' <<< "$sig")"
if [ "$pkg_team" != "$MACOS_TEAM_ID" ]; then
echo "::error::The installer certificate's Team ID is not the expected MACOS_TEAM_ID."
exit 1
fi
xcrun stapler validate "$pkg"
spctl --assess --type install --verbose=2 "$pkg"
{
Expand Down
15 changes: 11 additions & 4 deletions .github/workflows/package-managers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:

- name: Upload the manifests
if: steps.version.outputs.version != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: package-manifests
path: out/
Expand Down Expand Up @@ -133,15 +133,15 @@ jobs:

- name: Download the manifests
if: steps.app.outputs.ready == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: package-manifests
path: out

- name: Mint a token for the tap and the bucket
if: steps.app.outputs.ready == 'true'
id: token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.PACKAGING_APP_CLIENT_ID }}
private-key: ${{ secrets.PACKAGING_APP_PRIVATE_KEY }}
Expand Down Expand Up @@ -232,6 +232,8 @@ jobs:
echo "::error::packaging/gpg.key (the public key of the signing subkey) is not in this release; the repositories cannot be published."
exit 1
fi
# Exactly one key, and the pinned one, before anything is signed or uploaded.
sh packaging/repo/keycheck.sh packaging/gpg.key

- name: Install cosign
if: steps.config.outputs.ready == 'true'
Expand Down Expand Up @@ -290,8 +292,13 @@ jobs:
set -euo pipefail
mkdir -m 700 "$GNUPGHOME"
printf '%s\n' "$SIGNING_KEY" | gpg --batch --quiet --import
# The public key carries the current binding signatures: after a renewal it is newer than the
# secret's copy, whose subkey gpg would refuse to sign with once the old expiry has passed.
gpg --batch --quiet --import packaging/gpg.key
# Only a signing subkey that is usable and whose secret is present counts: not a stub without
# its secret (field 15 is #), nor a revoked, expired or invalid one (field 2 is r, e or i).
fingerprints=$(gpg --batch --with-colons --list-secret-keys |
awk -F: '$1 == "ssb" { sign = ($12 ~ /s/) } $1 == "fpr" && sign { print $10; sign = 0 }')
awk -F: '$1 == "ssb" { sign = ($12 ~ /s/ && $15 !~ /#/ && $2 !~ /[rei]/) } $1 == "fpr" && sign { print $10; sign = 0 }')
if [ "$(printf '%s\n' "$fingerprints" | grep -c .)" != 1 ]; then
echo "::error::PACKAGES_GPG_SIGNING_KEY must hold exactly one signing subkey."
exit 1
Expand Down
Loading
Loading