From 545cf285881d093394e0b6c2674b7e1a5fbbc6ea Mon Sep 17 00:00:00 2001 From: Artem Redkin Date: Wed, 13 Jan 2021 13:02:32 +0000 Subject: [PATCH] Implement certified public key client auth Motivation: Right now there is no way to use certified public keys for user auth. Modifications: 1. Adds new init to PrivateKey 2. Updates UserAuthRequestMessage to support public key 3. Adds a test Result: Closes #63 --- .../UserAuthenticationMethod.swift | 11 +++- .../UserAuthenticationStateMachineTests.swift | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift b/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift index 1e2ef61..e2371ac 100644 --- a/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift +++ b/Sources/NIOSSH/User Authentication/UserAuthenticationMethod.swift @@ -153,9 +153,16 @@ extension NIOSSHUserAuthenticationOffer { extension NIOSSHUserAuthenticationOffer.Offer { public struct PrivateKey { public var privateKey: NIOSSHPrivateKey + public var publicKey: NIOSSHPublicKey public init(privateKey: NIOSSHPrivateKey) { self.privateKey = privateKey + self.publicKey = privateKey.publicKey + } + + public init(privateKey: NIOSSHPrivateKey, certifiedKey: NIOSSHCertifiedPublicKey) { + self.privateKey = privateKey + self.publicKey = NIOSSHPublicKey(certifiedKey) } } @@ -186,10 +193,10 @@ extension SSHMessage.UserAuthRequestMessage { sessionIdentifier: sessionID, userName: self.username, serviceName: self.service, - publicKey: privateKeyRequest.privateKey.publicKey + publicKey: privateKeyRequest.publicKey ) let signature = try privateKeyRequest.privateKey.sign(dataToSign) - self.method = .publicKey(.known(key: privateKeyRequest.privateKey.publicKey, signature: signature)) + self.method = .publicKey(.known(key: privateKeyRequest.publicKey, signature: signature)) case .password(let passwordRequest): self.method = .password(passwordRequest.password) case .hostBased: diff --git a/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift b/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift index 1bae3ac..00af845 100644 --- a/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift +++ b/Tests/NIOSSHTests/UserAuthenticationStateMachineTests.swift @@ -17,6 +17,26 @@ import NIO @testable import NIOSSH import XCTest +private enum Fixtures { + // P256 ECDSA key, generated using `ssh-keygen -m PEM -t ecdsa` + static let privateKey = """ + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIJFqt5pH9xGvuoaI5kzisthTa0EXVgy+fC4bAtdwBR07oAoGCCqGSM49 + AwEHoUQDQgAEyJP6dnY46GvyP65L9FgFxNdN+rNWy4PqIwCrwJWY6ss/sTSbMkdA + 4D7gh+fWyft3EdRtcAsw3raU/G2S+N1iAA== + -----END EC PRIVATE KEY----- + """ + + // Raw private key data, since `PrivateKey(pemRepresentation:)` is not available on every supported platform + static let privateKeyRaw = Data([145, 106, 183, 154, 71, 247, 17, 175, 186, 134, 136, 230, 76, 226, 178, 216, 83, 107, 65, 23, 86, 12, 190, 124, 46, 27, 2, 215, 112, 5, 29, 59]) + + // A P256 user key. id "User P256 key" serial 0 for foo,bar valid from 2020-06-03T17:50:15 to 2070-04-02T17:51:15 + // Generated using ssh-keygen -s ca-key -I "User P256 key" -n "foo,bar" -V "-1m:+2600w" user-p256 + static let certificateKey = """ + ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgHmvoERZ+BRKhlCAKoPlVQLcHO5oNxyGeXHnmI0DLL/8AAAAIbmlzdHAyNTYAAABBBMiT+nZ2OOhr8j+uS/RYBcTXTfqzVsuD6iMAq8CVmOrLP7E0mzJHQOA+4Ifn1sn7dxHUbXALMN62lPxtkvjdYgAAAAAAAAAAAAAAAAEAAAANVXNlciBQMjU2IGtleQAAAA4AAAADZm9vAAAAA2JhcgAAAABgABLaAAAAAL26NxYAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQR2JTEl2nF7dd6AS6TFxD9DkjMOaJHeXOxt4aIptTEf0x1DsjktgFUChKi2bPrXd2OsmAq6uUxlgzRmNnXyhV/fZy6iQqtpMUf/wj91IXq5GZ5+ruHluG4iy+8Tg6jTs5EAAACDAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAABoAAAAMHIoH34qNeg6LDTiSUF13KvPImQljh1Se5cxtrZZ3bCBAK2DUZQsAitxc8Ju4jY2zQAAADBkQfjSYa5wr2y61D54kWSIDiqOjgEAnfjJkyglQcYU4P1ULCFXJ15tIg3GRBY4U/s= artemredkin@Artems-MacBook-Pro.local + """ +} + /// An authentication delegate that yields passwords forever. final class InfinitePasswordDelegate: NIOSSHClientUserAuthenticationDelegate { func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) { @@ -34,6 +54,21 @@ final class InfinitePrivateKeyDelegate: NIOSSHClientUserAuthenticationDelegate { } } +final class InfiniteCertificateDelegate: NIOSSHClientUserAuthenticationDelegate { + let privateKey: NIOSSHPrivateKey + let certifiedKey: NIOSSHCertifiedPublicKey + + init() throws { + self.privateKey = try NIOSSHPrivateKey(p256Key: P256.Signing.PrivateKey(rawRepresentation: Fixtures.privateKeyRaw)) + self.certifiedKey = try NIOSSHCertifiedPublicKey(NIOSSHPublicKey(openSSHPublicKey: Fixtures.certificateKey))! + } + + func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) { + let request = NIOSSHUserAuthenticationOffer(username: "foo", serviceName: "", offer: .privateKey(.init(privateKey: self.privateKey, certifiedKey: self.certifiedKey))) + nextChallengePromise.succeed(request) + } +} + /// An authentication delegate that denies some number of requests and then accepts exactly one and fails the rest. final class DenyThenAcceptDelegate: NIOSSHServerUserAuthenticationDelegate { let supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods = .all @@ -720,4 +755,27 @@ final class UserAuthenticationStateMachineTests: XCTestCase { // Let's say we got a success. Happy path! XCTAssertNoThrow(try stateMachine.receiveUserAuthSuccess()) } + + func testCertificateClientAuthFlow() throws { + let delegate = try InfiniteCertificateDelegate() + var stateMachine = UserAuthenticationStateMachine(role: .client(.init(userAuthDelegate: delegate, serverAuthDelegate: AcceptAllHostKeysDelegate())), loop: self.loop, sessionID: self.sessionID) + + 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 signature = try delegate.privateKey.sign(dataToSign) + + let firstMessage = SSHMessage.UserAuthRequestMessage(username: "foo", service: "ssh-connection", method: .publicKey(.known(key: NIOSSHPublicKey(delegate.certifiedKey), signature: signature))) + XCTAssertNoThrow(try self.serviceAccepted(service: "ssh-userauth", nextMessage: firstMessage, userAuthPayload: dataToSign, stateMachine: &stateMachine)) + stateMachine.sendUserAuthRequest(firstMessage) + + // Oh no, a failure! We'll try again. + let failure = SSHMessage.UserAuthFailureMessage(authentications: ["publickey"], partialSuccess: false) + try self.authFailed(failure: failure, nextMessage: firstMessage, userAuthPayload: dataToSign, stateMachine: &stateMachine) + stateMachine.sendUserAuthRequest(firstMessage) + + // Let's say we got a success. Happy path! + XCTAssertNoThrow(try stateMachine.receiveUserAuthSuccess()) + } }