Skip to content

Add SSH certificate authentication support#1495

Open
labros-mediaalpha wants to merge 1 commit intomscdex:masterfrom
labros-mediaalpha:cert-auth-support
Open

Add SSH certificate authentication support#1495
labros-mediaalpha wants to merge 1 commit intomscdex:masterfrom
labros-mediaalpha:cert-auth-support

Conversation

@labros-mediaalpha
Copy link
Copy Markdown

@labros-mediaalpha labros-mediaalpha commented May 5, 2026

Transparency note: 100% Authored by Claude Sonnet 4.6

Problem

SSH certificates (ssh-ed25519-cert-v01@openssh.com and equivalent types for RSA/ECDSA) are widely used in enterprise environments where a CA signs user keys. This PR adds support for authenticating with them. Currently all three layers silently fail:

  • keyParser discards cert-type key blobs from agents and misparses cert pub files (only reads the inner public key, losing the cert fields the server needs)
  • Protocol.authPK sends the cert type name as the signature algorithm; the server requires the underlying key type (e.g. ssh-ed25519) and rejects the attempt
  • Client rejects pre-parsed key objects as privateKey, making it impossible to pass a cert key constructed outside the library

Changes

lib/protocol/keyParser.js

  • Adds a Cert_Public class that stores the full certificate blob and returns it from getPublicSSH(). This is what the server needs to verify the certificate against its trusted CA.
  • OpenSSH_Public.parse already matched cert types via its regex; it now routes them to Cert_Public instead of parseDER (which only handles plain public keys and would silently discard the certificate fields).
  • The binary key parsing path (used for SSH agent identity blobs) now detects cert types and creates Cert_Public objects rather than returning an error, so agent-held certificates are no longer silently filtered.

lib/protocol/Protocol.js

  • authPK was writing the certificate type name (e.g. ssh-ed25519-cert-v01@openssh.com) as the signature algorithm inside the signed SSH_MSG_USERAUTH_REQUEST. Per the SSH certificate protocol the signature algorithm must be the underlying key type (ssh-ed25519). Servers reject the auth attempt when these do not match.

lib/client.js

  • Accepts a publicKey config option (cert file as string/Buffer, or pre-parsed key) to use for identification when it differs from privateKey — the common case of id_ed25519 + id_ed25519-cert.pub as separate files.
  • Also accepts already-parsed key objects as privateKey/publicKey so callers that pre-parse keys can pass them directly.
  • The USERAUTH_PK_OK signing callback always uses privateKey (the real private key) regardless of whether a certificate is being used for identification.

Usage

// Explicit cert file
client.connect({
  host: 'example.com',
  username: 'user',
  privateKey: fs.readFileSync('/home/user/.ssh/id_ed25519'),
  publicKey: fs.readFileSync('/home/user/.ssh/id_ed25519-cert.pub'),
});

// Agent with cert identity — works automatically now
client.connect({
  host: 'example.com',
  username: 'user',
  agent: process.env.SSH_AUTH_SOCK,
});

When publicKey is omitted and no cert-type keys are involved, behaviour is identical to before this change.

Relation to #808

PR #808 proposed a similar publicKey option but targeted the old v0.x stream-based API. This PR targets the current v1.x API, adds the agent fix, and corrects the signature algorithm bug that would have caused #808 to fail against a real server even if merged.

SSH certificates (e.g. ssh-ed25519-cert-v01@openssh.com) are widely used
in enterprise environments where a CA signs user keys instead of managing
authorized_keys files. This change adds support for authenticating with
them.

Three components are needed:

keyParser: Add Cert_Public class
- Parses certificate public key files (id_xxx-cert.pub) and raw certificate
  blobs returned by SSH agents into a Cert_Public object.
- Cert_Public stores the full certificate blob and returns it from
  getPublicSSH(), which is what the server needs to verify the cert against
  its trusted CA.
- The existing OpenSSH_Public.parse already matched cert types via regex;
  it now routes them to Cert_Public instead of parseDER (which only handles
  plain public keys and would discard the cert fields).
- Binary key parsing (used for SSH agent identity blobs) similarly detects
  cert types and creates Cert_Public objects rather than failing.

Protocol: Fix signature algorithm for cert auth
- authPK was writing the certificate type name (e.g.
  "ssh-ed25519-cert-v01@openssh.com") as the signature algorithm in the
  signed USERAUTH_REQUEST. The SSH protocol requires the underlying key
  algorithm ("ssh-ed25519") in that field. The server rejects the
  authentication if these don't match.

Client: Support publicKey option and pre-parsed key objects
- Accepts a publicKey config option (cert file path/Buffer/parsed key) to
  use for identification when it differs from privateKey (e.g. when you
  have id_ed25519 + id_ed25519-cert.pub as separate files).
- Also accepts already-parsed key objects as privateKey/publicKey, so
  callers that pre-parse keys (e.g. to auto-detect certs alongside keys)
  can pass them directly without the client re-parsing.
- The USERAUTH_PK_OK handler signs challenges with privateKey so the
  signing key is always the real private key regardless of whether a
  certificate is being used for identification.

Usage:
  client.connect({
    host: 'example.com',
    username: 'user',
    privateKey: fs.readFileSync('~/.ssh/id_ed25519'),
    publicKey: fs.readFileSync('~/.ssh/id_ed25519-cert.pub'), // optional
  });

  When publicKey is omitted and privateKey is a cert-less key, behaviour
  is identical to before. SSH agents that return cert-type identities are
  now handled correctly too.
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