From 620d9315d81923caccfab340b067e9af4c0ea0d1 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Wed, 28 Oct 2020 21:36:07 +0100 Subject: [PATCH 01/20] Initial client-side RSA private key support --- Package.swift | 5 +- .../NIOSSHCertifiedPublicKey.swift | 3 + .../NIOSSHPrivateKey.swift | 17 ++ .../Keys And Signatures/NIOSSHPublicKey.swift | 46 +++- .../Keys And Signatures/NIOSSHSignature.swift | 24 +- Sources/NIOSSH/RSA.swift | 256 ++++++++++++++++++ Sources/NIOSSHClient/ExecHandler.swift | 1 + .../InteractivePasswordPromptDelegate.swift | 22 +- Sources/NIOSSHClient/main.swift | 9 +- 9 files changed, 351 insertions(+), 32 deletions(-) create mode 100644 Sources/NIOSSH/RSA.swift diff --git a/Package.swift b/Package.swift index 3c06a50b..353b527b 100644 --- a/Package.swift +++ b/Package.swift @@ -28,10 +28,11 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.21.0"), - .package(url: "https://github.com/apple/swift-crypto.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.2"), + .package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.0"), ], targets: [ - .target(name: "NIOSSH", dependencies: ["NIO", "NIOFoundationCompat", "Crypto"]), + .target(name: "NIOSSH", dependencies: ["NIO", "NIOFoundationCompat", "Crypto", "BigInt"]), .target(name: "NIOSSHClient", dependencies: ["NIO", "NIOSSH", "NIOConcurrencyHelpers"]), .target(name: "NIOSSHServer", dependencies: ["NIO", "NIOSSH", "NIOFoundationCompat", "Crypto"]), .target(name: "NIOSSHPerformanceTester", dependencies: ["NIO", "NIOSSH", "Crypto"]), diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift index 0d50e146..9f398850 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift @@ -316,6 +316,7 @@ extension NIOSSHCertifiedPublicKey { static let p521KeyPrefix = "ecdsa-sha2-nistp521-cert-v01@openssh.com".utf8 static let ed25519KeyPrefix = "ssh-ed25519-cert-v01@openssh.com".utf8 + static let rsaKeyPrefix = "ssh-rsa".utf8 internal var keyPrefix: String.UTF8View { switch self.key.backingKey { @@ -327,6 +328,8 @@ extension NIOSSHCertifiedPublicKey { return Self.p384KeyPrefix case .ecdsaP521: return Self.p521KeyPrefix + case .rsa: + return Self.rsaKeyPrefix case .certified: preconditionFailure("base key cannot be certified") } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift index 32f0529a..b7c6d6ab 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift @@ -45,6 +45,10 @@ public struct NIOSSHPrivateKey { public init(p521Key key: P521.Signing.PrivateKey) { self.backingKey = .ecdsaP521(key) } + + public init(rsa key: Insecure.RSA.Signing.PrivateKey) { + self.backingKey = .rsa(key) + } #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) public init(secureEnclaveP256Key key: SecureEnclave.P256.Signing.PrivateKey) { @@ -63,6 +67,8 @@ public struct NIOSSHPrivateKey { return ["ecdsa-sha2-nistp384"] case .ecdsaP521: return ["ecdsa-sha2-nistp521"] + case .rsa: + return ["ssh-rsa"] #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256: return ["ecdsa-sha2-nistp256"] @@ -78,6 +84,7 @@ extension NIOSSHPrivateKey { case ecdsaP256(P256.Signing.PrivateKey) case ecdsaP384(P384.Signing.PrivateKey) case ecdsaP521(P521.Signing.PrivateKey) + case rsa(Insecure.RSA.Signing.PrivateKey) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case secureEnclaveP256(SecureEnclave.P256.Signing.PrivateKey) @@ -108,6 +115,11 @@ extension NIOSSHPrivateKey { try key.signature(for: ptr) } return NIOSSHSignature(backingSignature: .ecdsaP521(signature)) + case .rsa(let key): + let signature = try digest.withUnsafeBytes { ptr in + try key.signature(for: ptr) + } + return NIOSSHSignature(backingSignature: .rsa(signature)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256(let key): @@ -133,6 +145,9 @@ extension NIOSSHPrivateKey { case .ecdsaP521(let key): let signature = try key.signature(for: payload.bytes.readableBytesView) return NIOSSHSignature(backingSignature: .ecdsaP521(signature)) + case .rsa(let key): + let signature = try key.signature(for: payload.bytes.readableBytesView) + return NIOSSHSignature(backingSignature: .rsa(signature)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256(let key): let signature = try key.signature(for: payload.bytes.readableBytesView) @@ -154,6 +169,8 @@ extension NIOSSHPrivateKey { return NIOSSHPublicKey(backingKey: .ecdsaP384(privateKey.publicKey)) case .ecdsaP521(let privateKey): return NIOSSHPublicKey(backingKey: .ecdsaP521(privateKey.publicKey)) + case .rsa(let privateKey): + return NIOSSHPublicKey(backingKey: .rsa(privateKey.publicKey)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256(let privateKey): return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey)) diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index aea810d1..0cb1efc0 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -88,12 +88,17 @@ extension NIOSSHPublicKey { return digest.withUnsafeBytes { digestPtr in key.isValidSignature(sig, for: digestPtr) } + case (.rsa(let key), .rsa(let sig)): + return digest.withUnsafeBytes { digestPtr in + key.isValidSignature(sig, for: digestPtr) + } case (.certified(let key), _): return key.isValidSignature(signature, for: digest) case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), - (.ecdsaP521, _): + (.ecdsaP521, _), + (.rsa, _): return false } } @@ -110,12 +115,15 @@ extension NIOSSHPublicKey { return key.isValidSignature(sig, for: bytes.readableBytesView) case (.ecdsaP521(let key), .ecdsaP521(let sig)): return key.isValidSignature(sig, for: bytes.readableBytesView) + case (.rsa(let key), .rsa(let sig)): + return key.isValidSignature(sig, for: bytes.readableBytesView) case (.certified(let key), _): return key.isValidSignature(signature, for: bytes) case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), - (.ecdsaP521, _): + (.ecdsaP521, _), + (.rsa, _): return false } } @@ -132,12 +140,15 @@ extension NIOSSHPublicKey { return key.isValidSignature(sig, for: payload.bytes.readableBytesView) case (.ecdsaP521(let key), .ecdsaP521(let sig)): return key.isValidSignature(sig, for: payload.bytes.readableBytesView) + case (.rsa(let key), .rsa(let sig)): + return key.isValidSignature(sig, for: payload.bytes.readableBytesView) case (.certified(let key), _): return key.isValidSignature(signature, for: payload) case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), - (.ecdsaP521, _): + (.ecdsaP521, _), + (.rsa, _): return false } } @@ -150,6 +161,7 @@ extension NIOSSHPublicKey { case ecdsaP256(P256.Signing.PublicKey) case ecdsaP384(P384.Signing.PublicKey) case ecdsaP521(P521.Signing.PublicKey) + case rsa(Insecure.RSA.Signing.PublicKey) case certified(NIOSSHCertifiedPublicKey) // This case recursively contains `NIOSSHPublicKey`. } @@ -164,6 +176,9 @@ extension NIOSSHPublicKey { /// The prefix of a P521 ECDSA public key. internal static let ecdsaP521PublicKeyPrefix = "ecdsa-sha2-nistp521".utf8 + + /// The prefix of a RSA public key. + internal static let rsaPublicKeyPrefix = "ssh-rsa".utf8 internal var keyPrefix: String.UTF8View { switch self.backingKey { @@ -175,13 +190,15 @@ extension NIOSSHPublicKey { return Self.ecdsaP384PublicKeyPrefix case .ecdsaP521: return Self.ecdsaP521PublicKeyPrefix + case .rsa: + return Self.rsaPublicKeyPrefix case .certified(let base): return base.keyPrefix } } internal static var knownAlgorithms: [String.UTF8View] { - [Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix] + [Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix, Self.rsaPublicKeyPrefix] } } @@ -197,12 +214,15 @@ extension NIOSSHPublicKey.BackingKey: Equatable { return lhs.rawRepresentation == rhs.rawRepresentation case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): return lhs.rawRepresentation == rhs.rawRepresentation + case (.rsa(let lhs), .rsa(let rhs)): + return lhs == rhs case (.certified(let lhs), .certified(let rhs)): return lhs == rhs case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), (.ecdsaP521, _), + (.rsa, _), (.certified, _): return false } @@ -224,8 +244,11 @@ extension NIOSSHPublicKey.BackingKey: Hashable { case .ecdsaP521(let pkey): hasher.combine(4) hasher.combine(pkey.rawRepresentation) - case .certified(let pkey): + case .rsa(let pkey): hasher.combine(5) + hasher.combine(pkey.rawRepresentation) + case .certified(let pkey): + hasher.combine(6) hasher.combine(pkey) } } @@ -250,6 +273,8 @@ extension ByteBuffer { case .ecdsaP521(let key): writtenBytes += self.writeSSHString(NIOSSHPublicKey.ecdsaP521PublicKeyPrefix) writtenBytes += self.writeECDSAP521PublicKey(baseKey: key) + case .rsa(let key): + writtenBytes += self.writeRSAPublicKey(baseKey: key) case .certified(let key): return self.writeCertifiedKey(key) } @@ -271,6 +296,8 @@ extension ByteBuffer { return self.writeECDSAP384PublicKey(baseKey: key) case .ecdsaP521(let key): return self.writeECDSAP521PublicKey(baseKey: key) + case .rsa(let key): + return self.writeRSAPublicKey(baseKey: key) case .certified: preconditionFailure("Certified keys are the only callers of this method, and cannot contain themselves") } @@ -336,6 +363,15 @@ extension ByteBuffer { writtenBytes += self.writeSSHString(baseKey.x963Representation) return writtenBytes } + + private mutating func writeRSAPublicKey(baseKey: Insecure.RSA.Signing.PublicKey) -> Int { + // For ssh-rsa, the format is public exponent `e` followed by modulus `n` + var writtenBytes = 0 + writtenBytes += self.writeSSHString("ssh-rsa".utf8) + writtenBytes += self.writePositiveMPInt(baseKey.publicExponent.serialize()) + writtenBytes += self.writePositiveMPInt(baseKey.modulus.serialize()) + return writtenBytes + } /// A helper function that reads an Ed25519 public key. /// diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift index 9e9cf0f0..3c64009c 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift @@ -36,6 +36,7 @@ extension NIOSSHSignature { case ecdsaP256(P256.Signing.ECDSASignature) case ecdsaP384(P384.Signing.ECDSASignature) case ecdsaP521(P521.Signing.ECDSASignature) + case rsa(Insecure.RSA.Signing.Signature) internal enum RawBytes { case byteBuffer(ByteBuffer) @@ -54,6 +55,9 @@ extension NIOSSHSignature { /// The prefix of a P521 ECDSA public key. fileprivate static let ecdsaP521SignaturePrefix = "ecdsa-sha2-nistp521".utf8 + + /// The prefix of a RSA public key. + fileprivate static let rsaSignaturePrefix = "ssh-rsa".utf8 } extension NIOSSHSignature.BackingSignature.RawBytes: Equatable { @@ -85,10 +89,13 @@ extension NIOSSHSignature.BackingSignature: Equatable { return lhs.rawRepresentation == rhs.rawRepresentation case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): return lhs.rawRepresentation == rhs.rawRepresentation + case (.rsa(let lhs), .rsa(let rhs)): + return lhs.rawRepresentation == rhs.rawRepresentation case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), - (.ecdsaP521, _): + (.ecdsaP521, _), + (.rsa, _): return false } } @@ -109,6 +116,9 @@ extension NIOSSHSignature.BackingSignature: Hashable { case .ecdsaP521(let sig): hasher.combine(3) hasher.combine(sig.rawRepresentation) + case .rsa(let sig): + hasher.combine(4) + hasher.combine(sig.rawRepresentation) } } } @@ -126,6 +136,8 @@ extension ByteBuffer { return self.writeECDSAP384Signature(baseSignature: sig) case .ecdsaP521(let sig): return self.writeECDSAP521Signature(baseSignature: sig) + case .rsa(let sig): + return self.writeRSASignature(baseSignature: sig) } } @@ -203,6 +215,16 @@ extension ByteBuffer { return writtenLength } + + private mutating func writeRSASignature(baseSignature: Insecure.RSA.Signing.Signature) -> Int { + var writtenLength = self.writeSSHString(NIOSSHSignature.rsaSignaturePrefix) + + // For SSH-RSA, the key format is the signature without lengths or paddings +// writtenLength += self.writeCompositeSSHString { buffer in + writtenLength += self.writeSSHString(baseSignature.rawRepresentation) +// } + return writtenLength + } mutating func readSSHSignature() throws -> NIOSSHSignature? { try self.rewindOnNilOrError { buffer in diff --git a/Sources/NIOSSH/RSA.swift b/Sources/NIOSSH/RSA.swift new file mode 100644 index 00000000..14381b5e --- /dev/null +++ b/Sources/NIOSSH/RSA.swift @@ -0,0 +1,256 @@ +import NIO +import NIOFoundationCompat +import BigInt +import CCryptoBoringSSL +import Foundation +import Crypto + +extension Insecure { + public enum RSA { + public enum Signing {} + } +} + +extension Insecure.RSA.Signing { + public struct PublicKey: Equatable, Hashable { + // PublicExponent e + public let publicExponent: BigUInt + + // Modulus n + public let modulus: BigUInt + + public var rawRepresentation: Data { + publicExponent.serialize() + modulus.serialize() + } + + enum PubkeyParseError: Error { + case invalidInitialSequence, invalidAlgorithmIdentifier, invalidSubjectPubkey, forbiddenTrailingData, invalidRSAPubkey + } + + public init(publicExponent: BigUInt, modulus: BigUInt) { + self.publicExponent = publicExponent + self.modulus = modulus + } + + public func encrypt(for message: D) throws -> EncrytpedMessage { + let message = BigUInt(Data(message)) + + guard message > .zero && message <= modulus - 1 else { + throw RSAError.messageRepresentativeOutOfRange + } + + let result = message.power(publicExponent, modulus: modulus) + return EncrytpedMessage(rawRepresentation: result.serialize()) + } + + public func isValidSignature(_ signature: Signature, for digest: D) -> Bool { + let signature = BigUInt(signature.rawRepresentation) + + guard signature > .zero && signature <= modulus - 1 else { + return false + } + + let m = signature.power(publicExponent, modulus: modulus) + return m.serialize() == Data(digest) + } + } + + public struct EncrytpedMessage: ContiguousBytes { + public let rawRepresentation: Data + + public init(rawRepresentation: D) where D : DataProtocol { + self.rawRepresentation = Data(rawRepresentation) + } + + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try rawRepresentation.withUnsafeBytes(body) + } + } + + public struct Signature: ContiguousBytes { + public let rawRepresentation: Data + + public init(rawRepresentation: D) where D : DataProtocol { + self.rawRepresentation = Data(rawRepresentation) + } + + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try rawRepresentation.withUnsafeBytes(body) + } + } + + public struct PrivateKey { + public enum Storage { + case privateExponent(d: BigUInt, n: BigUInt) + // TODO: Quintuple + } + + // Private Exponent + public let storage: Storage + + // Public Exponent e + public let publicKey: PublicKey + + public init(privateExponent: BigUInt, publicExponent: BigUInt, modulus: BigUInt) { + self.storage = .privateExponent(d: privateExponent, n: modulus) + self.publicKey = PublicKey(publicExponent: publicExponent, modulus: modulus) + } + + public init(bits: Int = 2047, publicExponent e: BigUInt = 65537) { + let p = BigUInt.randomPrime(bits: bits) + let q = BigUInt.randomPrime(bits: bits) + + let n = p * q // modulus + let phi = (p - 1) * (q - 1) + let d = e.inverse(phi)! + self.storage = .privateExponent(d: d, n: n) + + self.publicKey = PublicKey( + publicExponent: e, + modulus: n + ) + } + + public func signature(for message: D) throws -> Signature { + switch storage { + case .privateExponent(_, let n): + let message = try Self.encodePKCS1SHA1(message, length: (n.bitWidth + 7) / 8) + + let result = self.signature(for: BigUInt(Data(message))) + return Signature(rawRepresentation: result.serialize()) + } + } + + private static func encodePKCS1SHA1(_ data: D, length: Int) throws -> Data { + /* + * This is the magic ASN.1/DER prefix that goes in the decoded + * signature, between the string of FFs and the actual SHA-1 + * hash value. The meaning of it is: + * + * 00 -- this marks the end of the FFs; not part of the ASN.1 + * bit itself + * + * 30 21 -- a constructed SEQUENCE of length 0x21 + * 30 09 -- a constructed sub-SEQUENCE of length 9 + * 06 05 -- an object identifier, length 5 + * 2B 0E 03 02 1A -- object id { 1 3 14 3 2 26 } + * (the 1,3 comes from 0x2B = 43 = 40*1+3) + * 05 00 -- NULL + * 04 14 -- a primitive OCTET STRING of length 0x14 + * [0x14 bytes of hash data follows] + * + * The object id in the middle there is listed as `id-sha1' in + * ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1d2.asn + * (the ASN module for PKCS #1) and its expanded form is as + * follows: + * + * id-sha1 OBJECT IDENTIFIER ::= { + * iso(1) identified-organization(3) oiw(14) secsig(3) + * algorithms(2) 26 } + */ + let prefix: [UInt8] = [ + 0x00, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, + 0x0E, 0x03, 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14, + ] + + let padding = length - prefix.count - 2 - Insecure.SHA1.Digest.byteCount + + var buffer = ByteBuffer() + buffer.writeInteger(0 as UInt8) + buffer.writeInteger(1 as UInt8) + for _ in 0.. BigUInt { + switch storage { + case let .privateExponent(d, n): + return m.power(d, modulus: n) + } + } + + public func decrypt(_ signature: EncrytpedMessage) throws -> Data { + let signature = BigUInt(signature.rawRepresentation) + + switch storage { + case let .privateExponent(privateExponent, modulus): + guard signature >= .zero && signature <= privateExponent else { + throw RSAError.ciphertextRepresentativeOutOfRange + } + + return signature.power(privateExponent, modulus: modulus).serialize() + } + } + } +} + +public struct RSAError: Error { + let message: String + + static let messageRepresentativeOutOfRange = RSAError(message: "message representative out of range") + static let ciphertextRepresentativeOutOfRange = RSAError(message: "ciphertext representative out of range") + static let signatureRepresentativeOutOfRange = RSAError(message: "signature representative out of range") + static let invalidPem = RSAError(message: "invalid PEM") + static let pkcs1Error = RSAError(message: "PKCS1Error") +} + +extension BigUInt { + public static func randomPrime(bits: Int) -> BigUInt { + while true { + var privateExponent = BigUInt.randomInteger(withExactWidth: bits) + privateExponent |= 1 + + if privateExponent.isPrime() { + return privateExponent + } + } + } + + fileprivate init(boringSSL bignum: UnsafeMutablePointer) { + var data = [UInt8](repeating: 0, count: Int(CCryptoBoringSSL_BN_num_bytes(bignum))) + CCryptoBoringSSL_BN_bn2bin(bignum, &data) + self.init(Data(data)) + } +} + +extension BigUInt { + public static let diffieHellmanGroup14 = BigUInt(Data([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, 0xC2, 0x34, + 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, + 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, + 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, + 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, + 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, + 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, + 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, + 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, + 0xF4, 0x4C, 0x42, 0xE9, 0xA6, 0x37, 0xED, 0x6B, + 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, + 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, + 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, + 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE4, 0x5B, 0x3D, + 0xC2, 0x00, 0x7C, 0xB8, 0xA1, 0x63, 0xBF, 0x05, + 0x98, 0xDA, 0x48, 0x36, 0x1C, 0x55, 0xD3, 0x9A, + 0x69, 0x16, 0x3F, 0xA8, 0xFD, 0x24, 0xCF, 0x5F, + 0x83, 0x65, 0x5D, 0x23, 0xDC, 0xA3, 0xAD, 0x96, + 0x1C, 0x62, 0xF3, 0x56, 0x20, 0x85, 0x52, 0xBB, + 0x9E, 0xD5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6D, + 0x67, 0x0C, 0x35, 0x4E, 0x4A, 0xBC, 0x98, 0x04, + 0xF1, 0x74, 0x6C, 0x08, 0xCA, 0x18, 0x21, 0x7C, + 0x32, 0x90, 0x5E, 0x46, 0x2E, 0x36, 0xCE, 0x3B, + 0xE3, 0x9E, 0x77, 0x2C, 0x18, 0x0E, 0x86, 0x03, + 0x9B, 0x27, 0x83, 0xA2, 0xEC, 0x07, 0xA2, 0x8F, + 0xB5, 0xC5, 0x5D, 0xF0, 0x6F, 0x4C, 0x52, 0xC9, + 0xDE, 0x2B, 0xCB, 0xF6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7C, 0xEA, 0x95, 0x6A, 0xE5, + 0x15, 0xD2, 0x26, 0x18, 0x98, 0xFA, 0x05, 0x10, + 0x15, 0x72, 0x8E, 0x5A, 0x8A, 0xAC, 0xAA, 0x68, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + ] as [UInt8])) +} diff --git a/Sources/NIOSSHClient/ExecHandler.swift b/Sources/NIOSSHClient/ExecHandler.swift index 0b79c50d..c88c78c6 100644 --- a/Sources/NIOSSHClient/ExecHandler.swift +++ b/Sources/NIOSSHClient/ExecHandler.swift @@ -89,6 +89,7 @@ final class ExampleExecHandler: ChannelDuplexHandler { switch data.type { case .channel: + print(bytes.getString(at: 0, length: bytes.readableBytes)) // Channel data is forwarded on, the pipe channel will handle it. context.fireChannelRead(self.wrapInboundOut(bytes)) return diff --git a/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift b/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift index 22f8c311..e75711b3 100644 --- a/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift +++ b/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift @@ -15,20 +15,16 @@ import Dispatch import Foundation import NIO +import Crypto +import CCryptoBoringSSL import NIOSSH /// A client user auth delegate that provides an interactive prompt for password-based user auth. final class InteractivePasswordPromptDelegate: NIOSSHClientUserAuthenticationDelegate { private let queue: DispatchQueue - private var username: String? - - private var password: String? - - init(username: String?, password: String?) { + init() { self.queue = DispatchQueue(label: "io.swiftnio.ssh.InteractivePasswordPromptDelegate") - self.username = username - self.password = password } func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) { @@ -39,17 +35,7 @@ final class InteractivePasswordPromptDelegate: NIOSSHClientUserAuthenticationDel } self.queue.async { - if self.username == nil { - print("Username: ", terminator: "") - self.username = readLine() ?? "" - } - - if self.password == nil { - print("Password: ", terminator: "") - self.password = readLine() ?? "" - } - - nextChallengePromise.succeed(NIOSSHUserAuthenticationOffer(username: self.username!, serviceName: "", offer: .password(.init(password: self.password!)))) + nextChallengePromise.succeed(NIOSSHUserAuthenticationOffer(username: "joannis", serviceName: "", offer: .privateKey(NIOSSHUserAuthenticationOffer.Offer.PrivateKey(privateKey: .init(rsa: .init()))))) } } } diff --git a/Sources/NIOSSHClient/main.swift b/Sources/NIOSSHClient/main.swift index e01a7b15..ec30aa4a 100644 --- a/Sources/NIOSSHClient/main.swift +++ b/Sources/NIOSSHClient/main.swift @@ -38,9 +38,6 @@ final class AcceptAllHostKeysDelegate: NIOSSHClientServerAuthenticationDelegate } } -let parser = SimpleCLIParser() -let parseResult = parser.parse() - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { try! group.syncShutdownGracefully() @@ -48,12 +45,12 @@ defer { let bootstrap = ClientBootstrap(group: group) .channelInitializer { channel in - channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: InteractivePasswordPromptDelegate(username: parseResult.user, password: parseResult.password), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) + channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: InteractivePasswordPromptDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) } .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) -let channel = try bootstrap.connect(host: parseResult.host, port: parseResult.port).wait() +let channel = try bootstrap.connect(host: "orlandos.nl", port: 22).wait() // Let's try creating a child channel. let exitStatusPromise = channel.eventLoop.makePromise(of: Int.self) @@ -63,7 +60,7 @@ let childChannel: Channel = try! channel.pipeline.handler(type: NIOSSHHandler.se guard channelType == .session else { return channel.eventLoop.makeFailedFuture(SSHClientError.invalidChannelType) } - return childChannel.pipeline.addHandlers([ExampleExecHandler(command: parseResult.commandString, completePromise: exitStatusPromise), ErrorHandler()]) + return childChannel.pipeline.addHandlers([ExampleExecHandler(command: "ls -la", completePromise: exitStatusPromise), ErrorHandler()]) } return promise.futureResult }.wait() From cc00081ab8703a6f29e48c8bf584586ad78e0b93 Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Mon, 21 Dec 2020 12:15:10 +0100 Subject: [PATCH 02/20] Implement RSA in a separate module, to be removed before a merge --- Package.swift | 4 +- .../Keys And Signatures/CustomKeys.swift | 44 +++++++ .../NIOSSHCertifiedPublicKey.swift | 4 +- .../NIOSSHPrivateKey.swift | 22 ++-- .../Keys And Signatures/NIOSSHPublicKey.swift | 58 ++++----- .../Keys And Signatures/NIOSSHSignature.swift | 26 +--- Sources/{NIOSSH => NIOSSHRSA}/RSA.swift | 122 ++++++++++++++++-- 7 files changed, 207 insertions(+), 73 deletions(-) create mode 100644 Sources/NIOSSH/Keys And Signatures/CustomKeys.swift rename Sources/{NIOSSH => NIOSSHRSA}/RSA.swift (64%) diff --git a/Package.swift b/Package.swift index 353b527b..a2fe3770 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,7 @@ let package = Package( ], products: [ .library(name: "NIOSSH", targets: ["NIOSSH"]), + .library(name: "NIOSSHRSA", targets: ["NIOSSHRSA"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.21.0"), @@ -32,7 +33,8 @@ let package = Package( .package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.0"), ], targets: [ - .target(name: "NIOSSH", dependencies: ["NIO", "NIOFoundationCompat", "Crypto", "BigInt"]), + .target(name: "NIOSSH", dependencies: ["NIO", "NIOFoundationCompat", "Crypto"]), + .target(name: "NIOSSHRSA", dependencies: ["NIOSSH", "BigInt"]), .target(name: "NIOSSHClient", dependencies: ["NIO", "NIOSSH", "NIOConcurrencyHelpers"]), .target(name: "NIOSSHServer", dependencies: ["NIO", "NIOSSH", "NIOFoundationCompat", "Crypto"]), .target(name: "NIOSSHPerformanceTester", dependencies: ["NIO", "NIOSSH", "Crypto"]), diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift new file mode 100644 index 00000000..024e4de4 --- /dev/null +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -0,0 +1,44 @@ +import Foundation +import NIO + +public protocol NIOSSHSignatureProtocol { + static var signaturePrefix: String { get } + var rawRepresentation: Data { get } + + func write(to buffer: inout ByteBuffer) -> Int +} + +internal extension NIOSSHSignatureProtocol { + var signaturePrefix: String { + Self.signaturePrefix + } +} + +public protocol NIOSSHPublicKeyProtocol { + static var publicKeyPrefix: String { get } + var rawRepresentation: Data { get } + + func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool + + func write(to buffer: inout ByteBuffer) -> Int +} + +internal extension NIOSSHPublicKeyProtocol { + var publicKeyPrefix: String { + Self.publicKeyPrefix + } +} + +public protocol NIOSSHPrivateKeyProtocol { + static var keyPrefix: String { get } +// var rawRepresentation: Data { get } + var publicKey: NIOSSHPublicKeyProtocol { get } + + func signature(for data: D) throws -> NIOSSHSignatureProtocol +} + +internal extension NIOSSHPrivateKeyProtocol { + var keyPrefix: String { + Self.keyPrefix + } +} diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift index 9f398850..43e47235 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift @@ -328,8 +328,8 @@ extension NIOSSHCertifiedPublicKey { return Self.p384KeyPrefix case .ecdsaP521: return Self.p521KeyPrefix - case .rsa: - return Self.rsaKeyPrefix + case .custom(let backingKey): + return backingKey.publicKeyPrefix.utf8 case .certified: preconditionFailure("base key cannot be certified") } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift index b7c6d6ab..0ae74294 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift @@ -46,8 +46,8 @@ public struct NIOSSHPrivateKey { self.backingKey = .ecdsaP521(key) } - public init(rsa key: Insecure.RSA.Signing.PrivateKey) { - self.backingKey = .rsa(key) + public init(custom key: PrivateKey) { + self.backingKey = .custom(key) } #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) @@ -67,8 +67,8 @@ public struct NIOSSHPrivateKey { return ["ecdsa-sha2-nistp384"] case .ecdsaP521: return ["ecdsa-sha2-nistp521"] - case .rsa: - return ["ssh-rsa"] + case .custom(let backingKey): + return [Substring(backingKey.keyPrefix)] #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256: return ["ecdsa-sha2-nistp256"] @@ -84,7 +84,7 @@ extension NIOSSHPrivateKey { case ecdsaP256(P256.Signing.PrivateKey) case ecdsaP384(P384.Signing.PrivateKey) case ecdsaP521(P521.Signing.PrivateKey) - case rsa(Insecure.RSA.Signing.PrivateKey) + case custom(NIOSSHPrivateKeyProtocol) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case secureEnclaveP256(SecureEnclave.P256.Signing.PrivateKey) @@ -115,11 +115,11 @@ extension NIOSSHPrivateKey { try key.signature(for: ptr) } return NIOSSHSignature(backingSignature: .ecdsaP521(signature)) - case .rsa(let key): + case .custom(let key): let signature = try digest.withUnsafeBytes { ptr in try key.signature(for: ptr) } - return NIOSSHSignature(backingSignature: .rsa(signature)) + return NIOSSHSignature(backingSignature: .custom(signature)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256(let key): @@ -145,9 +145,9 @@ extension NIOSSHPrivateKey { case .ecdsaP521(let key): let signature = try key.signature(for: payload.bytes.readableBytesView) return NIOSSHSignature(backingSignature: .ecdsaP521(signature)) - case .rsa(let key): + case .custom(let key): let signature = try key.signature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: .rsa(signature)) + return NIOSSHSignature(backingSignature: .custom(signature)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256(let key): let signature = try key.signature(for: payload.bytes.readableBytesView) @@ -169,8 +169,8 @@ extension NIOSSHPrivateKey { return NIOSSHPublicKey(backingKey: .ecdsaP384(privateKey.publicKey)) case .ecdsaP521(let privateKey): return NIOSSHPublicKey(backingKey: .ecdsaP521(privateKey.publicKey)) - case .rsa(let privateKey): - return NIOSSHPublicKey(backingKey: .rsa(privateKey.publicKey)) + case .custom(let privateKey): + return NIOSSHPublicKey(backingKey: .custom(privateKey.publicKey)) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) case .secureEnclaveP256(let privateKey): return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey)) diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 0cb1efc0..cab440ae 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -88,7 +88,7 @@ extension NIOSSHPublicKey { return digest.withUnsafeBytes { digestPtr in key.isValidSignature(sig, for: digestPtr) } - case (.rsa(let key), .rsa(let sig)): + case (.custom(let key), .custom(let sig)): return digest.withUnsafeBytes { digestPtr in key.isValidSignature(sig, for: digestPtr) } @@ -98,7 +98,7 @@ extension NIOSSHPublicKey { (.ecdsaP256, _), (.ecdsaP384, _), (.ecdsaP521, _), - (.rsa, _): + (.custom, _): return false } } @@ -115,7 +115,7 @@ extension NIOSSHPublicKey { return key.isValidSignature(sig, for: bytes.readableBytesView) case (.ecdsaP521(let key), .ecdsaP521(let sig)): return key.isValidSignature(sig, for: bytes.readableBytesView) - case (.rsa(let key), .rsa(let sig)): + case (.custom(let key), .custom(let sig)): return key.isValidSignature(sig, for: bytes.readableBytesView) case (.certified(let key), _): return key.isValidSignature(signature, for: bytes) @@ -123,7 +123,7 @@ extension NIOSSHPublicKey { (.ecdsaP256, _), (.ecdsaP384, _), (.ecdsaP521, _), - (.rsa, _): + (.custom, _): return false } } @@ -140,7 +140,7 @@ extension NIOSSHPublicKey { return key.isValidSignature(sig, for: payload.bytes.readableBytesView) case (.ecdsaP521(let key), .ecdsaP521(let sig)): return key.isValidSignature(sig, for: payload.bytes.readableBytesView) - case (.rsa(let key), .rsa(let sig)): + case (.custom(let key), .custom(let sig)): return key.isValidSignature(sig, for: payload.bytes.readableBytesView) case (.certified(let key), _): return key.isValidSignature(signature, for: payload) @@ -148,7 +148,7 @@ extension NIOSSHPublicKey { (.ecdsaP256, _), (.ecdsaP384, _), (.ecdsaP521, _), - (.rsa, _): + (.custom, _): return false } } @@ -161,7 +161,7 @@ extension NIOSSHPublicKey { case ecdsaP256(P256.Signing.PublicKey) case ecdsaP384(P384.Signing.PublicKey) case ecdsaP521(P521.Signing.PublicKey) - case rsa(Insecure.RSA.Signing.PublicKey) + case custom(NIOSSHPublicKeyProtocol) case certified(NIOSSHCertifiedPublicKey) // This case recursively contains `NIOSSHPublicKey`. } @@ -177,9 +177,6 @@ extension NIOSSHPublicKey { /// The prefix of a P521 ECDSA public key. internal static let ecdsaP521PublicKeyPrefix = "ecdsa-sha2-nistp521".utf8 - /// The prefix of a RSA public key. - internal static let rsaPublicKeyPrefix = "ssh-rsa".utf8 - internal var keyPrefix: String.UTF8View { switch self.backingKey { case .ed25519: @@ -190,15 +187,20 @@ extension NIOSSHPublicKey { return Self.ecdsaP384PublicKeyPrefix case .ecdsaP521: return Self.ecdsaP521PublicKeyPrefix - case .rsa: - return Self.rsaPublicKeyPrefix + case .custom(let publicKey): + return publicKey.publicKeyPrefix.utf8 case .certified(let base): return base.keyPrefix } } - internal static var knownAlgorithms: [String.UTF8View] { - [Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix, Self.rsaPublicKeyPrefix] + internal static var knownAlgorithms: [String.UTF8View] = [ + Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix + ] + + // TODO: Replace this function with sensible code + static func registerPublicKeyType(_ type: PublicKey.Type) { + knownAlgorithms.append(type.publicKeyPrefix.utf8) } } @@ -214,15 +216,17 @@ extension NIOSSHPublicKey.BackingKey: Equatable { return lhs.rawRepresentation == rhs.rawRepresentation case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): return lhs.rawRepresentation == rhs.rawRepresentation - case (.rsa(let lhs), .rsa(let rhs)): - return lhs == rhs + case (.custom(let lhs), .custom(let rhs)): + return + lhs.publicKeyPrefix == rhs.publicKeyPrefix && + lhs.rawRepresentation == rhs.rawRepresentation case (.certified(let lhs), .certified(let rhs)): return lhs == rhs case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), (.ecdsaP521, _), - (.rsa, _), + (.custom, _), (.certified, _): return false } @@ -244,8 +248,9 @@ extension NIOSSHPublicKey.BackingKey: Hashable { case .ecdsaP521(let pkey): hasher.combine(4) hasher.combine(pkey.rawRepresentation) - case .rsa(let pkey): + case .custom(let pkey): hasher.combine(5) + hasher.combine(pkey.publicKeyPrefix) hasher.combine(pkey.rawRepresentation) case .certified(let pkey): hasher.combine(6) @@ -273,8 +278,8 @@ extension ByteBuffer { case .ecdsaP521(let key): writtenBytes += self.writeSSHString(NIOSSHPublicKey.ecdsaP521PublicKeyPrefix) writtenBytes += self.writeECDSAP521PublicKey(baseKey: key) - case .rsa(let key): - writtenBytes += self.writeRSAPublicKey(baseKey: key) + case .custom(let key): + writtenBytes += key.write(to: &self) case .certified(let key): return self.writeCertifiedKey(key) } @@ -296,8 +301,8 @@ extension ByteBuffer { return self.writeECDSAP384PublicKey(baseKey: key) case .ecdsaP521(let key): return self.writeECDSAP521PublicKey(baseKey: key) - case .rsa(let key): - return self.writeRSAPublicKey(baseKey: key) + case .custom(let key): + return key.write(to: &self) case .certified: preconditionFailure("Certified keys are the only callers of this method, and cannot contain themselves") } @@ -364,15 +369,6 @@ extension ByteBuffer { return writtenBytes } - private mutating func writeRSAPublicKey(baseKey: Insecure.RSA.Signing.PublicKey) -> Int { - // For ssh-rsa, the format is public exponent `e` followed by modulus `n` - var writtenBytes = 0 - writtenBytes += self.writeSSHString("ssh-rsa".utf8) - writtenBytes += self.writePositiveMPInt(baseKey.publicExponent.serialize()) - writtenBytes += self.writePositiveMPInt(baseKey.modulus.serialize()) - return writtenBytes - } - /// A helper function that reads an Ed25519 public key. /// /// Not safe to call from arbitrary code as this does not return the reader index on failure: it relies on the caller performing diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift index 3c64009c..fcd6057d 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift @@ -36,7 +36,7 @@ extension NIOSSHSignature { case ecdsaP256(P256.Signing.ECDSASignature) case ecdsaP384(P384.Signing.ECDSASignature) case ecdsaP521(P521.Signing.ECDSASignature) - case rsa(Insecure.RSA.Signing.Signature) + case custom(NIOSSHSignatureProtocol) internal enum RawBytes { case byteBuffer(ByteBuffer) @@ -55,9 +55,6 @@ extension NIOSSHSignature { /// The prefix of a P521 ECDSA public key. fileprivate static let ecdsaP521SignaturePrefix = "ecdsa-sha2-nistp521".utf8 - - /// The prefix of a RSA public key. - fileprivate static let rsaSignaturePrefix = "ssh-rsa".utf8 } extension NIOSSHSignature.BackingSignature.RawBytes: Equatable { @@ -89,13 +86,13 @@ extension NIOSSHSignature.BackingSignature: Equatable { return lhs.rawRepresentation == rhs.rawRepresentation case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): return lhs.rawRepresentation == rhs.rawRepresentation - case (.rsa(let lhs), .rsa(let rhs)): + case (.custom(let lhs), .custom(let rhs)): return lhs.rawRepresentation == rhs.rawRepresentation case (.ed25519, _), (.ecdsaP256, _), (.ecdsaP384, _), (.ecdsaP521, _), - (.rsa, _): + (.custom, _): return false } } @@ -116,8 +113,9 @@ extension NIOSSHSignature.BackingSignature: Hashable { case .ecdsaP521(let sig): hasher.combine(3) hasher.combine(sig.rawRepresentation) - case .rsa(let sig): + case .custom(let sig): hasher.combine(4) + hasher.combine(sig.signaturePrefix) hasher.combine(sig.rawRepresentation) } } @@ -136,8 +134,8 @@ extension ByteBuffer { return self.writeECDSAP384Signature(baseSignature: sig) case .ecdsaP521(let sig): return self.writeECDSAP521Signature(baseSignature: sig) - case .rsa(let sig): - return self.writeRSASignature(baseSignature: sig) + case .custom(let sig): + return sig.write(to: &self) } } @@ -215,16 +213,6 @@ extension ByteBuffer { return writtenLength } - - private mutating func writeRSASignature(baseSignature: Insecure.RSA.Signing.Signature) -> Int { - var writtenLength = self.writeSSHString(NIOSSHSignature.rsaSignaturePrefix) - - // For SSH-RSA, the key format is the signature without lengths or paddings -// writtenLength += self.writeCompositeSSHString { buffer in - writtenLength += self.writeSSHString(baseSignature.rawRepresentation) -// } - return writtenLength - } mutating func readSSHSignature() throws -> NIOSSHSignature? { try self.rewindOnNilOrError { buffer in diff --git a/Sources/NIOSSH/RSA.swift b/Sources/NIOSSHRSA/RSA.swift similarity index 64% rename from Sources/NIOSSH/RSA.swift rename to Sources/NIOSSHRSA/RSA.swift index 14381b5e..440cd9a7 100644 --- a/Sources/NIOSSH/RSA.swift +++ b/Sources/NIOSSHRSA/RSA.swift @@ -1,6 +1,7 @@ import NIO import NIOFoundationCompat import BigInt +import NIOSSH import CCryptoBoringSSL import Foundation import Crypto @@ -12,12 +13,14 @@ extension Insecure { } extension Insecure.RSA.Signing { - public struct PublicKey: Equatable, Hashable { + public struct PublicKey: Equatable, Hashable, NIOSSHPublicKeyProtocol { + public static let publicKeyPrefix = "ssh-rsa" + // PublicExponent e - public let publicExponent: BigUInt + private let publicExponent: BigUInt // Modulus n - public let modulus: BigUInt + private let modulus: BigUInt public var rawRepresentation: Data { publicExponent.serialize() + modulus.serialize() @@ -53,6 +56,23 @@ extension Insecure.RSA.Signing { let m = signature.power(publicExponent, modulus: modulus) return m.serialize() == Data(digest) } + + public func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D : DataProtocol { + guard let signature = signature as? Signature else { + return false + } + + return isValidSignature(signature, for: data) + } + + public func write(to buffer: inout ByteBuffer) -> Int { + // For ssh-rsa, the format is public exponent `e` followed by modulus `n` + var writtenBytes = 0 + writtenBytes += buffer.writeSSHString("ssh-rsa".utf8) + writtenBytes += buffer.writePositiveMPInt(publicExponent.serialize()) + writtenBytes += buffer.writePositiveMPInt(modulus.serialize()) + return writtenBytes + } } public struct EncrytpedMessage: ContiguousBytes { @@ -67,7 +87,9 @@ extension Insecure.RSA.Signing { } } - public struct Signature: ContiguousBytes { + public struct Signature: ContiguousBytes, NIOSSHSignatureProtocol { + public static let signaturePrefix = "ssh-rsa" + public let rawRepresentation: Data public init(rawRepresentation: D) where D : DataProtocol { @@ -77,23 +99,38 @@ extension Insecure.RSA.Signing { public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { try rawRepresentation.withUnsafeBytes(body) } + + public func write(to buffer: inout ByteBuffer) -> Int { + var writtenLength = buffer.writeSSHString(Self.signaturePrefix.utf8) + + // For SSH-RSA, the key format is the signature without lengths or paddings + writtenLength += buffer.writeSSHString(rawRepresentation) + + return writtenLength + } } - public struct PrivateKey { + public struct PrivateKey: NIOSSHPrivateKeyProtocol { + public static let keyPrefix = "ssh-rsa" + public enum Storage { case privateExponent(d: BigUInt, n: BigUInt) // TODO: Quintuple } // Private Exponent - public let storage: Storage + private let storage: Storage // Public Exponent e - public let publicKey: PublicKey + private let _publicKey: PublicKey + + public var publicKey: NIOSSHPublicKeyProtocol { + _publicKey + } public init(privateExponent: BigUInt, publicExponent: BigUInt, modulus: BigUInt) { self.storage = .privateExponent(d: privateExponent, n: modulus) - self.publicKey = PublicKey(publicExponent: publicExponent, modulus: modulus) + self._publicKey = PublicKey(publicExponent: publicExponent, modulus: modulus) } public init(bits: Int = 2047, publicExponent e: BigUInt = 65537) { @@ -105,7 +142,7 @@ extension Insecure.RSA.Signing { let d = e.inverse(phi)! self.storage = .privateExponent(d: d, n: n) - self.publicKey = PublicKey( + self._publicKey = PublicKey( publicExponent: e, modulus: n ) @@ -121,6 +158,10 @@ extension Insecure.RSA.Signing { } } + public func signature(for data: D) throws -> NIOSSHSignatureProtocol where D : DataProtocol { + return try self.signature(for: data) as Signature + } + private static func encodePKCS1SHA1(_ data: D, length: Int) throws -> Data { /* * This is the magic ASN.1/DER prefix that goes in the decoded @@ -254,3 +295,66 @@ extension BigUInt { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ] as [UInt8])) } + +extension ByteBuffer { + @discardableResult + fileprivate mutating func writePositiveMPInt(_ value: Buffer) -> Int where Buffer.Element == UInt8 { + // A positive MPInt must have its high bit set to zero, and not have leading zero bytes unless it needs that + // high bit set to zero. We address this by dropping all the leading zero bytes in the collection first. + let trimmed = value.drop(while: { $0 == 0 }) + let needsLeadingZero = ((trimmed.first ?? 0) & 0x80) == 0x80 + + // Now we write the length. + var writtenBytes: Int + + if needsLeadingZero { + writtenBytes = self.writeInteger(UInt32(trimmed.count + 1)) + writtenBytes += self.writeInteger(UInt8(0)) + } else { + writtenBytes = self.writeInteger(UInt32(trimmed.count)) + } + + writtenBytes += self.writeBytes(trimmed) + return writtenBytes + } + + /// Writes the given bytes as an SSH string at the writer index. Moves the writer index forward. + @discardableResult + mutating func writeSSHString(_ value: Buffer) -> Int where Buffer.Element == UInt8 { + let writtenBytes = self.setSSHString(value, at: self.writerIndex) + self.moveWriterIndex(forwardBy: writtenBytes) + return writtenBytes + } + + /// Sets the given bytes as an SSH string at the given offset. Does not mutate the writer index. + @discardableResult + mutating func setSSHString(_ value: Buffer, at offset: Int) -> Int where Buffer.Element == UInt8 { + // RFC 4251 § 5: + // + // > Arbitrary length binary string. Strings are allowed to contain + // > arbitrary binary data, including null characters and 8-bit + // > characters. They are stored as a uint32 containing its length + // > (number of bytes that follow) and zero (= empty string) or more + // > bytes that are the value of the string. Terminating null + // > characters are not used. + let lengthLength = self.setInteger(UInt32(value.count), at: offset) + let valueLength = self.setBytes(value, at: offset + lengthLength) + return lengthLength + valueLength + } + + /// Sets the readable bytes of a ByteBuffer as an SSH string at the given offset. Does not mutate the writer index. + @discardableResult + mutating func setSSHString(_ value: ByteBuffer, at offset: Int) -> Int { + // RFC 4251 § 5: + // + // > Arbitrary length binary string. Strings are allowed to contain + // > arbitrary binary data, including null characters and 8-bit + // > characters. They are stored as a uint32 containing its length + // > (number of bytes that follow) and zero (= empty string) or more + // > bytes that are the value of the string. Terminating null + // > characters are not used. + let lengthLength = self.setInteger(UInt32(value.readableBytes), at: offset) + let valueLength = self.setBuffer(value, at: offset + lengthLength) + return lengthLength + valueLength + } +} From 17f5cadd0499fce5a18bdc8107ba150e0e2c24bb Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Fri, 22 Jan 2021 00:54:43 +0100 Subject: [PATCH 03/20] Removed RSA, added a custom key test --- Package.swift | 3 - .../SSHKeyExchangeStateMachine.swift | 9 +- .../Keys And Signatures/CustomKeys.swift | 4 + .../Keys And Signatures/NIOSSHPublicKey.swift | 31 +- .../Keys And Signatures/NIOSSHSignature.swift | 11 +- Sources/NIOSSHClient/ExecHandler.swift | 1 - .../InteractivePasswordPromptDelegate.swift | 22 +- Sources/NIOSSHClient/main.swift | 9 +- Sources/NIOSSHRSA/RSA.swift | 360 ------------------ Tests/NIOSSHTests/EndToEndTests.swift | 95 ++++- 10 files changed, 167 insertions(+), 378 deletions(-) delete mode 100644 Sources/NIOSSHRSA/RSA.swift diff --git a/Package.swift b/Package.swift index a2fe3770..44923fce 100644 --- a/Package.swift +++ b/Package.swift @@ -25,16 +25,13 @@ let package = Package( ], products: [ .library(name: "NIOSSH", targets: ["NIOSSH"]), - .library(name: "NIOSSHRSA", targets: ["NIOSSHRSA"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.21.0"), .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.2"), - .package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.0"), ], targets: [ .target(name: "NIOSSH", dependencies: ["NIO", "NIOFoundationCompat", "Crypto"]), - .target(name: "NIOSSHRSA", dependencies: ["NIOSSH", "BigInt"]), .target(name: "NIOSSHClient", dependencies: ["NIO", "NIOSSH", "NIOConcurrencyHelpers"]), .target(name: "NIOSSHServer", dependencies: ["NIO", "NIOSSH", "NIOFoundationCompat", "Crypto"]), .target(name: "NIOSSHPerformanceTester", dependencies: ["NIO", "NIOSSH", "Crypto"]), diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 4326efe8..99907411 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -498,14 +498,19 @@ struct SSHKeyExchangeStateMachine { extension SSHKeyExchangeStateMachine { // For now this is a static list. - static let supportedKeyExchangeImplementations: [EllipticCurveKeyExchangeProtocol.Type] = [ + static var supportedKeyExchangeImplementations: [EllipticCurveKeyExchangeProtocol.Type] = [ EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, ] - static let supportedKeyExchangeAlgorithms: [Substring] = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } + static var supportedKeyExchangeAlgorithms: [Substring] { + let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } + let customAlgorithms = NIOSSHPublicKey.customPublicKeyAlgorithms.map { Substring($0.publicKeyPrefix) } + + return bundledAlgorithms + customAlgorithms + } /// All known host key algorithms. static let supportedServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index 024e4de4..7ad4e0ea 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -6,6 +6,8 @@ public protocol NIOSSHSignatureProtocol { var rawRepresentation: Data { get } func write(to buffer: inout ByteBuffer) -> Int + + static func read(from buffer: inout ByteBuffer) throws -> Self } internal extension NIOSSHSignatureProtocol { @@ -21,6 +23,8 @@ public protocol NIOSSHPublicKeyProtocol { func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool func write(to buffer: inout ByteBuffer) -> Int + + static func read(from buffer: inout ByteBuffer) throws -> Self } internal extension NIOSSHPublicKeyProtocol { diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index cab440ae..b67ced22 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -198,9 +198,24 @@ extension NIOSSHPublicKey { Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix ] + internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] + internal static var customSignatures: [NIOSSHSignatureProtocol.Type] = [] + // TODO: Replace this function with sensible code - static func registerPublicKeyType(_ type: PublicKey.Type) { - knownAlgorithms.append(type.publicKeyPrefix.utf8) + static func registerPublicKeyType< + PublicKey: NIOSSHPublicKeyProtocol, + Signature: NIOSSHSignatureProtocol + >( + _ type: PublicKey.Type, + signature: Signature.Type + ) { + let utf8format = type.publicKeyPrefix.utf8 + + if !knownAlgorithms.contains(where: { $0.elementsEqual(utf8format) }) { + knownAlgorithms.append(utf8format) + customPublicKeyAlgorithms.append(type) + customSignatures.append(signature) + } } } @@ -279,6 +294,7 @@ extension ByteBuffer { writtenBytes += self.writeSSHString(NIOSSHPublicKey.ecdsaP521PublicKeyPrefix) writtenBytes += self.writeECDSAP521PublicKey(baseKey: key) case .custom(let key): + writtenBytes += writeSSHString(key.publicKeyPrefix.utf8) writtenBytes += key.write(to: &self) case .certified(let key): return self.writeCertifiedKey(key) @@ -302,7 +318,9 @@ extension ByteBuffer { case .ecdsaP521(let key): return self.writeECDSAP521PublicKey(baseKey: key) case .custom(let key): - return key.write(to: &self) + var writtenBytes = writeSSHString(key.publicKeyPrefix.utf8) + writtenBytes += key.write(to: &self) + return writtenBytes case .certified: preconditionFailure("Certified keys are the only callers of this method, and cannot contain themselves") } @@ -331,6 +349,13 @@ extension ByteBuffer { } else if keyIdentifierBytes.elementsEqual(NIOSSHPublicKey.ecdsaP521PublicKeyPrefix) { return try buffer.readECDSAP521PublicKey() } else { + for type in NIOSSHPublicKey.customPublicKeyAlgorithms { + if keyIdentifierBytes.elementsEqual(type.publicKeyPrefix.utf8) { + let publicKey = try type.read(from: &buffer) + return NIOSSHPublicKey(backingKey: .custom(publicKey)) + } + } + // We don't know this public key type. Maybe the certified keys do. return try buffer.readCertifiedKeyWithoutKeyPrefix(keyIdentifierBytes).map(NIOSSHPublicKey.init) } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift index fcd6057d..826df2d3 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift @@ -135,7 +135,9 @@ extension ByteBuffer { case .ecdsaP521(let sig): return self.writeECDSAP521Signature(baseSignature: sig) case .custom(let sig): - return sig.write(to: &self) + var writtenBytes = writeSSHString(sig.signaturePrefix.utf8) + writtenBytes += sig.write(to: &self) + return writtenBytes } } @@ -232,6 +234,13 @@ extension ByteBuffer { } else if bytesView.elementsEqual(NIOSSHSignature.ecdsaP521SignaturePrefix) { return try buffer.readECDSAP521Signature() } else { + for signature in NIOSSHPublicKey.customSignatures { + if bytesView.elementsEqual(signature.signaturePrefix.utf8) { + let signature = try signature.read(from: &buffer) + return NIOSSHSignature(backingSignature: .custom(signature)) + } + } + // We don't know this signature type. let signature = signatureIdentifierBytes.readString(length: signatureIdentifierBytes.readableBytes) ?? "" throw NIOSSHError.unknownSignature(algorithm: signature) diff --git a/Sources/NIOSSHClient/ExecHandler.swift b/Sources/NIOSSHClient/ExecHandler.swift index c88c78c6..0b79c50d 100644 --- a/Sources/NIOSSHClient/ExecHandler.swift +++ b/Sources/NIOSSHClient/ExecHandler.swift @@ -89,7 +89,6 @@ final class ExampleExecHandler: ChannelDuplexHandler { switch data.type { case .channel: - print(bytes.getString(at: 0, length: bytes.readableBytes)) // Channel data is forwarded on, the pipe channel will handle it. context.fireChannelRead(self.wrapInboundOut(bytes)) return diff --git a/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift b/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift index e75711b3..22f8c311 100644 --- a/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift +++ b/Sources/NIOSSHClient/InteractivePasswordPromptDelegate.swift @@ -15,16 +15,20 @@ import Dispatch import Foundation import NIO -import Crypto -import CCryptoBoringSSL import NIOSSH /// A client user auth delegate that provides an interactive prompt for password-based user auth. final class InteractivePasswordPromptDelegate: NIOSSHClientUserAuthenticationDelegate { private let queue: DispatchQueue - init() { + private var username: String? + + private var password: String? + + init(username: String?, password: String?) { self.queue = DispatchQueue(label: "io.swiftnio.ssh.InteractivePasswordPromptDelegate") + self.username = username + self.password = password } func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) { @@ -35,7 +39,17 @@ final class InteractivePasswordPromptDelegate: NIOSSHClientUserAuthenticationDel } self.queue.async { - nextChallengePromise.succeed(NIOSSHUserAuthenticationOffer(username: "joannis", serviceName: "", offer: .privateKey(NIOSSHUserAuthenticationOffer.Offer.PrivateKey(privateKey: .init(rsa: .init()))))) + if self.username == nil { + print("Username: ", terminator: "") + self.username = readLine() ?? "" + } + + if self.password == nil { + print("Password: ", terminator: "") + self.password = readLine() ?? "" + } + + nextChallengePromise.succeed(NIOSSHUserAuthenticationOffer(username: self.username!, serviceName: "", offer: .password(.init(password: self.password!)))) } } } diff --git a/Sources/NIOSSHClient/main.swift b/Sources/NIOSSHClient/main.swift index ec30aa4a..e01a7b15 100644 --- a/Sources/NIOSSHClient/main.swift +++ b/Sources/NIOSSHClient/main.swift @@ -38,6 +38,9 @@ final class AcceptAllHostKeysDelegate: NIOSSHClientServerAuthenticationDelegate } } +let parser = SimpleCLIParser() +let parseResult = parser.parse() + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { try! group.syncShutdownGracefully() @@ -45,12 +48,12 @@ defer { let bootstrap = ClientBootstrap(group: group) .channelInitializer { channel in - channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: InteractivePasswordPromptDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) + channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: InteractivePasswordPromptDelegate(username: parseResult.user, password: parseResult.password), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) } .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) -let channel = try bootstrap.connect(host: "orlandos.nl", port: 22).wait() +let channel = try bootstrap.connect(host: parseResult.host, port: parseResult.port).wait() // Let's try creating a child channel. let exitStatusPromise = channel.eventLoop.makePromise(of: Int.self) @@ -60,7 +63,7 @@ let childChannel: Channel = try! channel.pipeline.handler(type: NIOSSHHandler.se guard channelType == .session else { return channel.eventLoop.makeFailedFuture(SSHClientError.invalidChannelType) } - return childChannel.pipeline.addHandlers([ExampleExecHandler(command: "ls -la", completePromise: exitStatusPromise), ErrorHandler()]) + return childChannel.pipeline.addHandlers([ExampleExecHandler(command: parseResult.commandString, completePromise: exitStatusPromise), ErrorHandler()]) } return promise.futureResult }.wait() diff --git a/Sources/NIOSSHRSA/RSA.swift b/Sources/NIOSSHRSA/RSA.swift deleted file mode 100644 index 440cd9a7..00000000 --- a/Sources/NIOSSHRSA/RSA.swift +++ /dev/null @@ -1,360 +0,0 @@ -import NIO -import NIOFoundationCompat -import BigInt -import NIOSSH -import CCryptoBoringSSL -import Foundation -import Crypto - -extension Insecure { - public enum RSA { - public enum Signing {} - } -} - -extension Insecure.RSA.Signing { - public struct PublicKey: Equatable, Hashable, NIOSSHPublicKeyProtocol { - public static let publicKeyPrefix = "ssh-rsa" - - // PublicExponent e - private let publicExponent: BigUInt - - // Modulus n - private let modulus: BigUInt - - public var rawRepresentation: Data { - publicExponent.serialize() + modulus.serialize() - } - - enum PubkeyParseError: Error { - case invalidInitialSequence, invalidAlgorithmIdentifier, invalidSubjectPubkey, forbiddenTrailingData, invalidRSAPubkey - } - - public init(publicExponent: BigUInt, modulus: BigUInt) { - self.publicExponent = publicExponent - self.modulus = modulus - } - - public func encrypt(for message: D) throws -> EncrytpedMessage { - let message = BigUInt(Data(message)) - - guard message > .zero && message <= modulus - 1 else { - throw RSAError.messageRepresentativeOutOfRange - } - - let result = message.power(publicExponent, modulus: modulus) - return EncrytpedMessage(rawRepresentation: result.serialize()) - } - - public func isValidSignature(_ signature: Signature, for digest: D) -> Bool { - let signature = BigUInt(signature.rawRepresentation) - - guard signature > .zero && signature <= modulus - 1 else { - return false - } - - let m = signature.power(publicExponent, modulus: modulus) - return m.serialize() == Data(digest) - } - - public func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D : DataProtocol { - guard let signature = signature as? Signature else { - return false - } - - return isValidSignature(signature, for: data) - } - - public func write(to buffer: inout ByteBuffer) -> Int { - // For ssh-rsa, the format is public exponent `e` followed by modulus `n` - var writtenBytes = 0 - writtenBytes += buffer.writeSSHString("ssh-rsa".utf8) - writtenBytes += buffer.writePositiveMPInt(publicExponent.serialize()) - writtenBytes += buffer.writePositiveMPInt(modulus.serialize()) - return writtenBytes - } - } - - public struct EncrytpedMessage: ContiguousBytes { - public let rawRepresentation: Data - - public init(rawRepresentation: D) where D : DataProtocol { - self.rawRepresentation = Data(rawRepresentation) - } - - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - try rawRepresentation.withUnsafeBytes(body) - } - } - - public struct Signature: ContiguousBytes, NIOSSHSignatureProtocol { - public static let signaturePrefix = "ssh-rsa" - - public let rawRepresentation: Data - - public init(rawRepresentation: D) where D : DataProtocol { - self.rawRepresentation = Data(rawRepresentation) - } - - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { - try rawRepresentation.withUnsafeBytes(body) - } - - public func write(to buffer: inout ByteBuffer) -> Int { - var writtenLength = buffer.writeSSHString(Self.signaturePrefix.utf8) - - // For SSH-RSA, the key format is the signature without lengths or paddings - writtenLength += buffer.writeSSHString(rawRepresentation) - - return writtenLength - } - } - - public struct PrivateKey: NIOSSHPrivateKeyProtocol { - public static let keyPrefix = "ssh-rsa" - - public enum Storage { - case privateExponent(d: BigUInt, n: BigUInt) - // TODO: Quintuple - } - - // Private Exponent - private let storage: Storage - - // Public Exponent e - private let _publicKey: PublicKey - - public var publicKey: NIOSSHPublicKeyProtocol { - _publicKey - } - - public init(privateExponent: BigUInt, publicExponent: BigUInt, modulus: BigUInt) { - self.storage = .privateExponent(d: privateExponent, n: modulus) - self._publicKey = PublicKey(publicExponent: publicExponent, modulus: modulus) - } - - public init(bits: Int = 2047, publicExponent e: BigUInt = 65537) { - let p = BigUInt.randomPrime(bits: bits) - let q = BigUInt.randomPrime(bits: bits) - - let n = p * q // modulus - let phi = (p - 1) * (q - 1) - let d = e.inverse(phi)! - self.storage = .privateExponent(d: d, n: n) - - self._publicKey = PublicKey( - publicExponent: e, - modulus: n - ) - } - - public func signature(for message: D) throws -> Signature { - switch storage { - case .privateExponent(_, let n): - let message = try Self.encodePKCS1SHA1(message, length: (n.bitWidth + 7) / 8) - - let result = self.signature(for: BigUInt(Data(message))) - return Signature(rawRepresentation: result.serialize()) - } - } - - public func signature(for data: D) throws -> NIOSSHSignatureProtocol where D : DataProtocol { - return try self.signature(for: data) as Signature - } - - private static func encodePKCS1SHA1(_ data: D, length: Int) throws -> Data { - /* - * This is the magic ASN.1/DER prefix that goes in the decoded - * signature, between the string of FFs and the actual SHA-1 - * hash value. The meaning of it is: - * - * 00 -- this marks the end of the FFs; not part of the ASN.1 - * bit itself - * - * 30 21 -- a constructed SEQUENCE of length 0x21 - * 30 09 -- a constructed sub-SEQUENCE of length 9 - * 06 05 -- an object identifier, length 5 - * 2B 0E 03 02 1A -- object id { 1 3 14 3 2 26 } - * (the 1,3 comes from 0x2B = 43 = 40*1+3) - * 05 00 -- NULL - * 04 14 -- a primitive OCTET STRING of length 0x14 - * [0x14 bytes of hash data follows] - * - * The object id in the middle there is listed as `id-sha1' in - * ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1d2.asn - * (the ASN module for PKCS #1) and its expanded form is as - * follows: - * - * id-sha1 OBJECT IDENTIFIER ::= { - * iso(1) identified-organization(3) oiw(14) secsig(3) - * algorithms(2) 26 } - */ - let prefix: [UInt8] = [ - 0x00, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, - 0x0E, 0x03, 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14, - ] - - let padding = length - prefix.count - 2 - Insecure.SHA1.Digest.byteCount - - var buffer = ByteBuffer() - buffer.writeInteger(0 as UInt8) - buffer.writeInteger(1 as UInt8) - for _ in 0.. BigUInt { - switch storage { - case let .privateExponent(d, n): - return m.power(d, modulus: n) - } - } - - public func decrypt(_ signature: EncrytpedMessage) throws -> Data { - let signature = BigUInt(signature.rawRepresentation) - - switch storage { - case let .privateExponent(privateExponent, modulus): - guard signature >= .zero && signature <= privateExponent else { - throw RSAError.ciphertextRepresentativeOutOfRange - } - - return signature.power(privateExponent, modulus: modulus).serialize() - } - } - } -} - -public struct RSAError: Error { - let message: String - - static let messageRepresentativeOutOfRange = RSAError(message: "message representative out of range") - static let ciphertextRepresentativeOutOfRange = RSAError(message: "ciphertext representative out of range") - static let signatureRepresentativeOutOfRange = RSAError(message: "signature representative out of range") - static let invalidPem = RSAError(message: "invalid PEM") - static let pkcs1Error = RSAError(message: "PKCS1Error") -} - -extension BigUInt { - public static func randomPrime(bits: Int) -> BigUInt { - while true { - var privateExponent = BigUInt.randomInteger(withExactWidth: bits) - privateExponent |= 1 - - if privateExponent.isPrime() { - return privateExponent - } - } - } - - fileprivate init(boringSSL bignum: UnsafeMutablePointer) { - var data = [UInt8](repeating: 0, count: Int(CCryptoBoringSSL_BN_num_bytes(bignum))) - CCryptoBoringSSL_BN_bn2bin(bignum, &data) - self.init(Data(data)) - } -} - -extension BigUInt { - public static let diffieHellmanGroup14 = BigUInt(Data([ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, 0xC2, 0x34, - 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, - 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, - 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, - 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, - 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, - 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, - 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, - 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, - 0xF4, 0x4C, 0x42, 0xE9, 0xA6, 0x37, 0xED, 0x6B, - 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, - 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, - 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, - 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE4, 0x5B, 0x3D, - 0xC2, 0x00, 0x7C, 0xB8, 0xA1, 0x63, 0xBF, 0x05, - 0x98, 0xDA, 0x48, 0x36, 0x1C, 0x55, 0xD3, 0x9A, - 0x69, 0x16, 0x3F, 0xA8, 0xFD, 0x24, 0xCF, 0x5F, - 0x83, 0x65, 0x5D, 0x23, 0xDC, 0xA3, 0xAD, 0x96, - 0x1C, 0x62, 0xF3, 0x56, 0x20, 0x85, 0x52, 0xBB, - 0x9E, 0xD5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6D, - 0x67, 0x0C, 0x35, 0x4E, 0x4A, 0xBC, 0x98, 0x04, - 0xF1, 0x74, 0x6C, 0x08, 0xCA, 0x18, 0x21, 0x7C, - 0x32, 0x90, 0x5E, 0x46, 0x2E, 0x36, 0xCE, 0x3B, - 0xE3, 0x9E, 0x77, 0x2C, 0x18, 0x0E, 0x86, 0x03, - 0x9B, 0x27, 0x83, 0xA2, 0xEC, 0x07, 0xA2, 0x8F, - 0xB5, 0xC5, 0x5D, 0xF0, 0x6F, 0x4C, 0x52, 0xC9, - 0xDE, 0x2B, 0xCB, 0xF6, 0x95, 0x58, 0x17, 0x18, - 0x39, 0x95, 0x49, 0x7C, 0xEA, 0x95, 0x6A, 0xE5, - 0x15, 0xD2, 0x26, 0x18, 0x98, 0xFA, 0x05, 0x10, - 0x15, 0x72, 0x8E, 0x5A, 0x8A, 0xAC, 0xAA, 0x68, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF - ] as [UInt8])) -} - -extension ByteBuffer { - @discardableResult - fileprivate mutating func writePositiveMPInt(_ value: Buffer) -> Int where Buffer.Element == UInt8 { - // A positive MPInt must have its high bit set to zero, and not have leading zero bytes unless it needs that - // high bit set to zero. We address this by dropping all the leading zero bytes in the collection first. - let trimmed = value.drop(while: { $0 == 0 }) - let needsLeadingZero = ((trimmed.first ?? 0) & 0x80) == 0x80 - - // Now we write the length. - var writtenBytes: Int - - if needsLeadingZero { - writtenBytes = self.writeInteger(UInt32(trimmed.count + 1)) - writtenBytes += self.writeInteger(UInt8(0)) - } else { - writtenBytes = self.writeInteger(UInt32(trimmed.count)) - } - - writtenBytes += self.writeBytes(trimmed) - return writtenBytes - } - - /// Writes the given bytes as an SSH string at the writer index. Moves the writer index forward. - @discardableResult - mutating func writeSSHString(_ value: Buffer) -> Int where Buffer.Element == UInt8 { - let writtenBytes = self.setSSHString(value, at: self.writerIndex) - self.moveWriterIndex(forwardBy: writtenBytes) - return writtenBytes - } - - /// Sets the given bytes as an SSH string at the given offset. Does not mutate the writer index. - @discardableResult - mutating func setSSHString(_ value: Buffer, at offset: Int) -> Int where Buffer.Element == UInt8 { - // RFC 4251 § 5: - // - // > Arbitrary length binary string. Strings are allowed to contain - // > arbitrary binary data, including null characters and 8-bit - // > characters. They are stored as a uint32 containing its length - // > (number of bytes that follow) and zero (= empty string) or more - // > bytes that are the value of the string. Terminating null - // > characters are not used. - let lengthLength = self.setInteger(UInt32(value.count), at: offset) - let valueLength = self.setBytes(value, at: offset + lengthLength) - return lengthLength + valueLength - } - - /// Sets the readable bytes of a ByteBuffer as an SSH string at the given offset. Does not mutate the writer index. - @discardableResult - mutating func setSSHString(_ value: ByteBuffer, at offset: Int) -> Int { - // RFC 4251 § 5: - // - // > Arbitrary length binary string. Strings are allowed to contain - // > arbitrary binary data, including null characters and 8-bit - // > characters. They are stored as a uint32 containing its length - // > (number of bytes that follow) and zero (= empty string) or more - // > bytes that are the value of the string. Terminating null - // > characters are not used. - let lengthLength = self.setInteger(UInt32(value.readableBytes), at: offset) - let valueLength = self.setBuffer(value, at: offset + lengthLength) - return lengthLength + valueLength - } -} diff --git a/Tests/NIOSSHTests/EndToEndTests.swift b/Tests/NIOSSHTests/EndToEndTests.swift index b08b57c8..1b05972a 100644 --- a/Tests/NIOSSHTests/EndToEndTests.swift +++ b/Tests/NIOSSHTests/EndToEndTests.swift @@ -18,7 +18,77 @@ import NIO import XCTest enum EndToEndTestError: Error { - case unableToCreateChildChannel + case unableToCreateChildChannel, invalidCustomPublicKey, invalidCustomSignature +} + +fileprivate let testKey = Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]) + +struct CustomPrivateKey: NIOSSHPrivateKeyProtocol { + static let keyPrefix = "custom-prefix" + + var publicKey: NIOSSHPublicKeyProtocol { + CustomPublicKey() + } + + func signature(for data: D) throws -> NIOSSHSignatureProtocol where D : DataProtocol { + var data = Data(data) + + let testKeySize = testKey.count + for i in 0.. Int { + buffer.writeSSHString(rawRepresentation) + } + + static func read(from buffer: inout ByteBuffer) throws -> CustomSignature { + guard var buffer = buffer.readSSHString() else { + throw EndToEndTestError.invalidCustomSignature + } + + let data = buffer.readData(length: buffer.readableBytes)! + return CustomSignature(rawRepresentation: data) + } +} + +struct CustomPublicKey: NIOSSHPublicKeyProtocol { + static let publicKeyPrefix = "custom-prefix" + + func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D : DataProtocol { + let testKeySize = testKey.count + var data = Data(data) + for i in 0.. Int { + return 0 + } + + var rawRepresentation: Data { + testKey + } + + static func read(from buffer: inout ByteBuffer) throws -> CustomPublicKey { + guard buffer.readableBytes == 0 else { + throw EndToEndTestError.invalidCustomPublicKey + } + + return CustomPublicKey() + } } class BackToBackEmbeddedChannel { @@ -433,6 +503,29 @@ class EndToEndTests: XCTestCase { XCTAssertEqual(self.channel.activeServerChannels.count, 1) #endif } + + func testCustomKeys() throws { + NIOSSHPublicKey.registerPublicKeyType(CustomPublicKey.self, signature: CustomSignature.self) + + // If we can't create this key, we skip the test. + let hostKey = NIOSSHPrivateKey(ed25519Key: .init()) + let clientAuthKey = NIOSSHPrivateKey(custom: CustomPrivateKey()) + + // We use the Secure Enclave keys for everything, just because we can. + var harness = TestHarness() + harness.serverHostKeys = [hostKey] + harness.clientAuthDelegate = PrivateKeyClientAuth(clientAuthKey) + harness.serverAuthDelegate = ExpectPublicKeyAuth(clientAuthKey.publicKey) + + XCTAssertNoThrow(try self.channel.configureWithHarness(harness)) + XCTAssertNoThrow(try self.channel.activate()) + XCTAssertNoThrow(try self.channel.interactInMemory()) + + // Create a channel, again, just because we can. + _ = try self.channel.createNewChannel() + XCTAssertNoThrow(try self.channel.interactInMemory()) + XCTAssertEqual(self.channel.activeServerChannels.count, 1) + } func testSupportClientInitiatedRekeying() throws { XCTAssertNoThrow(try self.channel.configureWithHarness(TestHarness())) From 077e954e0c0f029f01cd0976d9e86ccf19c1a4a8 Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Fri, 12 Feb 2021 23:35:08 +0100 Subject: [PATCH 04/20] Add docs --- .../Keys And Signatures/CustomKeys.swift | 29 ++++++++++++++++++- .../Keys And Signatures/NIOSSHPublicKey.swift | 4 +-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index 7ad4e0ea..3624eebb 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -1,12 +1,25 @@ import Foundation import NIO +/// A signature is a mathematical scheme for verifying the authenticity of digital messages or documents. +/// +/// This protocol can be implemented by a type that represents such a signature to NIOSSH. +/// +/// - See: https://en.wikipedia.org/wiki/Digital_signature public protocol NIOSSHSignatureProtocol { + /// An identifier that represents the type of signature used in an SSH packet. + /// This identifier MUST be unique to the signature implementation. + /// The returned value MUST NOT overlap with other signature implementations or a specifications that the signature does not implement. static var signaturePrefix: String { get } + + /// The raw reprentation of this signature as a blob. var rawRepresentation: Data { get } + /// Serializes and writes the signature to the buffer. The calling function SHOULD NOT keep track of the size of the written blob. + /// If the result is not a fixed size, the serialized format SHOULD include a length. func write(to buffer: inout ByteBuffer) -> Int + /// Reads this Signature from the buffer using the same format implemented in `write(to:)` static func read(from buffer: inout ByteBuffer) throws -> Self } @@ -17,13 +30,22 @@ internal extension NIOSSHSignatureProtocol { } public protocol NIOSSHPublicKeyProtocol { + /// An identifier that represents the type of public key used in an SSH packet. + /// This identifier MUST be unique to the public key implementation. + /// The returned value MUST NOT overlap with other public key implementations or a specifications that the public key does not implement. static var publicKeyPrefix: String { get } + + /// The raw reprentation of this signature as a blob. var rawRepresentation: Data { get } + /// Verifies that `signature` is the result of signing `data` using the private key that this public key is derived from. func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool + /// Serializes and writes the public key to the buffer. The calling function SHOULD NOT keep track of the size of the written blob. + /// If the result is not a fixed size, the serialized format SHOULD include a length. func write(to buffer: inout ByteBuffer) -> Int + /// Reads this Public Key from the buffer using the same format implemented in `write(to:)` static func read(from buffer: inout ByteBuffer) throws -> Self } @@ -34,10 +56,15 @@ internal extension NIOSSHPublicKeyProtocol { } public protocol NIOSSHPrivateKeyProtocol { + /// An identifier that represents the type of private key used in an SSH packet. + /// This identifier MUST be unique to the private key implementation. + /// The returned value MUST NOT overlap with other private key implementations or a specifications that the private key does not implement. static var keyPrefix: String { get } -// var rawRepresentation: Data { get } + + /// A public key instance that is able to verify signatures that are created using this private key. var publicKey: NIOSSHPublicKeyProtocol { get } + /// Creates a signature, proving that `data` has been sent by the holder of this private key, and can be verified by `publicKey`. func signature(for data: D) throws -> NIOSSHSignatureProtocol } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index b67ced22..828d5261 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -201,8 +201,8 @@ extension NIOSSHPublicKey { internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] internal static var customSignatures: [NIOSSHSignatureProtocol.Type] = [] - // TODO: Replace this function with sensible code - static func registerPublicKeyType< + /// Registers a custom type tuple for use in Public Key Authentication. + public static func registerPublicKeyType< PublicKey: NIOSSHPublicKeyProtocol, Signature: NIOSSHSignatureProtocol >( From 4ecf937ecb20e0547cddc0b8ab7cfb6273510de3 Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Fri, 25 Jun 2021 09:23:08 +0200 Subject: [PATCH 05/20] Support custom public key types for host keys --- .../NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 99907411..a6ce3b9f 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -513,7 +513,14 @@ extension SSHKeyExchangeStateMachine { } /// All known host key algorithms. - static let supportedServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] + static let bundledServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] + + static var supportedServerHostKeyAlgorithms: [Substring] { + let bundledAlgorithms = bundledServerHostKeyAlgorithms + let customAlgorithms = NIOSSHPublicKey.customPublicKeyAlgorithms.map { Substring($0.publicKeyPrefix) } + + return bundledAlgorithms + customAlgorithms + } } extension SSHKeyExchangeStateMachine { From 5852c0ccd2cab1b6b05dc8f428421c1ce31930d5 Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Fri, 2 Jul 2021 16:06:05 +0200 Subject: [PATCH 06/20] More transport options --- Sources/NIOSSH/ByteBuffer+SSH.swift | 2 +- .../AcceptsKeyExchangeMessages.swift | 2 +- .../SSHConnectionStateMachine.swift | 3 +- .../EllipticCurveKeyExchange.swift | 83 ++++++++++++------- .../Key Exchange/SSHKeyExchangeResult.swift | 44 +++++++--- .../SSHKeyExchangeStateMachine.swift | 36 +++++--- .../Keys And Signatures/CustomKeys.swift | 38 +++++++++ .../Keys And Signatures/NIOSSHPublicKey.swift | 26 ++---- .../SSHTransportProtection.swift | 2 +- Tests/NIOSSHTests/EndToEndTests.swift | 1 + 10 files changed, 159 insertions(+), 78 deletions(-) diff --git a/Sources/NIOSSH/ByteBuffer+SSH.swift b/Sources/NIOSSH/ByteBuffer+SSH.swift index dffe2505..206d4ee7 100644 --- a/Sources/NIOSSH/ByteBuffer+SSH.swift +++ b/Sources/NIOSSH/ByteBuffer+SSH.swift @@ -169,7 +169,7 @@ extension ByteBuffer { /// Writes a given number of SSH-acceptable padding bytes to this buffer. @discardableResult - mutating func writeSSHPaddingBytes(count: Int) -> Int { + public mutating func writeSSHPaddingBytes(count: Int) -> Int { // Annoyingly, the system random number generator can only give bytes to us 8 bytes at a time. precondition(count >= 0, "Cannot write negative number of padding bytes: \(count)") diff --git a/Sources/NIOSSH/Connection State Machine/Operations/AcceptsKeyExchangeMessages.swift b/Sources/NIOSSH/Connection State Machine/Operations/AcceptsKeyExchangeMessages.swift index b053d324..a9787f8b 100644 --- a/Sources/NIOSSH/Connection State Machine/Operations/AcceptsKeyExchangeMessages.swift +++ b/Sources/NIOSSH/Connection State Machine/Operations/AcceptsKeyExchangeMessages.swift @@ -30,7 +30,7 @@ extension AcceptsKeyExchangeMessages { } mutating func receiveKeyExchangeInitMessage(_ message: SSHMessage.KeyExchangeECDHInitMessage) throws -> SSHConnectionStateMachine.StateMachineInboundProcessResult { - let message = try self.keyExchangeStateMachine.handle(keyExchangeInit: message) + let message = try self.keyExchangeStateMachine.handle(keyExchangeInit: message.publicKey) if let message = message { return .emitMessage(message) diff --git a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift index f8373f92..0d134607 100644 --- a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift +++ b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift @@ -60,7 +60,7 @@ struct SSHConnectionStateMachine { /// The state of this state machine. private var state: State - private static let defaultTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ + internal static var defaultTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ AES256GCMOpenSSHTransportProtection.self, AES128GCMOpenSSHTransportProtection.self, ] @@ -181,6 +181,7 @@ struct SSHConnectionStateMachine { return .noMessage case .unimplemented(let unimplemented): throw NIOSSHError.remotePeerDoesNotSupportMessage(unimplemented) + default: // TODO: enforce RFC 4253: // diff --git a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift index 566a27ed..bebc0cb4 100644 --- a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift +++ b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift @@ -16,33 +16,51 @@ import Crypto import NIO import NIOFoundationCompat +public struct NIOSSHKeyExchangeServerReply { + let hostKey: NIOSSHPublicKey + let publicKey: ByteBuffer + let signature: NIOSSHSignature +} + /// This protocol defines a container used by the key exchange state machine to manage key exchange. /// This type erases the specific key exchanger. -protocol EllipticCurveKeyExchangeProtocol { +public protocol NIOSSHKeyExchangeAlgorithmProtocol { + static var keyExchangeInitMessageId: UInt8 { get } + static var keyExchangeReplyMessageId: UInt8 { get } + init(ourRole: SSHConnectionRole, previousSessionIdentifier: ByteBuffer?) - func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> SSHMessage.KeyExchangeECDHInitMessage - - mutating func completeKeyExchangeServerSide(clientKeyExchangeMessage message: SSHMessage.KeyExchangeECDHInitMessage, - serverHostKey: NIOSSHPrivateKey, - initialExchangeBytes: inout ByteBuffer, - allocator: ByteBufferAllocator, - expectedKeySizes: ExpectedKeySizes) throws -> (KeyExchangeResult, SSHMessage.KeyExchangeECDHReplyMessage) - - mutating func receiveServerKeyExchangePayload(serverKeyExchangeMessage message: SSHMessage.KeyExchangeECDHReplyMessage, - initialExchangeBytes: inout ByteBuffer, - allocator: ByteBufferAllocator, - expectedKeySizes: ExpectedKeySizes) throws -> KeyExchangeResult + func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> ByteBuffer + + mutating func completeKeyExchangeServerSide( + clientKeyExchangeMessage message: ByteBuffer, + serverHostKey: NIOSSHPrivateKey, + initialExchangeBytes: inout ByteBuffer, + allocator: ByteBufferAllocator, + expectedKeySizes: ExpectedKeySizes + ) throws -> (KeyExchangeResult, NIOSSHKeyExchangeServerReply) + + mutating func receiveServerKeyExchangePayload( + serverHostKey hostKey: NIOSSHPublicKey, + serverPublicKey publicKey: ByteBuffer, + serverSignature signature: NIOSSHSignature, + initialExchangeBytes: inout ByteBuffer, + allocator: ByteBufferAllocator, + expectedKeySizes: ExpectedKeySizes + ) throws -> KeyExchangeResult static var keyExchangeAlgorithmNames: [Substring] { get } } -struct EllipticCurveKeyExchange: EllipticCurveKeyExchangeProtocol { +struct EllipticCurveKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { private var previousSessionIdentifier: ByteBuffer? private var ourKey: PrivateKey private var theirKey: PrivateKey.PublicKey? private var ourRole: SSHConnectionRole private var sharedSecret: SharedSecret? + + static var keyExchangeInitMessageId: UInt8 { 30 } + static var keyExchangeReplyMessageId: UInt8 { 31 } init(ourRole: SSHConnectionRole, previousSessionIdentifier: ByteBuffer?) { self.ourRole = ourRole @@ -59,14 +77,13 @@ extension EllipticCurveKeyExchange { /// Initiates key exchange by producing an SSH message. /// /// For now, we just return the ByteBuffer containing the SSH string. - func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> SSHMessage.KeyExchangeECDHInitMessage { + func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> ByteBuffer { precondition(self.ourRole.isClient, "Only clients may initiate the client side key exchange!") // The largest key we're likely to end up with here is 256 bytes. var buffer = allocator.buffer(capacity: 256) self.ourKey.publicKey.write(to: &buffer) - - return .init(publicKey: buffer) + return buffer } /// Handles receiving the client key exchange payload on the server side. @@ -77,15 +94,15 @@ extension EllipticCurveKeyExchange { /// - initialExchangeBytes: The initial bytes of the exchange, suitable for writing into the exchange hash. /// - allocator: A `ByteBufferAllocator` suitable for this connection. /// - expectedKeySizes: The sizes of the keys we need to generate. - mutating func completeKeyExchangeServerSide(clientKeyExchangeMessage message: SSHMessage.KeyExchangeECDHInitMessage, + mutating func completeKeyExchangeServerSide(clientKeyExchangeMessage message: ByteBuffer, serverHostKey: NIOSSHPrivateKey, initialExchangeBytes: inout ByteBuffer, allocator: ByteBufferAllocator, - expectedKeySizes: ExpectedKeySizes) throws -> (KeyExchangeResult, SSHMessage.KeyExchangeECDHReplyMessage) { + expectedKeySizes: ExpectedKeySizes) throws -> (KeyExchangeResult, NIOSSHKeyExchangeServerReply) { precondition(self.ourRole.isServer, "Only servers may receive a client key exchange packet!") // With that, we have enough to finalize the key exchange. - let kexResult = try self.finalizeKeyExchange(theirKeyBytes: message.publicKey, + let kexResult = try self.finalizeKeyExchange(theirKeyBytes: message, initialExchangeBytes: &initialExchangeBytes, serverHostKey: serverHostKey.publicKey, allocator: allocator, @@ -100,9 +117,9 @@ extension EllipticCurveKeyExchange { self.ourKey.publicKey.write(to: &publicKeyBytes) // Now we have all we need. - let responseMessage = SSHMessage.KeyExchangeECDHReplyMessage(hostKey: serverHostKey.publicKey, - publicKey: publicKeyBytes, - signature: exchangeHashSignature) + let responseMessage = NIOSSHKeyExchangeServerReply(hostKey: serverHostKey.publicKey, + publicKey: publicKeyBytes, + signature: exchangeHashSignature) return (KeyExchangeResult(kexResult), responseMessage) } @@ -116,12 +133,16 @@ extension EllipticCurveKeyExchange { /// - initialExchangeBytes: The initial bytes of the exchange, suitable for writing into the exchange hash. /// - allocator: A `ByteBufferAllocator` suitable for this connection. /// - expectedKeySizes: The sizes of the keys we need to generate. - mutating func receiveServerKeyExchangePayload(serverKeyExchangeMessage message: SSHMessage.KeyExchangeECDHReplyMessage, - initialExchangeBytes: inout ByteBuffer, - allocator: ByteBufferAllocator, - expectedKeySizes: ExpectedKeySizes) throws -> KeyExchangeResult { + mutating func receiveServerKeyExchangePayload( + serverHostKey hostKey: NIOSSHPublicKey, + serverPublicKey publicKey: ByteBuffer, + serverSignature signature: NIOSSHSignature, + initialExchangeBytes: inout ByteBuffer, + allocator: ByteBufferAllocator, + expectedKeySizes: ExpectedKeySizes + ) throws -> KeyExchangeResult { precondition(self.ourRole.isClient, "Only clients may receive a server key exchange packet!") - + // Ok, we have a few steps here. Firstly, we need to extract the server's public key and generate our shared // secret. Then we need to validate that we didn't generate a weak shared secret (possible under some cases), // as this must fail the key exchange process. @@ -131,14 +152,14 @@ extension EllipticCurveKeyExchange { // // Finally, we return our generated keys to the state machine. - let kexResult = try self.finalizeKeyExchange(theirKeyBytes: message.publicKey, + let kexResult = try self.finalizeKeyExchange(theirKeyBytes: publicKey, initialExchangeBytes: &initialExchangeBytes, - serverHostKey: message.hostKey, + serverHostKey: hostKey, allocator: allocator, expectedKeySizes: expectedKeySizes) // We can now verify signature over the exchange hash. - guard message.hostKey.isValidSignature(message.signature, for: kexResult.exchangeHash) else { + guard hostKey.isValidSignature(signature, for: kexResult.exchangeHash) else { throw NIOSSHError.invalidExchangeHashSignature } diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift index c93e14bd..b96cc114 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift @@ -19,11 +19,16 @@ import NIO /// /// A round of key exchange generates a number of keys and also generates an exchange hash. /// This exchange hash is used for a number of purposes. -struct KeyExchangeResult { +public struct KeyExchangeResult { /// The session ID to use for this connection. Will be static across the lifetime of a connection. var sessionID: ByteBuffer var keys: NIOSSHSessionKeys + + public init(sessionID: ByteBuffer, keys: NIOSSHSessionKeys) { + self.sessionID = sessionID + self.keys = keys + } } extension KeyExchangeResult: Equatable {} @@ -46,18 +51,27 @@ extension KeyExchangeResult: Equatable {} /// Of these types, the encryption keys and the MAC keys are intended to be secret, and so /// we store them in the `SymmetricKey` types. The IVs do not need to be secret, and so are /// stored in regular heap buffers. -struct NIOSSHSessionKeys { - var initialInboundIV: [UInt8] +public struct NIOSSHSessionKeys { + public internal(set) var initialInboundIV: [UInt8] - var initialOutboundIV: [UInt8] + public internal(set) var initialOutboundIV: [UInt8] - var inboundEncryptionKey: SymmetricKey + public internal(set) var inboundEncryptionKey: SymmetricKey - var outboundEncryptionKey: SymmetricKey + public internal(set) var outboundEncryptionKey: SymmetricKey - var inboundMACKey: SymmetricKey + public internal(set) var inboundMACKey: SymmetricKey - var outboundMACKey: SymmetricKey + public internal(set) var outboundMACKey: SymmetricKey + + public init(initialInboundIV: [UInt8], initialOutboundIV: [UInt8], inboundEncryptionKey: SymmetricKey, outboundEncryptionKey: SymmetricKey, inboundMACKey: SymmetricKey, outboundMACKey: SymmetricKey) { + self.initialInboundIV = initialInboundIV + self.initialOutboundIV = initialOutboundIV + self.inboundEncryptionKey = inboundEncryptionKey + self.outboundEncryptionKey = outboundEncryptionKey + self.inboundMACKey = inboundMACKey + self.outboundMACKey = outboundMACKey + } } extension NIOSSHSessionKeys: Equatable {} @@ -68,10 +82,16 @@ extension NIOSSHSessionKeys: Equatable {} /// hash function invocations. The output of these hash functions is truncated to an appropriate /// length as needed, which means we need to ensure the code doing the calculation knows how /// to truncate appropriately. -struct ExpectedKeySizes { - var ivSize: Int +public struct ExpectedKeySizes { + public internal(set) var ivSize: Int - var encryptionKeySize: Int + public internal(set) var encryptionKeySize: Int - var macKeySize: Int + public internal(set) var macKeySize: Int + + public init(ivSize: Int, encryptionKeySize: Int, macKeySize: Int) { + self.ivSize = ivSize + self.encryptionKeySize = encryptionKeySize + self.macKeySize = macKeySize + } } diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index a6ce3b9f..368a6559 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -39,19 +39,19 @@ struct SSHKeyExchangeStateMachine { /// party can enter this state. The remote peer may be sending a guess as well. /// /// We store the message we sent for later. - case keyExchangeReceived(exchange: EllipticCurveKeyExchangeProtocol, negotiated: NegotiationResult, expectingGuess: Bool) + case keyExchangeReceived(exchange: NIOSSHKeyExchangeAlgorithmProtocol, negotiated: NegotiationResult, expectingGuess: Bool) /// The peer has guessed what key exchange init packet is coming, and guessed wrong. We need to wait for them to send that packet. - case awaitingKeyExchangeInitInvalidGuess(exchange: EllipticCurveKeyExchangeProtocol, negotiated: NegotiationResult) + case awaitingKeyExchangeInitInvalidGuess(exchange: NIOSSHKeyExchangeAlgorithmProtocol, negotiated: NegotiationResult) /// Both sides have sent their initial key exchange message but we have not begun actually performing a key exchange. - case awaitingKeyExchangeInit(exchange: EllipticCurveKeyExchangeProtocol, negotiated: NegotiationResult) + case awaitingKeyExchangeInit(exchange: NIOSSHKeyExchangeAlgorithmProtocol, negotiated: NegotiationResult) /// We've received the key exchange init, but not sent our reply yet. case keyExchangeInitReceived(result: KeyExchangeResult, negotiated: NegotiationResult) /// We've sent our keyExchangeInit, but not received the keyExchangeReply. - case keyExchangeInitSent(exchange: EllipticCurveKeyExchangeProtocol, negotiated: NegotiationResult) + case keyExchangeInitSent(exchange: NIOSSHKeyExchangeAlgorithmProtocol, negotiated: NegotiationResult) /// The keys have been exchanged. case keysExchanged(result: KeyExchangeResult, protection: NIOSSHTransportProtection, negotiated: NegotiationResult) @@ -129,7 +129,8 @@ struct SSHKeyExchangeStateMachine { let exchanger = try self.exchangerForAlgorithm(negotiated.negotiatedKeyExchangeAlgorithm) // Ok, we need to send the key exchange message. - let message = SSHMessage.keyExchangeInit(exchanger.initiateKeyExchangeClientSide(allocator: self.allocator)) + let publicKeyBuffer = exchanger.initiateKeyExchangeClientSide(allocator: self.allocator) + let message = SSHMessage.keyExchangeInit(.init(publicKey: publicKeyBuffer)) self.state = .awaitingKeyExchangeInit(exchange: exchanger, negotiated: negotiated) return SSHMultiMessage(message) case .server: @@ -165,7 +166,8 @@ struct SSHKeyExchangeStateMachine { let result: SSHMultiMessage switch self.role { case .client: - result = SSHMultiMessage(.keyExchange(ourMessage), SSHMessage.keyExchangeInit(exchanger.initiateKeyExchangeClientSide(allocator: self.allocator))) + let publicKeyBuffer = exchanger.initiateKeyExchangeClientSide(allocator: self.allocator) + result = SSHMultiMessage(.keyExchange(ourMessage), SSHMessage.keyExchangeInit(.init(publicKey: publicKeyBuffer))) case .server: result = SSHMultiMessage(.keyExchange(ourMessage)) } @@ -202,7 +204,7 @@ struct SSHKeyExchangeStateMachine { } } - mutating func handle(keyExchangeInit message: SSHMessage.KeyExchangeECDHInitMessage) throws -> SSHMultiMessage? { + mutating func handle(keyExchangeInit message: ByteBuffer) throws -> SSHMultiMessage? { switch self.state { case .awaitingKeyExchangeInitInvalidGuess(exchange: let exchanger, negotiated: let negotiated): // We're going to ignore this one, we already know it's wrong. @@ -222,7 +224,7 @@ struct SSHKeyExchangeStateMachine { allocator: self.allocator, expectedKeySizes: negotiated.negotiatedProtection.keySizes ) - let message = SSHMessage.keyExchangeReply(reply) + let message = SSHMessage.keyExchangeReply(.init(hostKey: reply.hostKey, publicKey: reply.publicKey, signature: reply.signature)) self.state = .keyExchangeInitReceived(result: result, negotiated: negotiated) return SSHMultiMessage(message, .newKeys) } @@ -253,7 +255,9 @@ struct SSHKeyExchangeStateMachine { } let result = try exchanger.receiveServerKeyExchangePayload( - serverKeyExchangeMessage: message, + serverHostKey: message.hostKey, + serverPublicKey: message.publicKey, + serverSignature: message.signature, initialExchangeBytes: &self.initialExchangeBytes, allocator: self.allocator, expectedKeySizes: negotiated.negotiatedProtection.keySizes @@ -447,12 +451,18 @@ struct SSHKeyExchangeStateMachine { } } - private func exchangerForAlgorithm(_ algorithm: Substring) throws -> EllipticCurveKeyExchangeProtocol { + private func exchangerForAlgorithm(_ algorithm: Substring) throws -> NIOSSHKeyExchangeAlgorithmProtocol { for implementation in Self.supportedKeyExchangeImplementations { if implementation.keyExchangeAlgorithmNames.contains(algorithm) { return implementation.init(ourRole: self.role, previousSessionIdentifier: self.previousSessionIdentifier) } } + + for implementation in customKeyExchangeAlgorithms { + if implementation.keyExchangeAlgorithmNames.contains(algorithm) { + return implementation.init(ourRole: self.role, previousSessionIdentifier: self.previousSessionIdentifier) + } + } // Huh, we didn't find it. Weird error. throw NIOSSHError.keyExchangeNegotiationFailure @@ -483,7 +493,7 @@ struct SSHKeyExchangeStateMachine { /// The MAC algorithms supported by this peer, in order of preference. private var supportedMacAlgorithms: [Substring] { - let schemes = self.protectionSchemes.compactMap { $0.macName.map { Substring($0) } } + var schemes = self.protectionSchemes.compactMap { $0.macName.map { Substring($0) } } // We do a weird thing here: if there are no MAC schemes, we lie and put one in. This is // because some schemes (such as AES-GCM in OpenSSH mode) ignore the MAC negotiation. @@ -498,7 +508,7 @@ struct SSHKeyExchangeStateMachine { extension SSHKeyExchangeStateMachine { // For now this is a static list. - static var supportedKeyExchangeImplementations: [EllipticCurveKeyExchangeProtocol.Type] = [ + static var supportedKeyExchangeImplementations: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = [ EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, @@ -507,7 +517,7 @@ extension SSHKeyExchangeStateMachine { static var supportedKeyExchangeAlgorithms: [Substring] { let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } - let customAlgorithms = NIOSSHPublicKey.customPublicKeyAlgorithms.map { Substring($0.publicKeyPrefix) } + let customAlgorithms = customKeyExchangeAlgorithms.reduce([]) { $0 + $1.keyExchangeAlgorithmNames } return bundledAlgorithms + customAlgorithms } diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index 3624eebb..1a7dc210 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -29,6 +29,44 @@ internal extension NIOSSHSignatureProtocol { } } +public enum NIOSSHAlgoritms { + public static func register(keyExchangeAlgorithm type: NIOSSHKeyExchangeAlgorithmProtocol.Type) { + if !customKeyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + customKeyExchangeAlgorithms.append(type) + } + } + + public static func register(transportProtectionScheme type: NIOSSHTransportProtection.Type) { + if !SSHConnectionStateMachine.defaultTransportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + SSHConnectionStateMachine.defaultTransportProtectionSchemes.append(type) + } + } + + /// Registers a custom type tuple for use in Public Key Authentication. + public static func register< + PublicKey: NIOSSHPublicKeyProtocol, + Signature: NIOSSHSignatureProtocol + >( + publicKey type: PublicKey.Type, + signature: Signature.Type + ) { + let utf8format = type.publicKeyPrefix.utf8 + + if !NIOSSHPublicKey.knownAlgorithms.contains(where: { $0.elementsEqual(utf8format) }) { + NIOSSHPublicKey.knownAlgorithms.append(utf8format) + NIOSSHPublicKey.customPublicKeyAlgorithms.append(type) + NIOSSHPublicKey.customSignatures.append(signature) + } + } +} + +//public protocol MACAlgorithmProtocol { +// +//} + +internal var customKeyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() +//internal var macAlgorithms = [MACAlgorithmProtocol.Type]() + public protocol NIOSSHPublicKeyProtocol { /// An identifier that represents the type of public key used in an SSH packet. /// This identifier MUST be unique to the public key implementation. diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 828d5261..9e93c570 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -65,7 +65,7 @@ public struct NIOSSHPublicKey: Hashable { extension NIOSSHPublicKey { /// Verifies that a given `NIOSSHSignature` was created by the holder of the private key associated with this /// public key. - internal func isValidSignature(_ signature: NIOSSHSignature, for digest: DigestBytes) -> Bool { + public func isValidSignature(_ signature: NIOSSHSignature, for digest: DigestBytes) -> Bool { switch (self.backingKey, signature.backingSignature) { case (.ed25519(let key), .ed25519(let sig)): return digest.withUnsafeBytes { digestPtr in @@ -200,23 +200,6 @@ extension NIOSSHPublicKey { internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] internal static var customSignatures: [NIOSSHSignatureProtocol.Type] = [] - - /// Registers a custom type tuple for use in Public Key Authentication. - public static func registerPublicKeyType< - PublicKey: NIOSSHPublicKeyProtocol, - Signature: NIOSSHSignatureProtocol - >( - _ type: PublicKey.Type, - signature: Signature.Type - ) { - let utf8format = type.publicKeyPrefix.utf8 - - if !knownAlgorithms.contains(where: { $0.elementsEqual(utf8format) }) { - knownAlgorithms.append(utf8format) - customPublicKeyAlgorithms.append(type) - customSignatures.append(signature) - } - } } extension NIOSSHPublicKey.BackingKey: Equatable { @@ -274,6 +257,13 @@ extension NIOSSHPublicKey.BackingKey: Hashable { } } +extension NIOSSHPublicKey { + @discardableResult + public func write(to buffer: inout ByteBuffer) -> Int { + return buffer.writeSSHHostKey(self) + } +} + extension ByteBuffer { /// Writes an SSH host key to this `ByteBuffer`. @discardableResult diff --git a/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift b/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift index 991169a6..fd168dfa 100644 --- a/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift +++ b/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift @@ -44,7 +44,7 @@ import NIO /// Implementers of this protocol **must not** expose unauthenticated plaintext, except for the length field. This /// is required by the SSH protocol, and swift-nio-ssh does its best to treat the length field as fundamentally /// untrusted information. -protocol NIOSSHTransportProtection: AnyObject { +public protocol NIOSSHTransportProtection: AnyObject { /// The name of the cipher portion of this transport protection scheme as negotiated on the wire. static var cipherName: String { get } diff --git a/Tests/NIOSSHTests/EndToEndTests.swift b/Tests/NIOSSHTests/EndToEndTests.swift index 8d51ac57..3355e11c 100644 --- a/Tests/NIOSSHTests/EndToEndTests.swift +++ b/Tests/NIOSSHTests/EndToEndTests.swift @@ -63,6 +63,7 @@ struct CustomSignature: NIOSSHSignatureProtocol { struct CustomPublicKey: NIOSSHPublicKeyProtocol { static let publicKeyPrefix = "custom-prefix" + static let keyExchangeAlgorithmNames: [Substring] = ["custom-handshake"] func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D : DataProtocol { let testKeySize = testKey.count From 569c03ed4ecea460e7b827f933e490641e6d7a61 Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Sun, 4 Jul 2021 17:52:17 +0200 Subject: [PATCH 07/20] Implemented passing sequence numbers, and adapter protocols so that other symmetric key algorithms can be used --- .../SSHKeyExchangeStateMachine.swift | 12 ++++++---- .../Keys And Signatures/NIOSSHPublicKey.swift | 23 +++++++++++++++++++ Sources/NIOSSH/SSHPacketParser.swift | 7 +++++- Sources/NIOSSH/SSHPacketSerializer.swift | 10 ++++++-- .../NIOSSH/TransportProtection/AESGCM.swift | 4 ++-- .../SSHTransportProtection.swift | 4 ++-- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 368a6559..eb556b19 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -493,7 +493,7 @@ struct SSHKeyExchangeStateMachine { /// The MAC algorithms supported by this peer, in order of preference. private var supportedMacAlgorithms: [Substring] { - var schemes = self.protectionSchemes.compactMap { $0.macName.map { Substring($0) } } + let schemes = self.protectionSchemes.compactMap { $0.macName.map { Substring($0) } } // We do a weird thing here: if there are no MAC schemes, we lie and put one in. This is // because some schemes (such as AES-GCM in OpenSSH mode) ignore the MAC negotiation. @@ -516,20 +516,22 @@ extension SSHKeyExchangeStateMachine { ] static var supportedKeyExchangeAlgorithms: [Substring] { - let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } +// let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } let customAlgorithms = customKeyExchangeAlgorithms.reduce([]) { $0 + $1.keyExchangeAlgorithmNames } - return bundledAlgorithms + customAlgorithms +// return bundledAlgorithms + customAlgorithms + return customAlgorithms } /// All known host key algorithms. static let bundledServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] static var supportedServerHostKeyAlgorithms: [Substring] { - let bundledAlgorithms = bundledServerHostKeyAlgorithms +// let bundledAlgorithms = bundledServerHostKeyAlgorithms let customAlgorithms = NIOSSHPublicKey.customPublicKeyAlgorithms.map { Substring($0.publicKeyPrefix) } - return bundledAlgorithms + customAlgorithms +// return bundledAlgorithms + customAlgorithms + return customAlgorithms } } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 9e93c570..8be2990a 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -262,9 +262,32 @@ extension NIOSSHPublicKey { public func write(to buffer: inout ByteBuffer) -> Int { return buffer.writeSSHHostKey(self) } + + @discardableResult + public func writeWithoutHeader(to buffer: inout ByteBuffer) -> Int { + return buffer.writeSSHHostKeyWithoutHeader(self) + } } extension ByteBuffer { + @discardableResult + mutating func writeSSHHostKeyWithoutHeader(_ key: NIOSSHPublicKey) -> Int { + switch key.backingKey { + case .ed25519(let key): + return self.writeEd25519PublicKey(baseKey: key) + case .ecdsaP256(let key): + return self.writeECDSAP256PublicKey(baseKey: key) + case .ecdsaP384(let key): + return self.writeECDSAP384PublicKey(baseKey: key) + case .ecdsaP521(let key): + return self.writeECDSAP521PublicKey(baseKey: key) + case .custom(let key): + return key.write(to: &self) + case .certified(let key): + return self.writeCertifiedKey(key) + } + } + /// Writes an SSH host key to this `ByteBuffer`. @discardableResult mutating func writeSSHHostKey(_ key: NIOSSHPublicKey) -> Int { diff --git a/Sources/NIOSSH/SSHPacketParser.swift b/Sources/NIOSSH/SSHPacketParser.swift index 68f37277..e6699dbe 100644 --- a/Sources/NIOSSH/SSHPacketParser.swift +++ b/Sources/NIOSSH/SSHPacketParser.swift @@ -25,6 +25,7 @@ struct SSHPacketParser { private var buffer: ByteBuffer private var state: State + private var sequenceNumber: UInt32 = 0 /// Testing only: the number of bytes we can discard from this buffer. internal var _discardableBytes: Int { @@ -73,6 +74,7 @@ struct SSHPacketParser { if let length = self.buffer.getInteger(at: self.buffer.readerIndex, as: UInt32.self) { if let message = try self.parsePlaintext(length: length) { self.state = .cleartextWaitingForLength + sequenceNumber = sequenceNumber &+ 1 return message } self.state = .cleartextWaitingForBytes(length) @@ -82,6 +84,7 @@ struct SSHPacketParser { case .cleartextWaitingForBytes(let length): if let message = try self.parsePlaintext(length: length) { self.state = .cleartextWaitingForLength + sequenceNumber = sequenceNumber &+ 1 return message } return nil @@ -92,6 +95,7 @@ struct SSHPacketParser { if let message = try self.parseCiphertext(length: length, protection: protection) { self.state = .encryptedWaitingForLength(protection) + sequenceNumber = sequenceNumber &+ 1 return message } self.state = .encryptedWaitingForBytes(length, protection) @@ -99,6 +103,7 @@ struct SSHPacketParser { case .encryptedWaitingForBytes(let length, let protection): if let message = try self.parseCiphertext(length: length, protection: protection) { self.state = .encryptedWaitingForLength(protection) + sequenceNumber = sequenceNumber &+ 1 return message } return nil @@ -160,7 +165,7 @@ struct SSHPacketParser { return nil } - var content = try protection.decryptAndVerifyRemainingPacket(&buffer) + var content = try protection.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: sequenceNumber) guard let message = try content.readSSHMessage(), content.readableBytes == 0, buffer.readableBytes == 0 else { // Throw this error if the content wasn't exactly the right length for the message. throw NIOSSHError.invalidPacketFormat diff --git a/Sources/NIOSSH/SSHPacketSerializer.swift b/Sources/NIOSSH/SSHPacketSerializer.swift index 825408ca..6effddc0 100644 --- a/Sources/NIOSSH/SSHPacketSerializer.swift +++ b/Sources/NIOSSH/SSHPacketSerializer.swift @@ -22,6 +22,7 @@ struct SSHPacketSerializer { } private var state: State = .initialized + private var sequenceNumber: UInt32 = 0 /// Encryption schemes can be added to a packet serializer whenever encryption is negotiated. mutating func addEncryption(_ protection: NIOSSHTransportProtection) { @@ -35,7 +36,10 @@ struct SSHPacketSerializer { } } - mutating func serialize(message: SSHMessage, to buffer: inout ByteBuffer) throws { + mutating func serialize( + message: SSHMessage, + to buffer: inout ByteBuffer + ) throws { switch self.state { case .initialized: switch message { @@ -75,9 +79,11 @@ struct SSHPacketSerializer { buffer.setInteger(UInt8(paddingLength), at: index + 4) /// random padding buffer.writeSSHPaddingBytes(count: paddingLength) + sequenceNumber = sequenceNumber &+ 1 case .encrypted(let protection): let payload = NIOSSHEncryptablePayload(message: message) - try protection.encryptPacket(payload, to: &buffer) + try protection.encryptPacket(payload, to: &buffer, sequenceNumber: sequenceNumber) + sequenceNumber = sequenceNumber &+ 1 } } } diff --git a/Sources/NIOSSH/TransportProtection/AESGCM.swift b/Sources/NIOSSH/TransportProtection/AESGCM.swift index 1ed945ba..ecaf4c5e 100644 --- a/Sources/NIOSSH/TransportProtection/AESGCM.swift +++ b/Sources/NIOSSH/TransportProtection/AESGCM.swift @@ -79,7 +79,7 @@ extension AESGCMTransportProtection: NIOSSHTransportProtection { // unencrypted! } - func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer { + func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer { var plaintext: Data // Establish a nested scope here to avoid the byte buffer views causing an accidental CoW. @@ -117,7 +117,7 @@ extension AESGCMTransportProtection: NIOSSHTransportProtection { return source.readSlice(length: plaintext.count)! } - func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer) throws { + func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer, sequenceNumber: UInt32) throws { // Keep track of where the length is going to be written. let packetLengthIndex = outboundBuffer.writerIndex let packetLengthLength = MemoryLayout.size diff --git a/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift b/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift index fd168dfa..13ce19e0 100644 --- a/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift +++ b/Sources/NIOSSH/TransportProtection/SSHTransportProtection.swift @@ -87,10 +87,10 @@ public protocol NIOSSHTransportProtection: AnyObject { /// length, the padding, or the MAC), and update source to indicate the consumed bytes. /// It must also perform any integrity checking that /// is required and throw if the integrity check fails. - func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer + func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer /// Encrypt an entire outbound packet - func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer) throws + func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer, sequenceNumber: UInt32) throws } extension NIOSSHTransportProtection { From 3837545ba7b26913b8d8c51f791d87bc89590caa Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Sun, 4 Jul 2021 18:35:53 +0200 Subject: [PATCH 08/20] Ignore the authentication banner --- .../SSHConnectionStateMachine.swift | 10 +++++- Sources/NIOSSH/SSHMessages.swift | 35 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift index 0d134607..c9dba20c 100644 --- a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift +++ b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift @@ -306,6 +306,11 @@ struct SSHConnectionStateMachine { let result = try state.receiveUserAuthRequest(message) self.state = .userAuthentication(state) return result + + case .userAuthBanner: + // Ignore the banner for now + self.state = .userAuthentication(state) + return .noMessage case .userAuthSuccess: let result = try state.receiveUserAuthSuccess() @@ -810,7 +815,10 @@ struct SSHConnectionStateMachine { case .userAuthRequest(let message): try state.writeUserAuthRequest(message, into: &buffer) self.state = .userAuthentication(state) - + + case .userAuthBanner: + // Nothing to be done, we don't display the banner (yet) + self.state = .userAuthentication(state) case .userAuthSuccess: try state.writeUserAuthSuccess(into: &buffer) // Ok we're good to go! diff --git a/Sources/NIOSSH/SSHMessages.swift b/Sources/NIOSSH/SSHMessages.swift index 55782eb4..d109bf99 100644 --- a/Sources/NIOSSH/SSHMessages.swift +++ b/Sources/NIOSSH/SSHMessages.swift @@ -36,6 +36,7 @@ enum SSHMessage: Equatable { case userAuthRequest(UserAuthRequestMessage) case userAuthFailure(UserAuthFailureMessage) case userAuthSuccess + case userAuthBanner(UserAuthBannerMessage) case userAuthPKOK(UserAuthPKOKMessage) case globalRequest(GlobalRequestMessage) case requestSuccess(RequestSuccessMessage) @@ -169,6 +170,13 @@ extension SSHMessage { static let id: UInt8 = 52 } + struct UserAuthBannerMessage: Equatable { + static let id: UInt8 = 53 + + let banner: String + let languageTag: String + } + struct UserAuthPKOKMessage: Equatable { // SSH_MSG_USERAUTH_PK_OK static let id: UInt8 = 60 @@ -419,6 +427,11 @@ extension ByteBuffer { return .userAuthFailure(message) case SSHMessage.UserAuthSuccessMessage.id: return .userAuthSuccess + case SSHMessage.UserAuthBannerMessage.id: + guard let message = self.readUserAuthBannerMessage() else { + return nil + } + return .userAuthBanner(message) case SSHMessage.UserAuthPKOKMessage.id: guard let message = try self.readUserAuthPKOKMessage() else { return nil @@ -716,6 +729,22 @@ extension ByteBuffer { return SSHMessage.UserAuthFailureMessage(authentications: authentications, partialSuccess: partialSuccess) } } + + mutating func readUserAuthBannerMessage() -> SSHMessage.UserAuthBannerMessage? { + self.rewindReaderOnNil { `self` in + guard + let banner = self.readSSHStringAsString(), + let languageTag = self.readSSHStringAsString() + else { + return nil + } + + return SSHMessage.UserAuthBannerMessage( + banner: banner, + languageTag: languageTag + ) + } + } mutating func readUserAuthPKOKMessage() throws -> SSHMessage.UserAuthPKOKMessage? { try self.rewindOnNilOrError { `self` in @@ -1133,7 +1162,11 @@ extension ByteBuffer { writtenBytes += self.writeInteger(SSHMessage.UserAuthFailureMessage.id) writtenBytes += self.writeUserAuthFailureMessage(message) case .userAuthSuccess: - writtenBytes += self.writeInteger(52 as UInt8) + writtenBytes += self.writeInteger(SSHMessage.UserAuthSuccessMessage.id) + case .userAuthBanner(let message): + writtenBytes += self.writeInteger(SSHMessage.UserAuthBannerMessage.id) + writtenBytes += self.writeSSHString(message.banner.utf8) + writtenBytes += self.writeSSHString(message.languageTag.utf8) case .userAuthPKOK(let message): writtenBytes += self.writeInteger(SSHMessage.UserAuthPKOKMessage.id) writtenBytes += self.writeUserAuthPKOKMessage(message) From 316c8d6282e43df82595e3d0f5d5bc9f10e9f856 Mon Sep 17 00:00:00 2001 From: Joannis orlandos Date: Wed, 21 Jul 2021 18:02:51 +0200 Subject: [PATCH 09/20] Enable old algorithms --- .../Key Exchange/SSHKeyExchangeStateMachine.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index eb556b19..30456f2e 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -516,22 +516,20 @@ extension SSHKeyExchangeStateMachine { ] static var supportedKeyExchangeAlgorithms: [Substring] { -// let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } + let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } let customAlgorithms = customKeyExchangeAlgorithms.reduce([]) { $0 + $1.keyExchangeAlgorithmNames } -// return bundledAlgorithms + customAlgorithms - return customAlgorithms + return bundledAlgorithms + customAlgorithms } /// All known host key algorithms. static let bundledServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] static var supportedServerHostKeyAlgorithms: [Substring] { -// let bundledAlgorithms = bundledServerHostKeyAlgorithms + let bundledAlgorithms = bundledServerHostKeyAlgorithms let customAlgorithms = NIOSSHPublicKey.customPublicKeyAlgorithms.map { Substring($0.publicKeyPrefix) } -// return bundledAlgorithms + customAlgorithms - return customAlgorithms + return bundledAlgorithms + customAlgorithms } } From d8c64faf1b0b9e86f653c49415e8bc911656097a Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 16 Nov 2021 16:59:51 +0100 Subject: [PATCH 10/20] Remove conflicts with PR #98 --- .../SSHConnectionStateMachine.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift index da710ef4..26b343ea 100644 --- a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift +++ b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift @@ -307,11 +307,6 @@ struct SSHConnectionStateMachine { self.state = .userAuthentication(state) return result - case .userAuthBanner: - // Ignore the banner for now - self.state = .userAuthentication(state) - return .noMessage - case .userAuthSuccess: let result = try state.receiveUserAuthSuccess() // Hey, auth succeeded! @@ -825,9 +820,6 @@ struct SSHConnectionStateMachine { try state.writeUserAuthRequest(message, into: &buffer) self.state = .userAuthentication(state) - case .userAuthBanner: - // Nothing to be done, we don't display the banner (yet) - self.state = .userAuthentication(state) case .userAuthSuccess: try state.writeUserAuthSuccess(into: &buffer) // Ok we're good to go! From a50df125089fbc987319338afb2f7fd4c87c5b56 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 16 Nov 2021 21:54:30 +0100 Subject: [PATCH 11/20] Fixed broken tests after merge. Added tests for all algorithms using frankenstein algorithms that test the correctness of SwiftNIO's agnostic approach to cryptograhic choices --- .../EllipticCurveKeyExchange.swift | 14 +- .../SSHKeyExchangeStateMachine.swift | 8 +- .../Keys And Signatures/CustomKeys.swift | 4 - Sources/NIOSSH/SSHMessages.swift | 18 +- Tests/NIOSSHTests/AESGCMTests.swift | 20 +- Tests/NIOSSHTests/ECKeyExchangeTests.swift | 10 +- Tests/NIOSSHTests/EndToEndTests.swift | 361 +++++++++++++++++- .../SSHKeyExchangeStateMachineTests.swift | 8 +- Tests/NIOSSHTests/Utilities.swift | 7 + 9 files changed, 399 insertions(+), 51 deletions(-) diff --git a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift index bebc0cb4..5563ade3 100644 --- a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift +++ b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift @@ -41,9 +41,7 @@ public protocol NIOSSHKeyExchangeAlgorithmProtocol { ) throws -> (KeyExchangeResult, NIOSSHKeyExchangeServerReply) mutating func receiveServerKeyExchangePayload( - serverHostKey hostKey: NIOSSHPublicKey, - serverPublicKey publicKey: ByteBuffer, - serverSignature signature: NIOSSHSignature, + serverKeyExchangeMessage: NIOSSHKeyExchangeServerReply, initialExchangeBytes: inout ByteBuffer, allocator: ByteBufferAllocator, expectedKeySizes: ExpectedKeySizes @@ -134,9 +132,7 @@ extension EllipticCurveKeyExchange { /// - allocator: A `ByteBufferAllocator` suitable for this connection. /// - expectedKeySizes: The sizes of the keys we need to generate. mutating func receiveServerKeyExchangePayload( - serverHostKey hostKey: NIOSSHPublicKey, - serverPublicKey publicKey: ByteBuffer, - serverSignature signature: NIOSSHSignature, + serverKeyExchangeMessage: NIOSSHKeyExchangeServerReply, initialExchangeBytes: inout ByteBuffer, allocator: ByteBufferAllocator, expectedKeySizes: ExpectedKeySizes @@ -152,14 +148,14 @@ extension EllipticCurveKeyExchange { // // Finally, we return our generated keys to the state machine. - let kexResult = try self.finalizeKeyExchange(theirKeyBytes: publicKey, + let kexResult = try self.finalizeKeyExchange(theirKeyBytes: serverKeyExchangeMessage.publicKey, initialExchangeBytes: &initialExchangeBytes, - serverHostKey: hostKey, + serverHostKey: serverKeyExchangeMessage.hostKey, allocator: allocator, expectedKeySizes: expectedKeySizes) // We can now verify signature over the exchange hash. - guard hostKey.isValidSignature(signature, for: kexResult.exchangeHash) else { + guard serverKeyExchangeMessage.hostKey.isValidSignature(serverKeyExchangeMessage.signature, for: kexResult.exchangeHash) else { throw NIOSSHError.invalidExchangeHashSignature } diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 30456f2e..87e136bb 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -255,9 +255,11 @@ struct SSHKeyExchangeStateMachine { } let result = try exchanger.receiveServerKeyExchangePayload( - serverHostKey: message.hostKey, - serverPublicKey: message.publicKey, - serverSignature: message.signature, + serverKeyExchangeMessage: .init( + hostKey: message.hostKey, + publicKey: message.publicKey, + signature: message.signature + ), initialExchangeBytes: &self.initialExchangeBytes, allocator: self.allocator, expectedKeySizes: negotiated.negotiatedProtection.keySizes diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index 1a7dc210..16dacf6b 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -60,10 +60,6 @@ public enum NIOSSHAlgoritms { } } -//public protocol MACAlgorithmProtocol { -// -//} - internal var customKeyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() //internal var macAlgorithms = [MACAlgorithmProtocol.Type]() diff --git a/Sources/NIOSSH/SSHMessages.swift b/Sources/NIOSSH/SSHMessages.swift index fd102cc6..50e809c3 100644 --- a/Sources/NIOSSH/SSHMessages.swift +++ b/Sources/NIOSSH/SSHMessages.swift @@ -733,22 +733,6 @@ extension ByteBuffer { return SSHMessage.UserAuthFailureMessage(authentications: authentications, partialSuccess: partialSuccess) } } - - mutating func readUserAuthBannerMessage() -> SSHMessage.UserAuthBannerMessage? { - self.rewindReaderOnNil { `self` in - guard - let banner = self.readSSHStringAsString(), - let languageTag = self.readSSHStringAsString() - else { - return nil - } - - return SSHMessage.UserAuthBannerMessage( - banner: banner, - languageTag: languageTag - ) - } - } mutating func readUserAuthBannerMessage() -> SSHMessage.UserAuthBannerMessage? { self.rewindReaderOnNil { `self` in @@ -1178,10 +1162,10 @@ extension ByteBuffer { writtenBytes += self.writeInteger(SSHMessage.UserAuthFailureMessage.id) writtenBytes += self.writeUserAuthFailureMessage(message) case .userAuthSuccess: + writtenBytes += self.writeInteger(SSHMessage.UserAuthSuccessMessage.id) case .userAuthBanner(let message): writtenBytes += self.writeInteger(SSHMessage.UserAuthBannerMessage.id) writtenBytes += self.writeUserAuthBannerMessage(message) - writtenBytes += self.writeInteger(SSHMessage.UserAuthSuccessMessage.id) case .userAuthPKOK(let message): writtenBytes += self.writeInteger(SSHMessage.UserAuthPKOKMessage.id) writtenBytes += self.writeUserAuthPKOKMessage(message) diff --git a/Tests/NIOSSHTests/AESGCMTests.swift b/Tests/NIOSSHTests/AESGCMTests.swift index 5c565147..59ff7448 100644 --- a/Tests/NIOSSHTests/AESGCMTests.swift +++ b/Tests/NIOSSHTests/AESGCMTests.swift @@ -42,7 +42,7 @@ final class AESGCMTests: XCTestCase { let initialKeys = self.generateKeys(keySize: .bits128) let aes128Encryptor = try assertNoThrowWithValue(AES128GCMOpenSSHTransportProtection(initialKeys: initialKeys)) - XCTAssertNoThrow(try aes128Encryptor.encryptPacket(NIOSSHEncryptablePayload(message: .newKeys), to: &self.buffer)) + XCTAssertNoThrow(try aes128Encryptor.encryptPacket(NIOSSHEncryptablePayload(message: .newKeys), to: &self.buffer, sequenceNumber: 0)) // The newKeys message is very straightforward: a single byte. Because of that, we expect that we will need // 14 padding bytes: one byte for the padding length, then 14 more to get out to one block size. Thus, the total @@ -59,7 +59,7 @@ final class AESGCMTests: XCTestCase { XCTAssertEqual(bufferCopy, self.buffer) /// After decryption the plaintext should be a newKeys message. - var plaintext = try assertNoThrowWithValue(aes128Decryptor.decryptAndVerifyRemainingPacket(&bufferCopy)) + var plaintext = try assertNoThrowWithValue(aes128Decryptor.decryptAndVerifyRemainingPacket(&bufferCopy, sequenceNumber: 0)) XCTAssertEqual(bufferCopy.readableBytes, 0) XCTAssertNotEqual(plaintext, self.buffer) XCTAssertEqual(plaintext.readableBytes, 1) @@ -77,7 +77,7 @@ final class AESGCMTests: XCTestCase { let initialKeys = self.generateKeys(keySize: .bits256) let aes256Encryptor = try assertNoThrowWithValue(AES256GCMOpenSSHTransportProtection(initialKeys: initialKeys)) - XCTAssertNoThrow(try aes256Encryptor.encryptPacket(NIOSSHEncryptablePayload(message: .newKeys), to: &self.buffer)) + XCTAssertNoThrow(try aes256Encryptor.encryptPacket(NIOSSHEncryptablePayload(message: .newKeys), to: &self.buffer, sequenceNumber: 0)) // The newKeys message is very straightforward: a single byte. Because of that, we expect that we will need // 14 padding bytes: one byte for the padding length, then 14 more to get out to one block size. Thus, the total @@ -94,7 +94,7 @@ final class AESGCMTests: XCTestCase { XCTAssertEqual(bufferCopy, self.buffer) /// After decryption the plaintext should be a newKeys message. - var plaintext = try assertNoThrowWithValue(aes256Decryptor.decryptAndVerifyRemainingPacket(&bufferCopy)) + var plaintext = try assertNoThrowWithValue(aes256Decryptor.decryptAndVerifyRemainingPacket(&bufferCopy, sequenceNumber: 0)) XCTAssertEqual(bufferCopy.readableBytes, 0) XCTAssertNotEqual(plaintext, self.buffer) XCTAssertEqual(plaintext.readableBytes, 1) @@ -300,7 +300,7 @@ final class AESGCMTests: XCTestCase { buffer.clear() buffer.writeRepeatingByte(42, count: ciphertextSize) - XCTAssertThrowsError(try aes128.decryptAndVerifyRemainingPacket(&buffer)) { error in + XCTAssertThrowsError(try aes128.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 0)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .invalidEncryptedPacketLength) } } @@ -320,7 +320,7 @@ final class AESGCMTests: XCTestCase { buffer.clear() buffer.writeRepeatingByte(42, count: ciphertextSize) - XCTAssertThrowsError(try aes256.decryptAndVerifyRemainingPacket(&buffer)) { error in + XCTAssertThrowsError(try aes256.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 0)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .invalidEncryptedPacketLength) } } @@ -350,7 +350,7 @@ final class AESGCMTests: XCTestCase { // We can now attempt to decrypt this packet. let aes128 = try assertNoThrowWithValue(AES128GCMOpenSSHTransportProtection(initialKeys: keys)) - XCTAssertThrowsError(try aes128.decryptAndVerifyRemainingPacket(&buffer)) { error in + XCTAssertThrowsError(try aes128.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 0)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .excessPadding) } } @@ -379,7 +379,7 @@ final class AESGCMTests: XCTestCase { // We can now attempt to decrypt this packet. let aes256 = try assertNoThrowWithValue(AES256GCMOpenSSHTransportProtection(initialKeys: keys)) - XCTAssertThrowsError(try aes256.decryptAndVerifyRemainingPacket(&buffer)) { error in + XCTAssertThrowsError(try aes256.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 0)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .excessPadding) } } @@ -408,7 +408,7 @@ final class AESGCMTests: XCTestCase { // We can now attempt to decrypt this packet. let aes128 = try assertNoThrowWithValue(AES128GCMOpenSSHTransportProtection(initialKeys: keys)) - XCTAssertThrowsError(try aes128.decryptAndVerifyRemainingPacket(&buffer)) { error in + XCTAssertThrowsError(try aes128.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 0)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .insufficientPadding) } } @@ -437,7 +437,7 @@ final class AESGCMTests: XCTestCase { // We can now attempt to decrypt this packet. let aes256 = try assertNoThrowWithValue(AES256GCMOpenSSHTransportProtection(initialKeys: keys)) - XCTAssertThrowsError(try aes256.decryptAndVerifyRemainingPacket(&buffer)) { error in + XCTAssertThrowsError(try aes256.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 0)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .insufficientPadding) } } diff --git a/Tests/NIOSSHTests/ECKeyExchangeTests.swift b/Tests/NIOSSHTests/ECKeyExchangeTests.swift index 3c29b941..4428169a 100644 --- a/Tests/NIOSSHTests/ECKeyExchangeTests.swift +++ b/Tests/NIOSSHTests/ECKeyExchangeTests.swift @@ -279,7 +279,8 @@ final class KeyExchangeTests: XCTestCase { } func testWeValidateTheExchangeHash() throws { - var server = EllipticCurveKeyExchange(ourRole: .server([.init(ed25519Key: .init())]), previousSessionIdentifier: nil) + let serverPrivateKey = NIOSSHPrivateKey(ed25519Key: .init()) + var server = EllipticCurveKeyExchange(ourRole: .server([serverPrivateKey]), previousSessionIdentifier: nil) var client = EllipticCurveKeyExchange(ourRole: .client, previousSessionIdentifier: nil) let serverHostKey = NIOSSHPrivateKey(ed25519Key: .init()) @@ -297,7 +298,12 @@ final class KeyExchangeTests: XCTestCase { initialExchangeBytes.clear() // Ok, the server has sent a signature over the exchange hash. Let's change that signature. - serverResponse.signature = try assertNoThrowWithValue(serverHostKey.sign(digest: SHA256.hash(data: [1, 2, 3, 4, 5]))) + let badServerSignature = try serverHostKey.sign(digest: SHA256.hash(data: [1, 2, 3, 4, 5])) + serverResponse = NIOSSHKeyExchangeServerReply( + hostKey: serverResponse.hostKey, + publicKey: serverResponse.publicKey, + signature: badServerSignature + ) XCTAssertThrowsError( try client.receiveServerKeyExchangePayload(serverKeyExchangeMessage: serverResponse, diff --git a/Tests/NIOSSHTests/EndToEndTests.swift b/Tests/NIOSSHTests/EndToEndTests.swift index e9f5beca..4bc50eb7 100644 --- a/Tests/NIOSSHTests/EndToEndTests.swift +++ b/Tests/NIOSSHTests/EndToEndTests.swift @@ -23,6 +23,131 @@ enum EndToEndTestError: Error { fileprivate let testKey = Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]) +final class CustomTransportProtection: NIOSSHTransportProtection { + static let cipherName = "xor-with-42" + static let macName: String? = "insecure-sha1" + static var wasUsed = false + + static var keySizes: ExpectedKeySizes { + .init(ivSize: 19, encryptionKeySize: 17, macKeySize: 15) + } + + required init(initialKeys: NIOSSHSessionKeys) throws {} + + static var cipherBlockSize: Int { 18 } + var macBytes: Int { 20 } + + func updateKeys(_ newKeys: NIOSSHSessionKeys) throws {} + + func decryptFirstBlock(_: inout ByteBuffer) throws { + // For us, decrypting the first block is very easy: do nothing. The length bytes are already + // unencrypted! + } + + func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer { + Self.wasUsed = true + var plaintext: Data + + // The first 4 bytes are the length. The last 16 are the tag. Everything else is ciphertext. We expect + // that the ciphertext is a clean multiple of the block size, and to be non-zero. + guard + let lengthView: UInt32 = source.readInteger(), + var ciphertext = source.readData(length: Int(lengthView)), + let mac = source.readData(length: Insecure.SHA1.byteCount), + ciphertext.count > 0, ciphertext.count % Self.cipherBlockSize == 0 else { + // The only way this fails is if the payload doesn't match this encryption scheme. + throw NIOSSHError.invalidEncryptedPacketLength + } + + for i in 0..= plaintext.count { + throw NIOSSHError.invalidDecryptedPlaintextLength + } + + // All good! A quick soundness check to verify that the length of the plaintext is ok. + guard plaintext.count % Self.cipherBlockSize == 0, plaintext.count == ciphertext.count else { + throw NIOSSHError.invalidDecryptedPlaintextLength + } + + // Remove padding + plaintext.removeFirst() + plaintext.removeLast(Int(paddingBytes)) + + return ByteBuffer(data: plaintext) + } + + func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer, sequenceNumber: UInt32) throws { + // Keep track of where the length is going to be written. + let packetLengthIndex = outboundBuffer.writerIndex + let packetLengthLength = MemoryLayout.size + let packetPaddingIndex = outboundBuffer.writerIndex + packetLengthLength + let packetPaddingLength = MemoryLayout.size + + outboundBuffer.moveWriterIndex(forwardBy: packetLengthLength + packetPaddingLength) + + // First, we write the packet. + let payloadBytes = outboundBuffer.writeEncryptablePayload(packet) + + // Ok, now we need to pad. The rules for padding for AES GCM are: + // + // 1. We must pad out such that the total encrypted content (padding length byte, + // plus content bytes, plus padding bytes) is a multiple of the block size. + // 2. At least 4 bytes of padding MUST be added. + // 3. This padding SHOULD be random. + // + // Note that, unlike other protection modes, the length is not encrypted, and so we + // must exclude it from the padding calculation. + // + // So we check how many bytes we've already written, use modular arithmetic to work out + // how many more bytes we need, and then if that's fewer than 4 we add a block size to it + // to fill it out. + var encryptedBufferSize = payloadBytes + packetPaddingLength + var necessaryPaddingBytes = Self.cipherBlockSize - (encryptedBufferSize % Self.cipherBlockSize) + if necessaryPaddingBytes < 4 { + necessaryPaddingBytes += Self.cipherBlockSize + } + + // We now want to write that many padding bytes to the end of the buffer. These are supposed to be + // random bytes. We're going to get those from the system random number generator. + encryptedBufferSize += outboundBuffer.writeSSHPaddingBytes(count: necessaryPaddingBytes) + precondition(encryptedBufferSize % Self.cipherBlockSize == 0, "Incorrectly counted buffer size; got \(encryptedBufferSize)") + + // We now know the length: it's going to be "encrypted buffer size". The length does not include the tag, so don't add it. + // Let's write that in. We also need to write the number of padding bytes in. + outboundBuffer.setInteger(UInt32(encryptedBufferSize), at: packetLengthIndex) + outboundBuffer.setInteger(UInt8(necessaryPaddingBytes), at: packetPaddingIndex) + + // Ok, nice! Now we need to encrypt the data. We pass the length field as additional authenticated data, and the encrypted + // payload portion as the data to encrypt. We know these views will be valid, so we forcibly unwrap them: if they're invalid, + // our math was wrong and we cannot recover. + let plaintext = outboundBuffer.getBytes(at: packetPaddingIndex, length: encryptedBufferSize)! + let hash = Insecure.SHA1.hash(data: plaintext) + + var ciphertext = plaintext + for i in 0.. [UInt8] { + return Array(testKey.reversed()) + } + func signature(for data: D) throws -> NIOSSHSignatureProtocol where D : DataProtocol { var data = Data(data) @@ -64,6 +193,7 @@ struct CustomSignature: NIOSSHSignatureProtocol { struct CustomPublicKey: NIOSSHPublicKeyProtocol { static let publicKeyPrefix = "custom-prefix" static let keyExchangeAlgorithmNames: [Substring] = ["custom-handshake"] + static var wasUsed = false func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D : DataProtocol { let testKeySize = testKey.count @@ -75,6 +205,7 @@ struct CustomPublicKey: NIOSSHPublicKeyProtocol { return data == signature.rawRepresentation } + @discardableResult func write(to buffer: inout ByteBuffer) -> Int { return 0 } @@ -88,10 +219,144 @@ struct CustomPublicKey: NIOSSHPublicKeyProtocol { throw EndToEndTestError.invalidCustomPublicKey } + wasUsed = true return CustomPublicKey() } } +struct CustomKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { + static var keyExchangeInitMessageId: UInt8 { 0xff } + static var keyExchangeReplyMessageId: UInt8 { 0xff } + static var wasUsed = false + + private var previousSessionIdentifier: ByteBuffer? + private var ourKey: CustomPrivateKey + private var theirKey: CustomPublicKey? + private var ourRole: SSHConnectionRole + private var sharedSecret: [UInt8]? + + init(ourRole: SSHConnectionRole, previousSessionIdentifier: ByteBuffer?) { + self.ourRole = ourRole + self.ourKey = CustomPrivateKey() + self.previousSessionIdentifier = previousSessionIdentifier + } + + func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> ByteBuffer { + var buffer = ByteBuffer() + _ = ourKey.publicKey.write(to: &buffer) + return buffer + } + + mutating func completeKeyExchangeServerSide( + clientKeyExchangeMessage message: ByteBuffer, + serverHostKey: NIOSSHPrivateKey, + initialExchangeBytes: inout ByteBuffer, + allocator: ByteBufferAllocator, + expectedKeySizes: ExpectedKeySizes + ) throws -> (KeyExchangeResult, NIOSSHKeyExchangeServerReply) { + var theirKeyBuffer = message + let theirKey = try CustomPublicKey.read(from: &theirKeyBuffer) + self.theirKey = theirKey + + // Shared secet is "expanded" + // That should make it usable by most transport encryption, at least the one used in our test + var sharedSecret = try self.ourKey.generatedSharedSecret(with: theirKey) + sharedSecret += sharedSecret + sharedSecret += sharedSecret + sharedSecret += sharedSecret + sharedSecret += sharedSecret + + self.sharedSecret = sharedSecret + + var hasher = SHA512() + hasher.update(data: initialExchangeBytes.readableBytesView) + hasher.update(data: sharedSecret) + + let exchangeHash = hasher.finalize() + + let sessionID: ByteBuffer + if let previousSessionIdentifier = self.previousSessionIdentifier { + sessionID = previousSessionIdentifier + } else { + sessionID = ByteBuffer(bytes: SHA512.hash(data: Data(exchangeHash))) + } + + let kexResult = KeyExchangeResult( + sessionID: sessionID, + keys: NIOSSHSessionKeys( + initialInboundIV: Array(sharedSecret[0.. KeyExchangeResult { + var theirKeyBuffer = serverKeyExchangeMessage.publicKey + let theirKey = try CustomPublicKey.read(from: &theirKeyBuffer) + self.theirKey = theirKey + + // Shared secet is "expanded" + // That should make it usable by most transport encryption, at least the one used in our test + var sharedSecret = try self.ourKey.generatedSharedSecret(with: theirKey) + sharedSecret += sharedSecret + sharedSecret += sharedSecret + sharedSecret += sharedSecret + sharedSecret += sharedSecret + self.sharedSecret = sharedSecret + + var hasher = SHA512() + hasher.update(data: initialExchangeBytes.readableBytesView) + hasher.update(data: sharedSecret) + let exchangeHash = hasher.finalize() + + let sessionID: ByteBuffer + if let previousSessionIdentifier = self.previousSessionIdentifier { + sessionID = previousSessionIdentifier + } else { + sessionID = ByteBuffer(bytes: SHA512.hash(data: Data(exchangeHash))) + } + + guard serverKeyExchangeMessage.hostKey.isValidSignature(serverKeyExchangeMessage.signature, for: exchangeHash) else { + throw NIOSSHError.invalidExchangeHashSignature + } + + Self.wasUsed = true + return KeyExchangeResult( + sessionID: sessionID, + keys: NIOSSHSessionKeys( + initialInboundIV: Array(sharedSecret[0..(_ body: @autoclosure () throws -> T, defaultValue: T? = nil, message: String? = nil, file: StaticString = #file, line: UInt = #line) throws -> T { @@ -27,3 +28,9 @@ func assertNoThrowWithValue(_ body: @autoclosure () throws -> T, defaultValue } } } + +extension SSHKeyExchangeStateMachine { + mutating func handle(keyExchangeInit message: SSHMessage.KeyExchangeECDHInitMessage) throws -> SSHMultiMessage? { + return try handle(keyExchangeInit: message.publicKey) + } +} From f4efdbc47108e76483c4a88e9a3dc4aea470762b Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Thu, 25 Nov 2021 17:12:50 +0100 Subject: [PATCH 12/20] Define transport protection & key exchange types on the client/server configuration objects --- .../SSHConnectionStateMachine.swift | 6 +- .../States/ActiveState.swift | 5 -- .../States/IdleState.swift | 7 +- .../States/KeyExchangeState.swift | 5 +- .../ReceivedKexInitWhenActiveState.swift | 5 +- .../States/ReceivedNewKeysState.swift | 3 - .../States/RekeyingReceivedNewKeysState.swift | 3 - .../States/RekeyingSentNewKeysState.swift | 3 - .../States/RekeyingState.swift | 4 - .../States/SentKexInitWhenActiveState.swift | 5 +- .../States/SentNewKeysState.swift | 3 - .../States/SentVersionState.swift | 3 - .../States/UserAuthenticationState.swift | 4 - .../Key Exchange/SSHKeyExchangeResult.swift | 22 ++--- .../SSHKeyExchangeStateMachine.swift | 41 ++++----- .../Keys And Signatures/CustomKeys.swift | 36 +------- .../NIOSSHCertifiedPublicKey.swift | 1 - .../Keys And Signatures/NIOSSHPublicKey.swift | 87 ++++++++++++++++++- Sources/NIOSSH/Role.swift | 22 +++++ Sources/NIOSSH/SSHClientConfiguration.swift | 6 ++ Sources/NIOSSH/SSHServerConfiguration.swift | 8 +- Tests/NIOSSHTests/EndToEndTests.swift | 51 ++++++----- .../SSHKeyExchangeStateMachineTests.swift | 54 ++++++------ 23 files changed, 213 insertions(+), 171 deletions(-) diff --git a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift index 26b343ea..2832bbaa 100644 --- a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift +++ b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift @@ -60,12 +60,12 @@ struct SSHConnectionStateMachine { /// The state of this state machine. private var state: State - internal static var defaultTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ + public static let bundledTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ AES256GCMOpenSSHTransportProtection.self, AES128GCMOpenSSHTransportProtection.self, ] - init(role: SSHConnectionRole, protectionSchemes: [NIOSSHTransportProtection.Type] = Self.defaultTransportProtectionSchemes) { - self.state = .idle(IdleState(role: role, protectionSchemes: protectionSchemes)) + init(role: SSHConnectionRole) { + self.state = .idle(IdleState(role: role)) } func start() -> SSHMultiMessage? { diff --git a/Sources/NIOSSH/Connection State Machine/States/ActiveState.swift b/Sources/NIOSSH/Connection State Machine/States/ActiveState.swift index c243fac1..6d4bd1e7 100644 --- a/Sources/NIOSSH/Connection State Machine/States/ActiveState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/ActiveState.swift @@ -27,8 +27,6 @@ extension SSHConnectionStateMachine { internal var remoteVersion: String - internal var protectionSchemes: [NIOSSHTransportProtection.Type] - internal var sessionIdentifier: ByteBuffer init(_ previous: UserAuthenticationState) { @@ -36,7 +34,6 @@ extension SSHConnectionStateMachine { self.serializer = previous.serializer self.parser = previous.parser self.remoteVersion = previous.remoteVersion - self.protectionSchemes = previous.protectionSchemes self.sessionIdentifier = previous.sessionIdentifier } @@ -45,7 +42,6 @@ extension SSHConnectionStateMachine { self.serializer = previous.serializer self.parser = previous.parser self.remoteVersion = previous.remoteVersion - self.protectionSchemes = previous.protectionSchemes self.sessionIdentifier = previous.sessionIdentifier } @@ -54,7 +50,6 @@ extension SSHConnectionStateMachine { self.serializer = previous.serializer self.parser = previous.parser self.remoteVersion = previous.remoteVersion - self.protectionSchemes = previous.protectionSchemes self.sessionIdentifier = previous.sessionIdentifier } } diff --git a/Sources/NIOSSH/Connection State Machine/States/IdleState.swift b/Sources/NIOSSH/Connection State Machine/States/IdleState.swift index 0ea83d30..01a91f7b 100644 --- a/Sources/NIOSSH/Connection State Machine/States/IdleState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/IdleState.swift @@ -22,11 +22,14 @@ extension SSHConnectionStateMachine { internal var serializer: SSHPacketSerializer internal var protectionSchemes: [NIOSSHTransportProtection.Type] + + internal var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] - init(role: SSHConnectionRole, protectionSchemes: [NIOSSHTransportProtection.Type]) { + init(role: SSHConnectionRole) { self.role = role self.serializer = SSHPacketSerializer() - self.protectionSchemes = protectionSchemes + self.protectionSchemes = role.transportProtectionSchemes + self.keyExchangeAlgorithms = role.keyExchangeAlgorithms } } } diff --git a/Sources/NIOSSH/Connection State Machine/States/KeyExchangeState.swift b/Sources/NIOSSH/Connection State Machine/States/KeyExchangeState.swift index 983326a0..ebdc8e32 100644 --- a/Sources/NIOSSH/Connection State Machine/States/KeyExchangeState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/KeyExchangeState.swift @@ -28,8 +28,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - /// The backing state machine. var keyExchangeStateMachine: SSHKeyExchangeStateMachine @@ -38,8 +36,7 @@ extension SSHConnectionStateMachine { self.parser = state.parser self.serializer = state.serializer self.remoteVersion = remoteVersion - self.protectionSchemes = state.protectionSchemes - self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: state.role, remoteVersion: remoteVersion, protectionSchemes: state.protectionSchemes, previousSessionIdentifier: nil) + self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: state.role, remoteVersion: remoteVersion, keyExchangeAlgorithms: state.role.keyExchangeAlgorithms, transportProtectionSchemes: state.role.transportProtectionSchemes, previousSessionIdentifier: nil) } } } diff --git a/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift b/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift index 751b0786..ce09c147 100644 --- a/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift @@ -28,8 +28,6 @@ extension SSHConnectionStateMachine { internal var remoteVersion: String - internal var protectionSchemes: [NIOSSHTransportProtection.Type] - internal var keyExchangeStateMachine: SSHKeyExchangeStateMachine internal var sessionIdentifier: ByteBuffer @@ -39,9 +37,8 @@ extension SSHConnectionStateMachine { self.serializer = previous.serializer self.parser = previous.parser self.remoteVersion = previous.remoteVersion - self.protectionSchemes = previous.protectionSchemes self.sessionIdentifier = previous.sessionIdentifier - self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: previous.role, remoteVersion: previous.remoteVersion, protectionSchemes: previous.protectionSchemes, previousSessionIdentifier: self.sessionIdentifier) + self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: previous.role, remoteVersion: previous.remoteVersion, keyExchangeAlgorithms: role.keyExchangeAlgorithms, transportProtectionSchemes: role.transportProtectionSchemes, previousSessionIdentifier: self.sessionIdentifier) } } } diff --git a/Sources/NIOSSH/Connection State Machine/States/ReceivedNewKeysState.swift b/Sources/NIOSSH/Connection State Machine/States/ReceivedNewKeysState.swift index e7ac34a0..607fc3ed 100644 --- a/Sources/NIOSSH/Connection State Machine/States/ReceivedNewKeysState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/ReceivedNewKeysState.swift @@ -29,8 +29,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - var sessionIdentifier: ByteBuffer /// The backing state machine. @@ -45,7 +43,6 @@ extension SSHConnectionStateMachine { self.parser = state.parser self.serializer = state.serializer self.remoteVersion = state.remoteVersion - self.protectionSchemes = state.protectionSchemes self.keyExchangeStateMachine = state.keyExchangeStateMachine // We force unwrap the session ID because it's programmer error to not have it at this time. diff --git a/Sources/NIOSSH/Connection State Machine/States/RekeyingReceivedNewKeysState.swift b/Sources/NIOSSH/Connection State Machine/States/RekeyingReceivedNewKeysState.swift index 37a9c5be..ac90ee44 100644 --- a/Sources/NIOSSH/Connection State Machine/States/RekeyingReceivedNewKeysState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/RekeyingReceivedNewKeysState.swift @@ -29,8 +29,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - var sessionIdentifier: ByteBuffer /// The backing state machine. @@ -41,7 +39,6 @@ extension SSHConnectionStateMachine { self.parser = previousState.parser self.serializer = previousState.serializer self.remoteVersion = previousState.remoteVersion - self.protectionSchemes = previousState.protectionSchemes self.sessionIdentifier = previousState.sessionIdentifier self.keyExchangeStateMachine = previousState.keyExchangeStateMachine } diff --git a/Sources/NIOSSH/Connection State Machine/States/RekeyingSentNewKeysState.swift b/Sources/NIOSSH/Connection State Machine/States/RekeyingSentNewKeysState.swift index 9e6af700..42cc92e0 100644 --- a/Sources/NIOSSH/Connection State Machine/States/RekeyingSentNewKeysState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/RekeyingSentNewKeysState.swift @@ -29,8 +29,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - var sessionIdentifier: ByteBuffer /// The backing state machine. @@ -41,7 +39,6 @@ extension SSHConnectionStateMachine { self.parser = previousState.parser self.serializer = previousState.serializer self.remoteVersion = previousState.remoteVersion - self.protectionSchemes = previousState.protectionSchemes self.sessionIdentifier = previousState.sessionIdentifier self.keyExchangeStateMachine = previousState.keyExchangeStateMachine } diff --git a/Sources/NIOSSH/Connection State Machine/States/RekeyingState.swift b/Sources/NIOSSH/Connection State Machine/States/RekeyingState.swift index 000e4158..84af03f8 100644 --- a/Sources/NIOSSH/Connection State Machine/States/RekeyingState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/RekeyingState.swift @@ -28,8 +28,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - var sessionIdentifier: ByteBuffer /// The backing state machine. @@ -40,7 +38,6 @@ extension SSHConnectionStateMachine { self.parser = previousState.parser self.serializer = previousState.serializer self.remoteVersion = previousState.remoteVersion - self.protectionSchemes = previousState.protectionSchemes self.sessionIdentifier = previousState.sessionIdentifier self.keyExchangeStateMachine = previousState.keyExchangeStateMachine } @@ -50,7 +47,6 @@ extension SSHConnectionStateMachine { self.parser = previousState.parser self.serializer = previousState.serializer self.remoteVersion = previousState.remoteVersion - self.protectionSchemes = previousState.protectionSchemes self.sessionIdentifier = previousState.sessionIdentitifier self.keyExchangeStateMachine = previousState.keyExchangeStateMachine } diff --git a/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift b/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift index df896976..1be69c52 100644 --- a/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift @@ -28,8 +28,6 @@ extension SSHConnectionStateMachine { internal var remoteVersion: String - internal var protectionSchemes: [NIOSSHTransportProtection.Type] - internal var sessionIdentitifier: ByteBuffer internal var keyExchangeStateMachine: SSHKeyExchangeStateMachine @@ -39,9 +37,8 @@ extension SSHConnectionStateMachine { self.serializer = previous.serializer self.parser = previous.parser self.remoteVersion = previous.remoteVersion - self.protectionSchemes = previous.protectionSchemes self.sessionIdentitifier = previous.sessionIdentifier - self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: self.role, remoteVersion: self.remoteVersion, protectionSchemes: self.protectionSchemes, previousSessionIdentifier: previous.sessionIdentifier) + self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: self.role, remoteVersion: self.remoteVersion, keyExchangeAlgorithms: role.keyExchangeAlgorithms, transportProtectionSchemes: role.transportProtectionSchemes, previousSessionIdentifier: previous.sessionIdentifier) } } } diff --git a/Sources/NIOSSH/Connection State Machine/States/SentNewKeysState.swift b/Sources/NIOSSH/Connection State Machine/States/SentNewKeysState.swift index 5452fb57..b2e7c119 100644 --- a/Sources/NIOSSH/Connection State Machine/States/SentNewKeysState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/SentNewKeysState.swift @@ -29,8 +29,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - var sessionIdentifier: ByteBuffer /// The backing state machine. @@ -46,7 +44,6 @@ extension SSHConnectionStateMachine { self.serializer = state.serializer self.keyExchangeStateMachine = state.keyExchangeStateMachine self.remoteVersion = state.remoteVersion - self.protectionSchemes = state.protectionSchemes // We force unwrap the session ID here because it's programmer error to not have it at this stage. self.sessionIdentifier = self.keyExchangeStateMachine.sessionID! diff --git a/Sources/NIOSSH/Connection State Machine/States/SentVersionState.swift b/Sources/NIOSSH/Connection State Machine/States/SentVersionState.swift index a7a83fba..8a170e9b 100644 --- a/Sources/NIOSSH/Connection State Machine/States/SentVersionState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/SentVersionState.swift @@ -26,14 +26,11 @@ extension SSHConnectionStateMachine { /// The packet serializer used by this state machine. var serializer: SSHPacketSerializer - var protectionSchemes: [NIOSSHTransportProtection.Type] - private let allocator: ByteBufferAllocator init(idleState state: IdleState, allocator: ByteBufferAllocator) { self.role = state.role self.serializer = state.serializer - self.protectionSchemes = state.protectionSchemes self.parser = SSHPacketParser(allocator: allocator) self.allocator = allocator diff --git a/Sources/NIOSSH/Connection State Machine/States/UserAuthenticationState.swift b/Sources/NIOSSH/Connection State Machine/States/UserAuthenticationState.swift index 7b6d2b7e..a2689c60 100644 --- a/Sources/NIOSSH/Connection State Machine/States/UserAuthenticationState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/UserAuthenticationState.swift @@ -28,8 +28,6 @@ extension SSHConnectionStateMachine { var remoteVersion: String - var protectionSchemes: [NIOSSHTransportProtection.Type] - var sessionIdentifier: ByteBuffer /// The backing state machine. @@ -41,7 +39,6 @@ extension SSHConnectionStateMachine { self.serializer = state.serializer self.userAuthStateMachine = state.userAuthStateMachine self.remoteVersion = state.remoteVersion - self.protectionSchemes = state.protectionSchemes self.sessionIdentifier = state.sessionIdentifier } @@ -51,7 +48,6 @@ extension SSHConnectionStateMachine { self.serializer = state.serializer self.userAuthStateMachine = state.userAuthStateMachine self.remoteVersion = state.remoteVersion - self.protectionSchemes = state.protectionSchemes self.sessionIdentifier = state.sessionIdentifier } } diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift index b96cc114..3e9f032d 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift @@ -21,9 +21,9 @@ import NIO /// This exchange hash is used for a number of purposes. public struct KeyExchangeResult { /// The session ID to use for this connection. Will be static across the lifetime of a connection. - var sessionID: ByteBuffer + public var sessionID: ByteBuffer - var keys: NIOSSHSessionKeys + public var keys: NIOSSHSessionKeys public init(sessionID: ByteBuffer, keys: NIOSSHSessionKeys) { self.sessionID = sessionID @@ -52,17 +52,17 @@ extension KeyExchangeResult: Equatable {} /// we store them in the `SymmetricKey` types. The IVs do not need to be secret, and so are /// stored in regular heap buffers. public struct NIOSSHSessionKeys { - public internal(set) var initialInboundIV: [UInt8] + public var initialInboundIV: [UInt8] - public internal(set) var initialOutboundIV: [UInt8] + public var initialOutboundIV: [UInt8] - public internal(set) var inboundEncryptionKey: SymmetricKey + public var inboundEncryptionKey: SymmetricKey - public internal(set) var outboundEncryptionKey: SymmetricKey + public var outboundEncryptionKey: SymmetricKey - public internal(set) var inboundMACKey: SymmetricKey + public var inboundMACKey: SymmetricKey - public internal(set) var outboundMACKey: SymmetricKey + public var outboundMACKey: SymmetricKey public init(initialInboundIV: [UInt8], initialOutboundIV: [UInt8], inboundEncryptionKey: SymmetricKey, outboundEncryptionKey: SymmetricKey, inboundMACKey: SymmetricKey, outboundMACKey: SymmetricKey) { self.initialInboundIV = initialInboundIV @@ -83,11 +83,11 @@ extension NIOSSHSessionKeys: Equatable {} /// length as needed, which means we need to ensure the code doing the calculation knows how /// to truncate appropriately. public struct ExpectedKeySizes { - public internal(set) var ivSize: Int + public var ivSize: Int - public internal(set) var encryptionKeySize: Int + public var encryptionKeySize: Int - public internal(set) var macKeySize: Int + public var macKeySize: Int public init(ivSize: Int, encryptionKeySize: Int, macKeySize: Int) { self.ivSize = ivSize diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 87e136bb..7dcf80d6 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -71,16 +71,18 @@ struct SSHKeyExchangeStateMachine { private let role: SSHConnectionRole private var state: State private var initialExchangeBytes: ByteBuffer - private var protectionSchemes: [NIOSSHTransportProtection.Type] + private var transportProtectionSchemes: [NIOSSHTransportProtection.Type] + private var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] private var previousSessionIdentifier: ByteBuffer? - init(allocator: ByteBufferAllocator, loop: EventLoop, role: SSHConnectionRole, remoteVersion: String, protectionSchemes: [NIOSSHTransportProtection.Type], previousSessionIdentifier: ByteBuffer?) { + init(allocator: ByteBufferAllocator, loop: EventLoop, role: SSHConnectionRole, remoteVersion: String, keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [NIOSSHTransportProtection.Type] = SSHConnectionStateMachine.bundledTransportProtectionSchemes, previousSessionIdentifier: ByteBuffer?) { self.allocator = allocator self.loop = loop self.role = role self.initialExchangeBytes = allocator.buffer(capacity: 1024) self.state = .idle - self.protectionSchemes = protectionSchemes + self.keyExchangeAlgorithms = keyExchangeAlgorithms + self.transportProtectionSchemes = transportProtectionSchemes self.previousSessionIdentifier = previousSessionIdentifier switch self.role { @@ -103,7 +105,7 @@ struct SSHKeyExchangeStateMachine { return .init( cookie: rng.randomCookie(allocator: self.allocator), - keyExchangeAlgorithms: Self.supportedKeyExchangeAlgorithms, + keyExchangeAlgorithms: role.keyExchangeAlgorithmNames, serverHostKeyAlgorithms: self.supportedHostKeyAlgorithms, encryptionAlgorithmsClientToServer: encryptionAlgorithms, encryptionAlgorithmsServerToClient: encryptionAlgorithms, @@ -334,7 +336,7 @@ struct SSHKeyExchangeStateMachine { } // Ok, now we need to find the right transport protection scheme. This can technically fail. - guard let scheme = self.protectionSchemes.first(where: { $0.cipherName == clientEncryption && ($0.macName == nil || $0.macName! == clientMAC) }) else { + guard let scheme = self.transportProtectionSchemes.first(where: { $0.cipherName == clientEncryption && ($0.macName == nil || $0.macName! == clientMAC) }) else { throw NIOSSHError.keyExchangeNegotiationFailure } @@ -375,13 +377,13 @@ struct SSHKeyExchangeStateMachine { switch self.role { case .client: - clientAlgorithms = Self.supportedKeyExchangeAlgorithms + clientAlgorithms = role.keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } serverAlgorithms = peerKeyExchangeAlgorithms clientHostKeyAlgorithms = self.supportedHostKeyAlgorithms serverHostKeyAlgorithms = peerHostKeyAlgorithms case .server: clientAlgorithms = peerKeyExchangeAlgorithms - serverAlgorithms = Self.supportedKeyExchangeAlgorithms + serverAlgorithms = role.keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } clientHostKeyAlgorithms = peerHostKeyAlgorithms serverHostKeyAlgorithms = self.supportedHostKeyAlgorithms } @@ -454,26 +456,20 @@ struct SSHKeyExchangeStateMachine { } private func exchangerForAlgorithm(_ algorithm: Substring) throws -> NIOSSHKeyExchangeAlgorithmProtocol { - for implementation in Self.supportedKeyExchangeImplementations { - if implementation.keyExchangeAlgorithmNames.contains(algorithm) { - return implementation.init(ourRole: self.role, previousSessionIdentifier: self.previousSessionIdentifier) - } - } - - for implementation in customKeyExchangeAlgorithms { + for implementation in keyExchangeAlgorithms { if implementation.keyExchangeAlgorithmNames.contains(algorithm) { return implementation.init(ourRole: self.role, previousSessionIdentifier: self.previousSessionIdentifier) } } - // Huh, we didn't find it. Weird error. + // We didn't find a match throw NIOSSHError.keyExchangeNegotiationFailure } private func expectingIncorrectGuess(_ kexMessage: SSHMessage.KeyExchangeMessage) -> Bool { // A guess is wrong if the key exchange algorithm and/or the host key algorithm differ from our preference. kexMessage.firstKexPacketFollows && ( - kexMessage.keyExchangeAlgorithms.first != Self.supportedKeyExchangeAlgorithms.first || + kexMessage.keyExchangeAlgorithms.first != role.keyExchangeAlgorithmNames.first || kexMessage.serverHostKeyAlgorithms.first != self.supportedHostKeyAlgorithms.first ) } @@ -490,12 +486,12 @@ struct SSHKeyExchangeStateMachine { /// The encryption algorithms supported by this peer, in order of preference. private var supportedEncryptionAlgorithms: [Substring] { - self.protectionSchemes.map { Substring($0.cipherName) } + self.transportProtectionSchemes.map { Substring($0.cipherName) } } /// The MAC algorithms supported by this peer, in order of preference. private var supportedMacAlgorithms: [Substring] { - let schemes = self.protectionSchemes.compactMap { $0.macName.map { Substring($0) } } + let schemes = self.transportProtectionSchemes.compactMap { $0.macName.map { Substring($0) } } // We do a weird thing here: if there are no MAC schemes, we lie and put one in. This is // because some schemes (such as AES-GCM in OpenSSH mode) ignore the MAC negotiation. @@ -510,20 +506,13 @@ struct SSHKeyExchangeStateMachine { extension SSHKeyExchangeStateMachine { // For now this is a static list. - static var supportedKeyExchangeImplementations: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = [ + static let bundledKeyExchangeImplementations: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = [ EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, EllipticCurveKeyExchange.self, ] - static var supportedKeyExchangeAlgorithms: [Substring] { - let bundledAlgorithms = supportedKeyExchangeImplementations.flatMap { $0.keyExchangeAlgorithmNames } - let customAlgorithms = customKeyExchangeAlgorithms.reduce([]) { $0 + $1.keyExchangeAlgorithmNames } - - return bundledAlgorithms + customAlgorithms - } - /// All known host key algorithms. static let bundledServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index 16dacf6b..9f1219c6 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -29,47 +29,13 @@ internal extension NIOSSHSignatureProtocol { } } -public enum NIOSSHAlgoritms { - public static func register(keyExchangeAlgorithm type: NIOSSHKeyExchangeAlgorithmProtocol.Type) { - if !customKeyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - customKeyExchangeAlgorithms.append(type) - } - } - - public static func register(transportProtectionScheme type: NIOSSHTransportProtection.Type) { - if !SSHConnectionStateMachine.defaultTransportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - SSHConnectionStateMachine.defaultTransportProtectionSchemes.append(type) - } - } - - /// Registers a custom type tuple for use in Public Key Authentication. - public static func register< - PublicKey: NIOSSHPublicKeyProtocol, - Signature: NIOSSHSignatureProtocol - >( - publicKey type: PublicKey.Type, - signature: Signature.Type - ) { - let utf8format = type.publicKeyPrefix.utf8 - - if !NIOSSHPublicKey.knownAlgorithms.contains(where: { $0.elementsEqual(utf8format) }) { - NIOSSHPublicKey.knownAlgorithms.append(utf8format) - NIOSSHPublicKey.customPublicKeyAlgorithms.append(type) - NIOSSHPublicKey.customSignatures.append(signature) - } - } -} - -internal var customKeyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() -//internal var macAlgorithms = [MACAlgorithmProtocol.Type]() - public protocol NIOSSHPublicKeyProtocol { /// An identifier that represents the type of public key used in an SSH packet. /// This identifier MUST be unique to the public key implementation. /// The returned value MUST NOT overlap with other public key implementations or a specifications that the public key does not implement. static var publicKeyPrefix: String { get } - /// The raw reprentation of this signature as a blob. + /// The raw reprentation of this publc key as a blob. var rawRepresentation: Data { get } /// Verifies that `signature` is the result of signing `data` using the private key that this public key is derived from. diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift index 43e47235..d7e6b975 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift @@ -316,7 +316,6 @@ extension NIOSSHCertifiedPublicKey { static let p521KeyPrefix = "ecdsa-sha2-nistp521-cert-v01@openssh.com".utf8 static let ed25519KeyPrefix = "ssh-ed25519-cert-v01@openssh.com".utf8 - static let rsaKeyPrefix = "ssh-rsa".utf8 internal var keyPrefix: String.UTF8View { switch self.key.backingKey { diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 8be2990a..5a0f3a0e 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -176,7 +176,7 @@ extension NIOSSHPublicKey { /// The prefix of a P521 ECDSA public key. internal static let ecdsaP521PublicKeyPrefix = "ecdsa-sha2-nistp521".utf8 - + internal var keyPrefix: String.UTF8View { switch self.backingKey { case .ed25519: @@ -194,14 +194,93 @@ extension NIOSSHPublicKey { } } - internal static var knownAlgorithms: [String.UTF8View] = [ + private static let bundledAlgorithms: [String.UTF8View] = [ Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix ] - internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] - internal static var customSignatures: [NIOSSHSignatureProtocol.Type] = [] + internal static var knownAlgorithms: [String.UTF8View] { + bundledAlgorithms + customPublicKeyAlgorithms.map { $0.publicKeyPrefix.utf8 } + } + + internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + return _customPublicKeyAlgorithms + } + + internal static var customSignatures: [NIOSSHSignatureProtocol.Type] { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + return _customSignatures + } +} + +public enum NIOSSHAlgorithms { + public static func register(keyExchangeAlgorithm type: NIOSSHKeyExchangeAlgorithmProtocol.Type) { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + + if !_customKeyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _customKeyExchangeAlgorithms.append(type) + } + } + + public static func register(transportProtectionScheme type: NIOSSHTransportProtection.Type) { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + + if !_customTransportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _customTransportProtectionSchemes.append(type) + } + } + + /// Registers a custom type tuple for use in Public Key Authentication. + public static func register< + PublicKey: NIOSSHPublicKeyProtocol, + Signature: NIOSSHSignatureProtocol + >( + publicKey type: PublicKey.Type, + signature: Signature.Type + ) { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + + if !_customPublicKeyAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _customPublicKeyAlgorithms.append(type) + _customSignatures.append(signature) + } + } + + /// Used for our unit tests + internal static func unregisterAlgorithms() { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + + _customKeyExchangeAlgorithms = [] + _customSignatures = [] + _customPublicKeyAlgorithms = [] + _customTransportProtectionSchemes = [] + } +} + +internal var customTransportProtectionSchemes: [NIOSSHTransportProtection.Type] { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + return _customTransportProtectionSchemes } +internal var customKeyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { + customAlgorithmsLock.lock() + defer { customAlgorithmsLock.unlock() } + return _customKeyExchangeAlgorithms +} + +internal let customAlgorithmsLock = NSLock() +fileprivate var _customTransportProtectionSchemes = [NIOSSHTransportProtection.Type]() +fileprivate var _customKeyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() +fileprivate var _customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] +fileprivate var _customSignatures: [NIOSSHSignatureProtocol.Type] = [] + extension NIOSSHPublicKey.BackingKey: Equatable { static func == (lhs: NIOSSHPublicKey.BackingKey, rhs: NIOSSHPublicKey.BackingKey) -> Bool { // We implement equatable in terms of the key representation. diff --git a/Sources/NIOSSH/Role.swift b/Sources/NIOSSH/Role.swift index 2af28188..cc0bff21 100644 --- a/Sources/NIOSSH/Role.swift +++ b/Sources/NIOSSH/Role.swift @@ -34,4 +34,26 @@ public enum SSHConnectionRole { return true } } + + internal var transportProtectionSchemes: [NIOSSHTransportProtection.Type] { + switch self { + case .client(let client): + return client.transportProtectionSchemes + case .server(let server): + return server.transportProtectionSchemes + } + } + + internal var keyExchangeAlgorithmNames: [Substring] { + keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } + } + + internal var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { + switch self { + case .client(let client): + return client.keyExchangeAlgorithms + case .server(let server): + return server.keyExchangeAlgorithms + } + } } diff --git a/Sources/NIOSSH/SSHClientConfiguration.swift b/Sources/NIOSSH/SSHClientConfiguration.swift index 7ecbbc4f..e1df1070 100644 --- a/Sources/NIOSSH/SSHClientConfiguration.swift +++ b/Sources/NIOSSH/SSHClientConfiguration.swift @@ -22,6 +22,12 @@ public struct SSHClientConfiguration { /// The global request delegate to be used with this client. public var globalRequestDelegate: GlobalRequestDelegate + + /// The enabled TransportProtectionSchemes + public var transportProtectionSchemes: [NIOSSHTransportProtection.Type] = SSHConnectionStateMachine.bundledTransportProtectionSchemes + + /// The enabled KeyExchangeAlgorithms + public var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations public init(userAuthDelegate: NIOSSHClientUserAuthenticationDelegate, serverAuthDelegate: NIOSSHClientServerAuthenticationDelegate, diff --git a/Sources/NIOSSH/SSHServerConfiguration.swift b/Sources/NIOSSH/SSHServerConfiguration.swift index fcd8ed07..460d8fe1 100644 --- a/Sources/NIOSSH/SSHServerConfiguration.swift +++ b/Sources/NIOSSH/SSHServerConfiguration.swift @@ -25,7 +25,13 @@ public struct SSHServerConfiguration { /// The ssh banner to display to clients upon authentication public var banner: UserAuthBanner? - + + /// The enabled TransportProtectionSchemes + public var transportProtectionSchemes: [NIOSSHTransportProtection.Type] = SSHConnectionStateMachine.bundledTransportProtectionSchemes + + /// The enabled KeyExchangeAlgorithms + public var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations + public init(hostKeys: [NIOSSHPrivateKey], userAuthDelegate: NIOSSHServerUserAuthenticationDelegate, globalRequestDelegate: GlobalRequestDelegate? = nil, banner: UserAuthBanner? = nil) { self.hostKeys = hostKeys self.userAuthDelegate = userAuthDelegate diff --git a/Tests/NIOSSHTests/EndToEndTests.swift b/Tests/NIOSSHTests/EndToEndTests.swift index 4bc50eb7..0b103763 100644 --- a/Tests/NIOSSHTests/EndToEndTests.swift +++ b/Tests/NIOSSHTests/EndToEndTests.swift @@ -416,10 +416,23 @@ class BackToBackEmbeddedChannel { } func configureWithHarness(_ harness: TestHarness) throws { - let clientHandler = NIOSSHHandler(role: .client(.init(userAuthDelegate: harness.clientAuthDelegate, serverAuthDelegate: harness.clientServerAuthDelegate, globalRequestDelegate: harness.clientGlobalRequestDelegate)), + var clientConfiguration = SSHClientConfiguration(userAuthDelegate: harness.clientAuthDelegate, serverAuthDelegate: harness.clientServerAuthDelegate, globalRequestDelegate: harness.clientGlobalRequestDelegate) + var serverConfiguration = SSHServerConfiguration(hostKeys: harness.serverHostKeys, userAuthDelegate: harness.serverAuthDelegate, globalRequestDelegate: harness.serverGlobalRequestDelegate, banner: harness.serverAuthBanner) + + if let transportProtectionAlgoritms = harness.transportProtectionAlgoritms { + clientConfiguration.transportProtectionSchemes = transportProtectionAlgoritms + serverConfiguration.transportProtectionSchemes = transportProtectionAlgoritms + } + + if let keyExchangeAlgorithms = harness.keyExchangeAlgorithms { + clientConfiguration.keyExchangeAlgorithms = keyExchangeAlgorithms + serverConfiguration.keyExchangeAlgorithms = keyExchangeAlgorithms + } + + let clientHandler = NIOSSHHandler(role: .client(clientConfiguration), allocator: self.client.allocator, inboundChildChannelInitializer: nil) - let serverHandler = NIOSSHHandler(role: .server(.init(hostKeys: harness.serverHostKeys, userAuthDelegate: harness.serverAuthDelegate, globalRequestDelegate: harness.serverGlobalRequestDelegate, banner: harness.serverAuthBanner)), + let serverHandler = NIOSSHHandler(role: .server(serverConfiguration), allocator: self.server.allocator) { channel, _ in self.activeServerChannels.append(channel) channel.closeFuture.whenComplete { _ in self.activeServerChannels.removeAll(where: { $0 === channel }) } @@ -465,7 +478,11 @@ struct TestHarness { var serverGlobalRequestDelegate: GlobalRequestDelegate? var serverHostKeys: [NIOSSHPrivateKey] = [.init(ed25519Key: .init())] + + var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type]? + var transportProtectionAlgoritms: [NIOSSHTransportProtection.Type]? + var serverAuthBanner: SSHServerConfiguration.UserAuthBanner? } @@ -773,8 +790,9 @@ class EndToEndTests: XCTestCase { } func testCustomPublicKeyAlgorithms() throws { + NIOSSHAlgorithms.unregisterAlgorithms() CustomPublicKey.wasUsed = false - NIOSSHAlgoritms.register(publicKey: CustomPublicKey.self, signature: CustomSignature.self) + NIOSSHAlgorithms.register(publicKey: CustomPublicKey.self, signature: CustomSignature.self) // If we can't create this key, we skip the test. let hostKey = NIOSSHPrivateKey(ed25519Key: .init()) @@ -798,8 +816,9 @@ class EndToEndTests: XCTestCase { } func testCustomHostKeyAlgorithms() throws { + NIOSSHAlgorithms.unregisterAlgorithms() CustomPublicKey.wasUsed = false - NIOSSHAlgoritms.register(publicKey: CustomPublicKey.self, signature: CustomSignature.self) + NIOSSHAlgorithms.register(publicKey: CustomPublicKey.self, signature: CustomSignature.self) // If we can't create this key, we skip the test. let hostKey = NIOSSHPrivateKey(custom: CustomPrivateKey()) @@ -823,15 +842,9 @@ class EndToEndTests: XCTestCase { } func testCustomTransportProtectionAlgorithms() throws { - let originalTransportProtectionSchemes = SSHConnectionStateMachine.defaultTransportProtectionSchemes - SSHConnectionStateMachine.defaultTransportProtectionSchemes = [] - defer { - SSHConnectionStateMachine.defaultTransportProtectionSchemes = originalTransportProtectionSchemes - } - + NIOSSHAlgorithms.unregisterAlgorithms() CustomKeyExchange.wasUsed = false - NIOSSHAlgoritms.register(transportProtectionScheme: CustomTransportProtection.self) - XCTAssertEqual(SSHConnectionStateMachine.defaultTransportProtectionSchemes.count, 1) + NIOSSHAlgorithms.register(transportProtectionScheme: CustomTransportProtection.self) // If we can't create this key, we skip the test. let hostKey = NIOSSHPrivateKey(ed25519Key: .init()) @@ -839,6 +852,7 @@ class EndToEndTests: XCTestCase { // We use the Secure Enclave keys for everything, just because we can. var harness = TestHarness() + harness.transportProtectionAlgoritms = [CustomTransportProtection.self] harness.serverHostKeys = [hostKey] harness.clientAuthDelegate = PrivateKeyClientAuth(clientAuthKey) harness.serverAuthDelegate = ExpectPublicKeyAuth(clientAuthKey.publicKey) @@ -855,16 +869,10 @@ class EndToEndTests: XCTestCase { } func testCustomKeyExchangeAlgorithms() throws { - let originalSSHKeyExchangeAlgorithms = SSHKeyExchangeStateMachine.supportedKeyExchangeImplementations - SSHKeyExchangeStateMachine.supportedKeyExchangeImplementations = [] - defer { - SSHKeyExchangeStateMachine.supportedKeyExchangeImplementations = originalSSHKeyExchangeAlgorithms - } - + NIOSSHAlgorithms.unregisterAlgorithms() CustomKeyExchange.wasUsed = false - NIOSSHAlgoritms.register(keyExchangeAlgorithm: CustomKeyExchange.self) - NIOSSHAlgoritms.register(publicKey: CustomPublicKey.self, signature: CustomSignature.self) - XCTAssertEqual(SSHKeyExchangeStateMachine.supportedKeyExchangeAlgorithms.count, 1) + NIOSSHAlgorithms.register(keyExchangeAlgorithm: CustomKeyExchange.self) + NIOSSHAlgorithms.register(publicKey: CustomPublicKey.self, signature: CustomSignature.self) // If we can't create this key, we skip the test. let hostKey = NIOSSHPrivateKey(custom: CustomPrivateKey()) @@ -872,6 +880,7 @@ class EndToEndTests: XCTestCase { // We use the Secure Enclave keys for everything, just because we can. var harness = TestHarness() + harness.keyExchangeAlgorithms = [CustomKeyExchange.self] harness.serverHostKeys = [hostKey] harness.clientAuthDelegate = PrivateKeyClientAuth(clientAuthKey) harness.serverAuthDelegate = ExpectPublicKeyAuth(clientAuthKey.publicKey) diff --git a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift index 4387b393..49e7b19d 100644 --- a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift +++ b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift @@ -201,7 +201,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -209,7 +209,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -267,7 +267,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -275,7 +275,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -320,7 +320,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil) // Server generates a key exchange message. @@ -362,7 +362,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) let serverMessage = server.createKeyExchangeMessage() @@ -393,7 +393,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -401,7 +401,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -459,7 +459,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -467,7 +467,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [hostKey], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -513,7 +513,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) let clientMessage = client.createKeyExchangeMessage() @@ -539,7 +539,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -547,7 +547,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES128GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -579,7 +579,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES128GCMOpenSSHTransportProtection.self, AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self, AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -587,7 +587,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self, AES128GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self, AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -634,7 +634,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES128GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -642,7 +642,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES128GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -689,7 +689,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES128GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -697,7 +697,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES128GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -768,7 +768,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -776,7 +776,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: keys, userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -828,7 +828,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -836,7 +836,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [NIOSSHPrivateKey(p256Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -888,7 +888,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: RejectHostKeyDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -896,7 +896,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [NIOSSHPrivateKey(p256Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -952,7 +952,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: hostKeyDelegate)), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) var server = SSHKeyExchangeStateMachine( @@ -960,7 +960,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [serverHostKey], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, - protectionSchemes: [AES256GCMOpenSSHTransportProtection.self], + transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) From 642d9615bb47aafd3e577bd96dad1b263cf9a52c Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Thu, 25 Nov 2021 17:14:19 +0100 Subject: [PATCH 13/20] Remove whitespace --- Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift | 2 +- Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift | 2 +- Sources/NIOSSH/TransportProtection/AESGCM.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift index 5563ade3..7e5033e1 100644 --- a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift +++ b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift @@ -138,7 +138,7 @@ extension EllipticCurveKeyExchange { expectedKeySizes: ExpectedKeySizes ) throws -> KeyExchangeResult { precondition(self.ourRole.isClient, "Only clients may receive a server key exchange packet!") - + // Ok, we have a few steps here. Firstly, we need to extract the server's public key and generate our shared // secret. Then we need to validate that we didn't generate a weak shared secret (possible under some cases), // as this must fail the key exchange process. diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 5a0f3a0e..cc2f97e7 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -485,7 +485,7 @@ extension ByteBuffer { writtenBytes += self.writeSSHString(baseKey.x963Representation) return writtenBytes } - + /// A helper function that reads an Ed25519 public key. /// /// Not safe to call from arbitrary code as this does not return the reader index on failure: it relies on the caller performing diff --git a/Sources/NIOSSH/TransportProtection/AESGCM.swift b/Sources/NIOSSH/TransportProtection/AESGCM.swift index ecaf4c5e..8ad5ac2c 100644 --- a/Sources/NIOSSH/TransportProtection/AESGCM.swift +++ b/Sources/NIOSSH/TransportProtection/AESGCM.swift @@ -43,7 +43,7 @@ internal class AESGCMTransportProtection { required init(initialKeys: NIOSSHSessionKeys) throws { guard initialKeys.outboundEncryptionKey.bitCount == Self.keySizes.encryptionKeySize * 8, initialKeys.inboundEncryptionKey.bitCount == Self.keySizes.encryptionKeySize * 8 else { - throw NIOSSHError.invalidKeySize +N throw NIOSSHError.invalidKeySize } self.outboundEncryptionKey = initialKeys.outboundEncryptionKey From c5ccb887892f827492a7c2c5e11361661f3fc4dc Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sat, 4 Dec 2021 13:37:56 -0600 Subject: [PATCH 14/20] Fix typo --- Sources/NIOSSH/TransportProtection/AESGCM.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NIOSSH/TransportProtection/AESGCM.swift b/Sources/NIOSSH/TransportProtection/AESGCM.swift index 8ad5ac2c..ecaf4c5e 100644 --- a/Sources/NIOSSH/TransportProtection/AESGCM.swift +++ b/Sources/NIOSSH/TransportProtection/AESGCM.swift @@ -43,7 +43,7 @@ internal class AESGCMTransportProtection { required init(initialKeys: NIOSSHSessionKeys) throws { guard initialKeys.outboundEncryptionKey.bitCount == Self.keySizes.encryptionKeySize * 8, initialKeys.inboundEncryptionKey.bitCount == Self.keySizes.encryptionKeySize * 8 else { -N throw NIOSSHError.invalidKeySize + throw NIOSSHError.invalidKeySize } self.outboundEncryptionKey = initialKeys.outboundEncryptionKey From a4f1a814deefe13e9a87dd333f97b33e9d250a21 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sat, 4 Dec 2021 13:50:37 -0600 Subject: [PATCH 15/20] Address some of the PR feedback --- Package.swift | 1 + .../SSHConnectionStateMachine.swift | 6 +++--- .../Key Exchange/EllipticCurveKeyExchange.swift | 14 ++++++++++---- .../Key Exchange/SSHKeyExchangeStateMachine.swift | 4 ++-- .../Keys And Signatures/NIOSSHPublicKey.swift | 3 ++- Sources/NIOSSH/Role.swift | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 3c9c3b1a..dff59e15 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,7 @@ let package = Package( name: "NIOSSH", dependencies: [ .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), .product(name: "Crypto", package: "swift-crypto"), ] diff --git a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift index 2832bbaa..20163ff2 100644 --- a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift +++ b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift @@ -60,7 +60,7 @@ struct SSHConnectionStateMachine { /// The state of this state machine. private var state: State - public static let bundledTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ + static let bundledTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ AES256GCMOpenSSHTransportProtection.self, AES128GCMOpenSSHTransportProtection.self, ] @@ -306,7 +306,7 @@ struct SSHConnectionStateMachine { let result = try state.receiveUserAuthRequest(message) self.state = .userAuthentication(state) return result - + case .userAuthSuccess: let result = try state.receiveUserAuthSuccess() // Hey, auth succeeded! @@ -819,7 +819,7 @@ struct SSHConnectionStateMachine { case .userAuthRequest(let message): try state.writeUserAuthRequest(message, into: &buffer) self.state = .userAuthentication(state) - + case .userAuthSuccess: try state.writeUserAuthSuccess(into: &buffer) // Ok we're good to go! diff --git a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift index 7e5033e1..8968582e 100644 --- a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift +++ b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift @@ -17,9 +17,15 @@ import NIO import NIOFoundationCompat public struct NIOSSHKeyExchangeServerReply { - let hostKey: NIOSSHPublicKey - let publicKey: ByteBuffer - let signature: NIOSSHSignature + public var hostKey: NIOSSHPublicKey + public var publicKey: ByteBuffer + public var signature: NIOSSHSignature + + public init(hostKey: NIOSSHPublicKey, publicKey: ByteBuffer, signature: NIOSSHSignature) { + self.hostKey = hostKey + self.publicKey = publicKey + self.signature = signature + } } /// This protocol defines a container used by the key exchange state machine to manage key exchange. @@ -50,7 +56,7 @@ public protocol NIOSSHKeyExchangeAlgorithmProtocol { static var keyExchangeAlgorithmNames: [Substring] { get } } -struct EllipticCurveKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { +struct EllipticCurveKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { private var previousSessionIdentifier: ByteBuffer? private var ourKey: PrivateKey private var theirKey: PrivateKey.PublicKey? diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 7dcf80d6..3efe6c72 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -377,13 +377,13 @@ struct SSHKeyExchangeStateMachine { switch self.role { case .client: - clientAlgorithms = role.keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } + clientAlgorithms = role.keyExchangeAlgorithmNames serverAlgorithms = peerKeyExchangeAlgorithms clientHostKeyAlgorithms = self.supportedHostKeyAlgorithms serverHostKeyAlgorithms = peerHostKeyAlgorithms case .server: clientAlgorithms = peerKeyExchangeAlgorithms - serverAlgorithms = role.keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } + serverAlgorithms = role.keyExchangeAlgorithmNames clientHostKeyAlgorithms = peerHostKeyAlgorithms serverHostKeyAlgorithms = self.supportedHostKeyAlgorithms } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index cc2f97e7..85ff4042 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Crypto +import NIOConcurrencyHelpers import Foundation import NIO @@ -275,7 +276,7 @@ internal var customKeyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Ty return _customKeyExchangeAlgorithms } -internal let customAlgorithmsLock = NSLock() +internal let customAlgorithmsLock = Lock() fileprivate var _customTransportProtectionSchemes = [NIOSSHTransportProtection.Type]() fileprivate var _customKeyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() fileprivate var _customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] diff --git a/Sources/NIOSSH/Role.swift b/Sources/NIOSSH/Role.swift index cc0bff21..c3048a2a 100644 --- a/Sources/NIOSSH/Role.swift +++ b/Sources/NIOSSH/Role.swift @@ -45,7 +45,7 @@ public enum SSHConnectionRole { } internal var keyExchangeAlgorithmNames: [Substring] { - keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } + self.keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } } internal var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { From df0b47eb5050ff527412e83b4320c2d4910facd5 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 15 May 2022 02:45:45 -0500 Subject: [PATCH 16/20] Remove defaulted parameters per PR feedback and fix a pile of broken tests. --- .../SSHKeyExchangeStateMachine.swift | 2 +- .../SSHConnectionStateMachineTests.swift | 5 ++-- .../SSHKeyExchangeStateMachineTests.swift | 27 +++++++++++++++++++ Tests/NIOSSHTests/Utilities.swift | 4 +-- Tests/NIOSSHTests/UtilitiesTests.swift | 4 +-- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index 4da86fc9..d7dfb373 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -75,7 +75,7 @@ struct SSHKeyExchangeStateMachine { private var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] private var previousSessionIdentifier: ByteBuffer? - init(allocator: ByteBufferAllocator, loop: EventLoop, role: SSHConnectionRole, remoteVersion: String, keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [NIOSSHTransportProtection.Type] = SSHConnectionStateMachine.bundledTransportProtectionSchemes, previousSessionIdentifier: ByteBuffer?) { + init(allocator: ByteBufferAllocator, loop: EventLoop, role: SSHConnectionRole, remoteVersion: String, keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type], transportProtectionSchemes: [NIOSSHTransportProtection.Type], previousSessionIdentifier: ByteBuffer?) { self.allocator = allocator self.loop = loop self.role = role diff --git a/Tests/NIOSSHTests/SSHConnectionStateMachineTests.swift b/Tests/NIOSSHTests/SSHConnectionStateMachineTests.swift index 55bac3b7..a2e8e4aa 100644 --- a/Tests/NIOSSHTests/SSHConnectionStateMachineTests.swift +++ b/Tests/NIOSSHTests/SSHConnectionStateMachineTests.swift @@ -594,9 +594,8 @@ final class SSHConnectionStateMachineTests: XCTestCase { func testFirstBlockDecodedOnce() throws { let allocator = ByteBufferAllocator() let loop = EmbeddedEventLoop() - let schemes: [NIOSSHTransportProtection.Type] = [TestTransportProtection.self] - var client = SSHConnectionStateMachine(role: .client(.init(userAuthDelegate: InfinitePasswordDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), protectionSchemes: schemes) - var server = SSHConnectionStateMachine(role: .server(.init(hostKeys: [NIOSSHPrivateKey(ed25519Key: .init())], userAuthDelegate: DenyThenAcceptDelegate(messagesToDeny: 0))), protectionSchemes: schemes) + var client = SSHConnectionStateMachine(role: .client(.init(userAuthDelegate: InfinitePasswordDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate()))) + var server = SSHConnectionStateMachine(role: .server(.init(hostKeys: [NIOSSHPrivateKey(ed25519Key: .init())], userAuthDelegate: DenyThenAcceptDelegate(messagesToDeny: 0)))) try assertSuccessfulConnection(client: &client, server: &server, allocator: allocator, loop: loop) let message = SSHMessage.channelData(.init(recipientChannel: 1, data: ByteBuffer(repeating: 17, count: 5))) diff --git a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift index 7ab61eb6..627664ab 100644 --- a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift +++ b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift @@ -202,6 +202,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -210,6 +211,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -268,6 +270,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -276,6 +279,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -321,6 +325,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil) @@ -363,6 +368,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -394,6 +400,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -402,6 +409,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -460,6 +468,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -468,6 +477,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [hostKey], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -514,6 +524,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -540,6 +551,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -548,6 +560,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -580,6 +593,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self, AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -588,6 +602,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self, AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -635,6 +650,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -643,6 +659,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -690,6 +707,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -698,6 +716,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [.init(ed25519Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES128GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -769,6 +788,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -777,6 +797,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: keys, userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -829,6 +850,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: AcceptAllHostKeysDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -837,6 +859,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [NIOSSHPrivateKey(p256Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -889,6 +912,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: RejectHostKeyDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -897,6 +921,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [NIOSSHPrivateKey(p256Key: .init())], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -953,6 +978,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .client(.init(userAuthDelegate: ExplodingAuthDelegate(), serverAuthDelegate: hostKeyDelegate)), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) @@ -961,6 +987,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { loop: loop, role: .server(.init(hostKeys: [serverHostKey], userAuthDelegate: DenyAllServerAuthDelegate())), remoteVersion: Constants.version, + keyExchangeAlgorithms: SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations, transportProtectionSchemes: [AES256GCMOpenSSHTransportProtection.self], previousSessionIdentifier: nil ) diff --git a/Tests/NIOSSHTests/Utilities.swift b/Tests/NIOSSHTests/Utilities.swift index a98a4a76..17fb928b 100644 --- a/Tests/NIOSSHTests/Utilities.swift +++ b/Tests/NIOSSHTests/Utilities.swift @@ -183,7 +183,7 @@ class TestTransportProtection: NIOSSHTransportProtection { source.setBytes(plaintext.readableBytesView, at: index) } - func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer { + func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer { defer { self.lastFirstBlock = nil } @@ -218,7 +218,7 @@ class TestTransportProtection: NIOSSHTransportProtection { return plaintext.readSlice(length: plaintext.readableBytes - Int(paddingLength))! } - func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer) throws { + func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer, sequenceNumber: UInt32) throws { let packetLengthIndex = outboundBuffer.writerIndex let packetLengthLength = MemoryLayout.size let packetPaddingIndex = outboundBuffer.writerIndex + packetLengthLength diff --git a/Tests/NIOSSHTests/UtilitiesTests.swift b/Tests/NIOSSHTests/UtilitiesTests.swift index 96a4c33f..9eaeda36 100644 --- a/Tests/NIOSSHTests/UtilitiesTests.swift +++ b/Tests/NIOSSHTests/UtilitiesTests.swift @@ -52,9 +52,9 @@ final class UtilitiesTests: XCTestCase { let message = SSHMessage.channelRequest(.init(recipientChannel: 1, type: .exec("uname"), wantReply: false)) let allocator = ByteBufferAllocator() var buffer = allocator.buffer(capacity: 1024) - XCTAssertNoThrow(try client.encryptPacket(.init(message: message), to: &buffer)) + XCTAssertNoThrow(try client.encryptPacket(.init(message: message), to: &buffer, sequenceNumber: 1)) XCTAssertNoThrow(try server.decryptFirstBlock(&buffer)) - var decoded = try server.decryptAndVerifyRemainingPacket(&buffer) + var decoded = try server.decryptAndVerifyRemainingPacket(&buffer, sequenceNumber: 1) XCTAssertEqual(message, try decoded.readSSHMessage()) } } From f8b8addb22d01057e9c56b9073db21fb3cb52bf2 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 15 May 2022 02:49:51 -0500 Subject: [PATCH 17/20] Encapsulate globals in an enum per PR feedback --- .../Keys And Signatures/NIOSSHPublicKey.swift | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index dd6ac6c7..598305ad 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -202,36 +202,35 @@ extension NIOSSHPublicKey { internal static var knownAlgorithms: [String.UTF8View] { bundledAlgorithms + customPublicKeyAlgorithms.map { $0.publicKeyPrefix.utf8 } } - internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - return _customPublicKeyAlgorithms + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + return _CustomAlgorithms.publicKeyAlgorithms } - + internal static var customSignatures: [NIOSSHSignatureProtocol.Type] { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - return _customSignatures + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + return _CustomAlgorithms.signatures } } public enum NIOSSHAlgorithms { public static func register(keyExchangeAlgorithm type: NIOSSHKeyExchangeAlgorithmProtocol.Type) { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - - if !_customKeyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - _customKeyExchangeAlgorithms.append(type) + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + + if !_CustomAlgorithms.keyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _CustomAlgorithms.keyExchangeAlgorithms.append(type) } } public static func register(transportProtectionScheme type: NIOSSHTransportProtection.Type) { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - - if !_customTransportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - _customTransportProtectionSchemes.append(type) + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + + if !_CustomAlgorithms.transportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _CustomAlgorithms.transportProtectionSchemes.append(type) } } @@ -243,44 +242,46 @@ public enum NIOSSHAlgorithms { publicKey type: PublicKey.Type, signature: Signature.Type ) { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - - if !_customPublicKeyAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - _customPublicKeyAlgorithms.append(type) - _customSignatures.append(signature) + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + + if !_CustomAlgorithms.publicKeyAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _CustomAlgorithms.publicKeyAlgorithms.append(type) + _CustomAlgorithms.signatures.append(signature) } } /// Used for our unit tests internal static func unregisterAlgorithms() { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - - _customKeyExchangeAlgorithms = [] - _customSignatures = [] - _customPublicKeyAlgorithms = [] - _customTransportProtectionSchemes = [] + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + + _CustomAlgorithms.keyExchangeAlgorithms = [] + _CustomAlgorithms.signatures = [] + _CustomAlgorithms.publicKeyAlgorithms = [] + _CustomAlgorithms.transportProtectionSchemes = [] } } internal var customTransportProtectionSchemes: [NIOSSHTransportProtection.Type] { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - return _customTransportProtectionSchemes + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + return _CustomAlgorithms.transportProtectionSchemes } internal var customKeyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { - customAlgorithmsLock.lock() - defer { customAlgorithmsLock.unlock() } - return _customKeyExchangeAlgorithms + _CustomAlgorithms.lock.lock() + defer { _CustomAlgorithms.lock.unlock() } + return _CustomAlgorithms.keyExchangeAlgorithms } -internal let customAlgorithmsLock = Lock() -fileprivate var _customTransportProtectionSchemes = [NIOSSHTransportProtection.Type]() -fileprivate var _customKeyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() -fileprivate var _customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] -fileprivate var _customSignatures: [NIOSSHSignatureProtocol.Type] = [] +fileprivate enum _CustomAlgorithms { + static var lock = Lock() + static var transportProtectionSchemes = [NIOSSHTransportProtection.Type]() + static var keyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() + static var publicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] + static var signatures: [NIOSSHSignatureProtocol.Type] = [] +} extension NIOSSHPublicKey.BackingKey: Equatable { static func == (lhs: NIOSSHPublicKey.BackingKey, rhs: NIOSSHPublicKey.BackingKey) -> Bool { From b66b64fd4068a66203e7cb88f563fd35a8839542 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 15 May 2022 02:54:26 -0500 Subject: [PATCH 18/20] Use fine-grained locking per PR feedback --- .../Keys And Signatures/NIOSSHPublicKey.swift | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 598305ad..1185e280 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -202,35 +202,34 @@ extension NIOSSHPublicKey { internal static var knownAlgorithms: [String.UTF8View] { bundledAlgorithms + customPublicKeyAlgorithms.map { $0.publicKeyPrefix.utf8 } } + internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - return _CustomAlgorithms.publicKeyAlgorithms + return _CustomAlgorithms.publicKeyAlgorithmsLock.withLock { + return _CustomAlgorithms.publicKeyAlgorithms + } } internal static var customSignatures: [NIOSSHSignatureProtocol.Type] { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - return _CustomAlgorithms.signatures + return _CustomAlgorithms.signaturesLock.withLock { + return _CustomAlgorithms.signatures + } } } public enum NIOSSHAlgorithms { public static func register(keyExchangeAlgorithm type: NIOSSHKeyExchangeAlgorithmProtocol.Type) { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - - if !_CustomAlgorithms.keyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - _CustomAlgorithms.keyExchangeAlgorithms.append(type) + _CustomAlgorithms.keyExchangeAlgorithmsLock.withLockVoid { + if !_CustomAlgorithms.keyExchangeAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _CustomAlgorithms.keyExchangeAlgorithms.append(type) + } } } public static func register(transportProtectionScheme type: NIOSSHTransportProtection.Type) { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - - if !_CustomAlgorithms.transportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - _CustomAlgorithms.transportProtectionSchemes.append(type) + _CustomAlgorithms.transportProtectionSchemesLock.withLockVoid { + if !_CustomAlgorithms.transportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _CustomAlgorithms.transportProtectionSchemes.append(type) + } } } @@ -242,44 +241,51 @@ public enum NIOSSHAlgorithms { publicKey type: PublicKey.Type, signature: Signature.Type ) { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - - if !_CustomAlgorithms.publicKeyAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { - _CustomAlgorithms.publicKeyAlgorithms.append(type) - _CustomAlgorithms.signatures.append(signature) + _CustomAlgorithms.publicKeyAlgorithmsLock.withLockVoid { + if !_CustomAlgorithms.publicKeyAlgorithms.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { + _CustomAlgorithms.publicKeyAlgorithms.append(type) + _CustomAlgorithms.signatures.append(signature) + } } } /// Used for our unit tests internal static func unregisterAlgorithms() { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - - _CustomAlgorithms.keyExchangeAlgorithms = [] - _CustomAlgorithms.signatures = [] - _CustomAlgorithms.publicKeyAlgorithms = [] - _CustomAlgorithms.transportProtectionSchemes = [] + _CustomAlgorithms.transportProtectionSchemesLock.withLockVoid { + _CustomAlgorithms.transportProtectionSchemes = [] + } + _CustomAlgorithms.publicKeyAlgorithmsLock.withLockVoid { + _CustomAlgorithms.publicKeyAlgorithms = [] + } + _CustomAlgorithms.signaturesLock.withLockVoid { + _CustomAlgorithms.signatures = [] + } + _CustomAlgorithms.keyExchangeAlgorithmsLock.withLockVoid { + _CustomAlgorithms.keyExchangeAlgorithms = [] + } } } internal var customTransportProtectionSchemes: [NIOSSHTransportProtection.Type] { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - return _CustomAlgorithms.transportProtectionSchemes + return _CustomAlgorithms.transportProtectionSchemesLock.withLock { + return _CustomAlgorithms.transportProtectionSchemes + } } internal var customKeyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { - _CustomAlgorithms.lock.lock() - defer { _CustomAlgorithms.lock.unlock() } - return _CustomAlgorithms.keyExchangeAlgorithms + return _CustomAlgorithms.keyExchangeAlgorithmsLock.withLock { + return _CustomAlgorithms.keyExchangeAlgorithms + } } fileprivate enum _CustomAlgorithms { - static var lock = Lock() + static var transportProtectionSchemesLock = Lock() static var transportProtectionSchemes = [NIOSSHTransportProtection.Type]() + static var keyExchangeAlgorithmsLock = Lock() static var keyExchangeAlgorithms = [NIOSSHKeyExchangeAlgorithmProtocol.Type]() + static var publicKeyAlgorithmsLock = Lock() static var publicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] = [] + static var signaturesLock = Lock() static var signatures: [NIOSSHSignatureProtocol.Type] = [] } From d1fc273cdf5f3772f2b9ade8a4ce8a4ed111e08b Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 23 May 2022 03:07:57 -0500 Subject: [PATCH 19/20] Address soundness.sh issues with correct version of swiftformat. --- .../SSHConnectionStateMachine.swift | 2 +- .../States/IdleState.swift | 2 +- .../ReceivedKexInitWhenActiveState.swift | 2 +- .../States/SentKexInitWhenActiveState.swift | 2 +- .../EllipticCurveKeyExchange.swift | 6 +- .../Key Exchange/SSHKeyExchangeResult.swift | 6 +- .../SSHKeyExchangeStateMachine.swift | 14 +- .../Keys And Signatures/CustomKeys.swift | 32 +++- .../NIOSSHPrivateKey.swift | 2 +- .../Keys And Signatures/NIOSSHPublicKey.swift | 40 ++--- .../Keys And Signatures/NIOSSHSignature.swift | 2 +- Sources/NIOSSH/Role.swift | 6 +- Sources/NIOSSH/SSHClientConfiguration.swift | 4 +- Sources/NIOSSH/SSHPacketParser.swift | 8 +- Sources/NIOSSH/SSHPacketSerializer.swift | 6 +- Sources/NIOSSH/SSHServerConfiguration.swift | 6 +- Tests/NIOSSHTests/EndToEndTests.swift | 168 +++++++++--------- Tests/NIOSSHTests/Utilities.swift | 4 +- 18 files changed, 163 insertions(+), 149 deletions(-) diff --git a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift index fa87383a..2935be9d 100644 --- a/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift +++ b/Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift @@ -182,7 +182,7 @@ struct SSHConnectionStateMachine { return .noMessage case .unimplemented(let unimplemented): throw NIOSSHError.remotePeerDoesNotSupportMessage(unimplemented) - + default: // TODO: enforce RFC 4253: // diff --git a/Sources/NIOSSH/Connection State Machine/States/IdleState.swift b/Sources/NIOSSH/Connection State Machine/States/IdleState.swift index 01a91f7b..edba5ea1 100644 --- a/Sources/NIOSSH/Connection State Machine/States/IdleState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/IdleState.swift @@ -22,7 +22,7 @@ extension SSHConnectionStateMachine { internal var serializer: SSHPacketSerializer internal var protectionSchemes: [NIOSSHTransportProtection.Type] - + internal var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] init(role: SSHConnectionRole) { diff --git a/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift b/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift index c8e135b3..5b578aeb 100644 --- a/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/ReceivedKexInitWhenActiveState.swift @@ -38,7 +38,7 @@ extension SSHConnectionStateMachine { self.parser = previous.parser self.remoteVersion = previous.remoteVersion self.sessionIdentifier = previous.sessionIdentifier - self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: previous.role, remoteVersion: previous.remoteVersion, keyExchangeAlgorithms: role.keyExchangeAlgorithms, transportProtectionSchemes: role.transportProtectionSchemes, previousSessionIdentifier: self.sessionIdentifier) + self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: previous.role, remoteVersion: previous.remoteVersion, keyExchangeAlgorithms: self.role.keyExchangeAlgorithms, transportProtectionSchemes: self.role.transportProtectionSchemes, previousSessionIdentifier: self.sessionIdentifier) } } } diff --git a/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift b/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift index f867cf77..f0a1fc55 100644 --- a/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift +++ b/Sources/NIOSSH/Connection State Machine/States/SentKexInitWhenActiveState.swift @@ -38,7 +38,7 @@ extension SSHConnectionStateMachine { self.parser = previous.parser self.remoteVersion = previous.remoteVersion self.sessionIdentitifier = previous.sessionIdentifier - self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: self.role, remoteVersion: self.remoteVersion, keyExchangeAlgorithms: role.keyExchangeAlgorithms, transportProtectionSchemes: role.transportProtectionSchemes, previousSessionIdentifier: previous.sessionIdentifier) + self.keyExchangeStateMachine = SSHKeyExchangeStateMachine(allocator: allocator, loop: loop, role: self.role, remoteVersion: self.remoteVersion, keyExchangeAlgorithms: self.role.keyExchangeAlgorithms, transportProtectionSchemes: self.role.transportProtectionSchemes, previousSessionIdentifier: previous.sessionIdentifier) } } } diff --git a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift index 0ba5d456..b0c72e26 100644 --- a/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift +++ b/Sources/NIOSSH/Key Exchange/EllipticCurveKeyExchange.swift @@ -20,7 +20,7 @@ public struct NIOSSHKeyExchangeServerReply { public var hostKey: NIOSSHPublicKey public var publicKey: ByteBuffer public var signature: NIOSSHSignature - + public init(hostKey: NIOSSHPublicKey, publicKey: ByteBuffer, signature: NIOSSHSignature) { self.hostKey = hostKey self.publicKey = publicKey @@ -33,7 +33,7 @@ public struct NIOSSHKeyExchangeServerReply { public protocol NIOSSHKeyExchangeAlgorithmProtocol { static var keyExchangeInitMessageId: UInt8 { get } static var keyExchangeReplyMessageId: UInt8 { get } - + init(ourRole: SSHConnectionRole, previousSessionIdentifier: ByteBuffer?) func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> ByteBuffer @@ -62,7 +62,7 @@ struct EllipticCurveKeyExchange: NIOSSHKey private var theirKey: PrivateKey.PublicKey? private var ourRole: SSHConnectionRole private var sharedSecret: SharedSecret? - + static var keyExchangeInitMessageId: UInt8 { 30 } static var keyExchangeReplyMessageId: UInt8 { 31 } diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift index b11dc963..a7bf006a 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeResult.swift @@ -24,7 +24,7 @@ public struct KeyExchangeResult { public var sessionID: ByteBuffer public var keys: NIOSSHSessionKeys - + public init(sessionID: ByteBuffer, keys: NIOSSHSessionKeys) { self.sessionID = sessionID self.keys = keys @@ -63,7 +63,7 @@ public struct NIOSSHSessionKeys { public var inboundMACKey: SymmetricKey public var outboundMACKey: SymmetricKey - + public init(initialInboundIV: [UInt8], initialOutboundIV: [UInt8], inboundEncryptionKey: SymmetricKey, outboundEncryptionKey: SymmetricKey, inboundMACKey: SymmetricKey, outboundMACKey: SymmetricKey) { self.initialInboundIV = initialInboundIV self.initialOutboundIV = initialOutboundIV @@ -88,7 +88,7 @@ public struct ExpectedKeySizes { public var encryptionKeySize: Int public var macKeySize: Int - + public init(ivSize: Int, encryptionKeySize: Int, macKeySize: Int) { self.ivSize = ivSize self.encryptionKeySize = encryptionKeySize diff --git a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift index d7dfb373..3a075e5c 100644 --- a/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift +++ b/Sources/NIOSSH/Key Exchange/SSHKeyExchangeStateMachine.swift @@ -105,7 +105,7 @@ struct SSHKeyExchangeStateMachine { return .init( cookie: rng.randomCookie(allocator: self.allocator), - keyExchangeAlgorithms: role.keyExchangeAlgorithmNames, + keyExchangeAlgorithms: self.role.keyExchangeAlgorithmNames, serverHostKeyAlgorithms: self.supportedHostKeyAlgorithms, encryptionAlgorithmsClientToServer: encryptionAlgorithms, encryptionAlgorithmsServerToClient: encryptionAlgorithms, @@ -377,13 +377,13 @@ struct SSHKeyExchangeStateMachine { switch self.role { case .client: - clientAlgorithms = role.keyExchangeAlgorithmNames + clientAlgorithms = self.role.keyExchangeAlgorithmNames serverAlgorithms = peerKeyExchangeAlgorithms clientHostKeyAlgorithms = self.supportedHostKeyAlgorithms serverHostKeyAlgorithms = peerHostKeyAlgorithms case .server: clientAlgorithms = peerKeyExchangeAlgorithms - serverAlgorithms = role.keyExchangeAlgorithmNames + serverAlgorithms = self.role.keyExchangeAlgorithmNames clientHostKeyAlgorithms = peerHostKeyAlgorithms serverHostKeyAlgorithms = self.supportedHostKeyAlgorithms } @@ -456,7 +456,7 @@ struct SSHKeyExchangeStateMachine { } private func exchangerForAlgorithm(_ algorithm: Substring) throws -> NIOSSHKeyExchangeAlgorithmProtocol { - for implementation in keyExchangeAlgorithms { + for implementation in self.keyExchangeAlgorithms { if implementation.keyExchangeAlgorithmNames.contains(algorithm) { return implementation.init(ourRole: self.role, previousSessionIdentifier: self.previousSessionIdentifier) } @@ -469,7 +469,7 @@ struct SSHKeyExchangeStateMachine { private func expectingIncorrectGuess(_ kexMessage: SSHMessage.KeyExchangeMessage) -> Bool { // A guess is wrong if the key exchange algorithm and/or the host key algorithm differ from our preference. kexMessage.firstKexPacketFollows && ( - kexMessage.keyExchangeAlgorithms.first != role.keyExchangeAlgorithmNames.first || + kexMessage.keyExchangeAlgorithms.first != self.role.keyExchangeAlgorithmNames.first || kexMessage.serverHostKeyAlgorithms.first != self.supportedHostKeyAlgorithms.first ) } @@ -515,11 +515,11 @@ extension SSHKeyExchangeStateMachine { /// All known host key algorithms. static let bundledServerHostKeyAlgorithms: [Substring] = ["ssh-ed25519", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp521"] - + static var supportedServerHostKeyAlgorithms: [Substring] { let bundledAlgorithms = bundledServerHostKeyAlgorithms let customAlgorithms = NIOSSHPublicKey.customPublicKeyAlgorithms.map { Substring($0.publicKeyPrefix) } - + return bundledAlgorithms + customAlgorithms } } diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index 9f1219c6..b5980d27 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) YEARS Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import Foundation import NIO @@ -11,14 +25,14 @@ public protocol NIOSSHSignatureProtocol { /// This identifier MUST be unique to the signature implementation. /// The returned value MUST NOT overlap with other signature implementations or a specifications that the signature does not implement. static var signaturePrefix: String { get } - + /// The raw reprentation of this signature as a blob. var rawRepresentation: Data { get } - + /// Serializes and writes the signature to the buffer. The calling function SHOULD NOT keep track of the size of the written blob. /// If the result is not a fixed size, the serialized format SHOULD include a length. func write(to buffer: inout ByteBuffer) -> Int - + /// Reads this Signature from the buffer using the same format implemented in `write(to:)` static func read(from buffer: inout ByteBuffer) throws -> Self } @@ -34,17 +48,17 @@ public protocol NIOSSHPublicKeyProtocol { /// This identifier MUST be unique to the public key implementation. /// The returned value MUST NOT overlap with other public key implementations or a specifications that the public key does not implement. static var publicKeyPrefix: String { get } - + /// The raw reprentation of this publc key as a blob. var rawRepresentation: Data { get } - + /// Verifies that `signature` is the result of signing `data` using the private key that this public key is derived from. func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool - + /// Serializes and writes the public key to the buffer. The calling function SHOULD NOT keep track of the size of the written blob. /// If the result is not a fixed size, the serialized format SHOULD include a length. func write(to buffer: inout ByteBuffer) -> Int - + /// Reads this Public Key from the buffer using the same format implemented in `write(to:)` static func read(from buffer: inout ByteBuffer) throws -> Self } @@ -60,10 +74,10 @@ public protocol NIOSSHPrivateKeyProtocol { /// This identifier MUST be unique to the private key implementation. /// The returned value MUST NOT overlap with other private key implementations or a specifications that the private key does not implement. static var keyPrefix: String { get } - + /// A public key instance that is able to verify signatures that are created using this private key. var publicKey: NIOSSHPublicKeyProtocol { get } - + /// Creates a signature, proving that `data` has been sent by the holder of this private key, and can be verified by `publicKey`. func signature(for data: D) throws -> NIOSSHSignatureProtocol } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift index 16798879..5a858b4f 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift @@ -45,7 +45,7 @@ public struct NIOSSHPrivateKey { public init(p521Key key: P521.Signing.PrivateKey) { self.backingKey = .ecdsaP521(key) } - + public init(custom key: PrivateKey) { self.backingKey = .custom(key) } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index 1185e280..a4a334d0 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -13,8 +13,8 @@ //===----------------------------------------------------------------------===// import Crypto -import NIOConcurrencyHelpers import Foundation +import NIOConcurrencyHelpers import NIOCore /// An SSH public key. @@ -196,22 +196,22 @@ extension NIOSSHPublicKey { } private static let bundledAlgorithms: [String.UTF8View] = [ - Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix + Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix, ] - + internal static var knownAlgorithms: [String.UTF8View] { bundledAlgorithms + customPublicKeyAlgorithms.map { $0.publicKeyPrefix.utf8 } } internal static var customPublicKeyAlgorithms: [NIOSSHPublicKeyProtocol.Type] { - return _CustomAlgorithms.publicKeyAlgorithmsLock.withLock { - return _CustomAlgorithms.publicKeyAlgorithms + _CustomAlgorithms.publicKeyAlgorithmsLock.withLock { + _CustomAlgorithms.publicKeyAlgorithms } } internal static var customSignatures: [NIOSSHSignatureProtocol.Type] { - return _CustomAlgorithms.signaturesLock.withLock { - return _CustomAlgorithms.signatures + _CustomAlgorithms.signaturesLock.withLock { + _CustomAlgorithms.signatures } } } @@ -224,7 +224,7 @@ public enum NIOSSHAlgorithms { } } } - + public static func register(transportProtectionScheme type: NIOSSHTransportProtection.Type) { _CustomAlgorithms.transportProtectionSchemesLock.withLockVoid { if !_CustomAlgorithms.transportProtectionSchemes.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(type) }) { @@ -232,7 +232,7 @@ public enum NIOSSHAlgorithms { } } } - + /// Registers a custom type tuple for use in Public Key Authentication. public static func register< PublicKey: NIOSSHPublicKeyProtocol, @@ -248,7 +248,7 @@ public enum NIOSSHAlgorithms { } } } - + /// Used for our unit tests internal static func unregisterAlgorithms() { _CustomAlgorithms.transportProtectionSchemesLock.withLockVoid { @@ -267,18 +267,18 @@ public enum NIOSSHAlgorithms { } internal var customTransportProtectionSchemes: [NIOSSHTransportProtection.Type] { - return _CustomAlgorithms.transportProtectionSchemesLock.withLock { - return _CustomAlgorithms.transportProtectionSchemes + _CustomAlgorithms.transportProtectionSchemesLock.withLock { + _CustomAlgorithms.transportProtectionSchemes } } internal var customKeyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { - return _CustomAlgorithms.keyExchangeAlgorithmsLock.withLock { - return _CustomAlgorithms.keyExchangeAlgorithms + _CustomAlgorithms.keyExchangeAlgorithmsLock.withLock { + _CustomAlgorithms.keyExchangeAlgorithms } } -fileprivate enum _CustomAlgorithms { +private enum _CustomAlgorithms { static var transportProtectionSchemesLock = Lock() static var transportProtectionSchemes = [NIOSSHTransportProtection.Type]() static var keyExchangeAlgorithmsLock = Lock() @@ -347,12 +347,12 @@ extension NIOSSHPublicKey.BackingKey: Hashable { extension NIOSSHPublicKey { @discardableResult public func write(to buffer: inout ByteBuffer) -> Int { - return buffer.writeSSHHostKey(self) + buffer.writeSSHHostKey(self) } - + @discardableResult public func writeWithoutHeader(to buffer: inout ByteBuffer) -> Int { - return buffer.writeSSHHostKeyWithoutHeader(self) + buffer.writeSSHHostKeyWithoutHeader(self) } } @@ -374,7 +374,7 @@ extension ByteBuffer { return self.writeCertifiedKey(key) } } - + /// Writes an SSH host key to this `ByteBuffer`. @discardableResult mutating func writeSSHHostKey(_ key: NIOSSHPublicKey) -> Int { @@ -455,7 +455,7 @@ extension ByteBuffer { return NIOSSHPublicKey(backingKey: .custom(publicKey)) } } - + // We don't know this public key type. Maybe the certified keys do. return try buffer.readCertifiedKeyWithoutKeyPrefix(keyIdentifierBytes).map(NIOSSHPublicKey.init) } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift index c7173401..2317d298 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift @@ -240,7 +240,7 @@ extension ByteBuffer { return NIOSSHSignature(backingSignature: .custom(signature)) } } - + // We don't know this signature type. let signature = signatureIdentifierBytes.readString(length: signatureIdentifierBytes.readableBytes) ?? "" throw NIOSSHError.unknownSignature(algorithm: signature) diff --git a/Sources/NIOSSH/Role.swift b/Sources/NIOSSH/Role.swift index c3048a2a..a02fb865 100644 --- a/Sources/NIOSSH/Role.swift +++ b/Sources/NIOSSH/Role.swift @@ -34,7 +34,7 @@ public enum SSHConnectionRole { return true } } - + internal var transportProtectionSchemes: [NIOSSHTransportProtection.Type] { switch self { case .client(let client): @@ -43,11 +43,11 @@ public enum SSHConnectionRole { return server.transportProtectionSchemes } } - + internal var keyExchangeAlgorithmNames: [Substring] { self.keyExchangeAlgorithms.flatMap { $0.keyExchangeAlgorithmNames } } - + internal var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] { switch self { case .client(let client): diff --git a/Sources/NIOSSH/SSHClientConfiguration.swift b/Sources/NIOSSH/SSHClientConfiguration.swift index e1df1070..233c54f7 100644 --- a/Sources/NIOSSH/SSHClientConfiguration.swift +++ b/Sources/NIOSSH/SSHClientConfiguration.swift @@ -22,10 +22,10 @@ public struct SSHClientConfiguration { /// The global request delegate to be used with this client. public var globalRequestDelegate: GlobalRequestDelegate - + /// The enabled TransportProtectionSchemes public var transportProtectionSchemes: [NIOSSHTransportProtection.Type] = SSHConnectionStateMachine.bundledTransportProtectionSchemes - + /// The enabled KeyExchangeAlgorithms public var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations diff --git a/Sources/NIOSSH/SSHPacketParser.swift b/Sources/NIOSSH/SSHPacketParser.swift index 1fbe0307..dcbc9dc6 100644 --- a/Sources/NIOSSH/SSHPacketParser.swift +++ b/Sources/NIOSSH/SSHPacketParser.swift @@ -74,7 +74,7 @@ struct SSHPacketParser { if let length = self.buffer.getInteger(at: self.buffer.readerIndex, as: UInt32.self) { if let message = try self.parsePlaintext(length: length) { self.state = .cleartextWaitingForLength - sequenceNumber = sequenceNumber &+ 1 + self.sequenceNumber = self.sequenceNumber &+ 1 return message } self.state = .cleartextWaitingForBytes(length) @@ -84,7 +84,7 @@ struct SSHPacketParser { case .cleartextWaitingForBytes(let length): if let message = try self.parsePlaintext(length: length) { self.state = .cleartextWaitingForLength - sequenceNumber = sequenceNumber &+ 1 + self.sequenceNumber = self.sequenceNumber &+ 1 return message } return nil @@ -95,7 +95,7 @@ struct SSHPacketParser { if let message = try self.parseCiphertext(length: length, protection: protection) { self.state = .encryptedWaitingForLength(protection) - sequenceNumber = sequenceNumber &+ 1 + self.sequenceNumber = self.sequenceNumber &+ 1 return message } self.state = .encryptedWaitingForBytes(length, protection) @@ -103,7 +103,7 @@ struct SSHPacketParser { case .encryptedWaitingForBytes(let length, let protection): if let message = try self.parseCiphertext(length: length, protection: protection) { self.state = .encryptedWaitingForLength(protection) - sequenceNumber = sequenceNumber &+ 1 + self.sequenceNumber = self.sequenceNumber &+ 1 return message } return nil diff --git a/Sources/NIOSSH/SSHPacketSerializer.swift b/Sources/NIOSSH/SSHPacketSerializer.swift index 092d0491..af39db96 100644 --- a/Sources/NIOSSH/SSHPacketSerializer.swift +++ b/Sources/NIOSSH/SSHPacketSerializer.swift @@ -79,11 +79,11 @@ struct SSHPacketSerializer { buffer.setInteger(UInt8(paddingLength), at: index + 4) /// random padding buffer.writeSSHPaddingBytes(count: paddingLength) - sequenceNumber = sequenceNumber &+ 1 + self.sequenceNumber = self.sequenceNumber &+ 1 case .encrypted(let protection): let payload = NIOSSHEncryptablePayload(message: message) - try protection.encryptPacket(payload, to: &buffer, sequenceNumber: sequenceNumber) - sequenceNumber = sequenceNumber &+ 1 + try protection.encryptPacket(payload, to: &buffer, sequenceNumber: self.sequenceNumber) + self.sequenceNumber = self.sequenceNumber &+ 1 } } } diff --git a/Sources/NIOSSH/SSHServerConfiguration.swift b/Sources/NIOSSH/SSHServerConfiguration.swift index 460d8fe1..c2b1c613 100644 --- a/Sources/NIOSSH/SSHServerConfiguration.swift +++ b/Sources/NIOSSH/SSHServerConfiguration.swift @@ -25,13 +25,13 @@ public struct SSHServerConfiguration { /// The ssh banner to display to clients upon authentication public var banner: UserAuthBanner? - + /// The enabled TransportProtectionSchemes public var transportProtectionSchemes: [NIOSSHTransportProtection.Type] = SSHConnectionStateMachine.bundledTransportProtectionSchemes - + /// The enabled KeyExchangeAlgorithms public var keyExchangeAlgorithms: [NIOSSHKeyExchangeAlgorithmProtocol.Type] = SSHKeyExchangeStateMachine.bundledKeyExchangeImplementations - + public init(hostKeys: [NIOSSHPrivateKey], userAuthDelegate: NIOSSHServerUserAuthenticationDelegate, globalRequestDelegate: GlobalRequestDelegate? = nil, banner: UserAuthBanner? = nil) { self.hostKeys = hostKeys self.userAuthDelegate = userAuthDelegate diff --git a/Tests/NIOSSHTests/EndToEndTests.swift b/Tests/NIOSSHTests/EndToEndTests.swift index 3d04796b..b517d69f 100644 --- a/Tests/NIOSSHTests/EndToEndTests.swift +++ b/Tests/NIOSSHTests/EndToEndTests.swift @@ -22,23 +22,23 @@ enum EndToEndTestError: Error { case unableToCreateChildChannel, invalidCustomPublicKey, invalidCustomSignature } -fileprivate let testKey = Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]) +private let testKey = Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]) final class CustomTransportProtection: NIOSSHTransportProtection { static let cipherName = "xor-with-42" static let macName: String? = "insecure-sha1" static var wasUsed = false - + static var keySizes: ExpectedKeySizes { .init(ivSize: 19, encryptionKeySize: 17, macKeySize: 15) } - + required init(initialKeys: NIOSSHSessionKeys) throws {} - + static var cipherBlockSize: Int { 18 } var macBytes: Int { 20 } - func updateKeys(_ newKeys: NIOSSHSessionKeys) throws {} + func updateKeys(_: NIOSSHSessionKeys) throws {} func decryptFirstBlock(_: inout ByteBuffer) throws { // For us, decrypting the first block is very easy: do nothing. The length bytes are already @@ -55,37 +55,37 @@ final class CustomTransportProtection: NIOSSHTransportProtection { let lengthView: UInt32 = source.readInteger(), var ciphertext = source.readData(length: Int(lengthView)), let mac = source.readData(length: Insecure.SHA1.byteCount), - ciphertext.count > 0, ciphertext.count % Self.cipherBlockSize == 0 else { + ciphertext.count > 0, ciphertext.count % Self.cipherBlockSize == 0 else { // The only way this fails is if the payload doesn't match this encryption scheme. throw NIOSSHError.invalidEncryptedPacketLength } - for i in 0..= plaintext.count { throw NIOSSHError.invalidDecryptedPlaintextLength } - + // All good! A quick soundness check to verify that the length of the plaintext is ok. guard plaintext.count % Self.cipherBlockSize == 0, plaintext.count == ciphertext.count else { throw NIOSSHError.invalidDecryptedPlaintextLength } - + // Remove padding plaintext.removeFirst() plaintext.removeLast(Int(paddingBytes)) - + return ByteBuffer(data: plaintext) } @@ -135,9 +135,9 @@ final class CustomTransportProtection: NIOSSHTransportProtection { // our math was wrong and we cannot recover. let plaintext = outboundBuffer.getBytes(at: packetPaddingIndex, length: encryptedBufferSize)! let hash = Insecure.SHA1.hash(data: plaintext) - + var ciphertext = plaintext - for i in 0.. [UInt8] { - return Array(testKey.reversed()) + Array(testKey.reversed()) } - - func signature(for data: D) throws -> NIOSSHSignatureProtocol where D : DataProtocol { + + func signature(for data: D) throws -> NIOSSHSignatureProtocol where D: DataProtocol { var data = Data(data) - + let testKeySize = testKey.count - for i in 0.. Int { - buffer.writeSSHString(rawRepresentation) + buffer.writeSSHString(self.rawRepresentation) } - + static func read(from buffer: inout ByteBuffer) throws -> CustomSignature { guard var buffer = buffer.readSSHString() else { throw EndToEndTestError.invalidCustomSignature } - + let data = buffer.readData(length: buffer.readableBytes)! return CustomSignature(rawRepresentation: data) } @@ -195,47 +195,47 @@ struct CustomPublicKey: NIOSSHPublicKeyProtocol { static let publicKeyPrefix = "custom-prefix" static let keyExchangeAlgorithmNames: [Substring] = ["custom-handshake"] static var wasUsed = false - - func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D : DataProtocol { + + func isValidSignature(_ signature: NIOSSHSignatureProtocol, for data: D) -> Bool where D: DataProtocol { let testKeySize = testKey.count var data = Data(data) - for i in 0.. Int { - return 0 + 0 } - + var rawRepresentation: Data { testKey } - + static func read(from buffer: inout ByteBuffer) throws -> CustomPublicKey { guard buffer.readableBytes == 0 else { throw EndToEndTestError.invalidCustomPublicKey } - - wasUsed = true + + self.wasUsed = true return CustomPublicKey() } } struct CustomKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { - static var keyExchangeInitMessageId: UInt8 { 0xff } - static var keyExchangeReplyMessageId: UInt8 { 0xff } + static var keyExchangeInitMessageId: UInt8 { 0xFF } + static var keyExchangeReplyMessageId: UInt8 { 0xFF } static var wasUsed = false - + private var previousSessionIdentifier: ByteBuffer? private var ourKey: CustomPrivateKey private var theirKey: CustomPublicKey? private var ourRole: SSHConnectionRole private var sharedSecret: [UInt8]? - + init(ourRole: SSHConnectionRole, previousSessionIdentifier: ByteBuffer?) { self.ourRole = ourRole self.ourKey = CustomPrivateKey() @@ -244,7 +244,7 @@ struct CustomKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { func initiateKeyExchangeClientSide(allocator: ByteBufferAllocator) -> ByteBuffer { var buffer = ByteBuffer() - _ = ourKey.publicKey.write(to: &buffer) + _ = self.ourKey.publicKey.write(to: &buffer) return buffer } @@ -258,7 +258,7 @@ struct CustomKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { var theirKeyBuffer = message let theirKey = try CustomPublicKey.read(from: &theirKeyBuffer) self.theirKey = theirKey - + // Shared secet is "expanded" // That should make it usable by most transport encryption, at least the one used in our test var sharedSecret = try self.ourKey.generatedSharedSecret(with: theirKey) @@ -266,42 +266,42 @@ struct CustomKeyExchange: NIOSSHKeyExchangeAlgorithmProtocol { sharedSecret += sharedSecret sharedSecret += sharedSecret sharedSecret += sharedSecret - + self.sharedSecret = sharedSecret - + var hasher = SHA512() hasher.update(data: initialExchangeBytes.readableBytesView) hasher.update(data: sharedSecret) - + let exchangeHash = hasher.finalize() - + let sessionID: ByteBuffer if let previousSessionIdentifier = self.previousSessionIdentifier { sessionID = previousSessionIdentifier } else { sessionID = ByteBuffer(bytes: SHA512.hash(data: Data(exchangeHash))) } - + let kexResult = KeyExchangeResult( sessionID: sessionID, keys: NIOSSHSessionKeys( - initialInboundIV: Array(sharedSecret[0..(_ body: @autoclosure () throws -> T, defaultValue: T? = nil, message: String? = nil, file: StaticString = #file, line: UInt = #line) throws -> T { @@ -33,7 +33,7 @@ func assertNoThrowWithValue(_ body: @autoclosure () throws -> T, defaultValue extension SSHKeyExchangeStateMachine { mutating func handle(keyExchangeInit message: SSHMessage.KeyExchangeECDHInitMessage) throws -> SSHMultiMessage? { - return try handle(keyExchangeInit: message.publicKey) + try self.handle(keyExchangeInit: message.publicKey) } } From 4b0e7ec878ca02ffa4bf109c88f06417c364734a Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 23 May 2022 04:36:41 -0500 Subject: [PATCH 20/20] Fix copyright header year --- Sources/NIOSSH/Keys And Signatures/CustomKeys.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift index b5980d27..c1d86b93 100644 --- a/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift +++ b/Sources/NIOSSH/Keys And Signatures/CustomKeys.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) YEARS Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information