RELEASE.md
In docs/ · At e15865e
Preview
Text · 40.7 KiBCutting an mkit release
This is the single release runbook. It consolidates what used to live under
docs/release/ (checklist, signing/verification, supply-chain policy,
reproducibility, crates.io publishing) into one page.
Contents:
- What gets published
- Pre-release checklist
- Cutting a release
- Publishing to crates.io
- Signing and verification
- Supply-chain policy
- Reproducibility
- Required GitHub Actions secrets and variables
- One-time setup
What gets published
A single vX.Y.Z tag drives four decoupled channels:
- Binaries plus npm wasm via
.github/workflows/release.yml. - crates.io via
.github/workflows/crates-publish.yml. - Docs MCP corpus via
.github/workflows/mcp-release.yml, which indexes the tagged tree for the mkit docs MCP server so the served corpus always matches the published crates. - BSR proto modules (
buf.build/officialunofficial/mkit-rpcandbuf.build/officialunofficial/mkit-repo) via thebuf-pushjob in.github/workflows/crates-publish.yml, so third-party signer integrators andRepoServiceclients canbuf generatetyped bindings from a pinned tag without vendoring this repo. Dormant untilBUF_TOKENis provisioned.
All are gated on the same signed tag. Before release.yml publishes anything,
it verifies that the tag is strict semver, annotated, GPG-signed by an
allowlisted release fingerprint, and points at a commit reachable from
origin/main. It then produces:
GitHub Release with native
mkitbinaries for four targets:aarch64-apple-darwinx86_64-apple-darwinaarch64-unknown-linux-gnux86_64-unknown-linux-gnu
Windows is not a supported target (MKIT-6; see
docs/INVARIANTS.md). Windows users should run mkit under WSL, which uses the Linux binary.Each target ships one archive,
mkit-X.Y.Z-<target>.tar.gz: themkitbinary, licenses, README, optional changelog,share/man/man1/mkit.1, and shell completions undershare/completions/.Each archive is cosign-signed (keyless OIDC, Rekor logged) and ships alongside per-archive
.sig/.crt/.cosign.bundle, an aggregateSHA256SUMS(also cosign-signed), a CycloneDXsbom.cdx.json, aTHIRD-PARTY-NOTICESfile, and a standards-based SLSA build provenance attestation (actions/attest-build-provenance), verifiable withgh attestation verifyorslsa-verifierin addition to cosign. The SBOM and the notices cover the whole workspace.The
mkitbinary is built with-p mkit-cli --bin mkit: a bare--bin mkitselects every workspace member, and cargo unifies features across every selected package, so it would compile the shipped CLI with other members' features. After the build,scripts/check-release-artifact-features.shreads cargo's compiler-artifact messages and scans the stripped binary.mkitmust compile only packages listed inscripts/release/mkit-packages.golden, no server-only package or feature, tower-http only with reqwest's features, noSQLite, and no test seam. A new CLI dependency fails the check untilscripts/check-release-artifact-features.sh --update-goldenis run and the golden diff is committed with the change that brought it in.release-artifact-check.ymlruns the same build and check forx86_64-unknown-linux-gnuon PRs tomain(and pushes to it), and checks the golden list is current, so drift shows up before a tag rather than at it.- npm package
@officialunofficial/mkit-wasm@X.Y.Z. Built withwasm-pack --target bundlerand published withnpm publish --access public. The pkg tarball is also attached to the GitHub Release asmkit-wasm-X.Y.Z-npm.tar.gzfor offline mirroring. npm provenance is enabled withnpm publish --provenance; it binds the package to this GitHub Actions workflow run through GitHub OIDC. - crates.io — every workspace crate without
publish = false, in dependency order. See Publishing to crates.io.
Pre-release checklist
Homebrew tap status: provisioned, no formula yet. The
officialunofficial/homebrew-taprepository exists and is public, but holds only LICENSE plus README — there is noFormula/directory, sobrew installfails until a formula is pushed. The Distribution step that targets the tap is live and expected to be performed at the next release.
Run top to bottom. Do not skip steps.
Pre-tag
mainis green in CI (build plus test).cd rust && cargo test --workspacepasses on a fresh clone.- The release build passes for each release target (the command
release.ymlruns):cargo build --release --locked -p mkit-cli --bin mkit:--target=aarch64-apple-darwin--target=x86_64-apple-darwin--target=x86_64-unknown-linux-gnu--target=aarch64-unknown-linux-gnu
[workspace.package].versioninrust/Cargo.tomlis bumped to a version not already published (crates.io versions are immutable — a re-publish of an existing version fails the wholecargo publishrun).CHANGELOG.mdhas an entry for this version; move items from## [Unreleased]into## [X.Y.Z] - YYYY-MM-DD.- Version bumped wherever it is hard-coded: README install snippets,
contrib/homebrew/mkit.rb,docs/INSTALL.md(three sites: twoVERSION=snippets and the expectedmkit versionoutput), and thevX.Y.Zexamples ininstall.shusage text. - Signing and verification and Reproducibility still accurate for this release.
MKIT_RELEASE_GPG_FINGERPRINTSrepo/org Actions variable contains the release tag signing key fingerprint.- The release tag signing public key is published to
keys.openpgp.orgorkeyserver.ubuntu.comso the workflow can import it beforegit verify-tag. SECURITY.mddisclosure contact confirmed reachable.- Manually dispatch
rust.yml'skeystore-backendsjob (gh workflow run rust.yml --ref main) and confirm both legs (Linux Secret Service+systemd-creds+YubiKey, macOS Keychain+YubiKey) succeed. It staysworkflow_dispatch-only (2-OS matrix cost) and is not exercised by any automatic trigger, so this is the only per-release check that native keystore backends still work.
Wait for the release workflows
release.ymlsucceeded throughvalidate-release-tagand all four platform builds.crates-publish.ymlsucceeded (every publishable crate indexed).mcp-release.ymlsucceeded (docs MCP corpus indexed for the tag).- GitHub Release created as a non-draft.
- Archives present:
mkit-X.Y.Z-aarch64-apple-darwin.tar.gz,mkit-X.Y.Z-x86_64-apple-darwin.tar.gz,mkit-X.Y.Z-aarch64-unknown-linux-gnu.tar.gz, andmkit-X.Y.Z-x86_64-unknown-linux-gnu.tar.gz. sbom.cdx.jsonpresent.THIRD-PARTY-NOTICESpresent.SHA256SUMS,SHA256SUMS.sig,SHA256SUMS.crt,SHA256SUMS.cosign.bundlepresent.- Per-archive
.cosign.bundlepresent for every archive. mkit-X.Y.Z.provenance.jsonl(SLSA build provenance) present, andgh attestation verify <archive> --repo officialunofficial/mkitsucceeds for at least one archive.
Smoke test
On at least one Linux box and one macOS box, download the archive for your platform, then run the mechanized checklist below instead of the steps by hand:
# Linux / macOS
scripts/release-smoke.sh --archive mkit-X.Y.Z-<target>.tar.gz --version X.Y.ZThe script covers every item below in one pass: cosign signature, the
SHA256SUMS entry, mkit version against the tag, the bundled man page
and completions, a basic mkit init → mkit keygen → add a file →
mkit commit flow, and (unless --skip-npm) npm view
@officialunofficial/mkit-wasm@X.Y.Z plus npm audit signatures. It's the
same script used for a pre-flight dry run against a locally packaged
archive before tagging (missing sidecars like the cosign bundle are
skipped with a warning, not a failure, in that case) — this
checklist is the spec of record the script implements, so if the two ever
disagree, fix the script:
- Download the archive for your platform.
- Verify the cosign signature (see below).
- Verify
SHA256SUMSmatches the archive. - Extract and run
./mkit-X.Y.Z-<target>/mkit version— version string matches the tag. - Extracted archive contains
share/man/man1/mkit.1,share/completions/mkit.bash,share/completions/_mkit, andshare/completions/mkit.fish. - Basic flow:
mkit init→ add a file →mkit commit. npm view @officialunofficial/mkit-wasm@X.Y.Zandnpm audit signatures.
Distribution and announce
- In
officialunofficial/homebrew-tap, copycontrib/homebrew/mkit.rbintoFormula/mkit.rb, update the version, and replace everyPLACEHOLDER_SHA_*with the matching archive hash from releaseSHA256SUMS. - Release notes reviewed (auto-generated by
softprops/action-gh-releaseplus the signing snippet). - Pin the release in the repo sidebar; post in relevant channels.
- Once the Homebrew step above is done, dispatch
release-verify.yml(gh workflow run release-verify.yml -f version=X.Y.Z) to check every distribution channel end to end in an isolated environment —cargo install,cargo binstall, npm, the curl-pipe install script (Debian/Ubuntu/Fedora), and Homebrew. Not a substitute for the smoke test above (which verifies the archive itself); this verifies every OTHER way a user actually getsmkitonto their machine.
Server compatibility gates
On pushes to main, the Rust workflow runs library clippy for mkit-server
and mkit-server-worker with cargo hack --feature-powerset --depth 2.
The powerset covers no features, individual features and feature pairs.
Separate checks cover default features across all targets and all features
for the library. Library checks avoid dev-dependency feature unification;
the default check also catches test-only warnings. Each crate has a 30-minute
job limit. PRs enforce the server's API compatibility against 0.5.0 as the 0.6 release (a breaking-capable 0.x minor) while main accumulates it;
the publish gate checks the server too, including that pinned baseline.
Post-release
- Verify the release-prep PR left a fresh
## [Unreleased]heading at the top ofCHANGELOG.md. - After 0.6.0 is tagged, restore the
server-semverjob in.github/workflows/rust.ymlto--release-type patchagainst the 0.6.0 baseline (it checks the 0.6 release class against 0.5.0 until then). - After a crate's first publish, add co-owners:
cargo owner --add github:officialunofficial:makechain <crate>, thencargo owner --add <maintainer> <crate>for each maintainer. Confirm the owners withcargo owner --list <crate>. - File follow-up issues for anything discovered during smoke test.
Cutting a release
- Land everything on
main. Confirm CI is green. - Bump
[workspace.package].versioninrust/Cargo.toml(to a version not yet on crates.io). UpdateCHANGELOG.md(moveUnreleaseditems into[X.Y.Z] - YYYY-MM-DD, leaving a fresh[Unreleased]heading above it). - Open a release-prep PR, merge it.
- Tag the merge commit on
main:
Signed, annotated tags only. The signing key fingerprint must be listed in thegit tag -s vX.Y.Z -m "mkit X.Y.Z" git push origin vX.Y.ZMKIT_RELEASE_GPG_FINGERPRINTSrepository or organization variable and its public key must be available fromkeys.openpgp.orgorkeyserver.ubuntu.com. Verify the exact fingerprint before pushing:
The release workflow rejects lightweight tags, invalid signatures, fingerprints not listed ingit tag -v vX.Y.ZMKIT_RELEASE_GPG_FINGERPRINTS, tags outside the strictvX.Y.Z[-prerelease]form, and tag targets not reachable fromorigin/main. - Watch the workflows.
release.ymljob order is:validate-release-tag→build(× 4 archs) →sbom/third-party-notices(parallel) →release→publish-wasm.crates-publish.ymlrunscargo publish --workspace --lockedin dependency order. - Run the smoke test.
Publishing to crates.io
crates.io publishing is tag-driven, owned by
.github/workflows/crates-publish.yml.
On a pushed v*.*.* tag it runs cargo publish --workspace --locked, which
publishes every workspace member without publish = false to crates.io in
dependency order, waiting for each to index before the next. A guard refuses to
publish if the tag does not match the workspace version (mkit-core's version
in rust/Cargo.toml at the tagged tree).
What publishes vs. what doesn't
| Publishes to crates.io | Stays off crates.io (publish = false) |
|---|---|
the library crates (mkit-core, mkit-rpc, mkit-client, mkit-sync, mkit-attest, mkit-keystore, mkit-git-bridge, mkit-server, mkit-transport-{file,http,memory,s3,ssh,enc,connect}) plus mkit-cli (so cargo install mkit-cli works) | mkit-server-worker (the Cloudflare Workers adapter, consumed by git revision), mkit-server-conformance, mkit-wasm (npm-only), mkit-repo-client, mkit-test-util, mkit-core-wasm-check, mkit-formal-conformance, fuzz, benches. The contrib signers are a separate workspace under contrib/signers/ (not workspace members at all). |
The published crates depend only on each other, forming a closed,
dependency-ordered set; cargo publish --workspace computes that order and
skips any publish = false member.
Notes on the published set:
mkit-server— the runtime-agnostic server core, first published at 0.5.0.mkit-clidepends on it (featuressshandfs, for the stdiomkit servesession), so it must be on crates.io beforemkit-cli;cargo publish --workspaceorders the two from the dependency graph. There is no native server binary or container image: the Cloudflare Workers adapter (mkit-server-worker) is the production deployment and is not published.mkit-transport-encis deprecated and receives no further server counterpart.mkit-cli— published socargo install mkit-cliinstalls themkitbinary; it also ships via the signed GitHub Release archives andcargo install --git. Its library surface is unstable CLI internals, deliberately not a public API — kept out of the pre-publish semver gate (thecargo semver-checksstep incrates-publish.yml). Depend on themkit-*library crates, not onmkit_cli::….- The crates.io
mkitname belongs to an unrelated project; the CLI is published asmkit-cli. Do not runcargo install mkit.
Token
cargo publish authenticates with the CRATES_PACKAGE_KEY org-level secret
(scopes: publish-new plus publish-update), exposed to cargo as
CARGO_REGISTRY_TOKEN. A token (not trusted publishing) is required because
brand-new crates can't use trusted publishing on their first publish. The org
secret must grant this repo access, and its crates.io account must own the
crates (via the makechain team). Migrating to crates.io Trusted Publishing
(OIDC) to drop the long-lived token is a recommended follow-up.
Both jobs in
.github/workflows/release-plz.yml(release-plz-release,release-plz-pr) areworkflow_dispatch-only, gated on the repo variableRELEASE_PLZ_ENABLED == 'true'(currently true). Neither runs on push.This used to be different:
release-plz-releaseran on every push tomain, on the assumption thatrelease_always = falseinrust/release-plz.tomlwould always no-op it ("commit is not from a release PR"). That assumption broke on the v0.4.2 release (2026-09-02): the mkit-release skill's own procedure merges release-plz's own PR as the release-prep PR, and release-plz recognized that merge commit as a real release and actually rancargo publishfor every crate — racing (and mostly winning against) the tag-drivencrates-publish.ymlpublish, which then failed with a harmless-but-confusing "already exists" error. The push trigger was removed for this reason:crates-publish.ymlis the sole real publish path, and this job must never fire on its own.
Gotchas
- Immutable versions: crates.io never lets you overwrite a published version. Always bump before tagging; never re-publish an existing version.
- Partial publish: if a publish fails mid-train, the already-landed crates
can't be re-published. Re-runs must
--excludewhat landed (edit the publish step or publish the remainder by hand). - Native deps:
cargo publishbuilds each crate with default features, so nolibpcscliteis needed (mkit-keystore'sbackend-yubikeyis off by default); onlyprotocis required (installed in the workflow formkit-rpc).
Signing and verification
Every mkit release is signed with
cosign using keyless OIDC: the
signing identity is the GitHub Actions workflow itself
(.github/workflows/release.yml), authenticated by GitHub's OIDC provider, and
every signature is recorded in the
Rekor transparency log.
Cosign signatures are keyless and bound to the artifact hash, so a stolen
release signature cannot be reused on any other artifact. The installer
(install.sh) enforces this same trust boundary by default.
Artifacts attached to every release
For each of the four targets, release archives build the production
mkit-cli target for that platform. The CLI enables the matching keystore
software-protector feature so software keys are encrypted at rest on supported
targets without changing the lean default feature set of the mkit-keystore
library crate.
| File | Purpose |
|---|---|
mkit-X.Y.Z-<triple>.tar.gz | Binary, licenses, README, manpage, completions. |
...sha256 | SHA256 of the archive (convenience). Every archive has the full set of sidecars. |
...sig | Raw cosign signature (base64). |
...crt | Fulcio-issued code-signing certificate. |
...cosign.bundle | Bundle: sig plus cert plus Rekor entry. |
Plus one top-level set for the aggregate:
| File | Purpose |
|---|---|
SHA256SUMS | Hashes of every archive plus SBOMs plus notices. |
SHA256SUMS.{sig,crt,cosign.bundle} | Cosign signature of SHA256SUMS. |
sbom.cdx.json | CycloneDX SBOM of the release. |
THIRD-PARTY-NOTICES | Consolidated third-party license attribution for the Rust dependency graph, generated by cargo about generate (see NOTICE). |
mkit-X.Y.Z.provenance.jsonl | GitHub-native SLSA build provenance attestation (actions/attest-build-provenance) over the archives. Verified with gh attestation verify or slsa-verifier. |
Verify a downloaded archive
Install cosign: https://docs.sigstore.dev/cosign/installation/. Then:
VERSION=X.Y.Z
TARGET=aarch64-apple-darwin
ARCHIVE="mkit-${VERSION}-${TARGET}.tar.gz"
cosign verify-blob \
--certificate-identity-regexp '^https://github\.com/officialunofficial/mkit/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "${ARCHIVE}.cosign.bundle" \
"${ARCHIVE}"Expected output: Verified OK. The --certificate-identity-regexp pins the
signature to a tag build of mkit's release workflow; a signature produced by any
other workflow (a fork, a branch build, a locally-run cosign) fails
verification.
The archive's sibling .sha256 file is not an authenticity signal on its own
because it is served from the same origin as the archive. Treat it as
defense-in-depth after cosign, not instead of it.
Inspect the Rekor transparency log entry
The .cosign.bundle contains a Rekor log index. To view the public entry, add
--rekor-url "https://rekor.sigstore.dev" to the cosign verify-blob command
above, or search by the archive's sha256 at
https://search.sigstore.dev/?hash=sha256:DIGEST.
Verify the SBOM
The SBOM (sbom.cdx.json) is included in the top-level SHA256SUMS, which is
itself cosign-signed. The chain is:
- Verify
SHA256SUMS.cosign.bundlesignsSHA256SUMS(cosign). - Verify
sbom.cdx.json's sha256 matches its entry inSHA256SUMS.
cosign verify-blob \
--certificate-identity-regexp '^https://github\.com/officialunofficial/mkit/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle SHA256SUMS.cosign.bundle \
SHA256SUMS
grep ' sbom.cdx.json$' SHA256SUMS > sbom.cdx.json.sha256
sha256sum -c sbom.cdx.json.sha256 || shasum -a 256 -c sbom.cdx.json.sha256Verify THIRD-PARTY-NOTICES
THIRD-PARTY-NOTICES is generated by cargo about generate (config:
rust/about.toml plus rust/about.hbs) from the rust workspace's resolved
dependency graph, and is covered by the same SHA256SUMS chain as the SBOM:
grep ' THIRD-PARTY-NOTICES$' SHA256SUMS > THIRD-PARTY-NOTICES.sha256
sha256sum -c THIRD-PARTY-NOTICES.sha256 || shasum -a 256 -c THIRD-PARTY-NOTICES.sha256cargo about generate fails the third-party-notices job outright if any
dependency's license isn't in rust/about.toml's accepted list — that list
is kept in lockstep with rust/deny.toml's [licenses] allow list (the
license check cargo-deny already runs on every PR via cloudbuild/security.yaml).
.github/workflows/third-party-notices.yml runs the same cargo about
generate check on every PR that touches the dependency graph, so an
unapproved license is caught before a release attempt, not during one.
Verify the SLSA build provenance attestation
Every archive also carries a standard
SLSA build provenance attestation,
generated by actions/attest-build-provenance
and recorded in the Sigstore public-good transparency log. Unlike the cosign
signature above (an mkit-specific verification path), this attestation is
checkable with off-the-shelf, standards-aware tooling and requires no
mkit-specific parsing:
gh attestation verify mkit-X.Y.Z-<target>.tar.gz --repo officialunofficial/mkitor with slsa-verifier:
slsa-verifier verify-artifact mkit-X.Y.Z-<target>.tar.gz \
--provenance-path mkit-X.Y.Z.provenance.jsonl \
--source-uri github.com/officialunofficial/mkit \
--source-tag vX.Y.ZThis is additive: it does not replace the cosign signature, which remains in place and is independently verified above.
macOS Gatekeeper
macOS binaries are not Developer ID-signed and not notarized. The trust
boundary is the cosign verification above, not Apple notarization. Users on
Ventura+ may see a Gatekeeper warning when launching a freshly-downloaded binary
from Finder. After you verify the archive, either run mkit from a terminal or
clear quarantine explicitly:
xattr -d com.apple.quarantine /path/to/mkitNotarization is future work: release.yml has no notarization step today
(only a placeholder comment). When it is wired, it will authenticate with
APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID secrets. If
notarized macOS releases ever ship, the release notes and this document will
say so explicitly.
Supply-chain policy
mkit's supply chain is intentionally narrow. The binary users run traces back to a small, auditable set of inputs. This section is the contract.
Current state
- Rust dependencies: fully pinned via
Cargo.lock. Every transitive dep is hash-pinned on each release build. The workspace's direct deps are kept deliberately small — seerust/Cargo.tomland each crate's[dependencies]. - System libs: default release builds link musl (Linux) or the platform C
runtime (macOS). Optional keystore backend features may use platform services
(Security.framework, D-Bus Secret Service, PC/SC,
systemd-creds). These are off by default, platform-gated, and fail closed when the required service or device is unavailable. The Linux Secret Service feature selects the pure-Rust crypto runtime, not OpenSSL. Productionmkit-clibuilds enable the target-appropriate software protector feature sosoftware:<label>stores encrypted records by default, while themkit-keystorelibrary keeps an empty default feature set. - Build inputs: the Rust toolchain (pinned in
rust-toolchain.toml), the source tree at a tagged Git commit, and the--target=/ profile flags documented under Reproducibility.
Adding a Rust dependency
Every new direct dependency added to a workspace Cargo.toml must:
- Be version-pinned in
Cargo.tomland reflected inCargo.lock. - Have its source reviewed by two maintainers (sign-off in the PR).
- Have a stated purpose — what does mkit gain, and what would implementing it in-tree cost? The default answer should be "implement in-tree"; deps are the exception.
- Be compatible with MIT OR Apache-2.0. The CI dependency-review action
denies GPL/LGPL families when the repo has Dependency Graph enabled and
ENABLE_DEPENDENCY_REVIEW=1. - Have a cross-platform build. If the dep breaks on one of the four release targets, it doesn't land.
Keystore backend dependency review
The optional keystore backend and envelope-encryption dependencies (issue #104)
were reviewed for the V1 keystore. All are pinned in Cargo.toml, reflected in
Cargo.lock, sourced from crates.io, accepted by cargo deny, and isolated
from default builds unless listed as always-on.
| Dependency | Use | Gate | License |
|---|---|---|---|
chacha20poly1305 = 0.10.1 | XChaCha20-Poly1305 software-key envelope encryption | always-on keystore crypto | Apache-2.0 OR MIT |
zeroize = 1.8.2 | Secret-memory clearing | always-on keystore crypto | Apache-2.0 OR MIT |
security-framework = 3.7.0 | macOS Keychain bindings | backend-macos-keychain (macOS only) | MIT OR Apache-2.0 |
keyring-core = 1.0.0 | Shared keyring abstraction | backend-linux-secret-service | MIT OR Apache-2.0 |
zbus-secret-service-keyring-store = 1.0.0 | Linux Secret Service store | backend-linux-secret-service (Linux only; OpenSSL-free) | MIT OR Apache-2.0 |
card-backend-pcsc = 0.5.1 | PC/SC card discovery | backend-yubikey | MIT OR Apache-2.0 |
openpgp-card = 0.6.1 | OpenPGP card protocol | backend-yubikey | MIT OR Apache-2.0 |
secrecy = 0.10.3 | PIN handling wrappers | backend-yubikey | Apache-2.0 OR MIT |
der = 0.8.0 | DER parsing for PIV certs | backend-yubikey | Apache-2.0 OR MIT |
yubikey = 0.9.0-pre.0 | YubiKey PIV discovery plus signing | backend-yubikey | BSD-2-Clause |
The yubikey prerelease is accepted because V1 PIV support needs mature PIV
APDUs and no stable alternative matched the API; risk is contained by optional
feature gating, existing-key-only support, fail-closed behavior, CI feature
builds, and manual hardware validation. Remaining direct support deps are
exact-pinned (ed25519-dalek, getrandom, k256, p256, sha2, thiserror,
dev-only tempfile).
cargo deny check finishes with advisories ok, bans ok, licenses ok, sources
ok. Remaining warnings are duplicate crate versions from ecosystem splits and
optional YubiKey/RustCrypto prerelease transitive stacks; they are tracked as
warnings, not release blockers, because they introduce no unapproved sources,
yanked crates, or denied licenses. Live OS-native keystore tests are opt-in via
MKIT_RUN_NATIVE_KEYSTORE_TESTS=1.
GitHub Actions dependencies
All third-party actions in .github/workflows/ must be pinned to a major
version tag (@v4) or a full SHA. No @main, no @latest. Trusted publishers:
actions/*, dtolnay/rust-toolchain, sigstore/cosign-installer,
anchore/sbom-action, softprops/action-gh-release, ossf/scorecard-action.
Any new action from an untrusted publisher needs the same two-maintainer review
as a Rust dep.
Release artifact integrity, SBOM lifecycle, vulnerability response
Every release archive is hashed (per-archive SHA256SUMS), signed
(cosign keyless OIDC, tied to this repo's release workflow identity), logged
in the public Rekor transparency log, and inventoried in a CycloneDX SBOM.
Package-manager manifests must reference the GitHub Releases archive URL directly
(never a re-hosted copy); the sha256 in the manifest is the authoritative pin.
A fresh SBOM is generated at release time only (the sbom job in
release.yml); there is no scheduled SBOM regeneration for main. The
weekly cadence is cargo audit via rust-security.yml (Mondays 06:00 UTC),
which sweeps for newly published advisories against the unchanged dependency
tree. Restoring a scheduled SBOM-drift job is an open follow-up. A CVE against
any shipped dep is a P1: patch, test, tag a new release,
open a GitHub Security Advisory citing the CVE, update CHANGELOG.md under
### Security, and coordinate disclosure via the SECURITY.md contact.
Reproducibility
A mkit release binary is a function of a small, pinned set of inputs. If all of them match, any machine that can run the same Rust toolchain will produce a byte-identical binary to the one published on GitHub Releases.
The inputs
- Rust toolchain version — pinned in
rust-toolchain.toml; the workflow installs it verbatim. - Target triple — one of the four release triples, passed as
--target=<triple>. - Build profile —
release. - Source tree hash — the Git commit the tag points at; every source file is tracked in Git.
- Dependency fingerprint —
Cargo.lock, fully pinning every transitive dependency. Anycargo updateis a visible change to that file.
Reproducing a published binary
# 1. Clone at the release tag.
git clone --depth 1 --branch vX.Y.Z https://github.com/officialunofficial/mkit.git
cd mkit/rust
# 2. rustup picks up rust-toolchain.toml on first `cargo` invocation.
# 3. Build for your target with release.yml's command (`-p mkit-cli`
# selects only the CLI; a bare `--bin mkit` unifies other members'
# features and differs).
export RUSTFLAGS="-C codegen-units=1 -C strip=symbols"
TARGET=x86_64-unknown-linux-gnu
cargo build --release --locked --target "$TARGET" -p mkit-cli --bin mkit
# 4. Hash the binary.
shasum -a 256 "target/$TARGET/release/mkit"
# 5. Compare against the SHA256SUMS inside each release archive.If the hashes don't match, treat it as a supply-chain incident: open an issue
with your host OS, rustc --version, the sha256 you got, and the expected one.
diffoscope on the two binaries usually isolates the culprit.
CI safety net and what is not guaranteed
There is currently no automated reproducibility check: no CI job builds mkit twice and compares the outputs. The manual procedure above is the verification path. Restoring an automated double-build job is an open follow-up. A divergence between two builds of the same commit means a non-deterministic input has crept in (embedded timestamps, random seeds, absolute paths in debug info, unsorted directory reads) and must be fixed before the next tag.
Binaries built on your local macOS are not expected to match GitHub's macos-14
runner byte-for-byte unless your OS, SDK, and linker versions match. The Linux
x86_64 build is the most reliable reproducibility target for third parties.
Required GitHub Actions secrets and variables
| Secret | Purpose | Required for |
|---|---|---|
CRATES_PACKAGE_KEY (org-level) | crates.io publish token (publish-new plus publish-update) | crates-publish.yml |
CODECOV_TOKEN | Codecov upload auth — vestigial: no GitHub workflow reads it. Coverage runs on Cloud Build (cloudbuild/coverage.yaml); the live token is the GCP Secret Manager copy | — (see CODECOV_TOKEN) |
BUF_TOKEN | Buf Schema Registry API token (write access) — pushes the mkit-rpc/mkit-repo proto modules | crates-publish.yml's buf-push job (see BUF_TOKEN) |
cosign keyless and npm Trusted Publishing (auth plus provenance) both run on the GitHub OIDC token; no extra secrets are needed for those.
| Variable | Purpose | Required for |
|---|---|---|
MKIT_RELEASE_GPG_FINGERPRINTS | Space-separated trusted 40-hex GPG fingerprints allowed to sign release tags (public keys must be on keys.openpgp.org or keyserver.ubuntu.com) | release.yml preflight |
BUF_PUBLISH_ENABLED | Set to true to arm the buf-push job (job-level if: cannot read secrets, so this variable — not BUF_TOKEN's presence — is the gate) | crates-publish.yml's buf-push job |
One-time setup
CRATES_PACKAGE_KEY
Generate a crates.io API token (scopes: publish-new plus publish-update) on an
account that owns the mkit-* crates (via the makechain team), and add it as
an org-level secret named CRATES_PACKAGE_KEY with this repo granted access.
For the very first publish, ensure each crate name is available or already owned
by the org; crates.io rejects deps on unpublished crates, so the dependency-order
publish in crates-publish.yml handles a clean first release once the token and
version are in place.
CODECOV_TOKEN
Codecov receives the lcov report from the Cloud Build coverage build
(cloudbuild/coverage.yaml, trigger mkit-coverage-main) so the README badge,
trend chart, and PR-diff overlay work. The live token lives in GCP Secret
Manager, not GitHub: no GitHub workflow reads CODECOV_TOKEN anymore, so the
GitHub Actions secret of the same name is vestigial and safe to delete.
- Sign in at https://app.codecov.io/ with the GitHub account that admins the
officialunofficialorg. - Add the
officialunofficial/mkitrepo; copy the "Upload token". - Store it as the
CODECOV_TOKENsecret in GCP Secret Manager and grant the Cloud Build service account access (cloudbuild/coverage.yamlmountsprojects/${PROJECT_ID}/secrets/CODECOV_TOKEN/versions/latest). The upload is optional: if the secret is absent, the upload step is skipped and the report is still produced and logged. - Trigger a run: push to
main(mkit-coverage-mainfires on push tomainonly). The first run seeds the baseline; subsequent runs populate the badge. - Once the repo flips public, Codecov accepts tokenless uploads from public repos via GitHub OIDC — but that applies to GitHub Actions uploads, not Cloud Build, so the Secret Manager token stays.
npm Trusted Publishing (OIDC) — @officialunofficial/mkit-wasm
@officialunofficial/mkit-wasm is the npm package published by the release
workflow, under the officialunofficial npm org (matching the GitHub org —
renamed from the earlier @makechain/mkit-wasm once the repo went public).
publish-wasm authenticates via the GitHub Actions OIDC token directly (npm
Trusted Publishers) — there is
no MKIT_NPM_TOKEN-equivalent secret to provision, rotate, leak, or
revoke for the automated path. This replaced the original
Automation-token setup once the package had at least one successful
token-based release behind it (0.1.0–0.3.0, published as
@makechain/mkit-wasm).
One-time setup (npmjs.com, by an owner of the officialunofficial org):
If
@officialunofficial/mkit-wasmhas never been published, either cut a real tag (the workflow's first successful OIDC-authenticated publish claims the name) or publish a placeholder once from a maintainer workstation with a personal token:cd rust wasm-pack build crates/mkit-wasm --release --target bundler --out-dir pkg cd crates/mkit-wasm/pkg npm pkg set name=@officialunofficial/mkit-wasm npm version --no-git-tag-version --allow-same-version 0.0.0-init npm publish --access public --tag initwasm-pack's raw output is named unscopedmkit-wasm, hence thenpm pkg setstep.--tag initkeeps the placeholder off thelatestdist-tag once a real version publishes (it briefly holdslatesttoo, being the only version, until the first real tag push supersedes it).The npm registry's CDN edge cache can serve a stale 404 for a brand-new scope/package for a few minutes after a successful publish, even though
npm dist-tag ls,npm access list packages, andnpm install(corgi-format endpoint) already see it — don't diagnose a fresh publish as failed from a barenpm view/curl404, and don't re-query the same URL repeatedly (it just refreshes the negative-cache TTL). Wait a few minutes and recheck.- On the package's
npmjs.compage → Settings → Trusted Publisher, add a GitHub Actions publisher:- Organization or user:
officialunofficial - Repository:
mkit - Workflow filename:
release.yml(filename only, not the full.github/workflows/path) - Environment: leave blank (the job doesn't use a GitHub environment)
- Allowed actions:
npm publish
- Organization or user:
- Verify with
npm view @officialunofficial/mkit-wasmafter the first OIDC-authenticated publish; confirm provenance withnpm audit signatures.
Requires npm CLI ≥11.5.1 (the workflow pins this explicitly) and GitHub-hosted runners (self-hosted runners aren't supported for Trusted Publishing as of this writing).
BUF_TOKEN
buf.build/officialunofficial/mkit-rpc (common.proto/signer.proto/ssh.proto/
verify.proto, from the rust/crates/mkit-rpc/proto module) and
buf.build/officialunofficial/mkit-repo (repo.proto, from the
apps/repo-worker/proto module) are the two named modules in the repo-root
buf.yaml v2 workspace that the buf-push job in crates-publish.yml
publishes on every tagged release (issue #719). Both are pushed private —
signer.proto is the security-critical external-signer wire contract
(SPEC-EXTERNAL-SIGNER.md) and repo.proto is
this repo's internal RepoService contract; flip a module to public only as a
deliberate decision, not a default.
- Sign in (or create an account) at https://buf.build/ under the
officialunofficialorganization. The BSR org name is assumed to match the GitHub org (officialunofficial) — confirm this before the first push, or update thename:field on therust/crates/mkit-rpc/protoandapps/repo-worker/protomodules in the repo-rootbuf.yaml(and thebuf generate/buf pushreferences in this doc and thebuf-pushjob) to the actual BSR org if it differs. - Generate a BSR API token (
buf.build→ Settings → API Tokens) scoped to that organization, with write access. - Add it as an org-level (or repo) secret named
BUF_TOKEN, and set theBUF_PUBLISH_ENABLEDrepo/org variable totrue. Until the variable istrue,buf-pushis a clean skip (if: vars.BUF_PUBLISH_ENABLED == 'true') — same dormant-until-provisioned pattern asRELEASE_PLZ_ENABLED. Two knobs, not one, because a job-levelif:cannot read thesecretscontext to gate onBUF_TOKEN's presence directly. - First push auto-creates both BSR repositories (
--create --create-visibility private); no manual "create the module" step needed. - Verify: after a release tag,
buf generate buf.build/officialunofficial/mkit-repo:vX.Y.Z --template apps/repo-worker/proto/buf.gen.yaml(withbuf registry loginfirst, since the module is private) should produce TypeScript bindings without this repo checked out — this is exactly what thebuf-pushjob's own smoke-test step does on every tagged release.
Package name decision
The Rust crate remains mkit-wasm, but the release workflow publishes the npm
package as @officialunofficial/mkit-wasm, matching the GitHub org this repo
lives under (renamed from @makechain/mkit-wasm once the repo went public —
see npm Trusted Publishing
above). If the scope ever changes again, update
rust/crates/mkit-wasm/README.md's install snippet, the publish-wasm workflow
npm pkg set name=... line, the npm Trusted Publisher config on npmjs.com, and
the npm view smoke-test references in this doc.