This commit is contained in:
Max Goedjen 2025-09-06 17:30:05 -07:00
parent 11074999ad
commit 2efba1bc21
No known key found for this signature in database
6 changed files with 70 additions and 37 deletions

View File

@ -6,18 +6,38 @@ private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.Age
func handleRequest(_ request: XPCListener.IncomingSessionRequest) -> XPCListener.IncomingSessionRequest.Decision {
logger.log("Parser received inbound request")
return request.accept { message in
return request.accept { xpcDictionary in
xpcDictionary.handoffReply(to: .global(qos: .userInteractive)) {
logger.log("Parser accepted inbound request")
do {
let result = try SSHAgentInputParser().parse(data: message)
let parser = SSHAgentInputParser()
let result = try parser.parse(data: xpcDictionary.decode(as: Data.self))
logger.log("Parser parsed message as type \(result.debugDescription)")
return result
} catch {
xpcDictionary.reply(result)
} catch let error as SSHAgentInputParser.AgentParsingError {
logger.error("Parser failed with error \(error)")
return nil
xpcDictionary.reply(error)
} catch {
// This should never actually happen because SSHAgentInputParser is a typed thrower, but the type system doesn't seem to know that across framework boundaries?
logger.error("Parser failed with unknown error \(error)")
}
}
}
}
public struct WrappedError<Wrapped: Codable & Error>: Codable {
public struct DescriptionOnlyError: Error, Codable {
let localizedDescription: String
}
public let wrapped: Wrapped
public init(_ error: Wrapped) {
wrapped = error
}
}
do {
if #available(macOS 26.0, *) {

View File

@ -45,6 +45,8 @@ extension Agent {
response.append(SSHAgent.Response.agentSignResponse.data)
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
case .unknown(let value):
logger.error("Agent received unknown request of type \(value).")
default:
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
throw UnhandledRequestError()

View File

@ -7,25 +7,25 @@ final class OpenSSHReader {
/// Initialize the reader with an OpenSSH data payload.
/// - Parameter data: The data to read.
public init(data: Data) {
init(data: Data) {
remaining = Data(data)
}
/// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data.
public func readNextChunk(convertEndianness: Bool = true) throws -> Data {
func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data {
let littleEndianLength = try readNextBytes(as: UInt32.self)
let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength)
guard remaining.count >= length else { throw EndOfData() }
guard remaining.count >= length else { throw .beyondBounds }
let dataRange = 0..<length
let ret = Data(remaining[dataRange])
remaining.removeSubrange(dataRange)
return ret
}
public func readNextBytes<T>(as: T.Type) throws -> T {
func readNextBytes<T>(as: T.Type) throws(OpenSSHReaderError) -> T {
let size = MemoryLayout<T>.size
guard remaining.count >= size else { throw EndOfData() }
guard remaining.count >= size else { throw .beyondBounds }
let lengthRange = 0..<size
let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(lengthRange)
@ -33,10 +33,12 @@ final class OpenSSHReader {
}
public func readNextChunkAsString() throws -> String {
func readNextChunkAsString() throws -> String {
try String(decoding: readNextChunk(), as: UTF8.self)
}
public struct EndOfData: Error {}
}
public enum OpenSSHReaderError: Error, Codable {
case beyondBounds
}

View File

@ -16,10 +16,10 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
}
public func parse(data: Data) throws -> SSHAgent.Request {
public func parse(data: Data) throws(AgentParsingError) -> SSHAgent.Request {
logger.debug("Parsing new data")
guard data.count > 4 else {
throw InvalidDataProvidedError()
throw .invalidData
}
let specifiedLength = (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4
let rawRequestInt = data[4]
@ -29,7 +29,11 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
case SSHAgent.Request.requestIdentities.protocolID:
return .requestIdentities
case SSHAgent.Request.signRequest(.empty).protocolID:
do {
return .signRequest(try signatureRequestContext(from: body))
} catch {
throw .openSSHReader(error)
}
case SSHAgent.Request.addIdentity.protocolID:
return .addIdentity
case SSHAgent.Request.removeIdentity.protocolID:
@ -59,7 +63,7 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
extension SSHAgentInputParser {
func signatureRequestContext(from data: Data) throws -> SSHAgent.Request.SignatureRequestContext {
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
@ -95,8 +99,11 @@ extension SSHAgentInputParser {
extension SSHAgentInputParser {
struct AgentUnknownRequestError: Error {}
struct AgentUnhandledRequestError: Error {}
struct InvalidDataProvidedError: Error {}
public enum AgentParsingError: Error, Codable {
case unknownRequest
case unhandledRequest
case invalidData
case openSSHReader(OpenSSHReaderError)
}
}

View File

@ -35,13 +35,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
logger.debug("SecretAgent finished launching")
Task {
let inputParser = try XPCAgentInputParser()
Task {
try? await Task.sleep(for: .seconds(1))
var len = (5 as UInt32).littleEndian
var raw = SSHAgent.Request.requestIdentities.protocolID
let data = Data(bytes: &len, count: MemoryLayout<UInt32>.size) + Data(bytes: &raw, count: MemoryLayout<UInt8>.size)
print(try? await inputParser.parse(data: data))
}
for await session in socketController.sessions {
Task {
do {

View File

@ -1,6 +1,7 @@
import Foundation
import SecretAgentKit
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
private let session: XPCSession
@ -8,11 +9,15 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
public init() throws {
if #available(macOS 26.0, *) {
session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue, options: .inactive, requirement: .isFromSameTeam())
session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue, requirement: .isFromSameTeam())
} else {
session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue, options: .inactive)
session = try XPCSession(xpcService: "com.maxgoedjen.Secretive.AgentRequestParser", targetQueue: queue)
}
Task {
// Warm up the XPC endpoint.
_ = try? await parse(data: Data())
}
try session.activate()
}
public func parse(data: Data) async throws -> SSHAgent.Request {
@ -21,10 +26,12 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
try session.send(data) { result in
switch result {
case .success(let result):
do {
continuation.resume(returning: try result.decode(as: SSHAgent.Request.self))
} catch {
if let result = try? result.decode(as: SSHAgent.Request.self) {
continuation.resume(returning: result)
} else if let error = try? result.decode(as: SSHAgentInputParser.AgentParsingError.self) {
continuation.resume(throwing: error)
} else {
continuation.resume(throwing: UnknownMessageError())
}
case .failure(let error):
continuation.resume(throwing: error)
@ -40,4 +47,6 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
session.cancel(reason: "Done")
}
struct UnknownMessageError: Error {}
}