Verify releases
Every release of myriarch is built and signed by one GitHub Actions workflow, release.yml of the repository
myriarch/myriarch, from a v<version> tag. The workflow signs keyless with Sigstore
(ADR-0023, ADR-0029): there is no
long-lived signing key to steal or to distribute. A signature is bound to a short-lived certificate that Sigstore issued to
that workflow run, and the certificate names the workflow and the tag.
This guide has two parts. Part 1 covers the files of a release and how the installers check them. Part 2 covers the build provenance, the immutable release, the container images, the Helm charts and the macOS binaries. Every command below uses the identity of the workflow at the tag of the version you verify. The release notes of every release show the same commands with the tag, the commit and the image digests filled in.
What is signed
A release contains the archives and the bare myriarch-updater binaries, the SBOMs of the archives and of the web UI, LICENSE,
THIRD_PARTY_NOTICES and three files that carry the trust:
| File | Content |
|---|---|
checksums.txt | the SHA-256 of the 39 other files of the release (archives, bare binaries, SBOMs and the two license files), one line <hex> <file name> per file |
checksums.txt.sigstore.json | the Sigstore bundle: the signature over checksums.txt, the certificate and the transparency log proof |
myriarch_<version>_provenance.intoto.jsonl | the GitHub build provenance (SLSA) of the same 39 files, as a Sigstore bundle (part 2) |
You verify one signed file and then check every other file against it. The identity of a release is exactly this certificate identity (the Subject Alternative Name of the certificate), with this issuer:
| Item | Value for the release v<version> |
|---|---|
| Certificate identity | https://github.com/myriarch/myriarch/.github/workflows/release.yml@refs/tags/v<version> |
| OIDC issuer | https://token.actions.githubusercontent.com |
Always give the complete identity of the version you want, not a pattern that accepts any tag. A signature of the same
workflow for an older tag is genuine, too, and would pass a pattern such as .../release.yml@refs/tags/v.*.
Verify with cosign
You need cosign 3.0.4 or newer (cosign version; older versions have known
verification flaws and the installers refuse them). Download checksums.txt and checksums.txt.sigstore.json and the files
you want from the release page of the tag, then:
VERSION=0.2.0 # the release you verify, without the "v"; the newest one is on https://github.com/myriarch/myriarch/releases
cosign verify-blob \
--bundle checksums.txt.sigstore.json \
--certificate-identity "https://github.com/myriarch/myriarch/.github/workflows/release.yml@refs/tags/v${VERSION}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
Verified OK means that GitHub Actions ran release.yml of myriarch/myriarch for the tag v${VERSION} and signed exactly this
checksums.txt. Then check the files you downloaded against it:
sha256sum --check --ignore-missing checksums.txt # Linux
shasum -a 256 --check --ignore-missing checksums.txt # macOS
Every file you downloaded must say OK. --ignore-missing skips the lines of files you did not download; a file that is not in
checksums.txt at all is not part of the release. On Windows compare one file at a time:
$file = 'myriarch-updater_0.2.0_windows_amd64.exe'
$lines = @(Get-Content checksums.txt | Where-Object { $_.EndsWith(" $file", [StringComparison]::Ordinal) })
if ($lines.Count -ne 1) { throw "expected exactly one line for $file in checksums.txt" }
$want = $lines[0].Split(' ')[0]
$got = (Get-FileHash -Algorithm SHA256 $file).Hash.ToLowerInvariant()
if ($got -cne $want) { throw 'SHA-256 mismatch' }
If a check fails, do not run the file. A failure of the first command means that the file is not the one the workflow signed, was signed by another identity or for another tag, or that the bundle does not belong to it. A failure of the second means that the file was modified or corrupted after signing, or that you downloaded another version.
Offline and behind a proxy
cosign verify-blob fetches the Sigstore trusted root over the network. For a host without access, fetch the trusted root once
on a connected machine and pass it with every check:
myriarch-updater fetch-trusted-root --out trusted_root.json
cosign verify-blob --trusted-root trusted_root.json --bundle checksums.txt.sigstore.json ... checksums.txt
The bundle contains the proof of the transparency log, so the check itself needs no further network access. Fetch a fresh trusted root now and then: Sigstore rotates its keys.
Verify the archive that contains the installer before you run it
The release archives of myriarch-updater (.tar.gz for Linux, .zip for Windows) also contain the installation scripts
install-linux.sh and install-windows.ps1. A script that runs as root or Administrator is only as trustworthy as its
download, so check the archive before you extract and run anything from it. Download, check and unpack it in one folder
that only root (on Windows: SYSTEM and the administrators) can write, so that nobody can replace a file between the check and
the run. A file that you checked in a folder that other accounts can write, such as a download folder, can be replaced there
before you unpack it. Connect a host has the complete
blocks for both systems: they create the folder and end at the first failed check.
On Linux, in a root shell on the host, in a directory that only root can write, such as one below /root:
VERSION=0.2.0 # the release, without the "v"
BASE="https://github.com/myriarch/myriarch/releases/download/v${VERSION}"
install -d -m 0700 /root/myriarch-setup && cd /root/myriarch-setup &&
curl -fsSL -O "${BASE}/checksums.txt" -O "${BASE}/checksums.txt.sigstore.json" \
-O "${BASE}/myriarch-updater_${VERSION}_linux_amd64.tar.gz" &&
cosign verify-blob --bundle checksums.txt.sigstore.json \
--certificate-identity "https://github.com/myriarch/myriarch/.github/workflows/release.yml@refs/tags/v${VERSION}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com checksums.txt &&
sha256sum --check --ignore-missing checksums.txt &&
tar -xzf "myriarch-updater_${VERSION}_linux_amd64.tar.gz"
A host without cosign gets its files from an admin host: check them there, copy checksums.txt and the archive into such a
directory on the host, and run the sha256sum and tar commands of the block there.
On Windows, a browser marks the files it downloads (the Mark of the Web), and the usual execution policy of a client
(Restricted) refuses every script. Run Unblock-File only after the checks have passed, never to get past the prompt
first. Work in a folder that only administrators can write (a folder below C:\Program Files, not C:\Temp, a user profile
or a download folder), such as the setup folder that
Connect a host creates. Download the three files into it, run
the checks above there, and unpack from it. The unpack names the folder in its path, so it never takes a zip from another
folder:
$Setup = 'C:\Program Files\myriarch-setup'
Set-Location $Setup -ErrorAction Stop
# download checksums.txt, checksums.txt.sigstore.json and the zip here and check them here, as shown above, then:
Expand-Archive "$Setup\myriarch-updater_0.2.0_windows_amd64.zip" -DestinationPath "$Setup\package"
Unblock-File "$Setup\package\deploy\scripts\install-windows.ps1"
Then start the installer in a new PowerShell process with the execution policy for that process only
(powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File ...) and do not change the policy of the machine; the
command is in Connect a host. A host with App Control for Business and
script enforcement runs an unsigned script in Constrained Language Mode whatever the execution policy says. Allow the script
there with a hash rule that you create from the copy you have just verified: the hash that App Control uses for a script is
not the SHA-256 in checksums.txt, so the rule comes from the file, not from the list. From v0.2.0 the installer recognizes the
mode before it does anything and stops with language_mode_unsupported
(Language mode and App Control).
What the installers verify
You do not need to run these steps for a host that you install with the scripts: they verify the release themselves. See Connect a host for the options.
- With
--updater-checksums-url(-UpdaterChecksumsUrl) the installer downloadschecksums.txtand its bundle (<url>.sigstore.json, or--updater-bundle-url), verifies the bundle against the identity above, and takes the SHA-256 of the updater from the line of the file name in--updater-url. The version of the identity is--updater-versionor the version in that file name; if both are given, they must agree. - The verifier is the file given with
--verifier, otherwisecosign3.0.4 or newer if it is inPATH, otherwise an already installedmyriarch-updater. It is never the updater that is being installed, because that binary is only trusted through the file the verifier has to check. - Checks before the installers change anything (from v0.2.0). They protect the host from a half installation. They do not
replace the verification above, and none of them removes anything. The Windows installer checks first that PowerShell runs it
in the full language mode (
language_mode_unsupported). Both installers stop at a package of the upstream project, whose collector service would run next to the one that myriarch manages (conflicting_install), and the Windows installer stops on ARM64 without-Arch amd64(unsupported_platform), all before they download or write anything. When the installation step of the supervisor is over, they check that the file is there: a regular file at the path of the service, on Windows with the SHA-256 that was verified (supervisor_missing). The causes and the next steps are in Connect a host. - Hosts without a verifier (no
cosign, no installed updater): verifychecksums.txton an admin host as shown above, take the SHA-256 of the updater from it and pass--updater-sha256(-UpdaterSha256). The installer then checks the downloaded updater against that hash and uses it to verify the collector and supervisor bundles. - Upgrades: run the installer again with the new version. On Linux a
cosign3.0.4 or newer inPATHverifies the bundle of the new release, and the updater that is already installed does so on a host withoutcosign(on Windows the installed updater comes beforecosign.exe); then the new updater replaces it.
Mirrors
A mirror must carry checksums.txt and checksums.txt.sigstore.json unchanged. A modified checksums.txt fails the check,
whatever the mirror signed it with. The enterprise mirror toolkit verifies a
release keyless (verify-release.sh --keyless) and copies it unchanged (publish-mirror.sh --keyless).
Part 2: provenance, immutability, images and macOS
Part 2 needs gh (the release workflow itself uses gh 2.102.0), cosign 3.0.4 or newer and, for the images, crane or Docker. Set the version and the tag once:
VERSION=0.2.0 # the release you verify, without the "v"; the newest one is on https://github.com/myriarch/myriarch/releases
TAG="v${VERSION}"
Verify the build provenance
Each of the 39 files that checksums.txt lists has a build provenance attestation: a statement, signed by the same workflow
identity, that GitHub Actions built this exact file from this repository, this commit and this tag. The online check asks
GitHub’s attestation API, and needs a login of gh (gh auth login, or GH_TOKEN):
gh release download "${TAG}" --repo myriarch/myriarch --pattern 'checksums.txt*' --pattern "myriarch-updater_${VERSION}_linux_amd64"
gh attestation verify "myriarch-updater_${VERSION}_linux_amd64" --repo myriarch/myriarch \
--signer-workflow "myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}" \
--source-ref "refs/tags/${TAG}" --deny-self-hosted-runners
--signer-workflow with @refs/tags/${TAG} pins the workflow and the tag from gh 2.102.0 on. Older versions match that
value only as a prefix (@refs/tags/v0.1.0 would also accept v0.1.0-rc.3) or do not escape it at all, so use gh 2.102.0 or newer.
--source-ref pins the tag the build ran for and is an exact check in every version, so keep it. --deny-self-hosted-runners
refuses a build that did not run on a GitHub-hosted runner. To pin the commit as well, add
--source-digest <commit> (the commit of the tag, shown in the release notes).
The provenance is also a release asset, myriarch_<version>_provenance.intoto.jsonl. With --bundle the check needs no login
and no call to the attestation API. It still needs the Sigstore trusted root: gh caches it for one day and fetches it again
after that, so an offline host passes a trusted root file with --custom-trusted-root trusted_root.json (gh attestation trusted-root prints the current one; myriarch-updater fetch-trusted-root writes the same kind of file, see
Offline and behind a proxy):
gh release download "${TAG}" --repo myriarch/myriarch --pattern "myriarch_${VERSION}_provenance.intoto.jsonl"
gh attestation verify "myriarch-updater_${VERSION}_linux_amd64" --repo myriarch/myriarch \
--bundle "myriarch_${VERSION}_provenance.intoto.jsonl" \
--signer-workflow "myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}" \
--source-ref "refs/tags/${TAG}" --deny-self-hosted-runners
The signature of checksums.txt and the provenance are two independent proofs of the same identity. Run both for a release you
depend on.
Check that a release is immutable
Myriarch publishes releases as immutable releases: once a release is published, GitHub locks its assets and its tag: the assets cannot be changed, added or removed, and the tag cannot be moved. GitHub signs a release attestation when it publishes. Ask for it:
gh release verify "${TAG}" --repo myriarch/myriarch
gh release verify-asset "${TAG}" checksums.txt --repo myriarch/myriarch
gh release verify checks that the release of the tag is immutable and that its attestation is valid. gh release verify-asset
checks that a file you downloaded is an asset of that release, so a swapped file fails. Both commands need a login of gh
(gh auth login, or GH_TOKEN) even though the release is public. If the first command reports that the release is not
immutable, do not rely on the contents of the assets; report it in the issue tracker.
gh release verify-asset is the proof that a file was released. A signature or an attestation of the release identity proves
that the workflow run for that tag created the file, but the workflow creates signatures, attestations and image signatures before
it verifies the release and before the second approval, also in runs that never published (a failed attempt, an abandoned
candidate, a “Re-run all jobs”). Those credentials stay valid for the tag; deleting a draft revokes none of them. Only the
membership in the published, immutable release shows that the file was released.
Verify the container images
The images are ghcr.io/myriarch/myriarch (the server, with myrctl and myriarch-sim) and
ghcr.io/myriarch/myriarch-updater. They are signed by the same workflow identity as the files and carry a provenance
attestation (on the image index) and an SPDX SBOM attestation (on each platform image). A tag is only a name: verify and pin
the digest.
Resolve the tag of the version to its digest, or take the digest from the release notes:
crane digest "ghcr.io/myriarch/myriarch:${VERSION}" docker buildx imagetools inspect "ghcr.io/myriarch/myriarch:${VERSION}" # the line "Digest:" is the same valueVerify the signature by digest with the complete identity of the version:
DIGEST=sha256:... # the digest from step 1 or from the release notes cosign verify "ghcr.io/myriarch/myriarch@${DIGEST}" \ --certificate-identity "https://github.com/myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}" \ --certificate-oidc-issuer https://token.actions.githubusercontent.comRepeat it for
ghcr.io/myriarch/myriarch-updaterwith its own digest.cosign verifyalso accepts the provenance and SBOM attestations of the same identity as a “signature” of the digest; the JSON it prints shows the type of each verified entry, andhttps://sigstore.dev/cosign/sign/v1is the signature made bycosign sign.Verify the build provenance of the image index. The attestations are stored in the registry, next to the image:
gh attestation verify "oci://ghcr.io/myriarch/myriarch@${DIGEST}" --repo myriarch/myriarch --bundle-from-oci \ --signer-workflow "myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}"Verify the SBOM. It is attached to each platform image, so use the digest of the platform image (the manifest of the index lists them):
crane manifest "ghcr.io/myriarch/myriarch@${DIGEST}" | jq -r '.manifests[] | "\(.platform.os)/\(.platform.architecture) \(.digest)"' gh attestation verify "oci://ghcr.io/myriarch/myriarch@sha256:<platform digest>" --repo myriarch/myriarch --bundle-from-oci \ --predicate-type https://spdx.dev/Document/v2.3 \ --signer-workflow "myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}"ghneeds a login here (GH_TOKEN), but it reads the attestations from the registry, anonymously.
What the sha256-<hex> tags are. The package view of ghcr.io shows tags named sha256- followed by 64 hex digits, about
three for each image. ghcr.io has no API for attached artifacts, so cosign and the attestation step keep the signatures and
attestations in these tags. They are part of the release: do not delete them, and do not delete untagged versions of a released
image, or the checks above fail. There is no latest tag while the major version is 0; the version tags are X.Y.Z, X.Y
(the newest patch release of that minor version) and X.Y.Z-rc.N for release candidates.
Pin the digest in your deployment. With the Helm chart from a checkout of the tag (the OCI chart of v0.2.0 has the digest written into it, see the next section), set the digest of the server image (the digest wins over the tag):
helm install myriarch ./deploy/helm/myriarch --set image.digest="${DIGEST}" # from a checkout of the tag
For Compose, use QUICKSTART_IMAGE=ghcr.io/myriarch/myriarch@${DIGEST} (see the
Docker Compose quickstart).
Verify the Helm chart
Available from v0.2.0. The release of v0.1.0 has no OCI chart.
Two charts are published, the server chart and the collector chart; both are signed and attested like the images and are verified the same way. This section shows the server chart; the collector chart follows below.
The server chart is published at ghcr.io/myriarch/charts/myriarch with the version of the release as its tag (0.2.0; 0.2.0-rc.1
for a candidate). It is signed by the same workflow identity as the files and the images and carries a provenance attestation. The
workflow writes the digest of the signed server image into image.digest of the packaged chart, so the chart references that image
by digest only; before publication it checks with helm template (default values and the OpenShift profile) that every image of
the rendering is ghcr.io/myriarch/myriarch@<digest> of this release.
VERSION=0.2.0; TAG="v${VERSION}"
CHART_DIGEST="$(crane digest "ghcr.io/myriarch/charts/myriarch:${VERSION}")" # the digest is what you verify and install
cosign verify "ghcr.io/myriarch/charts/myriarch@${CHART_DIGEST}" \
--certificate-identity "https://github.com/myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify "oci://ghcr.io/myriarch/charts/myriarch@${CHART_DIGEST}" --repo myriarch/myriarch --bundle-from-oci \
--signer-workflow "myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}"
As with the images, cosign verify also accepts an attestation of the same identity; the JSON it prints shows the type of each
verified entry, and https://sigstore.dev/cosign/sign/v1 is the signature made by cosign sign. To see that the chart pins the
image, render it and compare the digest with the one in the release notes:
helm template myriarch "oci://ghcr.io/myriarch/charts/myriarch" --version "${VERSION}" | grep 'image:' # every line: ghcr.io/myriarch/myriarch@sha256:...
helm install oci://... takes a version, which is a tag; to install exactly what you verified, run helm pull "oci://ghcr.io/myriarch/charts/myriarch" --version "${VERSION}" and compare the digest that it prints with ${CHART_DIGEST}.
The sha256-<hex> tags of the chart package are part of the release like those of the images; do not delete them.
The collector chart
The collector chart is published at ghcr.io/myriarch/charts/myriarch-collector with the same tag form (0.2.0, 0.2.0-rc.1), signed
by the same workflow identity and carrying a provenance attestation. It runs the unchanged upstream OpAMP Supervisor and collector
(contrib) images, and its updater init container is the signed ghcr.io/myriarch/myriarch-updater of the release. The workflow
writes the digest of that updater image into images.updater.digest of the packaged chart; the two upstream images are pinned by
tag and digest in the chart (the pins of ci/versions.env) and are not changed. Before publication the workflow renders the package with
helm template (StatefulSet, DaemonSet, container mode and OpenShift) and requires that every image: is the updater at the digest
of this release or one of the two upstream pins, so no image is referenced by a tag alone.
VERSION=0.2.0; TAG="v${VERSION}"
COLLECTOR_CHART_DIGEST="$(crane digest "ghcr.io/myriarch/charts/myriarch-collector:${VERSION}")"
cosign verify "ghcr.io/myriarch/charts/myriarch-collector@${COLLECTOR_CHART_DIGEST}" \
--certificate-identity "https://github.com/myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify "oci://ghcr.io/myriarch/charts/myriarch-collector@${COLLECTOR_CHART_DIGEST}" --repo myriarch/myriarch --bundle-from-oci \
--signer-workflow "myriarch/myriarch/.github/workflows/release.yml@refs/tags/${TAG}"
helm template myriarch-collector "oci://ghcr.io/myriarch/charts/myriarch-collector" --version "${VERSION}" | grep 'image:'
# ghcr.io/myriarch/myriarch-updater@sha256:..., and the two docker.io/otel/... images by digest
Compare the updater digest with the one in the release notes. To install exactly what you verified, run helm pull "oci://ghcr.io/myriarch/charts/myriarch-collector" --version "${VERSION}" and compare the digest that it prints with
${COLLECTOR_CHART_DIGEST}; the chart needs the enrollment Secret and the server address (see its README).
macOS
The macOS binaries (myriarchd, myrctl and myriarch-sim for Apple silicon, myrctl also for Intel) are not signed with
an Apple Developer ID and not notarized. macOS Gatekeeper blocks an unsigned binary that carries the quarantine attribute, which
browsers set on downloads. Verify the file first (cosign and shasum -a 256 --check --ignore-missing checksums.txt, above), then
use one of these ways:
- download the archive with
curlorgh release download, which do not set the quarantine attribute; or - remove the attribute from a file you verified:
xattr -d com.apple.quarantine <file>(for an archive, extract it first and run the command on the extracted binary).
Do not turn Gatekeeper off for the system. Whoever needs signed and notarized macOS binaries can build from source.