diff --git a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift index 2c669db..b4651a8 100644 --- a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift @@ -19,7 +19,7 @@ public struct OpenSSHPublicKeyWriter: Sendable { ("nistp" + String(describing: secret.keyType.size)).lengthAndData + secret.publicKey.lengthAndData case .mldsa: - // https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt + // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05 openSSHIdentifier(for: secret.keyType).lengthAndData + secret.publicKey.lengthAndData case .rsa: diff --git a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift index 6378df2..f72b4af 100644 --- a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift @@ -39,6 +39,18 @@ public final class OpenSSHReader { return convertEndianness ? T(value.bigEndian) : T(value) } + public func readNextByteAsBool() throws(OpenSSHReaderError) -> Bool { + let size = MemoryLayout.size + guard remaining.count >= size else { throw .beyondBounds } + let lengthRange = 0.. String { try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self) } diff --git a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift index 25397db..8bdb939 100644 --- a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift @@ -17,7 +17,7 @@ public struct OpenSSHSignatureWriter: Sendable { // https://datatracker.ietf.org/doc/html/rfc5656#section-3.1 ecdsaSignature(signature, keyType: secret.keyType) case .mldsa: - // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-00#name-public-key-algorithms + // https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05 mldsaSignature(signature, keyType: secret.keyType) case .rsa: // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 diff --git a/Sources/Packages/Sources/SSHProtocolKit/SSHAgentProtocol.swift b/Sources/Packages/Sources/SSHProtocolKit/SSHAgentProtocol.swift index 0007989..1f1a36f 100644 --- a/Sources/Packages/Sources/SSHProtocolKit/SSHAgentProtocol.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/SSHAgentProtocol.swift @@ -19,7 +19,7 @@ extension SSHAgent { case lock case unlock case addSmartcardKeyConstrained - case protocolExtension + case protocolExtension(ProtocolExtension) case unknown(UInt8) public var protocolID: UInt8 { @@ -60,18 +60,82 @@ extension SSHAgent { public struct SignatureRequestContext: Sendable, Codable { public let keyBlob: Data - public let dataToSign: Data + public let dataToSign: SignaturePayload - public init(keyBlob: Data, dataToSign: Data) { + public init(keyBlob: Data, dataToSign: SignaturePayload) { self.keyBlob = keyBlob self.dataToSign = dataToSign } public static var empty: SignatureRequestContext { - SignatureRequestContext(keyBlob: Data(), dataToSign: Data()) + SignatureRequestContext(keyBlob: Data(), dataToSign: SignaturePayload(raw: Data(), decoded: nil)) + } + + public struct SignaturePayload: Sendable, Codable { + + public let raw: Data + public let decoded: DecodedPayload? + + public init( + raw: Data, + decoded: DecodedPayload? + ) { + self.raw = raw + self.decoded = decoded + } + + public enum DecodedPayload: Sendable, Codable { + case sshConnection(SSHConnectionPayload) + case sshSig(SSHSigPayload) + + public struct SSHConnectionPayload: Sendable, Codable { + + public let username: String + public let hasSignature: Bool + public let publicKeyAlgorithm: String + public let publicKey: Data + public let hostKey: Data + + public init( + username: String, + hasSignature: Bool, + publicKeyAlgorithm: String, + publicKey: Data, + hostKey: Data + ) { + self.username = username + self.hasSignature = hasSignature + self.publicKeyAlgorithm = publicKeyAlgorithm + self.publicKey = publicKey + self.hostKey = hostKey + } + + } + + public struct SSHSigPayload: Sendable, Codable { + + public let namespace: String + public let hashAlgorithm: String + public let hash: Data + + public init( + namespace: String, + hashAlgorithm: String, + hash: Data, + ) { + self.namespace = namespace + self.hashAlgorithm = hashAlgorithm + self.hash = hash + } + + } + + } + } } + } /// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1 @@ -88,8 +152,8 @@ extension SSHAgent { switch self { case .agentFailure: "SSH_AGENT_FAILURE" case .agentSuccess: "SSH_AGENT_SUCCESS" - case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER" - case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE" + case .agentIdentitiesAnswer: "SSH2_AGENT_IDENTITIES_ANSWER" + case .agentSignResponse: "SSH2_AGENT_SIGN_RESPONSE" case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE" case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE" } diff --git a/Sources/Packages/Sources/SSHProtocolKit/SSHProtocolExtensions.swift b/Sources/Packages/Sources/SSHProtocolKit/SSHProtocolExtensions.swift new file mode 100644 index 0000000..511926f --- /dev/null +++ b/Sources/Packages/Sources/SSHProtocolKit/SSHProtocolExtensions.swift @@ -0,0 +1,76 @@ +import Foundation + +// Extensions, as defined in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent + +extension SSHAgent { + + public enum ProtocolExtension: CustomDebugStringConvertible, Codable, Sendable { + case openSSH(OpenSSHExtension) + case unknown(String) + + public var debugDescription: String { + switch self { + case let .openSSH(protocolExtension): + protocolExtension.debugDescription + case .unknown(let string): + "Unknown (\(string))" + } + } + + public static var empty: ProtocolExtension { + .unknown("empty") + } + + private struct ProtocolExtensionParsingError: Error {} + + } + +} + +extension SSHAgent.ProtocolExtension { + + public enum OpenSSHExtension: CustomDebugStringConvertible, Codable, Sendable { + case sessionBind(SessionBindContext) + case unknown(String) + + public static var domain: String { + "openssh.com" + } + + public var name: String { + switch self { + case .sessionBind: + "session-bind" + case .unknown(let name): + name + } + } + + public var debugDescription: String { + "\(name)@\(OpenSSHExtension.domain)" + } + } + +} + +extension SSHAgent.ProtocolExtension.OpenSSHExtension { + + public struct SessionBindContext: Codable, Sendable { + + public let hostKey: Data + public let sessionID: Data + public let signature: Data + public let forwarding: Bool + + public init(hostKey: Data, sessionID: Data, signature: Data, forwarding: Bool) { + self.hostKey = hostKey + self.sessionID = sessionID + self.signature = signature + self.forwarding = forwarding + } + + public static let empty = SessionBindContext(hostKey: Data(), sessionID: Data(), signature: Data(), forwarding: false) + + } + +} diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index ba66603..99cfddf 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -33,6 +33,7 @@ public final class Agent: Sendable { extension Agent { public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data { + logger.debug("Agent received request of type \(request.debugDescription)") // Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting await reloadSecretsIfNeccessary() var response = Data() @@ -44,8 +45,14 @@ extension Agent { logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)") case .signRequest(let context): response.append(SSHAgent.Response.agentSignResponse.data) - response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance)) + response.append(try await sign(data: context.dataToSign.raw, keyBlob: context.keyBlob, provenance: provenance)) logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)") + case .protocolExtension(.openSSH(.sessionBind(let bind))): + response = SSHAgent.Response.agentSuccess.data + _ = bind + // FIXME: STORE BIND IN KEYCHAIN + // FIXME: CLEAR OUT BINDS BASED ON EXPIRATION? + logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)") case .unknown(let value): logger.error("Agent received unknown request of type \(value).") throw UnhandledRequestError() diff --git a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift index e8c4d61..2c6b815 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift @@ -3,6 +3,8 @@ import OSLog import SecretKit import SSHProtocolKit +import CryptoKit + public protocol SSHAgentInputParserProtocol { func parse(data: Data) async throws -> SSHAgent.Request @@ -53,8 +55,10 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol { return .unlock case SSHAgent.Request.addSmartcardKeyConstrained.protocolID: return .addSmartcardKeyConstrained - case SSHAgent.Request.protocolExtension.protocolID: - return .protocolExtension + case SSHAgent.Request.protocolExtension(.empty).protocolID: + return .protocolExtension(try protocolExtension(from: body)) +// case SSHAgent.Request.constrainExtension(.empty).protocolID: +// return .constrainExtension(try constrainExtension(from: body)) default: return .unknown(rawRequestInt) } @@ -64,12 +68,126 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol { extension SSHAgentInputParser { + private enum Constants { + static let userAuthMagic: UInt8 = 50 // SSH2_MSG_USERAUTH_REQUEST + static let sshSigMagic = Data("SSHSIG".utf8) + } + func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext { let reader = OpenSSHReader(data: data) let rawKeyBlob = try reader.readNextChunk() let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob - let dataToSign = try reader.readNextChunk() - return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: dataToSign) + let rawPayload = try reader.readNextChunk() + let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload + if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic { + // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L79 + let payloadReader = OpenSSHReader(data: rawPayload[6...]) + let namespace = try payloadReader.readNextChunkAsString() + _ = try payloadReader.readNextChunk() // reserved + let hashAlgorithm = try payloadReader.readNextChunkAsString() + let hash = try payloadReader.readNextChunk() + payload = .init( + raw: data, + decoded: .init( + .sshSig( + .init( + namespace: namespace, + hashAlgorithm: hashAlgorithm, + hash: hash + ) + ) + ) + ) + } else { + let payloadReader = OpenSSHReader(data: rawPayload) + do { + _ = try payloadReader.readNextChunk() + let magic = try payloadReader.readNextBytes(as: UInt8.self, convertEndianness: false) + if magic == Constants.userAuthMagic { + let username = try payloadReader.readNextChunkAsString() + _ = try payloadReader.readNextChunkAsString() // "ssh-connection" + _ = try payloadReader.readNextChunkAsString() // "publickey-hostbound-v00@openssh.com" + let hasSignature = try payloadReader.readNextByteAsBool() + let pkAlg = try payloadReader.readNextChunkAsString() + let pk = try payloadReader.readNextChunk() + let hostKey = try payloadReader.readNextChunk() + payload = .init( + raw: rawPayload, + decoded: .init( + .sshConnection( + .init( + username: username, + hasSignature: hasSignature, + publicKeyAlgorithm: pkAlg, + publicKey: pk, + hostKey: hostKey + ) + ) + ) + ) + } else { + throw AgentParsingError.unknownRequest + } + } catch { + payload = .init(raw: rawPayload, decoded: nil) + } + } + return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: payload) + } + + func protocolExtension(from data: Data) throws(AgentParsingError) -> SSHAgent.ProtocolExtension { + do { + let reader = OpenSSHReader(data: data) + let nameRaw = try reader.readNextChunkAsString() + let nameSplit = nameRaw.split(separator: "@") + guard nameSplit.count == 2 else { + throw AgentParsingError.invalidData + } + let (name, domain) = (nameSplit[0], nameSplit[1]) + switch domain { + case SSHAgent.ProtocolExtension.OpenSSHExtension.domain: + switch name { + case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name: + let hostkeyBlob = try reader.readNextChunkAsSubReader() + let hostKeyType = try hostkeyBlob.readNextChunkAsString() + let hostKeyData = try hostkeyBlob.readNextChunk() + let sessionID = try reader.readNextChunk() + let signatureBlob = try reader.readNextChunkAsSubReader() + _ = try signatureBlob.readNextChunk() // key type again + let signature = try signatureBlob.readNextChunk() + let forwarding = try reader.readNextByteAsBool() + switch hostKeyType { + // FIXME: FACTOR OUT? + // FIXME: HANDLE OTHER KEYS + case "ssh-ed25519": + let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData) + guard hostKey.isValidSignature(signature, for: sessionID) else { + throw AgentParsingError.invalidData + } + default: + throw AgentParsingError.unhandledRequest + } + let context = SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext( + hostKey: hostKeyData, + sessionID: sessionID, + signature: signature, + forwarding: forwarding + ) + return .openSSH(.sessionBind(context)) + default: + return .openSSH(.unknown(String(name))) + } + default: + return .unknown(nameRaw) + } + + } catch let error as OpenSSHReaderError { + throw .openSSHReader(error) + } catch let error as AgentParsingError { + throw error + } catch { + throw .unknownRequest + } } func certificatePublicKeyBlob(from hash: Data) -> Data? {