From fd014b82cabfd063daaee893ff7e4119c82a0db9 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Mon, 11 Sep 2023 18:24:29 +0200 Subject: [PATCH 1/3] Separate public/private keys and signatures into protocol conformances --- .../CryptoKit+PrivateKeyProtocols.swift | 141 ++++++++ .../CryptoKit+PublicKeyProtocols.swift | 179 ++++++++++ .../NIOSSHCertifiedPublicKey.swift | 196 +++++++---- .../NIOSSHPrivateKey.swift | 111 +----- .../NIOSSHPrivateKeyProtocol.swift | 35 ++ .../Keys And Signatures/NIOSSHPublicKey.swift | 328 +++--------------- .../NIOSSHPublicKeyProtocol.swift | 39 +++ .../Keys And Signatures/NIOSSHSignature.swift | 151 ++------ .../NIOSSHSignatureProtocol.swift | 44 +++ Sources/NIOSSH/NIOSSHError.swift | 5 + .../UserAuthenticationMethod.swift | 2 +- Tests/NIOSSHTests/CertifiedKeyTests.swift | 6 +- .../SSHKeyExchangeStateMachineTests.swift | 2 +- .../SSHPackerSerializerTests.swift | 12 +- .../UserAuthenticationStateMachineTests.swift | 4 +- 15 files changed, 686 insertions(+), 569 deletions(-) create mode 100644 Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift create mode 100644 Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift create mode 100644 Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift create mode 100644 Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKeyProtocol.swift create mode 100644 Sources/NIOSSH/Keys And Signatures/NIOSSHSignatureProtocol.swift diff --git a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift new file mode 100644 index 0000000..b02b99e --- /dev/null +++ b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 NIOCore +import CryptoKit + +struct Curve25519Signature: NIOSSHSignatureProtocol { + static let signaturePrefix = "ssh-ed25519" + + let rawRepresentation: Data + + func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeEd25519Signature(signature: self) + } + + static func read(from buffer: inout ByteBuffer) throws -> Self { + guard var rawRepresentation = buffer.readSSHString() else { + throw NIOSSHError.invalidSSHSignature + } + + return Self(rawRepresentation: rawRepresentation.readData(length: rawRepresentation.readableBytes)!) + } +} + +extension Curve25519.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { + public static var keyPrefix: String { "ssh-ed25519" } + + public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } + + public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { + return try Curve25519Signature(rawRepresentation: self.signature(for: data)) + } +} + +extension P256.Signing.ECDSASignature: NIOSSHSignatureProtocol { + public static var signaturePrefix: String { "ecdsa-sha2-nistp256" } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeECDSAP256Signature(baseSignature: self) + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self { + guard let rawRepresentation = buffer.readSSHString() else { + throw NIOSSHError.invalidSSHSignature + } + + return try Self(rawRepresentation: rawRepresentation.readableBytesView) + } +} + +extension P256.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { + public static var keyPrefix: String { "ecdsa-sha2-nistp256" } + + public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } + + public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { + let signature: P256.Signing.ECDSASignature = try self.signature(for: data) + return signature + } +} + +extension P384.Signing.ECDSASignature: NIOSSHSignatureProtocol { + public static var signaturePrefix: String { "ecdsa-sha2-nistp384" } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeECDSAP384Signature(baseSignature: self) + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self { + guard let rawRepresentation = buffer.readSSHString() else { + throw NIOSSHError.invalidSSHSignature + } + + return try Self(rawRepresentation: rawRepresentation.readableBytesView) + } +} + +extension P384.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { + public static var keyPrefix: String { "ecdsa-sha2-nistp384" } + + public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } + + public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { + let signature: P384.Signing.ECDSASignature = try self.signature(for: data) + return signature + } +} + +extension P521.Signing.ECDSASignature: NIOSSHSignatureProtocol { + public static var signaturePrefix: String { "ecdsa-sha2-nistp521" } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeECDSAP521Signature(baseSignature: self) + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self { + guard let rawRepresentation = buffer.readSSHString() else { + throw NIOSSHError.invalidSSHSignature + } + + return try Self(rawRepresentation: rawRepresentation.readableBytesView) + } +} + +extension P521.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { + public static var keyPrefix: String { + "ecdsa-sha2-nistp521" + } + + public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } + + public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { + let signature: P521.Signing.ECDSASignature = try self.signature(for: data) + return signature + } +} + +#if canImport(Darwin) +extension SecureEnclave.P256.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { + public static var keyPrefix: String { P256.Signing.PrivateKey.keyPrefix } + + public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } + + public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { + let signature: P256.Signing.ECDSASignature = try self.signature(for: data) + return signature + } +} +#endif \ No newline at end of file diff --git a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift new file mode 100644 index 0000000..59e2a52 --- /dev/null +++ b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 NIOCore +import CryptoKit + +extension Curve25519.Signing.PublicKey: NIOSSHPublicKeyProtocol { + internal static var prefix: String { "ssh-ed25519" } + public static var publicKeyPrefix: String? { Self.prefix } + public var publicKeyPrefix: String { Self.prefix } + + public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { + guard let signature = signature.backingSignature as? Curve25519Signature else { + return false + } + + return self.isValidSignature(signature.rawRepresentation, for: data) + } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeEd25519PublicKey(baseKey: self) + } + + public func writeHostKey(to buffer: inout ByteBuffer) -> Int { + var writtenBytes = 0 + writtenBytes += buffer.writeSSHString(self.publicKeyPrefix.utf8) + writtenBytes += self.write(to: &buffer) + return writtenBytes + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self? { + guard let qBytes = buffer.readSSHString() else { + return nil + } + + return try Curve25519.Signing.PublicKey(rawRepresentation: qBytes.readableBytesView) + } +} + +extension P256.Signing.PublicKey: NIOSSHPublicKeyProtocol { + internal static var prefix: String { "ecdsa-sha2-nistp256" } + public static var publicKeyPrefix: String? { Self.prefix } + public var publicKeyPrefix: String { Self.prefix } + + public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { + guard let signature = signature.backingSignature as? P256.Signing.ECDSASignature else { + return false + } + + return self.isValidSignature(signature, for: data) + } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeECDSAP256PublicKey(baseKey: self) + } + + public func writeHostKey(to buffer: inout ByteBuffer) -> Int { + var writtenBytes = 0 + writtenBytes += buffer.writeSSHString(self.publicKeyPrefix.utf8) + writtenBytes += self.write(to: &buffer) + return writtenBytes + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self? { + // For ECDSA-P256, the key format is the string "nistp256" followed by the + // the public point Q. + guard var domainParameter = buffer.readSSHString() else { + return nil + } + guard domainParameter.readableBytesView.elementsEqual("nistp256".utf8) else { + let unexpectedParameter = domainParameter.readString(length: domainParameter.readableBytes) ?? "" + throw NIOSSHError.invalidDomainParametersForKey(parameters: unexpectedParameter) + } + + guard let qBytes = buffer.readSSHString() else { + return nil + } + + return try P256.Signing.PublicKey(x963Representation: qBytes.readableBytesView) + } +} + +extension P384.Signing.PublicKey: NIOSSHPublicKeyProtocol { + internal static var prefix: String { "ecdsa-sha2-nistp384" } + public static var publicKeyPrefix: String? { Self.prefix } + public var publicKeyPrefix: String { Self.prefix } + + public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { + guard let signature = signature.backingSignature as? P384.Signing.ECDSASignature else { + return false + } + + return self.isValidSignature(signature, for: data) + } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeECDSAP384PublicKey(baseKey: self) + } + + public func writeHostKey(to buffer: inout ByteBuffer) -> Int { + var writtenBytes = 0 + writtenBytes += buffer.writeSSHString(self.publicKeyPrefix.utf8) + writtenBytes += self.write(to: &buffer) + return writtenBytes + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self? { + // For ECDSA-P384, the key format is the string "nistp384" followed by the + // the public point Q. + guard var domainParameter = buffer.readSSHString() else { + return nil + } + guard domainParameter.readableBytesView.elementsEqual("nistp384".utf8) else { + let unexpectedParameter = domainParameter.readString(length: domainParameter.readableBytes) ?? "" + throw NIOSSHError.invalidDomainParametersForKey(parameters: unexpectedParameter) + } + + guard let qBytes = buffer.readSSHString() else { + return nil + } + + return try P384.Signing.PublicKey(x963Representation: qBytes.readableBytesView) + } +} + +extension P521.Signing.PublicKey: NIOSSHPublicKeyProtocol { + internal static var prefix: String { "ecdsa-sha2-nistp521" } + public static var publicKeyPrefix: String? { Self.prefix } + public var publicKeyPrefix: String { Self.prefix } + + public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { + guard let signature = signature.backingSignature as? P521.Signing.ECDSASignature else { + return false + } + + return self.isValidSignature(signature, for: data) + } + + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeECDSAP521PublicKey(baseKey: self) + } + + public func writeHostKey(to buffer: inout ByteBuffer) -> Int { + var writtenBytes = 0 + writtenBytes += buffer.writeSSHString(self.publicKeyPrefix.utf8) + writtenBytes += self.write(to: &buffer) + return writtenBytes + } + + public static func read(from buffer: inout ByteBuffer) throws -> Self? { + // For ECDSA-P521, the key format is the string "nistp521" followed by the + // the public point Q. + guard var domainParameter = buffer.readSSHString() else { + return nil + } + guard domainParameter.readableBytesView.elementsEqual("nistp521".utf8) else { + let unexpectedParameter = domainParameter.readString(length: domainParameter.readableBytes) ?? "" + throw NIOSSHError.invalidDomainParametersForKey(parameters: unexpectedParameter) + } + + guard let qBytes = buffer.readSSHString() else { + return nil + } + + return try P521.Signing.PublicKey(x963Representation: qBytes.readableBytesView) + } +} \ No newline at end of file diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift index 0ca6158..71c6544 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift @@ -15,6 +15,7 @@ import Crypto import Dispatch import NIOCore +import Foundation #if canImport(Darwin) import Darwin #else @@ -44,6 +45,74 @@ import Glibc /// to check at runtime whether a given ``NIOSSHPublicKey`` is _actually_ a ``NIOSSHCertifiedPublicKey``, and allows /// users that have a ``NIOSSHCertifiedPublicKey`` to use it as though it were a ``NIOSSHPublicKey``. public struct NIOSSHCertifiedPublicKey { + /// The various key types that can be used with NIOSSHCertifiedPublicKey. + internal enum SupportedKey: Hashable, Sendable { + case ed25519(Curve25519.Signing.PublicKey) + case ecdsaP256(P256.Signing.PublicKey) + case ecdsaP384(P384.Signing.PublicKey) + case ecdsaP521(P521.Signing.PublicKey) + + init(_ key: NIOSSHPublicKey) throws { + switch key.backingKey { + case let key as Curve25519.Signing.PublicKey: + self = .ed25519(key) + case let key as P256.Signing.PublicKey: + self = .ecdsaP256(key) + case let key as P384.Signing.PublicKey: + self = .ecdsaP384(key) + case let key as P521.Signing.PublicKey: + self = .ecdsaP521(key) + default: + throw NIOSSHError.invalidCertificate(diagnostics: "Unsupported key type") + } + } + + var publicKey: NIOSSHPublicKeyProtocol { + switch self { + case .ed25519(let key): + return key + case .ecdsaP256(let key): + return key + case .ecdsaP384(let key): + return key + case .ecdsaP521(let key): + return key + } + } + + public static func ==(lhs: SupportedKey, rhs: SupportedKey) -> Bool { + switch (lhs, rhs) { + case (.ed25519(let lhs), .ed25519(let rhs)): + return lhs.rawRepresentation == rhs.rawRepresentation + case (.ecdsaP256(let lhs), .ecdsaP256(let rhs)): + return lhs.rawRepresentation == rhs.rawRepresentation + case (.ecdsaP384(let lhs), .ecdsaP384(let rhs)): + return lhs.rawRepresentation == rhs.rawRepresentation + case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): + return lhs.rawRepresentation == rhs.rawRepresentation + default: + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .ed25519(let key): + hasher.combine(1) + hasher.combine(key.rawRepresentation) + case .ecdsaP256(let key): + hasher.combine(2) + hasher.combine(key.rawRepresentation) + case .ecdsaP384(let key): + hasher.combine(3) + hasher.combine(key.rawRepresentation) + case .ecdsaP521(let key): + hasher.combine(4) + hasher.combine(key.rawRepresentation) + } + } + } + /// A CA-provided random bitstring of arbitrary length (typically 16 or 32 bytes). This defends against /// hash-collision attacks. public var nonce: ByteBuffer { @@ -82,11 +151,20 @@ public struct NIOSSHCertifiedPublicKey { /// The base public key associated with this certified public key. public var key: NIOSSHPublicKey { get { - self.backing.key + switch self.backing.key { + case .ed25519(let key): + return NIOSSHPublicKey(backingKey: key) + case .ecdsaP256(let key): + return NIOSSHPublicKey(backingKey: key) + case .ecdsaP384(let key): + return NIOSSHPublicKey(backingKey: key) + case .ecdsaP521(let key): + return NIOSSHPublicKey(backingKey: key) + } } set { self.ensureUniqueStorage() - self.backing.key = newValue + self.backing.key = try! SupportedKey(newValue) } } @@ -182,11 +260,11 @@ public struct NIOSSHCertifiedPublicKey { /// The public key corresponding to the private key used to sign this ``NIOSSHCertifiedPublicKey``. public var signatureKey: NIOSSHPublicKey { get { - self.backing.signatureKey + NIOSSHPublicKey(backingKey: self.backing.signatureKey.publicKey) } set { self.ensureUniqueStorage() - self.backing.signatureKey = newValue + self.backing.signatureKey = try! SupportedKey(newValue) } } @@ -219,14 +297,14 @@ public struct NIOSSHCertifiedPublicKey { self.backing = try Backing(nonce: nonce, serial: serial, type: type, - key: key, + key: SupportedKey(key), keyID: keyID, validPrincipals: validPrincipals, validAfter: validAfter, validBefore: validBefore, criticalOptions: criticalOptions, extensions: extensions, - signatureKey: signatureKey, + signatureKey: SupportedKey(signatureKey), signature: signature) } @@ -234,11 +312,11 @@ public struct NIOSSHCertifiedPublicKey { /// /// Not all public keys are certified, so this method will fail if the key is not. public init?(_ key: NIOSSHPublicKey) { - guard case .certified(let base) = key.backingKey else { + guard let key = key.backingKey as? NIOSSHCertifiedPublicKey else { return nil } - self = base + self = key } } @@ -320,16 +398,50 @@ extension NIOSSHCertifiedPublicKey { return bytes } - static let p256KeyPrefix = "ecdsa-sha2-nistp256-cert-v01@openssh.com".utf8 + static let p256KeyPrefix = "ecdsa-sha2-nistp256-cert-v01@openssh.com" - static let p384KeyPrefix = "ecdsa-sha2-nistp384-cert-v01@openssh.com".utf8 + static let p384KeyPrefix = "ecdsa-sha2-nistp384-cert-v01@openssh.com" - static let p521KeyPrefix = "ecdsa-sha2-nistp521-cert-v01@openssh.com".utf8 + static let p521KeyPrefix = "ecdsa-sha2-nistp521-cert-v01@openssh.com" - static let ed25519KeyPrefix = "ssh-ed25519-cert-v01@openssh.com".utf8 + static let ed25519KeyPrefix = "ssh-ed25519-cert-v01@openssh.com" internal var keyPrefix: String.UTF8View { - switch self.key.backingKey { + switch self.backing.key { + case .ed25519: + return Self.ed25519KeyPrefix.utf8 + case .ecdsaP256: + return Self.p256KeyPrefix.utf8 + case .ecdsaP384: + return Self.p384KeyPrefix.utf8 + case .ecdsaP521: + return Self.p521KeyPrefix.utf8 + } + } + + public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { + self.key.isValidSignature(signature, for: data) + } + + internal static func baseKeyPrefixForKeyPrefix(_ prefix: Bytes) throws -> String.UTF8View where Bytes.Element == UInt8 { + if prefix.elementsEqual(Self.ed25519KeyPrefix.utf8) { + return Curve25519.Signing.PublicKey.prefix.utf8 + } else if prefix.elementsEqual(Self.p256KeyPrefix.utf8) { + return P256.Signing.PublicKey.prefix.utf8 + } else if prefix.elementsEqual(Self.p384KeyPrefix.utf8) { + return P384.Signing.PublicKey.prefix.utf8 + } else if prefix.elementsEqual(Self.p521KeyPrefix.utf8) { + return P521.Signing.PublicKey.prefix.utf8 + } else { + throw NIOSSHError.unknownPublicKey(algorithm: String(decoding: prefix, as: UTF8.self)) + } + } +} + +extension NIOSSHCertifiedPublicKey: NIOSSHPublicKeyProtocol { + public static var publicKeyPrefix: String? { nil } + public var publicKeyPrefix: String { + switch self.backing.key { case .ed25519: return Self.ed25519KeyPrefix case .ecdsaP256: @@ -338,35 +450,20 @@ extension NIOSSHCertifiedPublicKey { return Self.p384KeyPrefix case .ecdsaP521: return Self.p521KeyPrefix - case .certified: - preconditionFailure("base key cannot be certified") } } + public var rawRepresentation: Data { key.backingKey.rawRepresentation } - internal func isValidSignature(_ signature: NIOSSHSignature, for digest: DigestBytes) -> Bool { - self.key.isValidSignature(signature, for: digest) - } - - internal func isValidSignature(_ signature: NIOSSHSignature, for bytes: ByteBuffer) -> Bool { - self.key.isValidSignature(signature, for: bytes) + public func write(to buffer: inout ByteBuffer) -> Int { + buffer.writeCertifiedKey(self) } - internal func isValidSignature(_ signature: NIOSSHSignature, for payload: UserAuthSignablePayload) -> Bool { - self.key.isValidSignature(signature, for: payload) + public func writeHostKey(to buffer: inout ByteBuffer) -> Int { + buffer.writeCertifiedKey(self) } - internal static func baseKeyPrefixForKeyPrefix(_ prefix: Bytes) throws -> String.UTF8View where Bytes.Element == UInt8 { - if prefix.elementsEqual(Self.ed25519KeyPrefix) { - return NIOSSHPublicKey.ed25519PublicKeyPrefix - } else if prefix.elementsEqual(Self.p256KeyPrefix) { - return NIOSSHPublicKey.ecdsaP256PublicKeyPrefix - } else if prefix.elementsEqual(Self.p384KeyPrefix) { - return NIOSSHPublicKey.ecdsaP384PublicKeyPrefix - } else if prefix.elementsEqual(Self.p521KeyPrefix) { - return NIOSSHPublicKey.ecdsaP521PublicKeyPrefix - } else { - throw NIOSSHError.unknownPublicKey(algorithm: String(decoding: prefix, as: UTF8.self)) - } + public static func read(from buffer: inout ByteBuffer) -> NIOSSHCertifiedPublicKey? { + try? buffer.readCertifiedKey() } } @@ -456,13 +553,7 @@ extension NIOSSHCertifiedPublicKey { fileprivate var nonce: ByteBuffer fileprivate var serial: UInt64 fileprivate var type: CertificateType - fileprivate var key: NIOSSHPublicKey { - willSet { - if case .certified = newValue.backingKey { - preconditionFailure("Certificate may not use certified key as public key") - } - } - } + fileprivate var key: SupportedKey fileprivate var keyID: String fileprivate var validPrincipals: [String] @@ -470,37 +561,22 @@ extension NIOSSHCertifiedPublicKey { fileprivate var validBefore: UInt64 fileprivate var criticalOptions: [String: String] fileprivate var extensions: [String: String] - fileprivate var signatureKey: NIOSSHPublicKey { - willSet { - if case .certified = newValue.backingKey { - preconditionFailure("Certificate may not be signed by certified key") - } - } - } + fileprivate var signatureKey: SupportedKey fileprivate var signature: NIOSSHSignature fileprivate init(nonce: ByteBuffer, serial: UInt64, type: CertificateType, - key: NIOSSHPublicKey, + key: SupportedKey, keyID: String, validPrincipals: [String], validAfter: UInt64, validBefore: UInt64, criticalOptions: [String: String], extensions: [String: String], - signatureKey: NIOSSHPublicKey, + signatureKey: SupportedKey, signature: NIOSSHSignature) throws { - // These two contrains are _very important_: without them, a number of NIOSSHPublicKey operations become infinitely - // recursive. - if case .certified = key.backingKey { - throw NIOSSHError.invalidCertificate(diagnostics: "Certificate may not use certified key as public key") - } - if case .certified = signatureKey.backingKey { - throw NIOSSHError.invalidCertificate(diagnostics: "Certificate may not be signed by certified key") - } - self.nonce = nonce self.serial = serial self.type = type diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift index 6618d27..db0377b 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift @@ -24,140 +24,57 @@ import NIOCore /// Users cannot do much with this key other than construct it, but NIO uses it internally. public struct NIOSSHPrivateKey: Sendable { /// The actual key structure used to perform the key operations. - internal var backingKey: BackingKey + internal var backingKey: NIOSSHPrivateKeyProtocol - private init(backingKey: BackingKey) { + private init(backingKey: NIOSSHPrivateKeyProtocol) { self.backingKey = backingKey } public init(ed25519Key key: Curve25519.Signing.PrivateKey) { - self.backingKey = .ed25519(key) + self.backingKey = key } public init(p256Key key: P256.Signing.PrivateKey) { - self.backingKey = .ecdsaP256(key) + self.backingKey = key } public init(p384Key key: P384.Signing.PrivateKey) { - self.backingKey = .ecdsaP384(key) + self.backingKey = key } public init(p521Key key: P521.Signing.PrivateKey) { - self.backingKey = .ecdsaP521(key) + self.backingKey = key } #if canImport(Darwin) public init(secureEnclaveP256Key key: SecureEnclave.P256.Signing.PrivateKey) { - self.backingKey = .secureEnclaveP256(key) + self.backingKey = key } #endif // The algorithms that apply to this host key. internal var hostKeyAlgorithms: [Substring] { - switch self.backingKey { - case .ed25519: - return ["ssh-ed25519"] - case .ecdsaP256: - return ["ecdsa-sha2-nistp256"] - case .ecdsaP384: - return ["ecdsa-sha2-nistp384"] - case .ecdsaP521: - return ["ecdsa-sha2-nistp521"] - #if canImport(Darwin) - case .secureEnclaveP256: - return ["ecdsa-sha2-nistp256"] - #endif - } - } -} - -extension NIOSSHPrivateKey { - /// The various key types that can be used with NIOSSH. - internal enum BackingKey { - case ed25519(Curve25519.Signing.PrivateKey) - case ecdsaP256(P256.Signing.PrivateKey) - case ecdsaP384(P384.Signing.PrivateKey) - case ecdsaP521(P521.Signing.PrivateKey) - - #if canImport(Darwin) - case secureEnclaveP256(SecureEnclave.P256.Signing.PrivateKey) - #endif + return [ Substring(backingKey.keyPrefix) ] } } extension NIOSSHPrivateKey { func sign(digest: DigestBytes) throws -> NIOSSHSignature { - switch self.backingKey { - case .ed25519(let key): - let signature = try digest.withUnsafeBytes { ptr in - try key.signature(for: ptr) - } - return NIOSSHSignature(backingSignature: .ed25519(.data(signature))) - case .ecdsaP256(let key): - let signature = try digest.withUnsafeBytes { ptr in - try key.signature(for: ptr) - } - return NIOSSHSignature(backingSignature: .ecdsaP256(signature)) - case .ecdsaP384(let key): - let signature = try digest.withUnsafeBytes { ptr in - try key.signature(for: ptr) - } - return NIOSSHSignature(backingSignature: .ecdsaP384(signature)) - case .ecdsaP521(let key): - let signature = try digest.withUnsafeBytes { ptr in - try key.signature(for: ptr) - } - return NIOSSHSignature(backingSignature: .ecdsaP521(signature)) - - #if canImport(Darwin) - case .secureEnclaveP256(let key): - let signature = try digest.withUnsafeBytes { ptr in - try key.signature(for: ptr) - } - return NIOSSHSignature(backingSignature: .ecdsaP256(signature)) - #endif + let signature = try digest.withUnsafeBytes { ptr in + try backingKey.sshSignature(for: ptr) } + return NIOSSHSignature(backingSignature: signature) } func sign(_ payload: UserAuthSignablePayload) throws -> NIOSSHSignature { - switch self.backingKey { - case .ed25519(let key): - let signature = try key.signature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: .ed25519(.data(signature))) - case .ecdsaP256(let key): - let signature = try key.signature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: .ecdsaP256(signature)) - case .ecdsaP384(let key): - let signature = try key.signature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: .ecdsaP384(signature)) - case .ecdsaP521(let key): - let signature = try key.signature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: .ecdsaP521(signature)) - #if canImport(Darwin) - case .secureEnclaveP256(let key): - let signature = try key.signature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: .ecdsaP256(signature)) - #endif - } + let signature = try backingKey.sshSignature(for: payload.bytes.readableBytesView) + return NIOSSHSignature(backingSignature: signature) } } extension NIOSSHPrivateKey { /// Obtains the public key for a corresponding private key. public var publicKey: NIOSSHPublicKey { - switch self.backingKey { - case .ed25519(let privateKey): - return NIOSSHPublicKey(backingKey: .ed25519(privateKey.publicKey)) - case .ecdsaP256(let privateKey): - return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey)) - case .ecdsaP384(let privateKey): - return NIOSSHPublicKey(backingKey: .ecdsaP384(privateKey.publicKey)) - case .ecdsaP521(let privateKey): - return NIOSSHPublicKey(backingKey: .ecdsaP521(privateKey.publicKey)) - #if canImport(Darwin) - case .secureEnclaveP256(let privateKey): - return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey)) - #endif - } + NIOSSHPublicKey(backingKey: backingKey.sshPublicKey) } } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift new file mode 100644 index 0000000..ff20981 --- /dev/null +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 NIOCore + +public protocol NIOSSHPrivateKeyProtocol: Sendable { + /// 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 } + + /// A public key instance that is able to verify signatures that are created using this private key. + var sshPublicKey: 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 sshSignature(for data: D) throws -> NIOSSHSignatureProtocol +} + +internal extension NIOSSHPrivateKeyProtocol { + var keyPrefix: String { + Self.keyPrefix + } +} diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift index d58cadd..3fc4084 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKey.swift @@ -25,9 +25,9 @@ import NIOCore /// This key is not capable of signing, only verifying. public struct NIOSSHPublicKey: Sendable, Hashable { /// The actual key structure used to perform the key operations. - internal var backingKey: BackingKey + internal var backingKey: NIOSSHPublicKeyProtocol - internal init(backingKey: BackingKey) { + internal init(backingKey: NIOSSHPublicKeyProtocol) { self.backingKey = backingKey } @@ -56,12 +56,14 @@ public struct NIOSSHPublicKey: Sendable, Hashable { self = key } - /// Encapsulate a ``NIOSSHCertifiedPublicKey`` in a ``NIOSSHPublicKey``. - /// - /// This initializer can be used to "wrap" a ``NIOSSHCertifiedPublicKey`` into the interface of ``NIOSSHPublicKey``. - /// It is typically used in cases where the fact that the key is certified is not relevant. - public init(_ certifiedKey: NIOSSHCertifiedPublicKey) { - self.backingKey = .certified(certifiedKey) + public func hash(into hasher: inout Hasher) { + hasher.combine(self.backingKey.publicKeyPrefix) + hasher.combine(self.backingKey.rawRepresentation) + } + + public static func == (lhs: NIOSSHPublicKey, rhs: NIOSSHPublicKey) -> Bool { + lhs.backingKey.publicKeyPrefix == rhs.backingKey.publicKeyPrefix && + lhs.backingKey.rawRepresentation == rhs.backingKey.rawRepresentation } } @@ -69,168 +71,36 @@ 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 { - switch (self.backingKey, signature.backingSignature) { - case (.ed25519(let key), .ed25519(let sig)): - return digest.withUnsafeBytes { digestPtr in - switch sig { - case .byteBuffer(let buf): - return key.isValidSignature(buf.readableBytesView, for: digestPtr) - case .data(let d): - return key.isValidSignature(d, for: digestPtr) - } - } - case (.ecdsaP256(let key), .ecdsaP256(let sig)): - return digest.withUnsafeBytes { digestPtr in - key.isValidSignature(sig, for: digestPtr) - } - case (.ecdsaP384(let key), .ecdsaP384(let sig)): - return digest.withUnsafeBytes { digestPtr in - key.isValidSignature(sig, for: digestPtr) - } - case (.ecdsaP521(let key), .ecdsaP521(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, _): - return false + digest.withUnsafeBytes { digestptr in + self.backingKey.isValidSignature(signature, for: digestptr) } } + internal func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { + self.backingKey.isValidSignature(signature, for: data) + } + internal func isValidSignature(_ signature: NIOSSHSignature, for bytes: ByteBuffer) -> Bool { - switch (self.backingKey, signature.backingSignature) { - case (.ed25519(let key), .ed25519(.byteBuffer(let buf))): - return key.isValidSignature(buf.readableBytesView, for: bytes.readableBytesView) - case (.ed25519(let key), .ed25519(.data(let buf))): - return key.isValidSignature(buf, for: bytes.readableBytesView) - case (.ecdsaP256(let key), .ecdsaP256(let sig)): - return key.isValidSignature(sig, for: bytes.readableBytesView) - case (.ecdsaP384(let key), .ecdsaP384(let sig)): - return key.isValidSignature(sig, for: bytes.readableBytesView) - case (.ecdsaP521(let key), .ecdsaP521(let sig)): - return key.isValidSignature(sig, for: bytes.readableBytesView) - case (.certified(let key), _): - return key.isValidSignature(signature, for: bytes) - case (.ed25519, _), - (.ecdsaP256, _), - (.ecdsaP384, _), - (.ecdsaP521, _): - return false - } + return backingKey.isValidSignature(signature, for: bytes.readableBytesView) } internal func isValidSignature(_ signature: NIOSSHSignature, for payload: UserAuthSignablePayload) -> Bool { - switch (self.backingKey, signature.backingSignature) { - case (.ed25519(let key), .ed25519(.byteBuffer(let sig))): - return key.isValidSignature(sig.readableBytesView, for: payload.bytes.readableBytesView) - case (.ed25519(let key), .ed25519(.data(let sig))): - return key.isValidSignature(sig, for: payload.bytes.readableBytesView) - case (.ecdsaP256(let key), .ecdsaP256(let sig)): - return key.isValidSignature(sig, for: payload.bytes.readableBytesView) - case (.ecdsaP384(let key), .ecdsaP384(let sig)): - return key.isValidSignature(sig, for: payload.bytes.readableBytesView) - case (.ecdsaP521(let key), .ecdsaP521(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, _): - return false - } + return backingKey.isValidSignature(signature, for: payload.bytes.readableBytesView) } } extension NIOSSHPublicKey { - /// The various key types that can be used with NIOSSH. - internal enum BackingKey { - case ed25519(Curve25519.Signing.PublicKey) - case ecdsaP256(P256.Signing.PublicKey) - case ecdsaP384(P384.Signing.PublicKey) - case ecdsaP521(P521.Signing.PublicKey) - case certified(NIOSSHCertifiedPublicKey) // This case recursively contains `NIOSSHPublicKey`. - } - - /// The prefix of an Ed25519 public key. - internal static let ed25519PublicKeyPrefix = "ssh-ed25519".utf8 - - /// The prefix of a P256 ECDSA public key. - internal static let ecdsaP256PublicKeyPrefix = "ecdsa-sha2-nistp256".utf8 - - /// The prefix of a P384 ECDSA public key. - internal static let ecdsaP384PublicKeyPrefix = "ecdsa-sha2-nistp384".utf8 - - /// 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: - return Self.ed25519PublicKeyPrefix - case .ecdsaP256: - return Self.ecdsaP256PublicKeyPrefix - case .ecdsaP384: - return Self.ecdsaP384PublicKeyPrefix - case .ecdsaP521: - return Self.ecdsaP521PublicKeyPrefix - case .certified(let base): - return base.keyPrefix - } + backingKey.publicKeyPrefix.utf8 } internal static var knownAlgorithms: [String.UTF8View] { - [Self.ed25519PublicKeyPrefix, Self.ecdsaP384PublicKeyPrefix, Self.ecdsaP256PublicKeyPrefix, Self.ecdsaP521PublicKeyPrefix] - } -} - -extension NIOSSHPublicKey.BackingKey: Equatable { - static func == (lhs: NIOSSHPublicKey.BackingKey, rhs: NIOSSHPublicKey.BackingKey) -> Bool { - // We implement equatable in terms of the key representation. - switch (lhs, rhs) { - case (.ed25519(let lhs), .ed25519(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.ecdsaP256(let lhs), .ecdsaP256(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.ecdsaP384(let lhs), .ecdsaP384(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.certified(let lhs), .certified(let rhs)): - return lhs == rhs - case (.ed25519, _), - (.ecdsaP256, _), - (.ecdsaP384, _), - (.ecdsaP521, _), - (.certified, _): - return false - } - } -} - -extension NIOSSHPublicKey.BackingKey: Hashable { - func hash(into hasher: inout Hasher) { - switch self { - case .ed25519(let pkey): - hasher.combine(1) - hasher.combine(pkey.rawRepresentation) - case .ecdsaP256(let pkey): - hasher.combine(2) - hasher.combine(pkey.rawRepresentation) - case .ecdsaP384(let pkey): - hasher.combine(3) - hasher.combine(pkey.rawRepresentation) - case .ecdsaP521(let pkey): - hasher.combine(4) - hasher.combine(pkey.rawRepresentation) - case .certified(let pkey): - hasher.combine(5) - hasher.combine(pkey) - } + [ + Curve25519.Signing.PublicKey.prefix.utf8, + P256.Signing.PublicKey.prefix.utf8, + P384.Signing.PublicKey.prefix.utf8, + P521.Signing.PublicKey.prefix.utf8 + ] } } @@ -238,26 +108,7 @@ extension ByteBuffer { /// Writes an SSH host key to this `ByteBuffer`. @discardableResult mutating func writeSSHHostKey(_ key: NIOSSHPublicKey) -> Int { - var writtenBytes = 0 - - switch key.backingKey { - case .ed25519(let key): - writtenBytes += self.writeSSHString(NIOSSHPublicKey.ed25519PublicKeyPrefix) - writtenBytes += self.writeEd25519PublicKey(baseKey: key) - case .ecdsaP256(let key): - writtenBytes += self.writeSSHString(NIOSSHPublicKey.ecdsaP256PublicKeyPrefix) - writtenBytes += self.writeECDSAP256PublicKey(baseKey: key) - case .ecdsaP384(let key): - writtenBytes += self.writeSSHString(NIOSSHPublicKey.ecdsaP384PublicKeyPrefix) - writtenBytes += self.writeECDSAP384PublicKey(baseKey: key) - case .ecdsaP521(let key): - writtenBytes += self.writeSSHString(NIOSSHPublicKey.ecdsaP521PublicKeyPrefix) - writtenBytes += self.writeECDSAP521PublicKey(baseKey: key) - case .certified(let key): - return self.writeCertifiedKey(key) - } - - return writtenBytes + key.backingKey.writeHostKey(to: &self) } /// Writes an SSH host key to this `ByteBuffer`, without a prefix. @@ -265,18 +116,7 @@ extension ByteBuffer { /// This is mostly used as part of the certified key structure. @discardableResult mutating func writePublicKeyWithoutPrefix(_ 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 .certified: - preconditionFailure("Certified keys are the only callers of this method, and cannot contain themselves") - } + key.backingKey.write(to: &self) } mutating func readSSHHostKey() throws -> NIOSSHPublicKey? { @@ -293,27 +133,32 @@ extension ByteBuffer { mutating func readPublicKeyWithoutPrefixForIdentifier(_ keyIdentifierBytes: Bytes) throws -> NIOSSHPublicKey? where Bytes.Element == UInt8 { try self.rewindOnNilOrError { buffer in - if keyIdentifierBytes.elementsEqual(NIOSSHPublicKey.ed25519PublicKeyPrefix) { - return try buffer.readEd25519PublicKey() - } else if keyIdentifierBytes.elementsEqual(NIOSSHPublicKey.ecdsaP256PublicKeyPrefix) { - return try buffer.readECDSAP256PublicKey() - } else if keyIdentifierBytes.elementsEqual(NIOSSHPublicKey.ecdsaP384PublicKeyPrefix) { - return try buffer.readECDSAP384PublicKey() - } else if keyIdentifierBytes.elementsEqual(NIOSSHPublicKey.ecdsaP521PublicKeyPrefix) { - return try buffer.readECDSAP521PublicKey() + if keyIdentifierBytes.elementsEqual(Curve25519.Signing.PublicKey.prefix.utf8) { + return try Curve25519.Signing.PublicKey.read(from: &buffer) + .map(NIOSSHPublicKey.init) + } else if keyIdentifierBytes.elementsEqual(P256.Signing.PublicKey.prefix.utf8) { + return try P256.Signing.PublicKey.read(from: &buffer) + .map(NIOSSHPublicKey.init) + } else if keyIdentifierBytes.elementsEqual(P384.Signing.PublicKey.prefix.utf8) { + return try P384.Signing.PublicKey.read(from: &buffer) + .map(NIOSSHPublicKey.init) + } else if keyIdentifierBytes.elementsEqual(P521.Signing.PublicKey.prefix.utf8) { + return try P521.Signing.PublicKey.read(from: &buffer) + .map(NIOSSHPublicKey.init) } else { // We don't know this public key type. Maybe the certified keys do. - return try buffer.readCertifiedKeyWithoutKeyPrefix(keyIdentifierBytes).map(NIOSSHPublicKey.init) + return try buffer.readCertifiedKeyWithoutKeyPrefix(keyIdentifierBytes) + .map(NIOSSHPublicKey.init) } } } - private mutating func writeEd25519PublicKey(baseKey: Curve25519.Signing.PublicKey) -> Int { + mutating func writeEd25519PublicKey(baseKey: Curve25519.Signing.PublicKey) -> Int { // For Ed25519 the key format is Q as a String. self.writeSSHString(baseKey.rawRepresentation) } - private mutating func writeECDSAP256PublicKey(baseKey: P256.Signing.PublicKey) -> Int { + mutating func writeECDSAP256PublicKey(baseKey: P256.Signing.PublicKey) -> Int { // For ECDSA-P256, the key format is the string "nistp256", followed by the // the public point Q. var writtenBytes = 0 @@ -322,7 +167,7 @@ extension ByteBuffer { return writtenBytes } - private mutating func writeECDSAP384PublicKey(baseKey: P384.Signing.PublicKey) -> Int { + mutating func writeECDSAP384PublicKey(baseKey: P384.Signing.PublicKey) -> Int { // For ECDSA-P384, the key format is the string "nistp384", followed by the // the public point Q. var writtenBytes = 0 @@ -331,7 +176,7 @@ extension ByteBuffer { return writtenBytes } - private mutating func writeECDSAP521PublicKey(baseKey: P521.Signing.PublicKey) -> Int { + mutating func writeECDSAP521PublicKey(baseKey: P521.Signing.PublicKey) -> Int { // For ECDSA-P521, the key format is the string "nistp521", followed by the // the public point Q. var writtenBytes = 0 @@ -340,89 +185,6 @@ extension ByteBuffer { 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 - /// the rewind. - private mutating func readEd25519PublicKey() throws -> NIOSSHPublicKey? { - // For ed25519 the key format is just Q encoded as a String. - guard let qBytes = self.readSSHString() else { - return nil - } - - let key = try Curve25519.Signing.PublicKey(rawRepresentation: qBytes.readableBytesView) - return NIOSSHPublicKey(backingKey: .ed25519(key)) - } - - /// A helper function that reads an ECDSA P-256 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 - /// the rewind. - private mutating func readECDSAP256PublicKey() throws -> NIOSSHPublicKey? { - // For ECDSA-P256, the key format is the string "nistp256" followed by the - // the public point Q. - guard var domainParameter = self.readSSHString() else { - return nil - } - guard domainParameter.readableBytesView.elementsEqual("nistp256".utf8) else { - let unexpectedParameter = domainParameter.readString(length: domainParameter.readableBytes) ?? "" - throw NIOSSHError.invalidDomainParametersForKey(parameters: unexpectedParameter) - } - - guard let qBytes = self.readSSHString() else { - return nil - } - - let key = try P256.Signing.PublicKey(x963Representation: qBytes.readableBytesView) - return NIOSSHPublicKey(backingKey: .ecdsaP256(key)) - } - - /// A helper function that reads an ECDSA P-384 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 - /// the rewind. - private mutating func readECDSAP384PublicKey() throws -> NIOSSHPublicKey? { - // For ECDSA-P384, the key format is the string "nistp384" followed by the - // the public point Q. - guard var domainParameter = self.readSSHString() else { - return nil - } - guard domainParameter.readableBytesView.elementsEqual("nistp384".utf8) else { - let unexpectedParameter = domainParameter.readString(length: domainParameter.readableBytes) ?? "" - throw NIOSSHError.invalidDomainParametersForKey(parameters: unexpectedParameter) - } - - guard let qBytes = self.readSSHString() else { - return nil - } - - let key = try P384.Signing.PublicKey(x963Representation: qBytes.readableBytesView) - return NIOSSHPublicKey(backingKey: .ecdsaP384(key)) - } - - /// A helper function that reads an ECDSA P-521 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 - /// the rewind. - private mutating func readECDSAP521PublicKey() throws -> NIOSSHPublicKey? { - // For ECDSA-P521, the key format is the string "nistp521" followed by the - // the public point Q. - guard var domainParameter = self.readSSHString() else { - return nil - } - guard domainParameter.readableBytesView.elementsEqual("nistp521".utf8) else { - let unexpectedParameter = domainParameter.readString(length: domainParameter.readableBytes) ?? "" - throw NIOSSHError.invalidDomainParametersForKey(parameters: unexpectedParameter) - } - - guard let qBytes = self.readSSHString() else { - return nil - } - - let key = try P521.Signing.PublicKey(x963Representation: qBytes.readableBytesView) - return NIOSSHPublicKey(backingKey: .ecdsaP521(key)) - } - /// A helper function for complex readers that will reset a buffer on nil or on error, as though the read /// never occurred. internal mutating func rewindOnNilOrError(_ body: (inout ByteBuffer) throws -> T?) rethrows -> T? { diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKeyProtocol.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKeyProtocol.swift new file mode 100644 index 0000000..0274b86 --- /dev/null +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPublicKeyProtocol.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 NIOCore + +public protocol NIOSSHPublicKeyProtocol: Sendable { + /// 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 } + 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: NIOSSHSignature, 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 + + func writeHostKey(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? +} \ No newline at end of file diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift index d1fd23a..ac1a511 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift @@ -22,94 +22,24 @@ import NIOFoundationCompat /// This type is intentionally highly opaque: we don't expect users to do anything with this directly. /// Instead, we expect them to work with other APIs available on our opaque types. public struct NIOSSHSignature: Hashable, Sendable { - internal var backingSignature: BackingSignature + public internal(set) var backingSignature: NIOSSHSignatureProtocol - internal init(backingSignature: BackingSignature) { - self.backingSignature = backingSignature - } -} - -extension NIOSSHSignature { - /// The various signature types that can be used with NIOSSH. - internal enum BackingSignature: Sendable { - case ed25519(RawBytes) // There is no structured Signature type for Curve25519, and we may want Data or ByteBuffer. - case ecdsaP256(P256.Signing.ECDSASignature) - case ecdsaP384(P384.Signing.ECDSASignature) - case ecdsaP521(P521.Signing.ECDSASignature) - - internal enum RawBytes { - case byteBuffer(ByteBuffer) - case data(Data) - } + public var rawRepresentation: Data { + backingSignature.rawRepresentation } - /// The prefix of an Ed25519 signature. - fileprivate static let ed25519SignaturePrefix = "ssh-ed25519".utf8 - - /// The prefix of a P256 ECDSA public key. - fileprivate static let ecdsaP256SignaturePrefix = "ecdsa-sha2-nistp256".utf8 - - /// The prefix of a P384 ECDSA public key. - fileprivate static let ecdsaP384SignaturePrefix = "ecdsa-sha2-nistp384".utf8 - - /// The prefix of a P521 ECDSA public key. - fileprivate static let ecdsaP521SignaturePrefix = "ecdsa-sha2-nistp521".utf8 -} - -extension NIOSSHSignature.BackingSignature.RawBytes: Equatable { - public static func == (lhs: NIOSSHSignature.BackingSignature.RawBytes, rhs: NIOSSHSignature.BackingSignature.RawBytes) -> Bool { - switch (lhs, rhs) { - case (.byteBuffer(let lhs), .byteBuffer(let rhs)): - return lhs == rhs - case (.data(let lhs), .data(let rhs)): - return lhs == rhs - case (.byteBuffer(let lhs), .data(let rhs)): - return lhs.readableBytesView.elementsEqual(rhs) - case (.data(let lhs), .byteBuffer(let rhs)): - return rhs.readableBytesView.elementsEqual(lhs) - } + internal init(backingSignature: NIOSSHSignatureProtocol) { + self.backingSignature = backingSignature } -} -extension NIOSSHSignature.BackingSignature.RawBytes: Hashable {} - -extension NIOSSHSignature.BackingSignature: Equatable { - static func == (lhs: NIOSSHSignature.BackingSignature, rhs: NIOSSHSignature.BackingSignature) -> Bool { - // We implement equatable in terms of the key representation. - switch (lhs, rhs) { - case (.ed25519(let lhs), .ed25519(let rhs)): - return lhs == rhs - case (.ecdsaP256(let lhs), .ecdsaP256(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.ecdsaP384(let lhs), .ecdsaP384(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.ecdsaP521(let lhs), .ecdsaP521(let rhs)): - return lhs.rawRepresentation == rhs.rawRepresentation - case (.ed25519, _), - (.ecdsaP256, _), - (.ecdsaP384, _), - (.ecdsaP521, _): - return false - } + public static func ==(lhs: NIOSSHSignature, rhs: NIOSSHSignature) -> Bool { + lhs.backingSignature.signaturePrefix == rhs.backingSignature.signaturePrefix && + lhs.backingSignature.rawRepresentation == rhs.backingSignature.rawRepresentation } -} -extension NIOSSHSignature.BackingSignature: Hashable { - func hash(into hasher: inout Hasher) { - switch self { - case .ed25519(let bytes): - hasher.combine(0) - hasher.combine(bytes) - case .ecdsaP256(let sig): - hasher.combine(1) - hasher.combine(sig.rawRepresentation) - case .ecdsaP384(let sig): - hasher.combine(2) - hasher.combine(sig.rawRepresentation) - case .ecdsaP521(let sig): - hasher.combine(3) - hasher.combine(sig.rawRepresentation) - } + public func hash(into hasher: inout Hasher) { + hasher.combine(backingSignature.signaturePrefix) + hasher.combine(backingSignature.rawRepresentation) } } @@ -117,35 +47,20 @@ extension ByteBuffer { /// Writes an SSH host key to this `ByteBuffer`. @discardableResult mutating func writeSSHSignature(_ sig: NIOSSHSignature) -> Int { - switch sig.backingSignature { - case .ed25519(let sig): - return self.writeEd25519Signature(signatureBytes: sig) - case .ecdsaP256(let sig): - return self.writeECDSAP256Signature(baseSignature: sig) - case .ecdsaP384(let sig): - return self.writeECDSAP384Signature(baseSignature: sig) - case .ecdsaP521(let sig): - return self.writeECDSAP521Signature(baseSignature: sig) - } + sig.backingSignature.write(to: &self) } - private mutating func writeEd25519Signature(signatureBytes: NIOSSHSignature.BackingSignature.RawBytes) -> Int { + mutating func writeEd25519Signature(signature: Curve25519Signature) -> Int { // The Ed25519 signature format is easy: the ed25519 signature prefix, followed by // the raw signature bytes. - var writtenLength = self.writeSSHString(NIOSSHSignature.ed25519SignaturePrefix) - - switch signatureBytes { - case .byteBuffer(var buf): - writtenLength += self.writeSSHString(&buf) - case .data(let d): - writtenLength += self.writeSSHString(d) - } + var writtenLength = self.writeSSHString(Curve25519Signature.signaturePrefix.utf8) + writtenLength += self.writeSSHString(signature.rawRepresentation) return writtenLength } - private mutating func writeECDSAP256Signature(baseSignature: P256.Signing.ECDSASignature) -> Int { - var writtenLength = self.writeSSHString(NIOSSHSignature.ecdsaP256SignaturePrefix) + mutating func writeECDSAP256Signature(baseSignature: P256.Signing.ECDSASignature) -> Int { + var writtenLength = self.writeSSHString(P256.Signing.ECDSASignature.signaturePrefix.utf8) // For ECDSA-P256, the key format is `mpint r` followed by `mpint s`. In this context, `r` is the // first 32 bytes, and `s` is the second. @@ -164,8 +79,8 @@ extension ByteBuffer { return writtenLength } - private mutating func writeECDSAP384Signature(baseSignature: P384.Signing.ECDSASignature) -> Int { - var writtenLength = self.writeSSHString(NIOSSHSignature.ecdsaP384SignaturePrefix) + mutating func writeECDSAP384Signature(baseSignature: P384.Signing.ECDSASignature) -> Int { + var writtenLength = self.writeSSHString(P384.Signing.ECDSASignature.signaturePrefix.utf8) // For ECDSA-P384, the key format is `mpint r` followed by `mpint s`. In this context, `r` is the // first 48 bytes, and `s` is the second. @@ -184,8 +99,8 @@ extension ByteBuffer { return writtenLength } - private mutating func writeECDSAP521Signature(baseSignature: P521.Signing.ECDSASignature) -> Int { - var writtenLength = self.writeSSHString(NIOSSHSignature.ecdsaP521SignaturePrefix) + mutating func writeECDSAP521Signature(baseSignature: P521.Signing.ECDSASignature) -> Int { + var writtenLength = self.writeSSHString(P521.Signing.ECDSASignature.signaturePrefix.utf8) // For ECDSA-P521, the key format is `mpint r` followed by `mpint s`. In this context, `r` is the // first 66 bytes, and `s` is the second. @@ -205,7 +120,7 @@ extension ByteBuffer { } mutating func readSSHSignature() throws -> NIOSSHSignature? { - try self.rewindOnNilOrError { buffer in + try self.rewindOnNilOrError { buffer -> NIOSSHSignature? in // The wire format always begins with an SSH string containing the signature format identifier. Let's grab that. guard var signatureIdentifierBytes = buffer.readSSHString() else { return nil @@ -213,13 +128,13 @@ extension ByteBuffer { // Now we need to check if they match our supported signature algorithms. let bytesView = signatureIdentifierBytes.readableBytesView - if bytesView.elementsEqual(NIOSSHSignature.ed25519SignaturePrefix) { + if bytesView.elementsEqual(Curve25519Signature.signaturePrefix.utf8) { return try buffer.readEd25519Signature() - } else if bytesView.elementsEqual(NIOSSHSignature.ecdsaP256SignaturePrefix) { + } else if bytesView.elementsEqual(P256.Signing.ECDSASignature.signaturePrefix.utf8) { return try buffer.readECDSAP256Signature() - } else if bytesView.elementsEqual(NIOSSHSignature.ecdsaP384SignaturePrefix) { + } else if bytesView.elementsEqual(P384.Signing.ECDSASignature.signaturePrefix.utf8) { return try buffer.readECDSAP384Signature() - } else if bytesView.elementsEqual(NIOSSHSignature.ecdsaP521SignaturePrefix) { + } else if bytesView.elementsEqual(P521.Signing.ECDSASignature.signaturePrefix.utf8) { return try buffer.readECDSAP521Signature() } else { // We don't know this signature type. @@ -235,11 +150,13 @@ extension ByteBuffer { /// the rewind. private mutating func readEd25519Signature() throws -> NIOSSHSignature? { // For ed25519 the signature is just r||s encoded as a String. - guard let sigBytes = self.readSSHString() else { + guard var sigBytes = self.readSSHString() else { return nil } - return NIOSSHSignature(backingSignature: .ed25519(.byteBuffer(sigBytes))) + return NIOSSHSignature(backingSignature: Curve25519Signature( + rawRepresentation: sigBytes.readData(length: sigBytes.readableBytes)! + )) } /// A helper function that reads an ECDSA P-256 signature. @@ -257,7 +174,9 @@ extension ByteBuffer { // Time to put these into the raw format that CryptoKit wants. This is r || s, with each // integer explicitly left-padded with zeros. - return try NIOSSHSignature(backingSignature: .ecdsaP256(ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes))) + return try NIOSSHSignature( + backingSignature: ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes) as P256.Signing.ECDSASignature + ) } /// A helper function that reads an ECDSA P-384 signature. @@ -275,7 +194,7 @@ extension ByteBuffer { // Time to put these into the raw format that CryptoKit wants. This is r || s, with each // integer explicitly left-padded with zeros. - return try NIOSSHSignature(backingSignature: .ecdsaP384(ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes))) + return try NIOSSHSignature(backingSignature: ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes) as P384.Signing.ECDSASignature) } /// A helper function that reads an ECDSA P-521 signature. @@ -293,7 +212,7 @@ extension ByteBuffer { // Time to put these into the raw format that CryptoKit wants. This is r || s, with each // integer explicitly left-padded with zeros. - return try NIOSSHSignature(backingSignature: .ecdsaP521(ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes))) + return try NIOSSHSignature(backingSignature: ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes) as P521.Signing.ECDSASignature) } } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignatureProtocol.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignatureProtocol.swift new file mode 100644 index 0000000..471c01c --- /dev/null +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignatureProtocol.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 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 NIOCore + +/// 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: Sendable { + /// 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 +} + +internal extension NIOSSHSignatureProtocol { + var signaturePrefix: String { + Self.signaturePrefix + } +} \ No newline at end of file diff --git a/Sources/NIOSSH/NIOSSHError.swift b/Sources/NIOSSH/NIOSSHError.swift index a093431..b2e43f9 100644 --- a/Sources/NIOSSH/NIOSSHError.swift +++ b/Sources/NIOSSH/NIOSSHError.swift @@ -47,6 +47,8 @@ extension NIOSSHError { internal static let invalidEncryptedPacketLength = NIOSSHError(type: .invalidEncryptedPacketLength, diagnostics: nil) + internal static let invalidSSHSignature = NIOSSHError(type: .invalidSSHSignature, diagnostics: nil) + internal static let invalidDecryptedPlaintextLength = NIOSSHError(type: .invalidDecryptedPlaintextLength, diagnostics: nil) internal static let invalidKeySize = NIOSSHError(type: .invalidKeySize, diagnostics: nil) @@ -180,6 +182,7 @@ extension NIOSSHError { case invalidHostKeyForKeyExchange case invalidOpenSSHPublicKey case invalidCertificate + case invalidSSHSignature } private var base: Base @@ -277,6 +280,8 @@ extension NIOSSHError { /// A certificate failed validation. public static let invalidCertificate: ErrorType = .init(.invalidCertificate) + + public static let invalidSSHSignature: ErrorType = .init(.invalidSSHSignature) } } diff --git a/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift b/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift index c6e1952..69b9332 100644 --- a/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift +++ b/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift @@ -210,7 +210,7 @@ extension NIOSSHUserAuthenticationOffer.Offer { public init(privateKey: NIOSSHPrivateKey, certifiedKey: NIOSSHCertifiedPublicKey) { self.privateKey = privateKey - self.publicKey = NIOSSHPublicKey(certifiedKey) + self.publicKey = NIOSSHPublicKey(backingKey: certifiedKey) } } diff --git a/Tests/NIOSSHTests/CertifiedKeyTests.swift b/Tests/NIOSSHTests/CertifiedKeyTests.swift index 60b85d3..26b2def 100644 --- a/Tests/NIOSSHTests/CertifiedKeyTests.swift +++ b/Tests/NIOSSHTests/CertifiedKeyTests.swift @@ -79,7 +79,7 @@ final class CertifiedKeyTests: XCTestCase { XCTFail("Key is not certified") return } - let secondKey = NIOSSHPublicKey(certifiedKey) + let secondKey = NIOSSHPublicKey(backingKey: certifiedKey) XCTAssertEqual(key, secondKey) let setOfKeys = Set([key, secondKey]) XCTAssertEqual(setOfKeys.count, 1) @@ -469,7 +469,7 @@ final class CertifiedKeyTests: XCTestCase { XCTAssertThrowsError(try NIOSSHCertifiedPublicKey(nonce: userKey.nonce, serial: userKey.serial, type: userKey.type, - key: NIOSSHPublicKey(hostKey), + key: NIOSSHPublicKey(backingKey: hostKey), keyID: userKey.keyID, validPrincipals: userKey.validPrincipals, validAfter: userKey.validAfter, @@ -496,7 +496,7 @@ final class CertifiedKeyTests: XCTestCase { validBefore: userKey.validBefore, criticalOptions: userKey.criticalOptions, extensions: userKey.extensions, - signatureKey: NIOSSHPublicKey(hostKey), + signatureKey: NIOSSHPublicKey(backingKey: hostKey), signature: userKey.signature)) { error in XCTAssertEqual((error as? NIOSSHError)?.type, .invalidCertificate) } diff --git a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift index ea4e60c..ff94488 100644 --- a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift +++ b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift @@ -378,7 +378,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { func testExtraECDHReplyForbidden() throws { let privateKey = Curve25519.Signing.PrivateKey() - let message = SSHMessage.keyExchangeReply(.init(hostKey: .init(backingKey: .ed25519(privateKey.publicKey)), publicKey: ByteBufferAllocator().buffer(capacity: 1024), signature: .init(backingSignature: .ed25519(.byteBuffer(ByteBufferAllocator().buffer(capacity: 1024)))))) + let message = SSHMessage.keyExchangeReply(.init(hostKey: .init(backingKey: privateKey.publicKey), publicKey: ByteBufferAllocator().buffer(capacity: 1024), signature: .init(backingSignature: Curve25519Signature(rawRepresentation: Data(repeating: 0x00, count: 1024))))) try self.assertSendingExtraMessageFails(message: message, allowedStages: .beforeReceiveKeyExchangeReplyClient) } diff --git a/Tests/NIOSSHTests/SSHPackerSerializerTests.swift b/Tests/NIOSSHTests/SSHPackerSerializerTests.swift index 60c5eec..02da6a5 100644 --- a/Tests/NIOSSHTests/SSHPackerSerializerTests.swift +++ b/Tests/NIOSSHTests/SSHPackerSerializerTests.swift @@ -187,9 +187,9 @@ final class SSHPacketSerializerTests: XCTestCase { let key = try Curve25519.Signing.PublicKey(rawRepresentation: [182, 37, 100, 183, 198, 201, 188, 148, 70, 200, 201, 225, 14, 66, 236, 124, 45, 246, 72, 46, 242, 24, 149, 170, 135, 58, 10, 18, 208, 163, 106, 118]) let signature = Data([18, 95, 167, 169, 241, 132, 161, 143, 58, 35, 228, 10, 66, 187, 185, 176, 60, 95, 53, 188, 238, 226, 202, 75, 45, 226, 101, 39, 51, 168, 2, 92, 211, 28, 235, 229, 200, 249, 234, 71, 231, 245, 198, 167, 222, 207, 11, 151, 144, 218, 148, 205, 15, 77, 69, 72, 201, 37, 125, 94, 227, 173, 194, 10]) let message = SSHMessage.keyExchangeReply(.init( - hostKey: NIOSSHPublicKey(backingKey: .ed25519(key)), + hostKey: NIOSSHPublicKey(backingKey: key), publicKey: ByteBuffer.of(bytes: [42, 42]), - signature: NIOSSHSignature(backingSignature: .ed25519(.data(signature))) + signature: NIOSSHSignature(backingSignature: Curve25519Signature(rawRepresentation: signature)) )) let allocator = ByteBufferAllocator() var serializer = SSHPacketSerializer() @@ -205,15 +205,15 @@ final class SSHPacketSerializerTests: XCTestCase { switch try parser.nextPacket() { case .keyExchangeReply(let message): switch message.hostKey.backingKey { - case .ed25519(let bytes): - XCTAssertEqual(key.rawRepresentation, bytes.rawRepresentation) + case let parsedKey as Curve25519.Signing.PublicKey: + XCTAssertEqual(key.rawRepresentation, parsedKey.rawRepresentation) default: XCTFail("Key is incorrect") } XCTAssertEqual(ByteBuffer.of(bytes: [42, 42]), message.publicKey) switch message.signature.backingSignature { - case .ed25519(.byteBuffer(let bytes)): - XCTAssertEqual(signature, Data(bytes.readableBytesView)) + case let parsedSignature as Curve25519Signature: + XCTAssertEqual(signature, Data(parsedSignature.rawRepresentation)) default: XCTFail("Signature is incorrect") } diff --git a/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift b/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift index 3dfd629..3e24a74 100644 --- a/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift +++ b/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift @@ -780,10 +780,10 @@ final class UserAuthenticationStateMachineTests: XCTestCase { XCTAssertNoThrow(try self.beginAuthentication(stateMachine: &stateMachine)) stateMachine.sendServiceRequest(.init(service: "ssh-userauth")) - let dataToSign = UserAuthSignablePayload(sessionIdentifier: self.sessionID, userName: "foo", serviceName: "ssh-connection", publicKey: NIOSSHPublicKey(delegate.certifiedKey)) + let dataToSign = UserAuthSignablePayload(sessionIdentifier: self.sessionID, userName: "foo", serviceName: "ssh-connection", publicKey: NIOSSHPublicKey(backingKey: delegate.certifiedKey)) let signature = try delegate.privateKey.sign(dataToSign) - let firstMessage = SSHMessage.UserAuthRequestMessage(username: "foo", service: "ssh-connection", method: .publicKey(.known(key: NIOSSHPublicKey(delegate.certifiedKey), signature: signature))) + let firstMessage = SSHMessage.UserAuthRequestMessage(username: "foo", service: "ssh-connection", method: .publicKey(.known(key: NIOSSHPublicKey(backingKey: delegate.certifiedKey), signature: signature))) XCTAssertNoThrow(try self.serviceAccepted(service: "ssh-userauth", nextMessage: firstMessage, userAuthPayload: dataToSign, stateMachine: &stateMachine)) stateMachine.sendUserAuthRequest(firstMessage) From 8bd14e5da5b896f650a4534dd5bdffcf0177addb Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Mon, 11 Sep 2023 18:31:39 +0200 Subject: [PATCH 2/3] Use Crypto instead of CryptoKit --- .../Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift | 2 +- .../Keys And Signatures/CryptoKit+PublicKeyProtocols.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift index b02b99e..be3ca62 100644 --- a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift +++ b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift @@ -14,7 +14,7 @@ import Foundation import NIOCore -import CryptoKit +import Crypto struct Curve25519Signature: NIOSSHSignatureProtocol { static let signaturePrefix = "ssh-ed25519" diff --git a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift index 59e2a52..4e3245a 100644 --- a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift +++ b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift @@ -14,7 +14,7 @@ import Foundation import NIOCore -import CryptoKit +import Crypto extension Curve25519.Signing.PublicKey: NIOSSHPublicKeyProtocol { internal static var prefix: String { "ssh-ed25519" } From fa483b5937eb5e7fcb9aecfaa66922f832d3c8f0 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 9 Apr 2024 16:48:45 +0200 Subject: [PATCH 3/3] Use an enum for the built-in signature types --- .../CryptoKit+PrivateKeyProtocols.swift | 25 +++--- .../CryptoKit+PublicKeyProtocols.swift | 8 +- .../NIOSSHPrivateKey.swift | 6 +- .../NIOSSHPrivateKeyProtocol.swift | 2 +- .../Keys And Signatures/NIOSSHSignature.swift | 90 +++++++++++++++---- .../SSHKeyExchangeStateMachineTests.swift | 2 +- .../SSHPackerSerializerTests.swift | 2 +- 7 files changed, 95 insertions(+), 40 deletions(-) diff --git a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift index be3ca62..68a4dc2 100644 --- a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift +++ b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PrivateKeyProtocols.swift @@ -39,8 +39,9 @@ extension Curve25519.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } - public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { - return try Curve25519Signature(rawRepresentation: self.signature(for: data)) + public func sshSignature(for data: D) throws -> NIOSSHSignature { + let signature = try Curve25519Signature(rawRepresentation: self.signature(for: data)) + return NIOSSHSignature(backingSignature: .ed25519(signature)) } } @@ -65,9 +66,8 @@ extension P256.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } - public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { - let signature: P256.Signing.ECDSASignature = try self.signature(for: data) - return signature + public func sshSignature(for data: D) throws -> NIOSSHSignature { + return try NIOSSHSignature(backingSignature: .p256(self.signature(for: data))) } } @@ -92,9 +92,8 @@ extension P384.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } - public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { - let signature: P384.Signing.ECDSASignature = try self.signature(for: data) - return signature + public func sshSignature(for data: D) throws -> NIOSSHSignature { + return try NIOSSHSignature(backingSignature: .p384(self.signature(for: data))) } } @@ -121,9 +120,8 @@ extension P521.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } - public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { - let signature: P521.Signing.ECDSASignature = try self.signature(for: data) - return signature + public func sshSignature(for data: D) throws -> NIOSSHSignature { + return try NIOSSHSignature(backingSignature: .p521(self.signature(for: data))) } } @@ -133,9 +131,8 @@ extension SecureEnclave.P256.Signing.PrivateKey: NIOSSHPrivateKeyProtocol { public var sshPublicKey: NIOSSHPublicKeyProtocol { publicKey } - public func sshSignature(for data: D) throws -> NIOSSHSignatureProtocol { - let signature: P256.Signing.ECDSASignature = try self.signature(for: data) - return signature + public func sshSignature(for data: D) throws -> NIOSSHSignature { + return try NIOSSHSignature(backingSignature: .p256(self.signature(for: data))) } } #endif \ No newline at end of file diff --git a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift index 4e3245a..3d47cae 100644 --- a/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift +++ b/Sources/NIOSSH/Keys And Signatures/CryptoKit+PublicKeyProtocols.swift @@ -22,7 +22,7 @@ extension Curve25519.Signing.PublicKey: NIOSSHPublicKeyProtocol { public var publicKeyPrefix: String { Self.prefix } public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { - guard let signature = signature.backingSignature as? Curve25519Signature else { + guard case .ed25519(let signature) = signature._backingSignature else { return false } @@ -55,7 +55,7 @@ extension P256.Signing.PublicKey: NIOSSHPublicKeyProtocol { public var publicKeyPrefix: String { Self.prefix } public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { - guard let signature = signature.backingSignature as? P256.Signing.ECDSASignature else { + guard case .p256(let signature) = signature._backingSignature else { return false } @@ -98,7 +98,7 @@ extension P384.Signing.PublicKey: NIOSSHPublicKeyProtocol { public var publicKeyPrefix: String { Self.prefix } public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { - guard let signature = signature.backingSignature as? P384.Signing.ECDSASignature else { + guard case .p384(let signature) = signature._backingSignature else { return false } @@ -141,7 +141,7 @@ extension P521.Signing.PublicKey: NIOSSHPublicKeyProtocol { public var publicKeyPrefix: String { Self.prefix } public func isValidSignature(_ signature: NIOSSHSignature, for data: D) -> Bool { - guard let signature = signature.backingSignature as? P521.Signing.ECDSASignature else { + guard case .p521(let signature) = signature._backingSignature else { return false } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift index db0377b..3ab4494 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift @@ -60,15 +60,13 @@ public struct NIOSSHPrivateKey: Sendable { extension NIOSSHPrivateKey { func sign(digest: DigestBytes) throws -> NIOSSHSignature { - let signature = try digest.withUnsafeBytes { ptr in + return try digest.withUnsafeBytes { ptr in try backingKey.sshSignature(for: ptr) } - return NIOSSHSignature(backingSignature: signature) } func sign(_ payload: UserAuthSignablePayload) throws -> NIOSSHSignature { - let signature = try backingKey.sshSignature(for: payload.bytes.readableBytesView) - return NIOSSHSignature(backingSignature: signature) + return try backingKey.sshSignature(for: payload.bytes.readableBytesView) } } diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift index ff20981..71d3e82 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKeyProtocol.swift @@ -25,7 +25,7 @@ public protocol NIOSSHPrivateKeyProtocol: Sendable { var sshPublicKey: 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 sshSignature(for data: D) throws -> NIOSSHSignatureProtocol + func sshSignature(for data: D) throws -> NIOSSHSignature } internal extension NIOSSHPrivateKeyProtocol { diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift index ac1a511..ae2f041 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHSignature.swift @@ -22,24 +22,84 @@ import NIOFoundationCompat /// This type is intentionally highly opaque: we don't expect users to do anything with this directly. /// Instead, we expect them to work with other APIs available on our opaque types. public struct NIOSSHSignature: Hashable, Sendable { - public internal(set) var backingSignature: NIOSSHSignatureProtocol + internal enum BackingSignature { + case ed25519(Curve25519Signature) + case p521(P521.Signing.ECDSASignature) + case p384(P384.Signing.ECDSASignature) + case p256(P256.Signing.ECDSASignature) + case other(NIOSSHSignatureProtocol) + + var signaturePrefix: String { + switch self { + case .ed25519: return Curve25519Signature.signaturePrefix + case .p521: return P521.Signing.ECDSASignature.signaturePrefix + case .p384: return P384.Signing.ECDSASignature.signaturePrefix + case .p256: return P256.Signing.ECDSASignature.signaturePrefix + case .other(let sig): return sig.signaturePrefix + } + } - public var rawRepresentation: Data { - backingSignature.rawRepresentation + var rawRepresentation: Data { + switch self { + case .ed25519(let sig): return sig.rawRepresentation + case .p521(let sig): return sig.rawRepresentation + case .p384(let sig): return sig.rawRepresentation + case .p256(let sig): return sig.rawRepresentation + case .other(let sig): return sig.rawRepresentation + } + } + + func write(to buffer: inout ByteBuffer) -> Int { + switch self { + case .ed25519(let sig): return buffer.writeEd25519Signature(signature: sig) + case .p521(let sig): return buffer.writeECDSAP521Signature(baseSignature: sig) + case .p384(let sig): return buffer.writeECDSAP384Signature(baseSignature: sig) + case .p256(let sig): return buffer.writeECDSAP256Signature(baseSignature: sig) + case .other(let sig): return sig.write(to: &buffer) + } + } } - internal init(backingSignature: NIOSSHSignatureProtocol) { - self.backingSignature = backingSignature + internal var _backingSignature: BackingSignature + public var backingSignature: NIOSSHSignatureProtocol { + switch self._backingSignature { + case .ed25519(let sig): return sig + case .p521(let sig): return sig + case .p384(let sig): return sig + case .p256(let sig): return sig + case .other(let sig): + return sig + } + } + + internal init(backingSignature: BackingSignature) { + self._backingSignature = backingSignature + } + + public init(_ signature: NIOSSHSignatureProtocol) { + // In case that end-users obtain one of the built-in signatures. + switch signature { + case let signature as Curve25519Signature: + _backingSignature = .ed25519(signature) + case let signature as P521.Signing.ECDSASignature: + _backingSignature = .p521(signature) + case let signature as P384.Signing.ECDSASignature: + _backingSignature = .p384(signature) + case let signature as P256.Signing.ECDSASignature: + _backingSignature = .p256(signature) + default: + _backingSignature = .other(signature) + } } public static func ==(lhs: NIOSSHSignature, rhs: NIOSSHSignature) -> Bool { - lhs.backingSignature.signaturePrefix == rhs.backingSignature.signaturePrefix && - lhs.backingSignature.rawRepresentation == rhs.backingSignature.rawRepresentation + lhs._backingSignature.signaturePrefix == rhs._backingSignature.signaturePrefix && + lhs._backingSignature.rawRepresentation == rhs._backingSignature.rawRepresentation } public func hash(into hasher: inout Hasher) { - hasher.combine(backingSignature.signaturePrefix) - hasher.combine(backingSignature.rawRepresentation) + hasher.combine(_backingSignature.signaturePrefix) + hasher.combine(_backingSignature.rawRepresentation) } } @@ -47,7 +107,7 @@ extension ByteBuffer { /// Writes an SSH host key to this `ByteBuffer`. @discardableResult mutating func writeSSHSignature(_ sig: NIOSSHSignature) -> Int { - sig.backingSignature.write(to: &self) + sig._backingSignature.write(to: &self) } mutating func writeEd25519Signature(signature: Curve25519Signature) -> Int { @@ -154,9 +214,9 @@ extension ByteBuffer { return nil } - return NIOSSHSignature(backingSignature: Curve25519Signature( + return NIOSSHSignature(backingSignature: .ed25519(Curve25519Signature( rawRepresentation: sigBytes.readData(length: sigBytes.readableBytes)! - )) + ))) } /// A helper function that reads an ECDSA P-256 signature. @@ -175,7 +235,7 @@ extension ByteBuffer { // Time to put these into the raw format that CryptoKit wants. This is r || s, with each // integer explicitly left-padded with zeros. return try NIOSSHSignature( - backingSignature: ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes) as P256.Signing.ECDSASignature + backingSignature: .p256(ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes)) ) } @@ -194,7 +254,7 @@ extension ByteBuffer { // Time to put these into the raw format that CryptoKit wants. This is r || s, with each // integer explicitly left-padded with zeros. - return try NIOSSHSignature(backingSignature: ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes) as P384.Signing.ECDSASignature) + return try NIOSSHSignature(backingSignature: .p384(ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes))) } /// A helper function that reads an ECDSA P-521 signature. @@ -212,7 +272,7 @@ extension ByteBuffer { // Time to put these into the raw format that CryptoKit wants. This is r || s, with each // integer explicitly left-padded with zeros. - return try NIOSSHSignature(backingSignature: ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes) as P521.Signing.ECDSASignature) + return try NIOSSHSignature(backingSignature: .p521(ECDSASignatureHelper.toECDSASignature(r: rBytes, s: sBytes))) } } diff --git a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift index ff94488..90fd386 100644 --- a/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift +++ b/Tests/NIOSSHTests/SSHKeyExchangeStateMachineTests.swift @@ -378,7 +378,7 @@ final class SSHKeyExchangeStateMachineTests: XCTestCase { func testExtraECDHReplyForbidden() throws { let privateKey = Curve25519.Signing.PrivateKey() - let message = SSHMessage.keyExchangeReply(.init(hostKey: .init(backingKey: privateKey.publicKey), publicKey: ByteBufferAllocator().buffer(capacity: 1024), signature: .init(backingSignature: Curve25519Signature(rawRepresentation: Data(repeating: 0x00, count: 1024))))) + let message = SSHMessage.keyExchangeReply(.init(hostKey: .init(backingKey: privateKey.publicKey), publicKey: ByteBufferAllocator().buffer(capacity: 1024), signature: .init(backingSignature: .ed25519(Curve25519Signature(rawRepresentation: Data(repeating: 0x00, count: 1024)))))) try self.assertSendingExtraMessageFails(message: message, allowedStages: .beforeReceiveKeyExchangeReplyClient) } diff --git a/Tests/NIOSSHTests/SSHPackerSerializerTests.swift b/Tests/NIOSSHTests/SSHPackerSerializerTests.swift index 02da6a5..6383c42 100644 --- a/Tests/NIOSSHTests/SSHPackerSerializerTests.swift +++ b/Tests/NIOSSHTests/SSHPackerSerializerTests.swift @@ -189,7 +189,7 @@ final class SSHPacketSerializerTests: XCTestCase { let message = SSHMessage.keyExchangeReply(.init( hostKey: NIOSSHPublicKey(backingKey: key), publicKey: ByteBuffer.of(bytes: [42, 42]), - signature: NIOSSHSignature(backingSignature: Curve25519Signature(rawRepresentation: signature)) + signature: NIOSSHSignature(backingSignature: .ed25519(Curve25519Signature(rawRepresentation: signature))) )) let allocator = ByteBufferAllocator() var serializer = SSHPacketSerializer()