Skip to content

NIOSSH doesn't advertise OpenSSH certificate support in public key algorithms #225

@jeffellin

Description

@jeffellin

Summary

NIOSSH supports OpenSSH certificates (ssh-*-cert-v01@openssh.com) for authentication but fails to advertise these algorithm names during key exchange and user authentication. This causes SSH servers to reject certificate-based authentication even though NIOSSH can correctly parse and use certificates.

Environment

  • SwiftNIO SSH version: main branch (commit 8f33cac)
  • Swift version: 6.0+
  • Platform: macOS 15.0, iOS 18.0

Problem Description

When NIOSSH performs key exchange or user authentication, it advertises supported host key algorithms via NIOSSHPrivateKey.hostKeyAlgorithms. However, this property only returns base algorithm names (e.g., ssh-ed25519, ecdsa-sha2-nistp256) and omits the corresponding certificate algorithm names (e.g., ssh-ed25519-cert-v01@openssh.com, ecdsa-sha2-nistp256-cert-v01@openssh.com).

Code Location

Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift lines 62-77

internal var hostKeyAlgorithms: [Substring] {
    switch self.backingKey {
    case .ed25519:
        return ["ssh-ed25519"]  // ❌ Missing: "ssh-ed25519-cert-v01@openssh.com"
    case .ecdsaP256:
        return ["ecdsa-sha2-nistp256"]  // ❌ Missing: "ecdsa-sha2-nistp256-cert-v01@openssh.com"
    case .ecdsaP384:
        return ["ecdsa-sha2-nistp384"]  // ❌ Missing: "ecdsa-sha2-nistp384-cert-v01@openssh.com"
    case .ecdsaP521:
        return ["ecdsa-sha2-nistp521"]  // ❌ Missing: "ecdsa-sha2-nistp521-cert-v01@openssh.com"
    // ...
    }
}

Impact

Observable Symptoms:

✅ NIOSSH can parse OpenSSH certificates
✅ NIOSSH can sign with certificate private keys
✅ NIOSSH can verify certificate signatures
❌ SSH servers reject authentication because client didn't advertise certificate support
❌ Certificate-based authentication fails even with valid certificates

Root Cause

During SSH protocol negotiation:

  1. Key Exchange (SSH_MSG_KEXINIT):

    • Client advertises: ["ssh-ed25519", "ecdsa-sha2-nistp256", ...]
    • Client omits: ["ssh-ed25519-cert-v01@openssh.com", "ecdsa-sha2-nistp256-cert-v01@openssh.com", ...]
    • Server doesn't know client supports certificates
  2. User Authentication (SSH_MSG_USERAUTH_REQUEST):

    • Client uses certificate for public key auth
    • Server checks if ssh-ed25519-cert-v01@openssh.com was in advertised algorithms
    • Server finds it missing → rejects authentication

RFC Requirements

RFC 4252 Section 7 "Public Key Authentication Method: publickey" states that clients should advertise all supported public key algorithms. OpenSSH certificate types are defined in PROTOCOL.certkeys and use the *-cert-v01@openssh.com naming convention per RFC 4250 Section 4.6.1.

Reproduction

Test with OpenSSH Certificate

# Generate certificate (using ssh-keygen)
ssh-keygen -t ed25519 -f test_key
ssh-keygen -s ca_key -I test_user -n test_user test_key.pub

# Try to authenticate with NIOSSH using test_key-cert.pub
# Result: Server rejects because client didn't advertise cert support

Expected vs Actual Behavior

Expected (OpenSSH client):

SSH_MSG_KEXINIT:
  server_host_key_algorithms: [
    "ssh-ed25519-cert-v01@openssh.com",
    "ssh-ed25519",
    "ecdsa-sha2-nistp256-cert-v01@openssh.com",
    "ecdsa-sha2-nistp256",
    ...
  ]

Actual (NIOSSH):

SSH_MSG_KEXINIT:
  server_host_key_algorithms: [
    "ssh-ed25519",
    "ecdsa-sha2-nistp256",
    ...
  ]
  // ❌ Certificate algorithms missing

Proposed Solution

Update hostKeyAlgorithms to include both base and certificate algorithm names:

internal var hostKeyAlgorithms: [Substring] {
    switch self.backingKey {
    case .ed25519:
        return ["ssh-ed25519-cert-v01@openssh.com", "ssh-ed25519"]
    case .ecdsaP256:
        return ["ecdsa-sha2-nistp256-cert-v01@openssh.com", "ecdsa-sha2-nistp256"]
    case .ecdsaP384:
        return ["ecdsa-sha2-nistp384-cert-v01@openssh.com", "ecdsa-sha2-nistp384"]
    case .ecdsaP521:
        return ["ecdsa-sha2-nistp521-cert-v01@openssh.com", "ecdsa-sha2-nistp521"]
    #if canImport(Darwin)
    case .secureEnclaveP256:
        return ["ecdsa-sha2-nistp256-cert-v01@openssh.com", "ecdsa-sha2-nistp256"]
    #endif
    }
}

Why This Works

  1. Backward Compatible: Servers that don't support certificates will use base algorithm names
  2. Enables Certificates: Servers that support certificates will see them advertised
  3. Matches OpenSSH: OpenSSH client advertises both base and certificate algorithms
  4. Minimal Change: 4-line change (one per key type)
  5. No Breaking Changes: Only adds capability, doesn't remove anything

Certificate Algorithm Names

These are defined in NIOSSHCertifiedPublicKey.swift (lines 344-350):

static let p256KeyPrefix = "ecdsa-sha2-nistp256-cert-v01@openssh.com".utf8
static let p384KeyPrefix = "ecdsa-sha2-nistp384-cert-v01@openssh.com".utf8
static let p521KeyPrefix = "ecdsa-sha2-nistp521-cert-v01@openssh.com".utf8
static let ed25519KeyPrefix = "ssh-ed25519-cert-v01@openssh.com".utf8

Test Coverage

This change is difficult to unit test without a full SSH server that requires certificates. However, it can be verified by:

  1. Integration test: Connect to SSH server with certificate requirement
  2. Packet capture: Verify certificate algorithms appear in SSH_MSG_KEXINIT
  3. Real-world testing: Authenticate with OpenSSH certificates

Breaking Changes

None. This is a pure capability addition:

  • Existing non-certificate authentication continues to work
  • Adds support for certificate-based authentication
  • No API changes
  • No behavior changes for existing code

Impact

This fix enables NIOSSH to use OpenSSH certificates for authentication with:

  • Enterprise SSH servers requiring certificate authentication
  • Bastion hosts and jump boxes using certificates
  • Zero-trust architectures using short-lived certificates
  • Organizations using certificate-based access control
  • Cloud providers requiring certificate authentication

Files Changed

  1. Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift
    • Line 65: Add "ssh-ed25519-cert-v01@openssh.com"
    • Line 67: Add "ecdsa-sha2-nistp256-cert-v01@openssh.com"
    • Line 69: Add "ecdsa-sha2-nistp384-cert-v01@openssh.com"
    • Line 71: Add "ecdsa-sha2-nistp521-cert-v01@openssh.com"

Total changes: 4 lines (one per key type)

Priority

IMPORTANT - Blocks certificate-based authentication use cases, which are increasingly common in enterprise and cloud environments.

References

  • RFC 4252 Section 7: Public Key Authentication Method
  • RFC 4250 Section 4.6.1: Naming conventions for extensions
  • OpenSSH PROTOCOL.certkeys: Certificate format specification
  • Existing code: NIOSSHCertifiedPublicKey.swift lines 344-350 (defines cert prefixes)

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/enhancementImprovements to existing feature.size/SSmall task. (A couple of hours of work.)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions