Skip to main content

whip-pair prototype

crates.io PyPI npm

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.

Pairing protocol (WP4)

WP4 pairing sequence

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-key parser are accepted. authorized_keys options 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 .ssh and authorized_keys with modes 0700 and 0600 when needed.

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

Select a specific reachable address when needed:

nix run github:KaminariOS/whip#whip-pair -- serve --bind 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 \
  --bind 192.168.1.10 \
  --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 selected or advertised host. When no --bind address is supplied, the interactive setup asks for this port and offers 22 as the default. Passing --ssh-port skips that question.

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 --bind 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 the restrict and command options, and read the current user's default ~/.ssh/authorized_keys file.
  • Automatic host-key discovery invokes ssh-keyscan; the Nix app supplies it.
  • The terminal QR uses error-correction level L to stay compact.
  • Whip's mobile scanner supports WP4. The request subcommand remains a protocol test client.

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.1
git push origin whip-pair-v0.1.1

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

whip_pair-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.2 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

whip_pair-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.1 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

whip_pair-0.1.0-py3-none-macosx_11_0_arm64.whl (1.1 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

whip_pair-0.1.0-py3-none-macosx_10_12_x86_64.whl (1.2 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file whip_pair-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for whip_pair-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0a7f3398dd6b0d7cb3d3811e3ce1b506d2a35a014ce3b0d539cd6a12b38076e7
MD5 bd99629369024a0d7bb01a497486bb9e
BLAKE2b-256 7e440940f9a1254f5dd87cabd2feb6591d964dce80bc2cffa3c75e236a90e766

See more details on using hashes here.

Provenance

The following attestation bundles were made for whip_pair-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish-whip-pair-packages.yml on KaminariOS/whip

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whip_pair-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for whip_pair-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 5b493471c5dbc15ceab9728c56608bb352cf31feeecb2bd6caec5d8df7c06182
MD5 d3c14f0764997e4d924a96844212fc1b
BLAKE2b-256 f32a7d52b3be623aadb374c7457b52f51c95fe3fd785ae7d9f17ed8c107f9ae3

See more details on using hashes here.

Provenance

The following attestation bundles were made for whip_pair-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish-whip-pair-packages.yml on KaminariOS/whip

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whip_pair-0.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whip_pair-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 30dfd03618f0bceb35c5043549463836a6ed367fa189ebcee32e15a48f57c3ed
MD5 2d0db0712d39a90fdde8fc869ea7e1ea
BLAKE2b-256 51361c4bcefab0ef248270870a4f666ec5609b6d4973f751fc6564f0b90669ed

See more details on using hashes here.

Provenance

The following attestation bundles were made for whip_pair-0.1.0-py3-none-macosx_11_0_arm64.whl:

Publisher: publish-whip-pair-packages.yml on KaminariOS/whip

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whip_pair-0.1.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whip_pair-0.1.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 a851617d2e1bca89ec4520303e020c0a7ae1b7fb0cce2a1e954888255988b040
MD5 e2bedcddeee6eda40074f6ebf31e0a31
BLAKE2b-256 1dfed74e2c7d6b719d274c1c4ca7cfff1fa8e5337eb8454fcb0716f74491b32b

See more details on using hashes here.

Provenance

The following attestation bundles were made for whip_pair-0.1.0-py3-none-macosx_10_12_x86_64.whl:

Publisher: publish-whip-pair-packages.yml on KaminariOS/whip

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.3

4 files

0.1.2

4 files

This release

0.1.0 This release

4 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page