github Institut-du-Numerique-Responsable/perf-sentinel v0.28.0

latest release: chart-v0.28.0
3 hours ago

What's new in v0.28.0

v0.28.0 is about periodic disclosures you can stand behind. A signed disclosure could carry figures edited after signing and still verify, an official report could claim a quarter its archive had pruned down to its last hours, and the default archive never held a quarter in the first place. The signature now covers the figures it was published with, verify-hash checks that it does, disclose refuses a period its archive no longer reaches, and the daemon writes daily rollups that hold a whole quarter on the default settings. No detector, normalizer or signature changes.

A signature that covers the figures

disclose --emit-attestation wrote an in-toto statement whose subject was the SHA-256 of report.json's bytes at disclose time. The documented signing flow then patched integrity.signature and integrity_level into report.json, so the published bytes no longer matched the signed subject, and verify-hash never read that subject anyway: it ran cosign verify-blob on the attestation alone. A report whose figures were edited after signing, its hash re-baked with hash-bake --allow-signed, kept its original attestation and bundle and could reach TRUSTED.

The statement's subject is now the report's canonical content hash, which leaves the signature locator and the integrity level out and so survives the patch. Its predicate says so with "subject_binding": "content_hash". verify-hash gains an Attestation binding check, run when an attestation is passed or fetched by --url, and TRUSTED now requires it. It fails, and the verdict turns UNTRUSTED, when the statement's _type, predicateType or predicate.report_uuid do not match the report, or when its subject is not the recomputed content hash. A CI job now replays the attack end to end with a real keyless signature: the untouched disclosure verifies TRUSTED, the re-baked one UNTRUSTED on the binding alone.

A statement written by 0.27.0 or earlier stays bound while the report bytes it pinned are untouched. Once integrity.signature has been patched in, it is reported as unbindable and caps the verdict at PARTIAL: emit the attestation again with this release and sign it again.

An official period the archive no longer reaches

The window archive rotates at max_size_mb and prunes to max_files by count, and a busy daemon writes one full report every trace_ttl_ms / 2. A lab measurement under synthetic load found lines of 350 to 900 KiB, which filled the default 12 files of 100 MiB in 6 to 16 hours. The pruned files left no trace, since the hash chain restarts with each file, so an official report could cover a quarter's last hours under the whole quarter's name.

disclose now checks that the oldest window across its inputs falls on the period's first day or earlier. An official run exits 2 without writing a report, an internal run warns, and both name the archive's first day so --period-type custom --from <that day> discloses what is still covered. When the inputs hold chained lines, only lines whose chain verifies count, so a line without a hash put in front of a chained archive cannot stand in for the pruned windows. An archive written before chaining still counts by its oldest line.

Daily rollups that hold a quarter

With [daemon.archive] configured, the daemon also writes one hash-chained line per UTC day to <stem>-daily-YYYY-MM.ndjson beside the window archive. Each line holds the aggregator's own state for that day's windows, a few tens of KiB, so a quarter takes a few MB. The chain runs across months and restarts, so a removed or edited day shows as a break. A snapshot every 20 windows bounds what a crash costs, and is never appended twice, a day the directory refuses is retried at the next close, and monthly files go once older than rollup_retention_days, 400 by default. [daemon.archive] windows = false keeps the rollups and stops the per-window lines.

disclose reads rollup files, window files or both, recognises rollup files by their content, and counts each window once: from the rollup line whose span holds it, from its window line otherwise, host by host. The upgrade day's earlier windows therefore still count. A property test over generated archives checks that rollups and windows give the same figures, up to floating-point rounding. /api/status reports how far back the rollups reach as disclosure_archive, backed by three label-free gauges, perf_sentinel_archive_oldest_day_timestamp_seconds, perf_sentinel_archive_rollup_days and perf_sentinel_archive_last_closed_day_timestamp_seconds. In the lab, disclose over a daemon's rollups alone gave the same window and anti-pattern counts as over its window lines.

Upgrade impact

  • perf-sentinel-core breaks struct literals of PerfSentinelPredicate and DaemonArchiveConfig. The first gains subject_binding, the second windows and rollup_retention_days, and neither is #[non_exhaustive], which is why this release bumps the minor. build_in_toto_statement keeps its signature, but its digest argument is now the canonical content hash without its sha256: prefix.
  • An attestation emitted by 0.27.0 or earlier no longer reaches TRUSTED once the report has been patched. It reads PARTIAL. Emit and sign the attestation again with this release.
  • An official disclosure over a pruned archive exits 2. A pipeline that ran disclose --intent official on a busy daemon's archive now stops there. Disclose the covered span with --period-type custom, or let the daily rollups accumulate from the period's first day.
  • The daemon writes new files beside its archive. One <stem>-daily-YYYY-MM.ndjson per month and a <stem>-daily.partial.json snapshot, owner-only, in the directory of [daemon.archive] path.
  • Daily rollups and window lines can differ in their last digits. The same period disclosed from each can therefore carry a different content_hash. Disclosing the same inputs twice stays reproducible.
  • rustls moves to 0.23.46 against RUSTSEC-2026-0346 to RUSTSEC-2026-0349. Only the unauthenticated ServerHello that could steer later connections to classical key exchange applies, to the outbound HTTP client.
  • No finding appears, disappears or changes signature, acknowledgments keep matching, /api/status and /metrics only gain fields, the published report schema does not change, the embedded reference data keeps its vintages, and MSRV stays 1.99.0.

Full detail in CHANGELOG.md.

Verifying this release

# Binary integrity via SLSA build provenance attestation (Build Level 2)
gh attestation verify perf-sentinel-linux-amd64 \
  --repo Institut-du-Numerique-Responsable/perf-sentinel

# A periodic disclosure produced by this binary, with its attestation and bundle
perf-sentinel verify-hash --report perf-sentinel-report.json \
  --attestation perf-sentinel-report.intoto.jsonl --bundle perf-sentinel-report.sig \
  --expected-identity "https://github.com/Institut-du-Numerique-Responsable/perf-sentinel/.github/workflows/release.yml@refs/tags/v0.28.0" \
  --expected-issuer "https://token.actions.githubusercontent.com" \
  --verify-binary ./perf-sentinel-linux-amd64

gh CLI 2.49 or newer required for gh attestation verify. Releases up to v0.26.2 were attested under robintra/perf-sentinel and keep their own --repo and identity.

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.