whip-pair prototype
whip-pair is a direct, one-shot SSH public-key enrollment prototype for
Whip. It uses the host's existing SSH port: no relay, TLS listener, additional
firewall rule, pairing account, or sshd_config change is required.
The host creates a temporary Ed25519 credential and adds only its public half
to the current user's authorized_keys with restrict and a forced command.
The QR contains the private seed, SSH endpoint, username, and SSH host-key pin.
Whip uses that credential for one restricted exchange, the host user approves
the submitted permanent public key, and whip-pair removes the temporary
authorization.
Install
All non-Nix installations require ssh-keyscan from an OpenSSH client package
to be available on PATH.
uv
Run the native binary from PyPI without installing Rust:
uvx whip-pair
uvx is an alias for uv tool run; either form creates an isolated tool
environment and runs whip-pair. Published wheels support macOS and Linux on
ARM64 and x64.
npm
Run the native binary from npm without installing Rust:
npx whip-pair
The npm package is a dependency-free launcher containing the same four native binaries. Node.js 18 or newer is required.
Cargo
Install the published crate with Rust 1.85 or newer:
cargo install --locked whip-pair
whip-pair
Nix
Run the public version:
nix run github:KaminariOS/whip#whip-pair
Without arguments, whip-pair lists the host's reachable interface addresses,
uses ifconfig.me to discover its public IP when curl is available, and also
offers a manual public-address choice:
Choose how Whip will reach this host:
1. Tailscale 100.84.12.5 tailscale0
2. Wi-Fi 192.168.1.20 wlan0
3. Public 203.0.113.10 ifconfig.me
4. Public/other Enter a public IP address or hostname
Selection [1]:
Public-IP discovery does not verify that SSH is reachable through the router or firewall. Choose Public/other to enter a DNS name or a different address, or select a specific reachable endpoint non-interactively:
nix run github:KaminariOS/whip#whip-pair -- serve \
--advertise-host 192.168.1.10
If SSH is already listening on a nonstandard port, advertise it with
--ssh-port:
nix run github:KaminariOS/whip#whip-pair -- serve \
--advertise-host ssh.example.com \
--ssh-port 2222
--ssh-port defaults to 22. It does not open a new port; it must match the
existing SSH service reachable at the advertised host. When no
--advertise-host is supplied, the interactive setup asks for the endpoint and
port, offering 22 as the default. Passing both options skips those questions.
whip-pair does not bind a TCP listener; Whip connects to the host's existing
SSH service.
For a public endpoint behind NAT, forward the advertised public port to the
host's SSH port and allow that SSH port through the relevant router, host, or
cloud firewalls. If the host cannot reach its own public endpoint because the
router lacks NAT loopback, automatic ssh-keyscan discovery will fail. Read
the local Ed25519 host-key fingerprint and pass it explicitly:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256
nix run github:KaminariOS/whip#whip-pair -- serve \
--advertise-host ssh.example.com \
--ssh-port 2222 \
--ssh-fingerprint 'SHA256:...'
From a local checkout:
nix run .#whip-pair
The Nix app includes ssh-keyscan. Automatic discovery requests Ed25519,
ECDSA, and RSA host keys in Whip's russh preference order. Use
--ssh-fingerprint to provide a fingerprint explicitly.
During development, print the underlying envelope:
nix run .#whip-pair -- serve --advertise-host 192.168.1.10 --print-code
Exercise the SSH-only protocol with a disposable public key:
nix run .#whip-pair -- request \
--code 'WP4:...' \
--public-key ~/.ssh/id_ed25519.pub \
--device-name 'Prototype client'
Inspect a copied envelope without exposing its temporary private seed:
nix run .#whip-pair -- inspect 'WP4:...'
Prototype limitations
- The SSH server must use OpenSSH-compatible
authorized_keys, support therestrictandcommandoptions, and read the current user's default~/.ssh/authorized_keysfile. - The interactive network selector uses
curlto offer the public IP reported byifconfig.mewhen available. Failure to fetch it does not block pairing. - Automatic host-key discovery invokes
ssh-keyscan; the Nix app supplies it. The Nix app also suppliescurlfor public-IP discovery. - The terminal QR uses error-correction level L to stay compact.
- Whip's mobile scanner supports WP4. The
requestsubcommand remains a protocol test client.
Pairing protocol (WP4)
The canonical Mermaid source is
docs/wp4-sequence.mmd.
Render and validate the tracked SVG with:
nix shell nixpkgs#mermaid-cli -c mmdc \
-i whip-pair/docs/wp4-sequence.mmd \
-o whip-pair/docs/wp4-sequence.svg
QR bootstrap envelope
The QR contains WP4: followed by Base45-encoded binary data.
| Field | Encoding | Purpose |
|---|---|---|
| Address type | 1 byte | 1 IPv4, 2 IPv6, or 3 hostname |
| SSH host | 4 or 16 bytes, or 1-byte length plus ASCII hostname | Address selected or advertised by the host |
| SSH port | 2-byte unsigned integer, big-endian | Existing SSH service port |
| SSH username | 1-byte length plus ASCII | Existing local account |
| Temporary Ed25519 seed | 32 random bytes | Restricted SSH bearer credential |
| SSH host-key SHA-256 digest | 32 bytes | Pins the SSH server identity during pairing |
For an IPv4 address and a five-character username, the complete code is 120 characters and renders as a 37-module QR at error-correction level L. Expiry is enforced by the running host process and is intentionally omitted.
Host authorization and exchange
Before displaying the QR, whip-pair adds an entry equivalent to:
restrict,command="whip-pair exchange --socket …" ssh-ed25519 AAAA… whip-pair-temporary
restrict disables shell access, PTY allocation, forwarding, agent forwarding,
X11, and user startup commands. The forced command accepts one bounded JSON
EnrollmentRequest on SSH stdin and relays it through a Unix socket in a
mode-0700 temporary directory to the visible whip-pair process.
The parent process validates the submitted OpenSSH public key, displays its
SHA-256 fingerprint, and asks the local user to approve it. On approval it
appends the permanent key safely and idempotently, replies with an
EnrollmentResponse, removes the temporary entry, and exits. Rejection leaves
the one-shot invitation available until its TTL expires.
Whip verifies the SSH host key against the digest in the QR before sending any enrollment data. After pairing, it stores the verified public host key in its global known-hosts list, so the saved host does not require a second trust prompt.
Security properties and boundaries
- The QR is a short-lived bearer credential. Anyone who can scan it and reach SSH can request enrollment, but the key can invoke only the forced exchange command and the host still asks for approval.
- WP4 transfers a temporary private-key seed, never the private half of the permanent key being enrolled. Normal SSH authentication later proves possession of that permanent key.
- The temporary key is removed on success, expiry, or Ctrl-C. If the process is killed before cleanup, the leftover entry remains restricted and its forced command cannot connect to the vanished private Unix socket.
- Bare OpenSSH public keys understood by Whip's
ssh-keyparser are accepted.authorized_keysoptions are rejected, comments are preserved, and duplicate permanent keys are not appended. - The writer refuses symlink targets and files not owned by the current user,
locks updates, uses atomic replacement for removal, and creates
.sshandauthorized_keyswith modes0700and0600when needed.
Publishing
The Publish whip-pair to crates.io
workflow verifies the crate version, formatting, Clippy, tests, and package
contents before publishing. Crates.io trusts only publish-whip-pair.yml in
KaminariOS/whip with the protected crates-io environment. The workflow
uses a short-lived OIDC credential; GitHub stores no crates.io API token.
The Publish whip-pair to PyPI and npm
workflow builds native PyPI wheels and npm binaries for macOS and Linux on
ARM64 and x64. PyPI should trust publish-whip-pair-packages.yml with the
pypi environment. After the first npm release, npm should trust the same
workflow with the npm environment. Both registries then publish with
short-lived GitHub OIDC credentials and provenance instead of stored tokens.
PyPI supports a pending trusted publisher for the first release. npm requires
the package to exist before trusted publishing can be configured, so add a
short-lived NPM_TOKEN secret to the protected npm environment, manually run
the workflow once with Publish the npm package and Bootstrap the first
npm release with NPM_TOKEN enabled, configure npm trusted publishing, and
then delete the secret.
To release a version, update version in whip-pair/Cargo.toml,
whip-pair/npm/package.json, and the mkWhipPair version in flake.nix,
refresh whip-pair/Cargo.lock, and run:
nix develop -c cargo test --locked --manifest-path whip-pair/Cargo.toml
nix develop -c cargo publish --locked --dry-run \
--manifest-path whip-pair/Cargo.toml
nix develop -c uvx maturin build --release --locked \
--manifest-path whip-pair/Cargo.toml \
--out whip-pair/dist
Commit those changes, then push a tag whose version matches Cargo.toml
exactly:
git tag whip-pair-v0.1.2
git push origin whip-pair-v0.1.2
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whip_pair-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: whip_pair-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
deb72749e1b00631d578bc7b52b0f5f82262eb9282f75adb87c16f73ec0aa75a
|
|
| MD5 |
af5d09b296759c668e77ad9436e112d8
|
|
| BLAKE2b-256 |
5416b064365604e618c0eeea6b8c73db5836688ba663ab3fb00d31e9ceacd4b4
|
Provenance
The following attestation bundles were made for whip_pair-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish-whip-pair-packages.yml on KaminariOS/whip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whip_pair-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
deb72749e1b00631d578bc7b52b0f5f82262eb9282f75adb87c16f73ec0aa75a - Sigstore transparency entry: 2559137942
- Sigstore integration time:
-
Permalink:
KaminariOS/whip@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Branch / Tag:
refs/tags/whip-pair-v0.1.2 - Owner: https://github.com/KaminariOS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-whip-pair-packages.yml@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file whip_pair-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: whip_pair-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.1 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
616a97d5944726c9e4b401c355524e83aa131bd3a87e4be86fe3c2adf37adcc0
|
|
| MD5 |
1b88bdb7dc0463dd0310442c8bbb019a
|
|
| BLAKE2b-256 |
d8e433ee859b1ed810ba211e61cc1b8b494549f97e175249a3b9a290158eab95
|
Provenance
The following attestation bundles were made for whip_pair-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
publish-whip-pair-packages.yml on KaminariOS/whip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whip_pair-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
616a97d5944726c9e4b401c355524e83aa131bd3a87e4be86fe3c2adf37adcc0 - Sigstore transparency entry: 2559138469
- Sigstore integration time:
-
Permalink:
KaminariOS/whip@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Branch / Tag:
refs/tags/whip-pair-v0.1.2 - Owner: https://github.com/KaminariOS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-whip-pair-packages.yml@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file whip_pair-0.1.2-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: whip_pair-0.1.2-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.1 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58d5cf1d66554c804b4c943c770573a466cb5af6069256a1b8812a0a3e78b34f
|
|
| MD5 |
9152dc8a32b30f0d091c399835f3c04e
|
|
| BLAKE2b-256 |
ef5c4316d7ade446168fcb2c3e6de9cba3ca67917a5983c02cef5b4b4753d027
|
Provenance
The following attestation bundles were made for whip_pair-0.1.2-py3-none-macosx_11_0_arm64.whl:
Publisher:
publish-whip-pair-packages.yml on KaminariOS/whip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whip_pair-0.1.2-py3-none-macosx_11_0_arm64.whl -
Subject digest:
58d5cf1d66554c804b4c943c770573a466cb5af6069256a1b8812a0a3e78b34f - Sigstore transparency entry: 2559138652
- Sigstore integration time:
-
Permalink:
KaminariOS/whip@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Branch / Tag:
refs/tags/whip-pair-v0.1.2 - Owner: https://github.com/KaminariOS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-whip-pair-packages.yml@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file whip_pair-0.1.2-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: whip_pair-0.1.2-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62a2829730f06da416c36cf2cd9ab52a1dab567f925bdd8cb0c1edda8dad8cec
|
|
| MD5 |
10203381f4ccae831a9ff14717092846
|
|
| BLAKE2b-256 |
a45ccc12e097781d18d15192759f7678af2fbfc7d48009c175ce0dc6ca8f749c
|
Provenance
The following attestation bundles were made for whip_pair-0.1.2-py3-none-macosx_10_12_x86_64.whl:
Publisher:
publish-whip-pair-packages.yml on KaminariOS/whip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whip_pair-0.1.2-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
62a2829730f06da416c36cf2cd9ab52a1dab567f925bdd8cb0c1edda8dad8cec - Sigstore transparency entry: 2559139210
- Sigstore integration time:
-
Permalink:
KaminariOS/whip@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Branch / Tag:
refs/tags/whip-pair-v0.1.2 - Owner: https://github.com/KaminariOS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-whip-pair-packages.yml@a4096a6a2d77524e0bbd1503550d3e9a501fa4a1 -
Trigger Event:
push
-
Statement type: