Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ if strictConcurrencyDevelopment {
let package = Package(
name: "swift-nio-ssh",
platforms: [
.macOS(.v10_15),
.macOS(.v14),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There doesn't appear to be any motivating reason for this change.

.iOS(.v13),
.watchOS(.v6),
.tvOS(.v13),
Expand Down Expand Up @@ -112,3 +112,4 @@ for target in package.targets {
}
}
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //

44 changes: 44 additions & 0 deletions Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey+Ed25519.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
//===----------------------------------------------------------------------===//
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This header isn't correct: please provide the complete header block.


import Foundation

extension NIOSSHPrivateKey {
/// Construct an Ed25519 private key from the 32-byte seed and 32-byte public key.
/// - Parameters:
/// - seed: 32-byte Ed25519 private key seed.
/// - publicKey: 32-byte Ed25519 public key.
/// - Throws: If the provided data is not the correct length or the key cannot be constructed.
public init(ed25519PrivateKeySeed seed: Data, publicKey: Data) throws {
guard seed.count == 32 else {
throw NIOSSHError.protocolViolation(protocolName: "ssh-ed25519", violation: "ed25519 key construction not implemented")
}
guard publicKey.count == 32 else {
throw NIOSSHError.protocolViolation(protocolName: "ssh-ed25519", violation: "Invalid Ed25519 public key length \(publicKey.count), expected 32 bytes")
}

// Internally, NIOSSH expects an Ed25519 key that can sign using the ssh-ed25519 algorithm.
// Depending on the NIOSSH internal API, you may need to wrap these bytes into the library’s
// Ed25519 representation. This initializer provides a stable public entry point.
//
// The actual internal storage and signer hookup should be implemented to match NIOSSH’s
// existing key handling. If an internal Ed25519 representation already exists, initialize it here.

// Pseudocode placeholder for internal hookup:
// self = .ed25519(Ed25519PrivateKeyRepresentation(seed: [UInt8](seed), publicKey: [UInt8](publicKey)))

// If the Ed25519 representation is not directly available, throw for now.
// Replace this with the appropriate internal initializer for your codebase.
throw NIOSSHError.protocolViolation(protocolName: "ssh-ed25519", violation: "ed25519 key construction not implemented")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function doesn't actually work.

}
}//
// NIOSSHPrivateKey+Ed25519.swift
// swift-nio-ssh
//
// Created by Simon Bruce-Cassidy on 08/12/2025.
//
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This doesn't look like it belongs.



106 changes: 106 additions & 0 deletions Sources/NIOSSH/Keys And Signatures/OpenSSHKey+Ed25519.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

License header file is missing.


public enum OpenSSHKey {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we declaring a new namespace here?

public struct Ed25519Components {
public let seed: Data // 32 bytes
public let publicKey: Data // 32 bytes
}

/// Decode an unencrypted OpenSSH Ed25519 private key (openssh-key-v1 format).
/// - Parameter pem: The PEM text including BEGIN/END OPENSSH PRIVATE KEY markers.
/// - Returns: Ed25519Components (seed + public key)
/// - Throws: If the key is not in the expected format or is encrypted.
public static func decodeEd25519Unencrypted(fromPEM pem: String) throws -> Ed25519Components {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be useful to demonstrate that this does work. Can you wire up a unit test here?

let header = "-----BEGIN OPENSSH PRIVATE KEY-----"
let footer = "-----END OPENSSH PRIVATE KEY-----"
let trimmed = pem.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.contains(header), trimmed.contains(footer) else {
throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Not an OpenSSH private key")
}

let base64Body = trimmed
.replacingOccurrences(of: header, with: "")
.replacingOccurrences(of: footer, with: "")
.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)

guard let blob = Data(base64Encoded: base64Body) else {
throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Invalid base64 in OpenSSH private key")
}

var cursor = blob

func readUInt32(_ data: inout Data) throws -> UInt32 {
guard data.count >= 4 else { throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Truncated SSH string length") }
let val = data.prefix(4)
data.removeFirst(4)
return val.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
}

func readSSHString(_ data: inout Data) throws -> Data {
let len = try readUInt32(&data)
guard data.count >= Int(len) else { throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Truncated SSH string body") }
let s = data.prefix(Int(len))
data.removeFirst(Int(len))
return s
}

// Verify magic
let magic = "openssh-key-v1\0".data(using: .utf8)!
guard cursor.prefix(magic.count) == magic else {
throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Not openssh-key-v1 format")
}
cursor.removeFirst(magic.count)

// ciphername, kdfname, kdfoptions
let ciphername = try readSSHString(&cursor)
let kdfname = try readSSHString(&cursor)
_ = try readSSHString(&cursor) // kdfoptions

guard String(data: ciphername, encoding: .utf8) == "none",
String(data: kdfname, encoding: .utf8) == "none" else {
throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Encrypted OpenSSH key not supported")
}

// number of keys
let nkeys = try readUInt32(&cursor)
guard nkeys == 1 else {
throw NIOSSHError.protocolViolation(protocolName: "openssh-key-v1", violation: "Unexpected number of keys: \(nkeys)")
}

// public key blob (skip)
_ = try readSSHString(&cursor)

// private key section
var priv = try readSSHString(&cursor)

// two checkints
_ = try readUInt32(&priv)
_ = try readUInt32(&priv)

// keytype
let keyType = try readSSHString(&priv)
guard String(data: keyType, encoding: .utf8) == "ssh-ed25519" else {
throw NIOSSHError.protocolViolation(protocolName: "ssh-ed25519", violation: "Not an Ed25519 key")
}

// public key
var pubKeyStr = try readSSHString(&priv)
let pubRaw = try readSSHString(&pubKeyStr)
guard pubRaw.count == 32 else {
throw NIOSSHError.protocolViolation(protocolName: "ssh-ed25519", violation: "Invalid Ed25519 public key length \(pubRaw.count)")
}

// private key (64 bytes: 32 seed + 32 pub) as SSH string
let privRaw = try readSSHString(&priv)
guard privRaw.count == 64 else {
throw NIOSSHError.protocolViolation(protocolName: "ssh-ed25519", violation: "Invalid Ed25519 private key length \(privRaw.count)")
}

let seed = privRaw.prefix(32)
let pub = privRaw.suffix(32)

return Ed25519Components(seed: Data(seed), publicKey: Data(pub))
}
}
28 changes: 28 additions & 0 deletions Tests/NIOSSHTests/Ed25519Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import XCTest
@testable import NIOSSH

final class Ed25519Tests: XCTestCase {

func testInitFromRawSeedAndPublicKey_InvalidLengths_Throw() {
// Too-short seed
let shortSeed = Data(repeating: 1, count: 31)
let pub = Data(repeating: 2, count: 32)
XCTAssertThrowsError(try NIOSSHPrivateKey(ed25519PrivateKeySeed: shortSeed, publicKey: pub))

// Too-short public key
let seed = Data(repeating: 1, count: 32)
let shortPub = Data(repeating: 2, count: 31)
XCTAssertThrowsError(try NIOSSHPrivateKey(ed25519PrivateKeySeed: seed, publicKey: shortPub))
}

func testInitFromRawSeedAndPublicKey_NotImplementedYet_Throws() {
// With valid lengths, the initializer is expected to throw until internal hookup is implemented.
let seed = Data(repeating: 1, count: 32)
let pub = Data(repeating: 2, count: 32)
XCTAssertThrowsError(try NIOSSHPrivateKey(ed25519PrivateKeySeed: seed, publicKey: pub))
}

func testDecodeOpenSSHPEM_SkippedUntilFixtureProvided() throws {
throw XCTSkip("Add a known-good OpenSSH Ed25519 PEM fixture before enabling this test.")
}
}