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,19 +6,39 @@ private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.Age
func handleRequest(_ request: XPCListener.IncomingSessionRequest) -> XPCListener.IncomingSessionRequest.Decision { func handleRequest(_ request: XPCListener.IncomingSessionRequest) -> XPCListener.IncomingSessionRequest.Decision {
logger.log("Parser received inbound request") logger.log("Parser received inbound request")
return request.accept { message in return request.accept { xpcDictionary in
logger.log("Parser accepted inbound request") xpcDictionary.handoffReply(to: .global(qos: .userInteractive)) {
do { logger.log("Parser accepted inbound request")
let result = try SSHAgentInputParser().parse(data: message) do {
logger.log("Parser parsed message as type \(result.debugDescription)") let parser = SSHAgentInputParser()
return result let result = try parser.parse(data: xpcDictionary.decode(as: Data.self))
} catch { logger.log("Parser parsed message as type \(result.debugDescription)")
logger.error("Parser failed with error \(error)") xpcDictionary.reply(result)
return nil } catch let error as SSHAgentInputParser.AgentParsingError {
logger.error("Parser failed with error \(error)")
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 { do {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
_ = try XPCListener( _ = try XPCListener(

View File

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

View File

@ -7,25 +7,25 @@ final class OpenSSHReader {
/// Initialize the reader with an OpenSSH data payload. /// Initialize the reader with an OpenSSH data payload.
/// - Parameter data: The data to read. /// - Parameter data: The data to read.
public init(data: Data) { init(data: Data) {
remaining = Data(data) remaining = Data(data)
} }
/// Reads the next chunk of data from the playload. /// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data. /// - 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 littleEndianLength = try readNextBytes(as: UInt32.self)
let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength) 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 dataRange = 0..<length
let ret = Data(remaining[dataRange]) let ret = Data(remaining[dataRange])
remaining.removeSubrange(dataRange) remaining.removeSubrange(dataRange)
return ret 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 let size = MemoryLayout<T>.size
guard remaining.count >= size else { throw EndOfData() } guard remaining.count >= size else { throw .beyondBounds }
let lengthRange = 0..<size let lengthRange = 0..<size
let lengthChunk = remaining[lengthRange] let lengthChunk = remaining[lengthRange]
remaining.removeSubrange(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) 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") logger.debug("Parsing new data")
guard data.count > 4 else { guard data.count > 4 else {
throw InvalidDataProvidedError() throw .invalidData
} }
let specifiedLength = (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4 let specifiedLength = (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4
let rawRequestInt = data[4] let rawRequestInt = data[4]
@ -29,7 +29,11 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
case SSHAgent.Request.requestIdentities.protocolID: case SSHAgent.Request.requestIdentities.protocolID:
return .requestIdentities return .requestIdentities
case SSHAgent.Request.signRequest(.empty).protocolID: case SSHAgent.Request.signRequest(.empty).protocolID:
return .signRequest(try signatureRequestContext(from: body)) do {
return .signRequest(try signatureRequestContext(from: body))
} catch {
throw .openSSHReader(error)
}
case SSHAgent.Request.addIdentity.protocolID: case SSHAgent.Request.addIdentity.protocolID:
return .addIdentity return .addIdentity
case SSHAgent.Request.removeIdentity.protocolID: case SSHAgent.Request.removeIdentity.protocolID:
@ -59,7 +63,7 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
extension SSHAgentInputParser { 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 reader = OpenSSHReader(data: data)
let rawKeyBlob = try reader.readNextChunk() let rawKeyBlob = try reader.readNextChunk()
let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob
@ -95,8 +99,11 @@ extension SSHAgentInputParser {
extension SSHAgentInputParser { extension SSHAgentInputParser {
struct AgentUnknownRequestError: Error {} public enum AgentParsingError: Error, Codable {
struct AgentUnhandledRequestError: Error {} case unknownRequest
struct InvalidDataProvidedError: Error {} case unhandledRequest
case invalidData
case openSSHReader(OpenSSHReaderError)
}
} }

View File

@ -35,13 +35,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
logger.debug("SecretAgent finished launching") logger.debug("SecretAgent finished launching")
Task { Task {
let inputParser = try XPCAgentInputParser() 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 { for await session in socketController.sessions {
Task { Task {
do { do {

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import SecretAgentKit import SecretAgentKit
/// Delegates all agent input parsing to an XPC service which wraps OpenSSH
public final class XPCAgentInputParser: SSHAgentInputParserProtocol { public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
private let session: XPCSession private let session: XPCSession
@ -8,11 +9,15 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
public init() throws { public init() throws {
if #available(macOS 26.0, *) { 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 { } 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 { public func parse(data: Data) async throws -> SSHAgent.Request {
@ -21,10 +26,12 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
try session.send(data) { result in try session.send(data) { result in
switch result { switch result {
case .success(let result): case .success(let result):
do { if let result = try? result.decode(as: SSHAgent.Request.self) {
continuation.resume(returning: try result.decode(as: SSHAgent.Request.self)) continuation.resume(returning: result)
} catch { } else if let error = try? result.decode(as: SSHAgentInputParser.AgentParsingError.self) {
continuation.resume(throwing: error) continuation.resume(throwing: error)
} else {
continuation.resume(throwing: UnknownMessageError())
} }
case .failure(let error): case .failure(let error):
continuation.resume(throwing: error) continuation.resume(throwing: error)
@ -40,4 +47,6 @@ public final class XPCAgentInputParser: SSHAgentInputParserProtocol {
session.cancel(reason: "Done") session.cancel(reason: "Done")
} }
struct UnknownMessageError: Error {}
} }