diff --git a/Sources/Citadel/SSHKeyTypeDetection.swift b/Sources/Citadel/SSHKeyTypeDetection.swift new file mode 100644 index 0000000..a1051b5 --- /dev/null +++ b/Sources/Citadel/SSHKeyTypeDetection.swift @@ -0,0 +1,266 @@ +import Foundation +import NIOCore + +/// Represents supported SSH key types that can be detected from key strings. +/// +/// A `struct` is used instead of a public `enum` so new algorithms can be +/// added later without breaking source or ABI stability. +public struct SSHKeyType: RawRepresentable, Equatable, Hashable, CaseIterable, CustomStringConvertible { + + // MARK: Backing storage for the algorithms currently bundled with Citadel. + internal enum BackingKeyType: String, CaseIterable { + case rsa = "ssh-rsa" + case ed25519 = "ssh-ed25519" + case ecdsaP256 = "ecdsa-sha2-nistp256" + case ecdsaP384 = "ecdsa-sha2-nistp384" + case ecdsaP521 = "ecdsa-sha2-nistp521" + } + + // MARK: RawRepresentable + let backing: BackingKeyType + public var rawValue: String { backing.rawValue } + + public init?(rawValue: String) { + guard let backing = BackingKeyType(rawValue: rawValue) else { return nil } + self.backing = backing + } + + // Internal convenience initialiser + internal init(backing: BackingKeyType) { + self.backing = backing + } + + // MARK: CaseIterable + public static var allCases: [SSHKeyType] { + BackingKeyType.allCases.map(SSHKeyType.init(backing:)) + } + + // MARK: Human-readable description (mirrors previous behaviour) + public var description: String { + switch backing { + case .rsa: return "RSA" + case .ed25519: return "ED25519" + case .ecdsaP256: return "ECDSA P-256" + case .ecdsaP384: return "ECDSA P-384" + case .ecdsaP521: return "ECDSA P-521" + } + } + + // MARK: Statically known key types + public static let rsa = SSHKeyType(backing: .rsa) + public static let ed25519 = SSHKeyType(backing: .ed25519) + public static let ecdsaP256 = SSHKeyType(backing: .ecdsaP256) + public static let ecdsaP384 = SSHKeyType(backing: .ecdsaP384) + public static let ecdsaP521 = SSHKeyType(backing: .ecdsaP521) +} + + +/// Errors that can occur during SSH key type detection. +public enum SSHKeyDetectionError: LocalizedError, Equatable { + case invalidKeyFormat(reason: String? = nil) + case unsupportedKeyType(type: String? = nil) + case invalidPrivateKeyFormat + case malformedKey + case encryptedPrivateKey // key is encrypted, no pass-phrase handled yet + case passphraseRequired // caller gave none + case incorrectPassphrase // caller gave one, but it was wrong + + // Equality only cares about the *case*, not the associated text. + public static func == (lhs: SSHKeyDetectionError, rhs: SSHKeyDetectionError) -> Bool { + switch (lhs, rhs) { + case (.invalidKeyFormat, .invalidKeyFormat), + (.unsupportedKeyType, .unsupportedKeyType), + (.invalidPrivateKeyFormat,.invalidPrivateKeyFormat), + (.malformedKey, .malformedKey), + (.encryptedPrivateKey, .encryptedPrivateKey), + (.passphraseRequired, .passphraseRequired), + (.incorrectPassphrase, .incorrectPassphrase): + return true + default: + return false + } + } + + public var errorDescription: String? { + switch self { + case .invalidKeyFormat(let reason): + return "The key string is not in a valid SSH-key format" + (reason.map { ": \($0)" } ?? "") + case .unsupportedKeyType(let type): + return "The key type is not supported" + (type.map { " (raw value: \($0))" } ?? "") + case .invalidPrivateKeyFormat: + return "The private key format is invalid or corrupted" + case .malformedKey: + return "The key string is malformed" + case .encryptedPrivateKey: + return "The private key is encrypted" + case .passphraseRequired: + return "A passphrase is required to decrypt the private key" + case .incorrectPassphrase: + return "The provided passphrase is incorrect" + } + } +} + +/// High-level utility for detecting SSH key types from their string representation. +public enum SSHKeyDetection { + + /// Detects the type of an SSH public key from its string representation. + /// + /// This function supports standard OpenSSH public key format: + /// - Public keys: Standard OpenSSH public key format (e.g., "ssh-rsa AAAAB3... user@host") + /// + /// - Parameter keyString: The SSH public key as a string + /// - Returns: The detected SSH key type + /// - Throws: `SSHKeyDetectionError` if the key format is invalid or unsupported + /// + /// Example usage: + /// ```swift + /// let publicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... user@example.com" + /// let keyType = try SSHKeyDetection.detectPublicKeyType(from: publicKey) + /// print(keyType) // .rsa + /// ``` + public static func detectPublicKeyType(from keyString: String) throws -> SSHKeyType { + let trimmedKey = keyString.trimmingCharacters(in: .whitespacesAndNewlines) + + // Check for public key formats + for keyType in SSHKeyType.allCases { + let prefix = keyType.rawValue + " " + if trimmedKey.hasPrefix(prefix) { + // Validate that there's actually content after the prefix + let remainder = String(trimmedKey.dropFirst(prefix.count)) + if !remainder.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return keyType + } + } + } + + throw SSHKeyDetectionError.invalidKeyFormat(reason: "The key string does not match any known SSH public key format.") + } + + /// Detects the type of an SSH private key from its string representation. + /// + /// This function supports OpenSSH private key format: + /// - Private keys: OpenSSH private key format (PEM-style with -----BEGIN OPENSSH PRIVATE KEY-----) + /// + /// - Parameter keyString: The SSH private key as a string + /// - Returns: The detected SSH key type + /// - Throws: `SSHKeyDetectionError` if the key format is invalid or unsupported + /// + /// Example usage: + /// ```swift + /// let privateKey = """ + /// -----BEGIN OPENSSH PRIVATE KEY----- + /// b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW... + /// -----END OPENSSH PRIVATE KEY----- + /// """ + /// let keyType = try SSHKeyDetection.detectPrivateKeyType(from: privateKey) + /// print(keyType) // .ed25519 + /// ``` + public static func detectPrivateKeyType(from keyString: String) throws -> SSHKeyType { + let trimmedKey = keyString.trimmingCharacters(in: .whitespacesAndNewlines) + + // Verify it's an OpenSSH private key format + guard trimmedKey.hasPrefix("-----BEGIN OPENSSH PRIVATE KEY-----") else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + return try parseOpenSSHPrivateKey(from: trimmedKey) + } + + /// Detects the type of an OpenSSH private key by parsing its structure. + private static func parseOpenSSHPrivateKey(from keyString: String) throws -> SSHKeyType { + var keyContent = keyString.replacingOccurrences(of: "\n", with: "") + + guard + keyContent.hasPrefix("-----BEGIN OPENSSH PRIVATE KEY-----"), + keyContent.hasSuffix("-----END OPENSSH PRIVATE KEY-----") + else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + // Extract the base64 content + keyContent.removeLast("-----END OPENSSH PRIVATE KEY-----".count) + keyContent.removeFirst("-----BEGIN OPENSSH PRIVATE KEY-----".count) + + guard let data = Data(base64Encoded: keyContent) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + // Parse the OpenSSH private key format + return try parseOpenSSHPrivateKeyType(from: data) + } + + /// Parses the OpenSSH private key format to extract the key type. + private static func parseOpenSSHPrivateKeyType(from data: Data) throws -> SSHKeyType { + var offset = 0 + + // Check magic bytes "openssh-key-v1\0" + let magic = "openssh-key-v1\0".utf8 + guard data.starts(with: magic) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + offset += magic.count + + // Skip cipher name length + cipher name + guard let cipherNameLength = readUInt32(from: data, at: &offset) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + offset += Int(cipherNameLength) + + // Skip KDF name length + KDF name + guard let kdfNameLength = readUInt32(from: data, at: &offset) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + offset += Int(kdfNameLength) + + // Skip KDF options length + KDF options + guard let kdfOptionsLength = readUInt32(from: data, at: &offset) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + offset += Int(kdfOptionsLength) + + // Number of keys (should be 1) + guard let numberOfKeys = readUInt32(from: data, at: &offset), + numberOfKeys == 1 else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + // Public key length (we don't need the value, just need to advance past it) + guard readUInt32(from: data, at: &offset) != nil else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + // Public key data starts here - first thing is the key type + guard let keyTypeLength = readUInt32(from: data, at: &offset) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + guard offset + Int(keyTypeLength) <= data.count else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + let keyTypeData = data.subdata(in: offset..<(offset + Int(keyTypeLength))) + guard let keyTypeString = String(data: keyTypeData, encoding: .utf8) else { + throw SSHKeyDetectionError.invalidPrivateKeyFormat + } + + guard let keyType = SSHKeyType(rawValue: keyTypeString) else { + throw SSHKeyDetectionError.unsupportedKeyType(type: keyTypeString) + } + + return keyType + } + + /// Helper function to read a 32-bit unsigned integer from data. + private static func readUInt32(from data: Data, at offset: inout Int) -> UInt32? { + // Fast path: require the 4 bytes to be present. + guard offset + 4 <= data.count else { return nil } + + // Wrap just the slice we need so we don’t copy the whole array. + var buf = ByteBuffer(bytes: data[offset ..< offset + 4]) + guard let value: UInt32 = buf.readInteger(endianness: .big) else { return nil } + + offset += 4 + return value + } +} diff --git a/Tests/CitadelTests/KeyTests.swift b/Tests/CitadelTests/KeyTests.swift index d838082..a2e78e1 100644 --- a/Tests/CitadelTests/KeyTests.swift +++ b/Tests/CitadelTests/KeyTests.swift @@ -124,4 +124,209 @@ final class KeyTests: XCTestCase { let privateKey2 = try Curve25519.Signing.PrivateKey(sshEd25519: key2) XCTAssertEqual(privateKey.rawRepresentation, privateKey2.rawRepresentation) } + + func testSSHKeyTypeDetection() throws { + // Test RSA public key detection + let rsaPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDD+BHa+uJzAU7CyKrjUIBndqBvyN test@example.com" + let rsaKeyType = try SSHKeyDetection.detectPublicKeyType(from: rsaPublicKey) + XCTAssertEqual(rsaKeyType, .rsa) + XCTAssertEqual(rsaKeyType.description, "RSA") + + // Test ED25519 public key detection + let ed25519PublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICLXXLFuC1kfRjboZkava/JUSsYWyR45j0fAcvFhuaQD test@example.com" + let ed25519KeyType = try SSHKeyDetection.detectPublicKeyType(from: ed25519PublicKey) + XCTAssertEqual(ed25519KeyType, .ed25519) + XCTAssertEqual(ed25519KeyType.description, "ED25519") + + // Test ECDSA P-256 public key detection + let ecdsaP256PublicKey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBK test@example.com" + let ecdsaP256KeyType = try SSHKeyDetection.detectPublicKeyType(from: ecdsaP256PublicKey) + XCTAssertEqual(ecdsaP256KeyType, .ecdsaP256) + XCTAssertEqual(ecdsaP256KeyType.description, "ECDSA P-256") + + // Test ECDSA P-384 public key detection + let ecdsaP384PublicKey = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBK test@example.com" + let ecdsaP384KeyType = try SSHKeyDetection.detectPublicKeyType(from: ecdsaP384PublicKey) + XCTAssertEqual(ecdsaP384KeyType, .ecdsaP384) + XCTAssertEqual(ecdsaP384KeyType.description, "ECDSA P-384") + + // Test ECDSA P-521 public key detection + let ecdsaP521PublicKey = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBK test@example.com" + let ecdsaP521KeyType = try SSHKeyDetection.detectPublicKeyType(from: ecdsaP521PublicKey) + XCTAssertEqual(ecdsaP521KeyType, .ecdsaP521) + XCTAssertEqual(ecdsaP521KeyType.description, "ECDSA P-521") + } + + func testSSHKeyTypeDetectionWithWhitespace() throws { + // Test that detection works with leading/trailing whitespace + let keyWithWhitespace = " \n\t ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDD+BHa test@example.com \n " + let keyType = try SSHKeyDetection.detectPublicKeyType(from: keyWithWhitespace) + XCTAssertEqual(keyType, .rsa) + } + + func testSSHKeyTypeDetectionErrors() { + // Test invalid key format + let invalidKey = "invalid-key-format" + XCTAssertThrowsError(try SSHKeyDetection.detectPublicKeyType(from: invalidKey)) { error in + XCTAssertTrue(error is SSHKeyDetectionError) + if let sshError = error as? SSHKeyDetectionError { + XCTAssertEqual(sshError, .invalidKeyFormat()) + } + } + + // Test empty string + XCTAssertThrowsError(try SSHKeyDetection.detectPublicKeyType(from: "")) + + // Test key type prefix without content + let emptyKey = "ssh-rsa " + XCTAssertThrowsError(try SSHKeyDetection.detectPublicKeyType(from: emptyKey)) + + // Test invalid private key format + let invalidPrivateKey = """ + -----BEGIN INVALID PRIVATE KEY----- + invalid-content + -----END INVALID PRIVATE KEY----- + """ + XCTAssertThrowsError(try SSHKeyDetection.detectPrivateKeyType(from: invalidPrivateKey)) { error in + XCTAssertTrue(error is SSHKeyDetectionError) + } + + // Test malformed OpenSSH private key (missing end marker) + let malformedPrivateKey = """ + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQ + """ + XCTAssertThrowsError(try SSHKeyDetection.detectPrivateKeyType(from: malformedPrivateKey)) + } + + func testSSHKeyTypeAllCases() { + // Ensure all key types are covered + let expectedTypes: Set = [.rsa, .ed25519, .ecdsaP256, .ecdsaP384, .ecdsaP521] + let allCases = Set(SSHKeyType.allCases) + XCTAssertEqual(allCases, expectedTypes) + + // Test that all key types have descriptions + for keyType in SSHKeyType.allCases { + XCTAssertFalse(keyType.description.isEmpty) + } + } + + func testSSHPrivateKeyTypeDetection() throws { + // Test ED25519 private key detection + let ed25519PrivateKey = """ + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACCUyt900D1i2/S69QmQiIwvYayx4Jr/666F1u7aJCyAyAAAAJB0ywhAdMsI + QAAAAAtzc2gtZWQyNTUxOQAAACCUyt900D1i2/S69QmQiIwvYayx4Jr/666F1u7aJCyAyA + AAAECapB+VUTcuar7jVPfBgleHuadfu/+7P07PSPeqz+P1yJTK33TQPWLb9Lr1CZCIjC9h + rLHgmv/rroXW7tokLIDIAAAAC3lvdUBleGFtcGxlAQI= + -----END OPENSSH PRIVATE KEY----- + """ + let ed25519KeyType = try SSHKeyDetection.detectPrivateKeyType(from: ed25519PrivateKey) + XCTAssertEqual(ed25519KeyType, .ed25519) + + // Test RSA 4096 private key detection + let rsa4096PrivateKey = """ + -----BEGIN OPENSSH PRIVATE KEY----- + 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 + -----END OPENSSH PRIVATE KEY----- + """ + let rsa4096KeyType = try SSHKeyDetection.detectPrivateKeyType(from: rsa4096PrivateKey) + XCTAssertEqual(rsa4096KeyType, .rsa) + + // Test ECDSA P-256 private key detection + let ecdsa256PrivateKey = """ + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS + 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR/G9rJovBSvdkd9XoGNURImI5vQP/2 + w7TQNb/b8hGI5oq844XjI7V4j8XDwjqlcNfeD7gqoHf8ekpmL4EUtzYaAAAAqFZzBpBWcw + aQAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH8b2smi8FK92R31 + egY1REiYjm9A//bDtNA1v9vyEYjmirzjheMjtXiPxcPCOqVw194PuCqgd/x6SmYvgRS3Nh + oAAAAgPV1jW6vy45i2F3WBFirMPgiJU7FgIl4rJy264fkhPU4AAAALeW91QGV4YW1wbGUB + AgMEBQ== + -----END OPENSSH PRIVATE KEY----- + """ + let ecdsa256KeyType = try SSHKeyDetection.detectPrivateKeyType(from: ecdsa256PrivateKey) + XCTAssertEqual(ecdsa256KeyType, .ecdsaP256) + + // Test ECDSA P-384 private key detection + let ecdsa384PrivateKey = """ + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS + 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTbanVgBsim5t0MwvPHpmbupOibZFVU + a9Teahi4S4YZsvEob0eX9wYSEA2VF6MNKCDM0wQFtm0tk/5vgG0vqSaqjefgXCsov7mFDx + BW0Trg0YqULpUlRR9l9f12TyZm050AAADY3IaN69yGjesAAAATZWNkc2Etc2hhMi1uaXN0 + cDM4NAAAAAhuaXN0cDM4NAAAAGEE22p1YAbIpubdDMLzx6Zm7qTom2RVVGvU3moYuEuGGb + LxKG9Hl/cGEhANlRejDSggzNMEBbZtLZP+b4BtL6kmqo3n4FwrKL+5hQ8QVtE64NGKlC6V + JUUfZfX9dk8mZtOdAAAAMQDtslLX7WTAyAIiTxRVtOl9WXp/GKn9agJIJ0/qOpuRaYGLtk + w3LPjfQfpJT1dh9CUAAAALeW91QGV4YW1wbGUBAgME + -----END OPENSSH PRIVATE KEY----- + """ + let ecdsa384KeyType = try SSHKeyDetection.detectPrivateKeyType(from: ecdsa384PrivateKey) + XCTAssertEqual(ecdsa384KeyType, .ecdsaP384) + + // Test ECDSA P-521 private key detection + let ecdsa521PrivateKey = """ + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS + 1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAuwrbbKlzQliuu1AmBtr9N7xG1Qic + MqizNJa5zWWnm9rvBvQwIl0u6NDmUMVTnLxscnk9hXARGaLnn2ufhGhrDWkBujkMnwfGy7 + f/eIIOmWwdoMh/fbam5qMtOgNIp5QO9I70QstcHF62ankrtmcgBZtdCBsvHAuIfL6IK2ts + BgG7cvMAAAEQktYcEpLWHBIAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ + AAAIUEALsK22ypc0JYrrtQJgba/Te8RtUInDKoszSWuc1lp5va7wb0MCJdLujQ5lDFU5y8 + bHJ5PYVwERmi559rn4Roaw1pAbo5DJ8Hxsu3/3iCDplsHaDIf322puajLToDSKeUDvSO9E + LLXBxetmp5K7ZnIAWbXQgbLxwLiHy+iCtrbAYBu3LzAAAAQgETL+ZErb1c9FwcOKtIuXgy + pS4OdBd4Il5mUSzCwJ/PKWO0L+KRTthlNrwZTRxrdGIsjonmEEoIh9kLfGM3Tpa0YQAAAA + t5b3VAZXhhbXBsZQECAwQFBgc= + -----END OPENSSH PRIVATE KEY----- + """ + let ecdsa521KeyType = try SSHKeyDetection.detectPrivateKeyType(from: ecdsa521PrivateKey) + XCTAssertEqual(ecdsa521KeyType, .ecdsaP521) + } }