Repository navigation
Releases: Hodeitek/hodeishield-attest-verifier
Release list
v1.6.2
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. NOTICEstates that the HodeiShield name and logo are trademarks of Hodeitek S.L. and are not covered by the Apache License.VERIFIER_VERSIONis 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.gzverify-attestation.sh sha256: 0f42748b168242730a05f356040b0f1326722970bb8f5d2097c47661a8f46c3f
Full changelog: v1.6.1...v1.6.2
v1.6.1
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
algis 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 nokid: with a key set the run ends inunknown_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.gzis built reproducibly fromweb/and the pinned module, listed inSHA256SUMS, 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/v1run 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_VERSIONis 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.mdlisted it as exit 1; the documentation now matches the behaviour. - The protected header is read as JSON (#106).
alg,typandkidcome from the JSON parse that checks the member set, not from asedpattern. 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 listintogrep -qunderpipefail, which could reportopenssl_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.gzverify-attestation.sh sha256: 6ee68950b4c715359c8b1af33f37dc4ccc5acd034a9be8890457867f1c2b6b09
Full changelog: v1.5.0...v1.6.1
v1.5.0
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_presentcheck 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.jsonis 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).
tools/mldsa-vectorsrunsaws-lc-rs's ML-DSA-65 overtests/vectors/v1. On the 47 cases it can check, it agrees with the script: 0 disagree.- The evaluation of Rust ML-DSA implementations is in docs/security/rust-mldsa-evaluation.md.
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_largeandstatus_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
~/.curlrcwithcompressedorlocationno longer affects a status fetch. The script passes--no-compressed --no-location, so a small gzip body cannot grow past--max-filesize. --status-listwithoutjq(#18).- The README, the container image and CI no longer install
jq. - The
jq_missingreason code is retired: it stays documented, is never emitted, and is not reused.
- The README, the container image and CI no longer install
- A licence header in the script (#79).
verify-attestation.shnow carriesSPDX-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-slimis pinned tosha256: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 verifywithout--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.jsonverify-attestation.sh sha256: 4e9360ae341cab2a5e1a1bebbd7262385a99bf7bcf116cbd909ee9195d721ba5
Full changelog: v1.4.2...v1.5.0
v1.4.2
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-keysand--statusURLs matched URL prefixes, so some URLs whose host is not loopback counted as loopback. Onlyhttp://localhost,http://127.0.0.1andhttp://[::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
~/.curlrcand 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-filedoes not already decide. Use--anchor-fileto bind the status key.
- The exception for
Fixed
- The container image is built from this release on. The image job for v1.4.0 downloaded only some of the files that
SHA256SUMSlists, so no v1.4.0 image exists. It now reads the list from the verifiedSHA256SUMS;ghcr.io/hodeitek/hodeishield-attest-verifier:v1.4.2is the first image. See A signed image for how to verify it. - Escaped command-line values (#71).
--expect-slug,--expect-issuer,--expect-nonce, the--statusand--status-keysURLs, 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 ifsemgrep --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.jsonverify-attestation.sh sha256: 23dc7a87ede8a68dad5300eec966e3f73bec1b5badd35aed0db0f1bca54fa4a2
Full changelog: v1.4.0...v1.4.2
v1.4.0
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-subjectand--check-generated-atreplaced the document's own kid, slug andgeneratedAtin the revocation check, so a document signed by a revoked key, or with a withdrawn subject, could endGOOD(exit 0). The document's own values are now always checked, and these options add one more check;--check-kidmust have the shape of a key ID.- Every
python3the verifier runs is nowpython3 -I -X utf8, so ajson.pyin the current directory,PYTHON*variables or the locale cannot change a check. - Numeric options are validated before use and never evaluated as shell arithmetic:
--nowand--max-age-dayscould run commands, and--max-age-seconds 60smade the freshness check pass. - Every internal variable is initialised when the script starts, so a standalone
--status-listquery no longer reads exported environment variables. - Status-list options given without
--status-listwere 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-slugand similar comparisons are against the signed value; a NUL byte is a failed check; --nowafter 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
slugorissis a failed check (slug_empty,iss_empty, exit 1).
- values read from a document keep their trailing newlines, so
--anchor-fileis 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_validcompares 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
--jsonfixes:anchor.verifiedis true only when every anchor check that applies ran and held; the string encoder is no longer redefined. - Documentation: the
python3commands in the README anddocs/security/run aspython3 -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-fileresult. - Exit codes do not change; an argument error is JSON too (
reasonusage, exit 2). - Schema
hodeishield.verifier.result.v1, in docs/security/json-output.md.
- 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
- 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-blobon 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).
--versionnow shows the verifier's version. - Needs cosign 3.1.3 or later, for this option only. See Anchor the key and the decision record.
- Runs
keys-statement.json, a new release asset (#62).- The machine-readable list of the issuer's signing keys (schema
hodeishield.keys.statement.v1, sourcedocs/security/keys.json, kept identical tokeys.mdby a CI check). - Listed in
SHA256SUMSand signed with Sigstore (keys-statement.json.sigstore.json). Use it with--anchor-file. - A daily check compares the live key sets with
keys.json.
- The machine-readable list of the issuer's signing keys (schema
- 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.ymlbuilds 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 markedrequires: ["cosign"]and skipped where cosign is missing. tests/vectors.sh --jsonchecks the--jsonschema against every published vector.tests/run.sh: new cases for every change above. Several self-checks that could pass without checking (an unevaluable--jsoncheck, the forged-verdict check, the globals check, the duplicate-function check) now fail when they should, and check themselves.- CI:
VERIFIER_VERSIONmust 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.jsonverify-attestation.sh sha256: dc6f227a0ae92d9e1fe8e9852fb0d4b22bfe6868f2690a6a0d91497348b01123
Full changelog: v1.3.0...v1.4.0
v1.3.0
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
VERIFIEDline. - 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
\xHHescapes.
- An edited document could make stdout show a forged "Attested content" block and a
-
Retired signing keys are enforced (#56). A key marked with
hs_retired_at(RFC 3339 UTC, seconds):- a document whose signed
generatedAtis at or after its retirement, or that has nogeneratedAtto compare, fails withretired_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.
- a document whose signed
-
Key sets that cannot be read one way are refused: a repeated
kid, orkeysthat is not an array of objects.--jwksexits 2;--status-keysexits 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
--rawand 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-b64urltogether with--jwksis 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.
- It requires the signing key's
-
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.mdand 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.shskips 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.shverify-attestation.sh sha256: 0e7d4a27f2834a69a7173731a7589bca6566355cedce11dfc9a4b71c25745552
Full changelog: v1.2.1...v1.3.0
v1.2.1
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
opensslon macOS and what Homebrew'slibresslinstalls. 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:
- that
openssl versionnames OpenSSL; - that its version is at least 3.5;
- that
openssl list -signature-algorithmsoffers 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.jsonwas 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.shverify-attestation.sh sha256: 16dcf76978f0556e259385f676699a8bf11abaa94288ae1cc05ddf596b1ce7d0
Full changelog: v1.2.0...v1.2.1
v1.2.0
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.
- the input files and the verifier arguments, with a fixed
- 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 intests/vectors/v1/README.md, and the reference runner istests/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.shchecks this property. It builds a copy of the verifier with the signature checks disabled, and requires that copy to accept everysignature_onlycase. That proves the reference rejects them because of the signature and nothing else.- 13 structural vectors cover signature size, a substituted
algand kid mismatches. Separate checks reject these before the signature is examined. tests/run.shruns 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.shverify-attestation.sh sha256: 2838c9295365e4a459198b3fa3173472c58b012f3858c13a4941aa7b4ac41010
Full changelog: v1.1.0...v1.2.0
v1.1.0
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:
It is followed by the advice to request a new attestation and the
EXPIRED — the signature is valid, but this attestation expired on <expiresAt>.curlcommand 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 reportsREVOKED.
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.
- an organisation shares the URL
- Run it without installing anything (#33). A
docker run/podman runcommand ondebian: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.shruns 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.shverify-attestation.sh sha256: 2838c9295365e4a459198b3fa3173472c58b012f3858c13a4941aa7b4ac41010
Full changelog: v1.0.1...v1.1.0
v1.0.1
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
--claimsfile: a duplicated member anywhere, including theattestationwrapper, orclaims/signatureboth 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.shverify-attestation.sh sha256: 4fa3f89e84e2812e37f513456d407a56940a1cac8bfb1f6c7ca0f695f816f75d
Full changelog: v1.0.0...v1.0.1