{"id":50716924,"url":"https://github.com/systemslibrarian/crypto-lab-ssh-handshake","last_synced_at":"2026-06-09T19:01:48.696Z","repository":{"id":363548274,"uuid":"1263762780","full_name":"systemslibrarian/crypto-lab-ssh-handshake","owner":"systemslibrarian","description":"Browser-based SSH handshake demo — real X25519 ECDH key exchange authenticated by an Ed25519 host key, known_hosts Trust-On-First-Use, MITM detection on host-key change. First contact is a leap of faith","archived":false,"fork":false,"pushed_at":"2026-06-09T10:31:18.000Z","size":135,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-09T12:06:32.776Z","etag":null,"topics":["crypto-lab","cryptography","ecdh","ed25519","forward-secrecy","host-key","key-exchange","known-hosts","mitm","ssh","tofu","trust-model","trust-on-first-use","x25519"],"latest_commit_sha":null,"homepage":"https://systemslibrarian.github.io/crypto-lab-ssh-handshake/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/systemslibrarian.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T08:43:30.000Z","updated_at":"2026-06-09T10:35:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/systemslibrarian/crypto-lab-ssh-handshake","commit_stats":null,"previous_names":["systemslibrarian/crypto-lab-ssh-handshake"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/systemslibrarian/crypto-lab-ssh-handshake","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fcrypto-lab-ssh-handshake","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fcrypto-lab-ssh-handshake/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fcrypto-lab-ssh-handshake/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fcrypto-lab-ssh-handshake/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/systemslibrarian","download_url":"https://codeload.github.com/systemslibrarian/crypto-lab-ssh-handshake/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fcrypto-lab-ssh-handshake/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34121022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crypto-lab","cryptography","ecdh","ed25519","forward-secrecy","host-key","key-exchange","known-hosts","mitm","ssh","tofu","trust-model","trust-on-first-use","x25519"],"created_at":"2026-06-09T19:01:46.814Z","updated_at":"2026-06-09T19:01:48.687Z","avatar_url":"https://github.com/systemslibrarian.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# crypto-lab-ssh-handshake\n\n## What It Is\n\nAn interactive model of the **SSH transport-layer handshake** and the **`known_hosts` / Trust-On-First-Use** flow that authenticates the server. The server holds a long-term host key; every connection generates fresh ephemeral ECDH keys on both sides; the server signs a single \"exchange hash\" — a SHA-256 binding the host name, the host public key, both ephemeral public keys, and the shared secret — to prove possession of its host private key, and the client verifies that signature and then compares the host fingerprint against its known_hosts pin. The crypto is real: ephemeral **X25519** with an automatic **ECDH P-256** fallback for key agreement, and **Ed25519** with an automatic **ECDSA P-256** fallback for the host signature, all via the Web Crypto API. The problem SSH solves with this is **authenticating a server you have no CA path and no web-of-trust path to**: the first connection pins the host key (an unverified leap of faith), and every connection after that detects whether the key changed — the model gets you forward-secret sessions and change detection without a central authority. What is deliberately **not** modelled is the SSH binary packet protocol, the RFC 4253 algorithm negotiation, channels, or user authentication: messages here are plain JSON objects. This is a faithful model of the transport-security and trust logic, not a re-implementation of OpenSSH's wire format.\n\n## When to Use It\n\n- **Understanding the SSH host-key prompt** — see exactly what the client is being asked to commit to when it prints `The authenticity of host '…' can't be established` and a fingerprint. The demo's `ask` mode rolls back the engine's auto-pin so you have to actually accept, reject, or verify the fingerprint yourself.\n- **Reasoning about `known_hosts` warnings** — when `REMOTE HOST IDENTIFICATION HAS CHANGED!` appears, this is what is happening underneath. The transcript inspector highlights the field that broke the handshake (host pubkey, signature, etc.) so you can see *which* part of the check fired.\n- **Contrasting the three trust models** — read alongside the sibling [`crypto-lab-pki-chain`](https://systemslibrarian.github.io/crypto-lab-pki-chain/) (hierarchical CA / TLS) and [`crypto-lab-web-of-trust`](https://systemslibrarian.github.io/crypto-lab-web-of-trust/) (decentralized PGP) demos. SSH sits between them: no CA, no graph, just a per-host pin — unless you opt into SSHFP+DNSSEC or OpenSSH `@cert-authority`, which the demo also models.\n- **Teaching ephemeral KEX + signature authentication** — the same shape shows up in TLS 1.3 and Noise; SSH is the cleanest place to see it because there is no certificate machinery in the way.\n- **Comparing `StrictHostKeyChecking` modes** — toggle between `yes` (refuse unknown), `ask` (prompt), `accept-new` (silent pin, reject change), and `no` (trust whatever responds) and watch the same connection produce very different decisions.\n- **Do NOT use this to reason about first-contact safety in production** — TOFU does **not** protect a first connection against an active man-in-the-middle. The demo includes the *MITM on first contact* and *DNS spoof of SSHFP without DNSSEC* scenarios specifically to make that limitation undeniable.\n- **Do NOT use this as a real SSH implementation** — this is a toy for learning. For production use OpenSSH, libssh, or another vetted library.\n\n## Live Demo\n\n[**https://systemslibrarian.github.io/crypto-lab-ssh-handshake/**](https://systemslibrarian.github.io/crypto-lab-ssh-handshake/)\n\nThe page walks through six sections.\n\n* **Start the server** generates a real host keypair in your browser and shows its `SHA256:` fingerprint. Two optional trust-bootstrap mechanisms also live here: publish an **SSHFP DNS record** (with a DNSSEC toggle, RFC 4255) and start an **OpenSSH host CA** that can sign the host's pubkey so clients can `@cert-authority` trust the CA instead of pinning each host.\n* **Connect** runs the handshake. A `StrictHostKeyChecking` selector lets you pick `yes` / `ask` / `accept-new` / `no` — `ask` produces an explicit Accept / Reject / Verify-out-of-band / Verify-via-SSHFP prompt instead of silently pinning. Each handshake exposes a full transcript inspector (client and server ephemerals, host pubkey, exchange hash, signature, decision) with copy-as-JSON; the field that broke the connection is highlighted. A realistic `~/.ssh/known_hosts` file view sits next to the pin list, alongside `ssh-keygen -F` and `ssh-keygen -R` outputs. A **Reset everything** control wipes the demo to its initial state.\n* **Break it (and recover)** runs eight scenarios — *MITM after pinning* (caught by known_hosts), *MITM on first contact* (TOFU pins the attacker — the honest limitation), *tampered host signature* (signature verification fires), *DNS spoof of SSHFP without DNSSEC* (the \"verify out of band\" path lies), *rogue CA signs the attacker's host* (rejected because the rogue CA is not the trust anchor), and three operational scenarios: *planned key rotation*, *emergency rotation*, and *rotate host under same CA* (the only one that connects without a warning). Each scenario has a \"Copy summary as Markdown\" button for sharing.\n* **Three trust models** compares hierarchical PKI, Web of Trust, and SSH TOFU, with first-contact-in-three-demos callouts linking to the sibling demos.\n* **In the real world** documents `~/.ssh/known_hosts`, `SHA256:` fingerprints, host key types, SSHFP DNS records, certificate-based SSH, and `StrictHostKeyChecking` modes, with honest \"what TOFU teaches\" cards.\n* **Scope \u0026 provenance** spells out what the demo models faithfully and what it deliberately omits, with links to RFC 4251 / 4253 / 4255 / 5656, OpenSSH `PROTOCOL.certkeys`, and the relevant `man` pages.\n\nThe URL accepts `?scenario=\u003cid\u003e` for deep links — e.g. `?scenario=mitm-after` auto-starts the server, pins the legitimate host once, then triggers the MITM-after-pinning scenario.\n\n## How to Run Locally\n\n```bash\ngit clone https://github.com/systemslibrarian/crypto-lab-ssh-handshake.git\ncd crypto-lab-ssh-handshake\nnpm install\nnpm run dev        # local dev server with HMR\nnpm run build      # type-check + production build to dist/\nnpm run preview    # serve the built dist/ locally\nnpm test           # vitest — 40 unit tests (engine, policy, wire format, SSHFP, CA)\nnpm run test:e2e   # playwright — 13 browser tests for the teaching flows (needs `npx playwright install chromium`)\n```\n\nNo environment variables, no API keys, no servers. Everything runs client-side in the browser. The engine in `src/engine.ts` is the verbatim source from the build prompt; the only post-hoc refinement is `fingerprint()`, which now hashes the canonical OpenSSH wire-format public-key blob (via `src/wire.ts`) instead of concatenated JWK coordinates so the demo's `SHA256:` strings match what `ssh-keygen -lf` prints. The other modules — StrictHostKeyChecking policy in `src/policy.ts`, SSHFP registry in `src/sshfp.ts`, host CA in `src/ca.ts`, transcript capture in `src/transcript.ts` — sit on top of the engine without touching its cryptographic logic.\n\n## Part of the Crypto-Lab Suite\n\nThis is one demo in a wider portfolio of interactive cryptography labs — see [systemslibrarian.github.io/crypto-lab](https://systemslibrarian.github.io/crypto-lab/) for the rest, including the five PQC families overview, hybrid TLS, harvest-now-decrypt-later timelines, and deep-dives on individual schemes.\n\n---\n\n\"So whether you eat or drink or whatever you do, do it all for the glory of God.\" — 1 Corinthians 10:31\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fcrypto-lab-ssh-handshake","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsystemslibrarian%2Fcrypto-lab-ssh-handshake","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fcrypto-lab-ssh-handshake/lists"}