Use GitHub's private vulnerability reporting ("Report a vulnerability" under the Security tab) so the report stays out of public issues until a fix ships. If that's unavailable, open an issue saying only that you have a sensitive report and a maintainer will arrange a private channel — please don't put details in the issue itself.
You can expect an acknowledgement within a week. There is no bounty program; credit in the changelog is offered.
Please give a reasonable window before public disclosure — the aim is a fix and an advisory within 90 days of the report, sooner for anything actively exploitable. If GitHub is not a workable channel for you, say so in a content-free issue and a maintainer will arrange an alternative.
Out of scope as vulnerabilities: keyroost displaying a secret you asked it to display on your own terminal or screen; a compromised host reading the process's memory; and physical or firmware attacks on the token itself (see the threat model below).
Only the latest release receives security fixes. The tool talks to local hardware and has no server component, so updating is the whole story.
What keyroost defends against:
- Malicious or malformed input files. Import parsers (otpauth URIs, Aegis/2FAS exports, encrypted vaults) are bounds-checked, reject attacker-controlled resource demands (e.g. hostile scrypt parameters), and authenticate ciphertexts before use.
- Malicious or buggy devices. Everything read from USB/NFC — APDU responses, TLV/BER structures, CBOR, CTAP-HID frames — is length-checked and bounded; a fuzzing device gets an error, not a hang or a panic.
- Accidental secret disclosure by the tool itself. keyroost persists no
secrets of its own accord — the only files it writes unprompted are the
friendly-name registry (
keys.json) and the GUI'ssettings.json, both created owner-only (0600) and neither holding key material. Secret output reaches disk only when you name a destination for it (e.g.openpgp decrypt --out), and those writes go through an owner-only temp file that is fsynced and atomically renamed, refusing to follow a symlink or to overwrite a file owned by another user.--debugtraces redact secret-bearing command bodies on every applet — Molto2 secure writes, OATHPUT/SET CODE/VALIDATE, OpenPGP key import and decipher, PIVGENERAL AUTHENTICATE, the Token2 OTP seed writes and code reads, and CTAP CBOR exchanges, in both directions where a response is as revealing as the request. Secret-typed memory is zeroized on drop where Rust allows — PINs, CTAP session secrets and per-credential largeBlob keys, RSA key components, imported TOTP seeds, and the decrypted-vault / QR-payload buffers they pass through (buffer reallocations and library-internal copies remain out of reach); secrets are accepted via env/stdin rather than argv.
What keyroost does not defend against:
- A compromised host. Code running as your user can read process memory and everything you can read. No host-side tool can fix this.
- Physical attacks on the token itself, or weaknesses in a device's own firmware/protocol. Notably, the Molto2's wire protocol (4-byte SM4-CBC-MAC truncation, SM4-ECB seed encryption keyed from the customer key) is fixed by the device; rotating the customer key away from the public factory default is the strongest available mitigation, and the CLI warns when you haven't.
- Other software with access to the same device. Anything the OS allows to open the token can talk to it; unplug keys you're not using.
- No network access, by design. No crate in this workspace opens a
network socket, resolves a hostname, or speaks HTTP; there is no
telemetry, no update check, no "cloud". A release that broke this would
be a security bug — report it as one. Two things look adjacent and are
not: the GUI's "Learn" link hands a documentation URL to your browser
(keyroost makes no connection itself), and the Wayland screenshot path
talks to
xdg-desktop-portalover the session bus, which is local IPC. unsafeonly in two scoped FFI shims.unsafe_code = "forbid"is the workspace-wide default; the sole exceptions are the Windows-onlykeyroost-winwebauthn(HID enumeration / shell launch) andkeyroost-screengrab(GDI screen capture) crates, which setunsafe_code = "allow"to confine their thinwindows-sysFFI. Both are inert on non-Windows targets. Nounsafeappears in any cross-platform crate.- Vendored protocol code, scoped dependencies. SM4, SHA-1/256/512, the
OATH HMAC, base32/hex, and the APDU/TLV/CBOR and
otpauth://parsers are implemented in-tree with no external crate;keyroost-protoandkeyroost-resolvecarry no external dependencies at all, and the OATH, OpenPGP, PIV and Token2 single-profile byte layers carry nothing butzeroize. That does not mean no external hash code is in the tree: where the CTAP and Token2-OTP paths need those primitives inside a larger construction (client-PIN, ECDH, largeBlob), they use RustCrypto'ssha2andhmacrather than a hand-rolled equivalent. What is pulled in is deliberate, confined to the crate that needs it, and annotated in theCargo.tomlthat declares it: the device and OS boundary (pcsc;hidapioff Linux, where sysfs andhidraware used directly;windows-syson Windows), the interface boundary (clapandclap_complete/clap_mangen;eframe/eguiwitharboard,rfd,pollster,pngand Linux'sx11rb/ashpd;serde/serde_jsonandbase64), and cryptography that would be irresponsible to hand-roll underforbid(unsafe_code)— RustCrypto (sha2,hmac,aes,des,cbc,cipher,p256,aes-gcm),getrandom,zeroize,scrypt(Aegis vaults),rsa/rand(host RSA keygen, confined tokeyroost-rsakey), andminiz_oxide(CTAP large-blob deflate). QR and image decoding (rqrr,png,jpeg-decoder) is confined tokeyroost-qr. The README's "Workspace layout" table lists them per crate. No new dependency lands without that justification. - One build script, Windows-only. The workspace has exactly one
build.rs(crates/keyroost/build.rs). It embeds the application icon and theVS_VERSION_INFOresource intokeyroost.exeso that CI,cargo installand vendor-signed builds all produce the same bytes plus a signature. It is#[cfg(windows)]— an emptymain()on every other target — and its sole build-dependency (winresource) is declared under[target.'cfg(windows)'.build-dependencies], so a Linux or macOS build neither fetches nor runs it. No build script in this workspace generates code, downloads anything, or reads outside its own package. - Continuously fuzzed and dependency-audited. The parsers named in the
threat model are covered by fifteen
cargo-fuzztargets (fuzz/fuzz_targets/) run on a weekly schedule; a RUSTSEC scan (cargo audit) runs weekly and on every change to a manifest or the lockfile, and Dependabot proposes monthly updates for both the cargo and the GitHub-Actions dependency sets. The fuzz harness is its own workspace so its nightly-only toolchain requirement never enters the shipped dependency tree. - Reviewable releases. Release binaries are built by CI from tagged
commits using SHA-pinned actions, without shared build caches (a tag
build would otherwise fall back to default-branch caches), with
SHA256SUMSand a build-provenance attestation published alongside (gh attestation verify <file> --repo framefilter/keyroost). The crates.io fanout authenticates through Trusted Publishing (OIDC), so no long-lived registry token exists to be stolen.
sha256sum -c SHA256SUMS --ignore-missing
gh attestation verify keyroost-*-linux-x86_64.tar.gz --repo framefilter/keyroostThe release assets are named keyroost-<tag>-linux-x86_64.tar.gz,
keyroost-<tag>-macos-universal2.tar.gz and
keyroost-<tag>-windows-x86_64.zip; SHA256SUMS covers all three.
Or skip the question entirely and build from source with
cargo build --release --locked.