Skip to content

fix(ssh): emit interoperable Ed25519 OpenSSH keys - #975

Open
trac3r00 wants to merge 1 commit into
release/product-value-20260912-05from
release/product-value-20260912-06
Open

trac3r00 wants to merge 1 commit into
release/product-value-20260912-05from
release/product-value-20260912-06

Conversation

@trac3r00

@trac3r00 trac3r00 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

fix(ssh): emit interoperable Ed25519 OpenSSH keys. This is logical change 6/11 in the dependency-ordered product-audit release stack.

Refs #969

What changed

  • fix(ssh): emit interoperable Ed25519 OpenSSH keys
  • Exact source commit: 2091ca61d6a7a4ac928c3edc8072164f9727967e; validated tree: c471025df8dcc0326cde39203b8fce487aa340be.
  • Depends on the preceding release-stack PR. Retarget to main only after its dependency is merged and deployed.

Why

Emit Ed25519 private keys that OpenSSH accepts and that match the public key shown to the user.

Verification

  • bun run build passed on this exact candidate tree.
  • npm test -- --maxWorkers=2 passed on this exact candidate tree.
  • Affected behavior manually exercised as described below.
  • Latest GitHub Build, Unit Tests (Vitest), and E2E Tests (Playwright) must all pass before merge.
Tests  766 passed (766)
CANDIDATE_BUILD_UNIT_GREEN
Committed tree equals validated tree: c471025df8dcc0326cde39203b8fce487aa340be

A real browser-generated Ed25519 key was accepted by ssh-keygen and derived the exact displayed public blob; temporary key files were removed.

Final combined tree additionally passed 801 unit tests and all 293 Playwright tests with retries disabled, plus all 48 primary tool workflows at desktop and mobile. The exploratory Color Converter exact-HEX boundary remains a documented pre-existing defect; its runtime is unchanged by this stack.

Risk & rollback

  • Risk: Dependent commits require earlier stack changes; merge in order.
  • Rollback: revert this PR through a new PR; do not revert dependencies beneath already-merged dependents.
  • Release: require an approving review and latest-SHA CI. Respect the 15-minute soak between deploy-affecting merges and verify the production deployment before continuing.

Summary by cubic

Fixes the SSH key generator so Ed25519 private keys are accepted by OpenSSH. Previously the generator exported PKCS#8 private keys that ssh-keygen rejected; now they're encoded in the OpenSSH private key format and match the displayed public key.

  • Adds an E2E test that loads the generated key with ssh-keygen and confirms it derives the displayed public key.

Written for commit 2091ca6. Summary will update on new commits.

Review in cubic

Encode the OpenSSH private-key envelope and verify displayed public-key equality with native ssh-keygen.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Confidence score: 4/5

  • tests/e2e/ssh-key-generator-interop.spec.js hard-depends on the external ssh-keygen binary without an availability guard, so environments without OpenSSH can fail the entire E2E suite—skip or conditionally run the test when the dependency is unavailable.
  • tests/e2e/ssh-key-generator-interop.spec.js does not explicitly select Ed25519, so a future default change could make the test validate a different key type—choose the Ed25519 option before asserting interop.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/e2e/ssh-key-generator-interop.spec.js">

<violation number="1" location="tests/e2e/ssh-key-generator-interop.spec.js:10">
P3: The test asserts Ed25519 interop but never selects the Ed25519 radio, depending on the current checked default in the route. If the default key type changes, this test silently starts validating a different key type. Click the ed25519 radio explicitly (and assert it) so the test's coverage matches its name and intent.</violation>

<violation number="2" location="tests/e2e/ssh-key-generator-interop.spec.js:46">
P2: The test hard-depends on the external `ssh-keygen` binary with no availability guard, so any machine running `npm run test:e2e` without the OpenSSH client (a dev box, a non-ubuntu runner) fails the whole suite rather than skipping this interop check. Detect the binary and `test.skip` cleanly when it is absent, e.g. `spawnSync("ssh-keygen", ["-V"])` up front and skip on error, while still failing hard when present but key parsing fails.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

try {
await writeFile(keyPath, `${generated.privateKey}\n`, { mode: 0o600 });
await chmod(keyPath, 0o600);
const derived = spawnSync("ssh-keygen", ["-y", "-f", keyPath], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The test hard-depends on the external ssh-keygen binary with no availability guard, so any machine running npm run test:e2e without the OpenSSH client (a dev box, a non-ubuntu runner) fails the whole suite rather than skipping this interop check. Detect the binary and test.skip cleanly when it is absent, e.g. spawnSync("ssh-keygen", ["-V"]) up front and skip on error, while still failing hard when present but key parsing fails.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/e2e/ssh-key-generator-interop.spec.js, line 46:

<comment>The test hard-depends on the external `ssh-keygen` binary with no availability guard, so any machine running `npm run test:e2e` without the OpenSSH client (a dev box, a non-ubuntu runner) fails the whole suite rather than skipping this interop check. Detect the binary and `test.skip` cleanly when it is absent, e.g. `spawnSync("ssh-keygen", ["-V"])` up front and skip on error, while still failing hard when present but key parsing fails.</comment>

<file context>
@@ -0,0 +1,60 @@
+  try {
+    await writeFile(keyPath, `${generated.privateKey}\n`, { mode: 0o600 });
+    await chmod(keyPath, 0o600);
+    const derived = spawnSync("ssh-keygen", ["-y", "-f", keyPath], {
+      encoding: "utf8",
+      stdio: ["ignore", "pipe", "pipe"],
</file context>

test("Ed25519 private output is loadable and matches its displayed public key", async ({
page,
}) => {
await page.goto("/ssh-key-generator", { waitUntil: "domcontentloaded" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test asserts Ed25519 interop but never selects the Ed25519 radio, depending on the current checked default in the route. If the default key type changes, this test silently starts validating a different key type. Click the ed25519 radio explicitly (and assert it) so the test's coverage matches its name and intent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/e2e/ssh-key-generator-interop.spec.js, line 10:

<comment>The test asserts Ed25519 interop but never selects the Ed25519 radio, depending on the current checked default in the route. If the default key type changes, this test silently starts validating a different key type. Click the ed25519 radio explicitly (and assert it) so the test's coverage matches its name and intent.</comment>

<file context>
@@ -0,0 +1,60 @@
+test("Ed25519 private output is loadable and matches its displayed public key", async ({
+  page,
+}) => {
+  await page.goto("/ssh-key-generator", { waitUntil: "domcontentloaded" });
+
+  const generated = await page.evaluate(() =>
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant