Skip to content

Releases: Hodeitek/hodeishield-attest-verifier

v1.6.2

Choose a tag to compare

@github-actions github-actions released this 16 Oct 08:34
v1.6.2
c2a1508

verify-attestation.sh changes in this release. The new sha256 is 0f42748b168242730a05f356040b0f1326722970bb8f5d2097c47661a8f46c3f; v1.6.1's was 6ee68950…. If you pin the script by hash, update the pin. The script changes only in VERIFIER_VERSION (1.6.2). The page checks exactly what it checked before and gives the same verdicts and reason codes. Only its look changes.

This release has the same ten files as v1.6.1. The page bundle is verify-page-v1.6.2.tar.gz.

Verify in your browser

The page is the asset verify-page-v1.6.2.tar.gz, with sha256 5ed1c6b65d6ac2840c011a48f70d912a0a58b8967108eeb044151e558badd925. It is listed in SHA256SUMS, which is signed.
Unpack it and open index.html from your disk, or compare it with the files the site serves: see docs/security/browser-verifier.md.

Changed

  • The in-browser page has a new look.
    • It is a dark page only, with no light variant. Printing a result still gives black on white.
    • The HodeiShield logo and wordmark are in the header, and the logo is the browser icon.
    • Text is set in Plus Jakarta Sans, and codes and hashes in JetBrains Mono. Both fonts are bundled with the page under the SIL Open Font License, in web/fonts/ with their licence texts. Nothing is fetched from another site.
    • A result keeps its word and its glyph. Its colour now appears only on the border, the glyph and the word.
  • The page's Content-Security-Policy gains font-src 'self', so the browser may load the bundled fonts from the page's own origin. Nothing else in the policy changes.
  • NOTICE states that the HodeiShield name and logo are trademarks of Hodeitek S.L. and are not covered by the Apache License.
  • VERIFIER_VERSION is 1.6.2. The page carries the same number.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  SHA256SUMS
cosign verify-blob --bundle keys-statement.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  keys-statement.json
cosign verify-blob --bundle verify-attestation.sh.spdx.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh.spdx.json
cosign verify-blob --bundle verify-page-v1.6.2.tar.gz.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-page-v1.6.2.tar.gz

verify-attestation.sh sha256: 0f42748b168242730a05f356040b0f1326722970bb8f5d2097c47661a8f46c3f

Full changelog: v1.6.1...v1.6.2

v1.6.1

Choose a tag to compare

@github-actions github-actions released this 15 Oct 11:44
v1.6.1
fabcde2

verify-attestation.sh changes in this release. The new sha256 is 6ee68950b4c715359c8b1af33f37dc4ccc5acd034a9be8890457867f1c2b6b09; v1.5.0's was 4e9360ae…. If you pin the script by hash, update the pin.

v1.6.0 was tagged but never released. Its release job refused a good page bundle, and nothing was signed or published. v1.6.1 is v1.6.0 with that job fixed, so everything below is released for the first time here.

An in-browser verifier page. It runs the same checks as the script, works offline from disk, and ships with this release as the signed asset verify-page-v1.6.1.tar.gz. The bash script remains the reference verifier.

Output changes. Some malformed inputs now end with a different exit code or reason code:

  • A JWS segment, public key or status-list segment that is not strict base64url is "could not check" (exit 2, or 3 for a status list) under the reason code of its segment. It used to end later under another reason, sometimes signature_invalid (exit 1).
  • A public key that is not 1952 bytes is public_key_size_invalid, exit 2. A key one byte too long used to end in exit 1.
  • A header whose alg is written with a JSON escape, or whose member name, colon and value sit on separate lines, now verifies. A header that is not JSON, or is nested more than 64 containers deep, has no kid: with a key set the run ends in unknown_kid (exit 2).

This release has ten files. The new ones are verify-page-v1.6.1.tar.gz and its signature bundle. The page bundle is listed in SHA256SUMS and signed like the rest.

Verify in your browser

The page is the asset verify-page-v1.6.1.tar.gz, with sha256 765bddda50431e137226627c1a18b7ad9e7c7f387ba9bf9241fb1bbf2309723f. It is listed in SHA256SUMS, which is signed.
Unpack it and open index.html from your disk, or compare it with the files the site serves: see docs/security/browser-verifier.md.

Added

  • The in-browser verifier page (#25).
    • The same checks as the script, in plain JavaScript, with no framework, no runtime dependency and no network request. It also works opened from disk.
    • The signature is checked by a reproducible WebAssembly build of aws-lc-rs's ML-DSA-65, never by the browser's WebCrypto. The page asks the module three known questions at start-up and gives no verdict if it answers wrongly.
    • The verdicts and reason codes of the script, a result a non-technical reader can act on, and the technical details behind a disclosure.
    • Six interface languages (Spanish, English, German, French, Italian, Basque; the Basque text awaits native review), light and dark, keyboard and screen-reader support.
    • A strict Content-Security-Policy: default-src 'none', scripts from the page itself and 'wasm-unsafe-eval' only.
    • It cannot check a signed key statement (--anchor-file), which needs Sigstore. Safari is not tested.
  • The page bundle in the release. verify-page-<tag>.tar.gz is built reproducibly from web/ and the pinned module, listed in SHA256SUMS, signed with Sigstore, and checked as a reader would before the draft is made.
  • The module's bytes are pinned in tools/mldsa-wasm/expected.sha256. CI and the release build fail on any other bytes.
  • Tests for the page. All 136 cases of tests/vectors/v1 run on the page as it ships, in headless Chrome and Firefox, from http and from disk. A differential test makes the page and the script agree on inputs the vectors do not cover. CI runs them on every pull request.

Changed

  • VERIFIER_VERSION is 1.6.1. The page carries the same number.

Fixed

  • Base64url is strict (#104). Segments are checked against A-Z a-z 0-9 - _ before they are decoded: no padding, no + or /, no spaces, no length of 1 modulo 4, no bits left over in the last character. A key written with leftover bits used to read as the same key.
  • A public key of the wrong size stops the run at once (#105) with public_key_size_invalid, exit 2. docs/security/reason-codes.md listed it as exit 1; the documentation now matches the behaviour.
  • The protected header is read as JSON (#106). alg, typ and kid come from the JSON parse that checks the member set, not from a sed pattern. A JSON escape is decoded, a value that is not a string reads as empty and fails its check, and nesting is limited to 64 containers.
  • The OpenSSL ML-DSA-65 check could misfire. The script piped openssl list into grep -q under pipefail, which could report openssl_mldsa65_missing (exit 2) on an OpenSSL that has ML-DSA-65. It now reads the whole list first. This never accepted anything.
  • The release job's bundle checks had the same pipe pattern, which refused the good v1.6.0 bundle and could have passed a bad one. They are now tools/verify-page/reader-check.sh, tested case by case.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  SHA256SUMS
cosign verify-blob --bundle keys-statement.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  keys-statement.json
cosign verify-blob --bundle verify-attestation.sh.spdx.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh.spdx.json
cosign verify-blob --bundle verify-page-v1.6.1.tar.gz.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.6.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-page-v1.6.1.tar.gz

verify-attestation.sh sha256: 6ee68950b4c715359c8b1af33f37dc4ccc5acd034a9be8890457867f1c2b6b09

Full changelog: v1.5.0...v1.6.1

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 07:43
v1.5.0
434b3fa

verify-attestation.sh changes in this release. The new sha256 is 4e9360ae341cab2a5e1a1bebbd7262385a99bf7bcf116cbd909ee9195d721ba5; v1.4.2's was 23dc7a87…. If you pin the script by hash, update the pin.

jq is no longer needed. --status-list reads the status list with python3, which the script already needs. No verdict, reason code or message changes for any document in the test suite or the published vectors.

Output changes. Exit codes keep their meaning. What prints differently:

  • An input over its new size limit is not read. A status list, status key set or status list header that is too large is UNKNOWN (exit 3) with a new reason code. This includes a download over the limit, which used to be a failed fetch (exit 2). An attestation header over 4,096 bytes is reported first as header_too_large, still exit 1.
  • The tools_present check reads "python3 present".
  • With --raw, the posture is always printed indented.

This release has eight files. The new one is verify-attestation.sh.spdx.json, an SPDX SBOM of the script, listed in SHA256SUMS and signed like the rest.

Added

  • An SBOM of the script (#78).
    • verify-attestation.sh.spdx.json is an SPDX 2.3 JSON document. It records the script's name, version, sha256, licence and supplier, and its runtime requirements as the README states them.
    • It is generated from the tagged commit, so the same tag gives the same bytes.
  • The live check runs the signed image (#22).
    • For the latest release, it verifies the image's signature and SBOM attestation as the README tells a reader to.
    • It then runs the production example inside the image, by digest only.
  • An independent ML-DSA-65 check of the vectors (#30).

Changed

  • Input size limits (#80).
    • The script reads at most 2,000,000 bytes of the status list, 1,000,000 of the status key set and 4,096 of a decoded protected header. A real header is about 100 bytes, the live list under 6,000 and the key set under 3,000.
    • Larger status inputs are UNKNOWN (exit 3), with the new reason codes status_unknown_document_too_large, status_unknown_keys_too_large and status_unknown_header_too_large.
    • A larger attestation header is a failed check (exit 1, header_too_large).
  • A padded status list no longer costs minutes and gigabytes (#80). The list is read in one pass per lookup. Padded to the limit, it takes about 10 seconds; it used to take up to three minutes and four gigabytes.
  • A ~/.curlrc with compressed or location no longer affects a status fetch. The script passes --no-compressed --no-location, so a small gzip body cannot grow past --max-filesize.
  • --status-list without jq (#18).
    • The README, the container image and CI no longer install jq.
    • The jq_missing reason code is retired: it stays documented, is never emitted, and is not reused.
  • A licence header in the script (#79). verify-attestation.sh now carries SPDX-License-Identifier: Apache-2.0. A CI check covers every script, workflow and Dockerfile.
  • Support period (#77). SECURITY.md states it.
    • Each major version gets security fixes for five years from its first release, so 1.x is supported until 2031-09-29.
    • An end-of-support notice comes twelve months before.
  • Coordinated disclosure (#76). SECURITY.md states the response times, the fix targets by severity, the disclosure timeline (at most 90 days) and a safe harbour.
  • Base image. debian:trixie-slim is pinned to sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f, for the README's container commands, the image and CI.

Fixed

  • The image's SBOM check. The README command and the image job's last step ran gh attestation verify without --predicate-type https://spdx.dev/Document/v2.3, so they got HTTP 404. Both now pass it.
  • The release job's tests (#84).
    • The release job and the pull request job now install the test packages from one list, and a check on every pull request fails if they drift.
    • The locale tests now run before a release is signed; they used to be skipped.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.5.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh
cosign verify-blob --bundle keys-statement.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.5.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  keys-statement.json
cosign verify-blob --bundle verify-attestation.sh.spdx.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.5.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh.spdx.json

verify-attestation.sh sha256: 4e9360ae341cab2a5e1a1bebbd7262385a99bf7bcf116cbd909ee9195d721ba5

Full changelog: v1.4.2...v1.5.0

v1.4.2

Choose a tag to compare

@github-actions github-actions released this 12 Oct 17:59
v1.4.2
28415bd

v1.4.1 was tagged but not released. The release job runs the test suite before it signs anything, and it did not install curl, which the new cleartext URL tests need, so the v1.4.1 tag failed them and nothing was signed or published. This release is v1.4.1 with that job fixed (#83); the script differs from the v1.4.1 tag's only in VERIFIER_VERSION. Everything below is released for the first time here.

verify-attestation.sh changes in this release. The new sha256 is 23dc7a87ede8a68dad5300eec966e3f73bec1b5badd35aed0db0f1bca54fa4a2; v1.4.0's was dc6f227a…. If you pin the script by hash, update the pin.

Output changes. Exit codes keep their meaning. Two things print differently: a value given on the command line and printed back, and curl's message about a failed fetch, now show control characters and non-ASCII bytes as \xHH; and a cleartext --status-keys URL whose host is not exactly localhost, 127.0.0.1 or [::1] is now refused instead of fetched with a warning.

Security

  • A stricter loopback check for cleartext status URLs (#70, #73; hardening, no advisory).
    • The exception for http:// --status-keys and --status URLs matched URL prefixes, so some URLs whose host is not loopback counted as loopback. Only http://localhost, http://127.0.0.1 and http://[::1], with an optional port, now count; the host is matched case-sensitively, and a ? or # directly after the host or port does not count.
    • A loopback fetch ignores ~/.curlrc and the proxy settings; with a proxy set, it used to go to the proxy in cleartext.
    • A {a,b} or [1-9] in a URL is no longer expanded into several fetches.
    • An attacker who can choose the URL could already serve their own keys over https, so no verdict changes that --anchor-file does not already decide. Use --anchor-file to bind the status key.

Fixed

  • The container image is built from this release on. The image job for v1.4.0 downloaded only some of the files that SHA256SUMS lists, so no v1.4.0 image exists. It now reads the list from the verified SHA256SUMS; ghcr.io/hodeitek/hodeishield-attest-verifier:v1.4.2 is the first image. See A signed image for how to verify it.
  • Escaped command-line values (#71). --expect-slug, --expect-issuer, --expect-nonce, the --status and --status-keys URLs, an unknown argument and curl's message about a failed fetch reached the terminal as given. They are now printed like the values of a document: control characters and bytes outside printable ASCII as \xHH.

Changed

  • More bash parsers read the script. The 10# arithmetic and two other constructs that Semgrep's bash parser rejected are written another way, with no change in behaviour. CI fails if semgrep --lang bash, pinned to an exact version, cannot parse the whole script.
  • README (#72): the "Attested content" block shows a non-printable character as ? and prints other text as UTF-8; check lines and messages use \xHH.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.4.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh
cosign verify-blob --bundle keys-statement.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.4.2" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  keys-statement.json

verify-attestation.sh sha256: 23dc7a87ede8a68dad5300eec966e3f73bec1b5badd35aed0db0f1bca54fa4a2

Full changelog: v1.4.0...v1.4.2

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 15:44
v1.4.0
5b4fdc2

verify-attestation.sh changes in this release. The new sha256 is dc6f227a0ae92d9e1fe8e9852fb0d4b22bfe6868f2690a6a0d91497348b01123; v1.3.0's was 0e7d4a27…. If you pin the script by hash, update the pin.

Output changes. The text output is unchanged and exit codes keep their meaning, with these exceptions, all in "Security" below: options that were silently ignored or malformed are now usage errors (exit 2), a few runs that ended VERIFIED or GOOD (exit 0) now end otherwise, and --anchor-file is new. If a wrapper passes --status, --status-keys, --check-kid, --check-subject, --check-generated-at or --min-seq without --status-list, or a non-numeric value to a numeric option, it now gets exit 2.

Security

  • Hardening of the standalone revocation query and the local environment (GHSA-h938-xchq-hhg4, medium; v1.0.0 to v1.3.0). No signature can be forged and no key is compromised; each issue needs a particular invocation or environment.
    • --check-kid, --check-subject and --check-generated-at replaced the document's own kid, slug and generatedAt in the revocation check, so a document signed by a revoked key, or with a withdrawn subject, could end GOOD (exit 0). The document's own values are now always checked, and these options add one more check; --check-kid must have the shape of a key ID.
    • Every python3 the verifier runs is now python3 -I -X utf8, so a json.py in the current directory, PYTHON* variables or the locale cannot change a check.
    • Numeric options are validated before use and never evaluated as shell arithmetic: --now and --max-age-days could run commands, and --max-age-seconds 60s made the freshness check pass.
    • Every internal variable is initialised when the script starts, so a standalone --status-list query no longer reads exported environment variables.
    • Status-list options given without --status-list were silently ignored; each is now a usage error (exit 2).
  • Other hardening (earlier releases are also affected):
    • values read from a document keep their trailing newlines, so --expect-slug and similar comparisons are against the signed value; a NUL byte is a failed check;
    • --now after 9999-12-31 is refused;
    • the subject rule, the TTL ceiling and the status-list validity ceiling compare exact instants, not whole seconds;
    • an empty signed slug or iss is a failed check (slug_empty, iss_empty, exit 1).
  • --anchor-file is new in this release, and its own fixes are included (no earlier release is affected): the bundle must be exactly a Sigstore v0.3 bundle before cosign runs, cosign is asked a second time for the exact identity of the certificate the release tag is read from, and the certificate must carry this repository's numeric GitHub ID.

Changed

  • Fixes to time and locale handling (earlier releases are also affected):
    • without GNU date (macOS), a time's offset could be ignored; it is now refused instead of misread;
    • not_yet_valid compares exact instants;
    • the verdict no longer depends on the locale: the protected header is read in the C locale and every file is read as bytes or UTF-8.
  • An option given without its value is a usage error (exit 2, error: --jws needs a value), not exit 1.
  • Input files are copied once into the private work directory and only the copy is read, so a file that changes during a run, or a pipe such as <(curl ...), is checked as one document.
  • Two --json fixes: anchor.verified is true only when every anchor check that applies ran and held; the string encoder is no longer redefined.
  • Documentation: the python3 commands in the README and docs/security/ run as python3 -I; §6 item 2 of the verification document no longer calls the Web PKI the only channel for the attestation key.

Added

  • --json (#38, #66).
    • One JSON object on stdout and nothing else: the verdict, the reason code that decided the exit code, every check, the attested content (only at exit 0; with --raw, also the signed posture), the unverified values of a document that did not verify, and the --anchor-file result.
    • Exit codes do not change; an argument error is JSON too (reason usage, exit 2).
    • Schema hodeishield.verifier.result.v1, in docs/security/json-output.md.
  • Machine reason codes for every check (#64), listed in docs/security/reason-codes.md. The text output is unchanged.
  • --anchor-file STATEMENT (and --anchor-bundle FILE) (#39, #65).
    • Runs cosign verify-blob on a release's key statement, with the signing identity fixed in the script, and requires the key that signed (and, with --status-list, the status-list key) to be listed under the right role with a matching retirement.
    • A statement that does not verify is exit 2, by design; a key it does not list is exit 1 (3 for the status-list key).
    • A key statement from a release older than the verifier is refused (exit 2). --version now shows the verifier's version.
    • Needs cosign 3.1.3 or later, for this option only. See Anchor the key and the decision record.
  • keys-statement.json, a new release asset (#62).
    • The machine-readable list of the issuer's signing keys (schema hodeishield.keys.statement.v1, source docs/security/keys.json, kept identical to keys.md by a CI check).
    • Listed in SHA256SUMS and signed with Sigstore (keys-statement.json.sigstore.json). Use it with --anchor-file.
    • A daily check compares the live key sets with keys.json.
  • A signed container image workflow (#22, #63). The container image was not built for v1.4.0; signed images start with the next release. image.yml builds the image from the release's verified script, signs it by digest with Sigstore keyless signing and attests an SBOM; the README ("A signed image") shows how to verify it.
  • An evaluation of Rust ML-DSA-65 implementations for a port of the verifier (#30, #61). It does not run any candidate against the test vectors; that stays open.

Tests and CI

  • Vectors: 3 appended to tests/vectors/v1/ for --anchor-file, with real Sigstore bundles. 118 in total; the first 115 are unchanged. They are marked requires: ["cosign"] and skipped where cosign is missing.
  • tests/vectors.sh --json checks the --json schema against every published vector.
  • tests/run.sh: new cases for every change above. Several self-checks that could pass without checking (an unevaluable --json check, the forged-verdict check, the globals check, the duplicate-function check) now fail when they should, and check themselves.
  • CI: VERIFIER_VERSION must equal the release tag and the changelog heading must be dated; a check of the latest release's key statement against the live workflow; the image is built and smoke-tested on pull requests.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.4.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh
cosign verify-blob --bundle keys-statement.json.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.4.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  keys-statement.json

verify-attestation.sh sha256: dc6f227a0ae92d9e1fe8e9852fb0d4b22bfe6868f2690a6a0d91497348b01123

Full changelog: v1.3.0...v1.4.0

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 04:32
v1.3.0
4341f5e

verify-attestation.sh changes in this release. The new sha256 is 0e7d4a27f2834a69a7173731a7589bca6566355cedce11dfc9a4b71c25745552; v1.2.1's was 16dcf769…. If you pin the script by hash, update the pin.

Output changes. The verifier prints claims about the organisation only for a document that verifies, and the full posture JSON only with --raw. Exit codes keep their meaning. If you read the band or the posture JSON from the output, see "Changed" below.

Security

  • Document fields were printed with their terminal control characters (GHSA-2mgq-37v5-x454, low; v1.0.0 to v1.2.1).

    • An edited document could make stdout show a forged "Attested content" block and a VERIFIED line.
    • The exit code and the verdict on stderr were always correct.
    • Every value taken from a document, key set or status list is now printed with control characters and non-ASCII bytes as visible \xHH escapes.
  • Retired signing keys are enforced (#56). A key marked with hs_retired_at (RFC 3339 UTC, seconds):

    • a document whose signed generatedAt is at or after its retirement, or that has no generatedAt to compare, fails with retired_key (exit 1);
    • a status list issued at or after the retirement of its key is unknown (exit 3);
    • a malformed marker makes the key set invalid.

    Earlier versions ignore the marker.

  • Key sets that cannot be read one way are refused: a repeated kid, or keys that is not an array of objects. --jwks exits 2; --status-keys exits 3.

Changed

  • Attested content only when verified (#14).
    • The overall band, frameworks, subject, visibility and dates appear in an "Attested content" block before the verdict, only when the document verifies.
    • Any other outcome prints attested content withheld: this document did not verify.
    • The full posture JSON, printed every time before, now needs --raw and follows the same rule.
  • Short --help (#15): the options, the exit codes and where to read more.
  • No references to a non-public document (#34). The messages cite §7.1 of docs/security/attest-verification.md, which has a new paragraph, "Unknown is not good".
  • --pub-b64url together with --jwks is a usage error (exit 2). Before, the key set was silently ignored.

Added

  • --expect-kid KID (#13).

    • It requires the signing key's kid, recomputed from the key bytes.
    • Repeat it to pin two keys through a rotation overlap.
    • A mismatch exits 1; a malformed value exits 2.
    • It is not --check-kid, and it does not apply to the status-list key.
  • docs/security/keys.md:

    • the current signing keys, their roles and dates;
    • how to check them yourself;
    • the rotation policy (12 months, 90-day overlap; retired keys stay published and marked);
    • how the file changes.

    The active attestation key is roeFReafBOA_WF3cqfilHA.

  • Compatibility policy (#20):

    • CHANGELOG.md and a README "Compatibility" section;
    • incompatible format changes are announced at least 90 days in advance;
    • only the latest release is supported.
  • Documentation:

    • issue templates;
    • links to the guides on docs.hodeishield.com;
    • the key fingerprint defined as the kid;
    • CONTRIBUTING "Issues" and "Design".

Tests and CI

  • Vectors: 14 appended to tests/vectors/v1/ for retired keys and ambiguous key sets. 115 in total; the first 101 are unchanged.
  • tests/run.sh: new cases cover every change above, including injected escape sequences.
  • Mutant check: VERIFIER_SKIP_MUTANTS=1 bash tests/run.sh skips it locally. CI always runs it and fails if the variable is set there.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.3.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh

verify-attestation.sh sha256: 0e7d4a27f2834a69a7173731a7589bca6566355cedce11dfc9a4b71c25745552

Full changelog: v1.2.1...v1.3.0

v1.2.1

Choose a tag to compare

@github-actions github-actions released this 06 Oct 21:25
v1.2.1
b564b3f

verify-attestation.sh changes in this release. The new sha256 is 16dcf76978f0556e259385f676699a8bf11abaa94288ae1cc05ddf596b1ce7d0; v1.2.0's was 2838c929…. If you pin the script by hash, update the pin.

Fixed

LibreSSL was reported as an ML-DSA capable OpenSSL (#48).

  • LibreSSL is the openssl on macOS and what Homebrew's libressl installs. It prints a version of 3.5 or later and has no ML-DSA.
  • The verifier printed PASS OpenSSL 4.1.2 (ML-DSA capable) and then failed with an unexplained "OpenSSL rejected the reconstructed public key".
  • The exit code was already correct (2, "could not check"). The output was not.

The toolchain gate now checks, in order:

  1. that openssl version names OpenSSL;
  2. that its version is at least 3.5;
  3. that openssl list -signature-algorithms offers ML-DSA-65.

Each failure says why ("not ML-DSA capable", or "is too old") and exits 2. On a capable system the PASS line now reads OpenSSL 3.5.7 offers ML-DSA-65. Exit codes and verdicts are unchanged for every supported toolchain, and all 101 published vectors pass unchanged.

Tests and CI

  • tests/run.sh: new assertions using PATH shims for a LibreSSL version string and for an OpenSSL without ML-DSA-65, 74 in total.
  • New CI job: "LibreSSL is exit 2, not 1" runs the verifier on a published vector with real LibreSSL in a digest-pinned Alpine image.

Maintenance

  • Renovate removed (#47). renovate.json was never acted on, because the app was never installed on the repository.
  • How pins are updated now:
    • The container image digests and the cosign version are bumped by hand in periodic dependency reviews, with a 7-day quarantine; see CONTRIBUTING, "Dependencies".
    • Dependabot keeps the SHA-pinned actions current.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.2.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh

verify-attestation.sh sha256: 16dcf76978f0556e259385f676699a8bf11abaa94288ae1cc05ddf596b1ce7d0

Full changelog: v1.2.0...v1.2.1

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 18:55
v1.2.0
4cb0425

verify-attestation.sh is byte-identical to v1.1.0 (sha256 2838c9295365e4a459198b3fa3173472c58b012f3858c13a4941aa7b4ac41010). This release adds tests and published vectors. The verifier's behaviour is unchanged, and if you pinned the v1.1.0 script you do not need to update it.

Test vectors for other implementations

#24: tests/vectors/v1/ publishes 101 fixed vectors for attest.attestation.v1 and attest.statuslist.v1, for anyone writing their own verifier.

  • Each vector gives:
    • the input files and the verifier arguments, with a fixed --now;
    • the expected exit code;
    • a stable reason code;
    • the reference script's output line.
  • Valid documents also carry the sha-256 of their canonical bytes, to help debug an encoder.
  • The keys are committed and marked test-only; never trust anything they sign.
  • The set is append-only and protected by SHA256SUMS. Its schema and evolution policy are in tests/vectors/v1/README.md, and the reference runner is tests/vectors.sh.

Proof that the signature check is what rejects forgeries

  • 24 vectors are marked signature_only. Every check except the signature passes on them, so only the ML-DSA-65 verification stands between them and exit 0. They cover:
    • one changed payload value;
    • signature bytes flipped at the same length;
    • a signature by another key under the issuer's kid;
    • a signature transplanted from another attestation;
    • a consistent kid switch;
    • the status-list equivalents.
  • tests/mutants.sh checks this property. It builds a copy of the verifier with the signature checks disabled, and requires that copy to accept every signature_only case. That proves the reference rejects them because of the signature and nothing else.
  • 13 structural vectors cover signature size, a substituted alg and kid mismatches. Separate checks reject these before the signature is examined.
  • tests/run.sh runs the vectors and the mutant check in CI and in the release workflow.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.2.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh

verify-attestation.sh sha256: 2838c9295365e4a459198b3fa3173472c58b012f3858c13a4941aa7b4ac41010

Full changelog: v1.1.0...v1.2.0

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 17:46
v1.1.0
d9109f5

Changed

A genuine expired attestation no longer reads like a tampered one (#32). Every attestation is valid for at most one hour, so most people who verify a real one a little late hit this case.

  • If the signature verifies and the only problem is age or expiry, the last line now reads:
    EXPIRED — the signature is valid, but this attestation expired on <expiresAt>.
    
    It is followed by the advice to request a new attestation and the curl command that fetches a fresh copy.
  • The exit code is still 1. An expired attestation is still not one to rely on, and integrations that test for 1 are unaffected.
  • Any other failure, alone or combined with expiry, still ends with VERIFICATION FAILED … Do not rely on this document.
  • With --status-list, a revoked key or subject still reports REVOKED.

Documentation

  • Share the link, not the file (#31). The README now says:
    • an organisation shares the URL https://app.hodeishield.com/api/public/attest/<slug>, or the slug;
    • how to read the slug off that URL;
    • fetch the document right before verifying, because a file forwarded by email or attached to a questionnaire fails the freshness check.
  • Run it without installing anything (#33). A docker run / podman run command on debian:trixie-slim, pinned by digest (the same image CI uses).
    • It is the supported route on macOS, Windows (Docker Desktop + WSL) and Ubuntu 24.04 LTS.
    • A second command checks revocation with --status-list.
    • The exit codes are the native ones.
  • CONTRIBUTING states the working language of the project (English).

Tests

  • tests/run.sh (70 assertions) checks that expired and tampered documents share exit 1 but never print each other's last line. This includes combinations with a wrong slug, an unsigned member and revocation lists.
  • tests/container.sh runs both README container commands exactly as written, with Docker and Podman, in a new "Container route" workflow:
    • against the live example;
    • against offline minted documents: valid, tampered, under an empty status list, and under a status list that revokes the key.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.1.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh

verify-attestation.sh sha256: 2838c9295365e4a459198b3fa3173472c58b012f3858c13a4941aa7b4ac41010

Full changelog: v1.0.1...v1.1.0

v1.0.1

Choose a tag to compare

@github-actions github-actions released this 29 Sep 13:56
v1.0.1
d9e452b

Hardening

The verifier now rejects any JSON member that appears more than once in the same object (duplicate_key).

JSON leaves duplicate member names undefined, and parsers disagree about which copy they keep. Every parser the script uses keeps the last one. In a document carrying duplicates, that was the signed value, so no verdict changed: v1.0.0 already accepted or rejected on the signed values. What could mislead was the output. A reader or tool keeping the first copy could see a value nobody signed, printed under the line "every member of the claims JSON is covered by the signature".

In v1.0.1:

  • Attestation or --claims file: a duplicated member anywhere, including the attestation wrapper, or claims/signature both beside and inside it, FAILs (exit 1). The coverage line is not printed.
  • Protected header: a duplicated member FAILs.
  • Key document (--jwks): a duplicated member, or anything that is not strict JSON, gives exit 2 ("could not check").
  • Status list, its key set and its header: a duplicated member, or anything that is not strict JSON, gives unknown (exit 3).
  • Strict parsing: the check requires UTF-8, no BOM and a single value, and walks the document without recursion. A file it cannot parse that way is treated as malformed, never as "no duplicates".
  • Key selection: the attestation key is selected from the JWKS as JSON, by kid.

tests/run.sh has 57 cases, each asserting both the exit code and the reason. The v1.0.0 script fails the 12 new ones.

The advisory GHSA-h44g-294g-9pm7, fixed in v1.0.0, is unchanged.

Verify this release

sha256sum -c SHA256SUMS
cosign verify-blob --bundle verify-attestation.sh.sigstore.json \
  --certificate-identity "https://github.com/Hodeitek/hodeishield-attest-verifier/.github/workflows/release.yml@refs/tags/v1.0.1" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  verify-attestation.sh

verify-attestation.sh sha256: 4fa3f89e84e2812e37f513456d407a56940a1cac8bfb1f6c7ca0f695f816f75d

Full changelog: v1.0.0...v1.0.1