mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-06 01:37:22 +01:00
Move downloader and socket input parsing to xpc services (#675)
* XPC updater POC * WIP * obo * WIP * Working * . * . * . * Cleanup * Cleanup * Throw restrict * Remove dead protocol. * . * Fix ECDSA test. * Scripts
This commit is contained in:
@@ -21,13 +21,16 @@ let package = Package(
|
||||
targets: ["SmartCardSecretKit"]),
|
||||
.library(
|
||||
name: "SecretAgentKit",
|
||||
targets: ["SecretAgentKit"]),
|
||||
targets: ["SecretAgentKit", "XPCWrappers"]),
|
||||
.library(
|
||||
name: "SecretAgentKitHeaders",
|
||||
targets: ["SecretAgentKitHeaders"]),
|
||||
.library(
|
||||
name: "Brief",
|
||||
targets: ["Brief"]),
|
||||
.library(
|
||||
name: "XPCWrappers",
|
||||
targets: ["XPCWrappers"]),
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
@@ -70,7 +73,7 @@ let package = Package(
|
||||
),
|
||||
.target(
|
||||
name: "Brief",
|
||||
dependencies: [],
|
||||
dependencies: ["XPCWrappers"],
|
||||
resources: [localization],
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
@@ -78,6 +81,10 @@ let package = Package(
|
||||
name: "BriefTests",
|
||||
dependencies: ["Brief"],
|
||||
),
|
||||
.target(
|
||||
name: "XPCWrappers",
|
||||
swiftSettings: swiftSettings,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import XPCWrappers
|
||||
|
||||
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
|
||||
@Observable public final class Updater: UpdaterProtocol, Sendable {
|
||||
@@ -33,27 +34,25 @@ import Observation
|
||||
) {
|
||||
self.osVersion = osVersion
|
||||
self.currentVersion = currentVersion
|
||||
if checkOnLaunch {
|
||||
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
|
||||
Task {
|
||||
await checkForUpdates()
|
||||
}
|
||||
}
|
||||
Task {
|
||||
if checkOnLaunch {
|
||||
try await checkForUpdates()
|
||||
}
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
|
||||
await checkForUpdates()
|
||||
try await checkForUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually trigger an update check.
|
||||
public func checkForUpdates() async {
|
||||
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL) else { return }
|
||||
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
|
||||
await evaluate(releases: releases)
|
||||
public func checkForUpdates() async throws {
|
||||
let session = try XPCTypedSession<[Release], Never>(serviceName: "com.maxgoedjen.Secretive.ReleasesDownloader")
|
||||
await evaluate(releases: try await session.send())
|
||||
session.complete()
|
||||
}
|
||||
|
||||
|
||||
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
|
||||
/// - Parameter release: The release to ignore.
|
||||
public func ignore(release: Release) async {
|
||||
@@ -100,11 +99,3 @@ extension Updater {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Updater {
|
||||
|
||||
enum Constants {
|
||||
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,49 +31,29 @@ public final class Agent: Sendable {
|
||||
|
||||
extension Agent {
|
||||
|
||||
/// Handles an incoming request.
|
||||
/// - Parameters:
|
||||
/// - data: The data to handle.
|
||||
/// - provenance: The origin of the request.
|
||||
/// - Returns: A response data payload.
|
||||
public func handle(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
logger.debug("Agent handling new data")
|
||||
guard data.count > 4 else {
|
||||
throw InvalidDataProvidedError()
|
||||
}
|
||||
let requestTypeInt = data[4]
|
||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription) for unknown request type \(requestTypeInt)")
|
||||
return SSHAgent.ResponseType.agentFailure.data.lengthAndData
|
||||
}
|
||||
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
||||
let subData = Data(data[5...])
|
||||
let response = await handle(requestType: requestType, data: subData, provenance: provenance)
|
||||
return response
|
||||
}
|
||||
|
||||
private func handle(requestType: SSHAgent.RequestType, data: Data, provenance: SigningRequestProvenance) async -> Data {
|
||||
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
|
||||
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
||||
await reloadSecretsIfNeccessary()
|
||||
var response = Data()
|
||||
do {
|
||||
switch requestType {
|
||||
switch request {
|
||||
case .requestIdentities:
|
||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||
response.append(SSHAgent.Response.agentIdentitiesAnswer.data)
|
||||
response.append(await identities())
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
||||
case .signRequest:
|
||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||
response.append(try await sign(data: data, provenance: provenance))
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
||||
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))
|
||||
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 \(requestType.debugDescription), but not currently supported.")
|
||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||
|
||||
logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
|
||||
throw UnhandledRequestError()
|
||||
}
|
||||
} catch {
|
||||
response = SSHAgent.ResponseType.agentFailure.data
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||
response = SSHAgent.Response.agentFailure.data
|
||||
logger.debug("Agent returned \(SSHAgent.Response.agentFailure.debugDescription)")
|
||||
}
|
||||
return response.lengthAndData
|
||||
}
|
||||
@@ -113,27 +93,16 @@ extension Agent {
|
||||
/// - data: The data to sign.
|
||||
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
|
||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
let payloadHash = try reader.readNextChunk()
|
||||
let hash: Data
|
||||
|
||||
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
||||
if let certificatePublicKey = await certificateHandler.publicKeyHash(from: payloadHash) {
|
||||
hash = certificatePublicKey
|
||||
} else {
|
||||
hash = payloadHash
|
||||
}
|
||||
|
||||
guard let (secret, store) = await secret(matching: hash) else {
|
||||
logger.debug("Agent did not have a key matching \(hash as NSData)")
|
||||
func sign(data: Data, keyBlob: Data, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
guard let (secret, store) = await secret(matching: keyBlob) else {
|
||||
let keyBlobHex = keyBlob.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }.joined()
|
||||
logger.debug("Agent did not have a key matching \(keyBlobHex)")
|
||||
throw NoMatchingKeyError()
|
||||
}
|
||||
|
||||
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
||||
|
||||
let dataToSign = try reader.readNextChunk()
|
||||
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
|
||||
let rawRepresentation = try await store.sign(data: data, with: secret, for: provenance)
|
||||
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)
|
||||
|
||||
try await witness?.witness(accessTo: secret, from: store, by: provenance)
|
||||
@@ -172,16 +141,16 @@ extension Agent {
|
||||
|
||||
extension Agent {
|
||||
|
||||
struct InvalidDataProvidedError: Error {}
|
||||
struct NoMatchingKeyError: Error {}
|
||||
struct UnhandledRequestError: Error {}
|
||||
|
||||
}
|
||||
|
||||
extension SSHAgent.ResponseType {
|
||||
extension SSHAgent.Response {
|
||||
|
||||
var data: Data {
|
||||
var raw = self.rawValue
|
||||
return Data(bytes: &raw, count: UInt8.bitWidth/8)
|
||||
return Data(bytes: &raw, count: MemoryLayout<UInt8>.size)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
extension FileHandle {
|
||||
|
||||
public var pidOfConnectedProcess: Int32 {
|
||||
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
|
||||
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<Int32>.size, alignment: 1)
|
||||
var len = socklen_t(MemoryLayout<Int32>.size)
|
||||
getsockopt(fileDescriptor, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
|
||||
return pidPointer.load(as: Int32.self)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SecretKit
|
||||
|
||||
/// Manages storage and lookup for OpenSSH certificates.
|
||||
public actor OpenSSHCertificateHandler: Sendable {
|
||||
@@ -25,33 +26,6 @@ public actor OpenSSHCertificateHandler: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
|
||||
/// - Parameter certBlock: The openssh certificate to extract the public key from
|
||||
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
||||
public func publicKeyHash(from hash: Data) -> Data? {
|
||||
let reader = OpenSSHReader(data: hash)
|
||||
do {
|
||||
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
|
||||
switch certType {
|
||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||
_ = try reader.readNextChunk() // nonce
|
||||
let curveIdentifier = try reader.readNextChunk()
|
||||
let publicKey = try reader.readNextChunk()
|
||||
|
||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||
/// - Parameter secret: The secret to search for a certificate with
|
||||
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||
@@ -1,42 +1,47 @@
|
||||
import Foundation
|
||||
|
||||
/// Reads OpenSSH protocol data.
|
||||
public final class OpenSSHReader {
|
||||
final class OpenSSHReader {
|
||||
|
||||
var remaining: Data
|
||||
|
||||
/// 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)
|
||||
return lengthChunk.bytes.unsafeLoad(as: T.self)
|
||||
}
|
||||
|
||||
|
||||
public func readNextChunkAsString() throws -> String {
|
||||
try String(decoding: readNextChunk(), as: UTF8.self)
|
||||
func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
|
||||
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
|
||||
}
|
||||
|
||||
public struct EndOfData: Error {}
|
||||
func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader {
|
||||
OpenSSHReader(data: try readNextChunk(convertEndianness: convertEndianness))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum OpenSSHReaderError: Error, Codable {
|
||||
case beyondBounds
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SecretKit
|
||||
|
||||
public protocol SSHAgentInputParserProtocol: Sendable {
|
||||
|
||||
func parse(data: Data) async throws -> SSHAgent.Request
|
||||
|
||||
}
|
||||
|
||||
public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "InputParser")
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func parse(data: Data) throws(AgentParsingError) -> SSHAgent.Request {
|
||||
logger.debug("Parsing new data")
|
||||
guard data.count > 4 else {
|
||||
throw .invalidData
|
||||
}
|
||||
let specifiedLength = (data[0..<4].bytes.unsafeLoad(as: UInt32.self).bigEndian) + 4
|
||||
let rawRequestInt = data[4]
|
||||
let remainingDataRange = 5..<min(Int(specifiedLength), data.count)
|
||||
lazy var body: Data = { Data(data[remainingDataRange]) }()
|
||||
switch rawRequestInt {
|
||||
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:
|
||||
return .removeIdentity
|
||||
case SSHAgent.Request.removeAllIdentities.protocolID:
|
||||
return .removeAllIdentities
|
||||
case SSHAgent.Request.addIDConstrained.protocolID:
|
||||
return .addIDConstrained
|
||||
case SSHAgent.Request.addSmartcardKey.protocolID:
|
||||
return .addSmartcardKey
|
||||
case SSHAgent.Request.removeSmartcardKey.protocolID:
|
||||
return .removeSmartcardKey
|
||||
case SSHAgent.Request.lock.protocolID:
|
||||
return .lock
|
||||
case SSHAgent.Request.unlock.protocolID:
|
||||
return .unlock
|
||||
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
|
||||
return .addSmartcardKeyConstrained
|
||||
case SSHAgent.Request.protocolExtension.protocolID:
|
||||
return .protocolExtension
|
||||
default:
|
||||
return .unknown(rawRequestInt)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SSHAgentInputParser {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func certificatePublicKeyBlob(from hash: Data) -> Data? {
|
||||
let reader = OpenSSHReader(data: hash)
|
||||
do {
|
||||
let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
|
||||
switch certType {
|
||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||
_ = try reader.readNextChunk() // nonce
|
||||
let curveIdentifier = try reader.readNextChunk()
|
||||
let publicKey = try reader.readNextChunk()
|
||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension SSHAgentInputParser {
|
||||
|
||||
public enum AgentParsingError: Error, Codable {
|
||||
case unknownRequest
|
||||
case unhandledRequest
|
||||
case invalidData
|
||||
case openSSHReader(OpenSSHReaderError)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,21 +6,39 @@ public enum SSHAgent {}
|
||||
extension SSHAgent {
|
||||
|
||||
/// The type of the SSH Agent Request, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
||||
public enum RequestType: UInt8, CustomDebugStringConvertible {
|
||||
public enum Request: CustomDebugStringConvertible, Codable, Sendable {
|
||||
|
||||
case requestIdentities = 11
|
||||
case signRequest = 13
|
||||
case addIdentity = 17
|
||||
case removeIdentity = 18
|
||||
case removeAllIdentities = 19
|
||||
case addIDConstrained = 25
|
||||
case addSmartcardKey = 20
|
||||
case removeSmartcardKey = 21
|
||||
case lock = 22
|
||||
case unlock = 23
|
||||
case addSmartcardKeyConstrained = 26
|
||||
case protocolExtension = 27
|
||||
case requestIdentities
|
||||
case signRequest(SignatureRequestContext)
|
||||
case addIdentity
|
||||
case removeIdentity
|
||||
case removeAllIdentities
|
||||
case addIDConstrained
|
||||
case addSmartcardKey
|
||||
case removeSmartcardKey
|
||||
case lock
|
||||
case unlock
|
||||
case addSmartcardKeyConstrained
|
||||
case protocolExtension
|
||||
case unknown(UInt8)
|
||||
|
||||
public var protocolID: UInt8 {
|
||||
switch self {
|
||||
case .requestIdentities: 11
|
||||
case .signRequest: 13
|
||||
case .addIdentity: 17
|
||||
case .removeIdentity: 18
|
||||
case .removeAllIdentities: 19
|
||||
case .addIDConstrained: 25
|
||||
case .addSmartcardKey: 20
|
||||
case .removeSmartcardKey: 21
|
||||
case .lock: 22
|
||||
case .unlock: 23
|
||||
case .addSmartcardKeyConstrained: 26
|
||||
case .protocolExtension: 27
|
||||
case .unknown(let value): value
|
||||
}
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
@@ -36,12 +54,28 @@ extension SSHAgent {
|
||||
case .unlock: "SSH_AGENTC_UNLOCK"
|
||||
case .addSmartcardKeyConstrained: "SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED"
|
||||
case .protocolExtension: "SSH_AGENTC_EXTENSION"
|
||||
case .unknown: "UNKNOWN_MESSAGE"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SignatureRequestContext: Sendable, Codable {
|
||||
public let keyBlob: Data
|
||||
public let dataToSign: Data
|
||||
|
||||
public init(keyBlob: Data, dataToSign: Data) {
|
||||
self.keyBlob = keyBlob
|
||||
self.dataToSign = dataToSign
|
||||
}
|
||||
|
||||
public static var empty: SignatureRequestContext {
|
||||
SignatureRequestContext(keyBlob: Data(), dataToSign: Data())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
||||
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
||||
public enum Response: UInt8, CustomDebugStringConvertible {
|
||||
|
||||
case agentFailure = 5
|
||||
case agentSuccess = 6
|
||||
|
||||
@@ -13,9 +13,8 @@ extension SigningRequestTracer {
|
||||
/// Generates a ``SecretKit.SigningRequestProvenance`` from a ``FileHandle``.
|
||||
/// - Parameter fileHandle: The reader involved in processing the request.
|
||||
/// - Returns: A ``SecretKit.SigningRequestProvenance`` describing the origin of the request.
|
||||
func provenance(from fileHandleReader: FileHandle) -> SigningRequestProvenance {
|
||||
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
|
||||
|
||||
func provenance(from fileHandle: FileHandle) -> SigningRequestProvenance {
|
||||
let firstInfo = process(from: fileHandle.pidOfConnectedProcess)
|
||||
var provenance = SigningRequestProvenance(root: firstInfo)
|
||||
while NSRunningApplication(processIdentifier: provenance.origin.pid) == nil && provenance.origin.parentPID != nil {
|
||||
provenance.chain.append(process(from: provenance.origin.parentPID!))
|
||||
|
||||
49
Sources/Packages/Sources/XPCWrappers/XPCWrappers.swift
Normal file
49
Sources/Packages/Sources/XPCWrappers/XPCWrappers.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
|
||||
public struct XPCTypedSession<ResponseType: Codable & Sendable, ErrorType: Error & Codable>: Sendable {
|
||||
|
||||
private let session: XPCSession
|
||||
|
||||
public init(serviceName: String, warmup: Bool = false) throws {
|
||||
if #available(macOS 26.0, *) {
|
||||
session = try XPCSession(xpcService: serviceName, requirement: .isFromSameTeam())
|
||||
} else {
|
||||
session = try XPCSession(xpcService: serviceName)
|
||||
}
|
||||
if warmup {
|
||||
Task { [self] in
|
||||
_ = try? await send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func send(_ message: some Encodable = Data()) async throws -> ResponseType {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
do {
|
||||
try session.send(message) { result in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
if let result = try? message.decode(as: ResponseType.self) {
|
||||
continuation.resume(returning: result)
|
||||
} else if let error = try? message.decode(as: ErrorType.self) {
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
continuation.resume(throwing: UnknownMessageError())
|
||||
}
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func complete() {
|
||||
session.cancel(reason: "Done")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct UnknownMessageError: Error, Codable {}
|
||||
@@ -8,19 +8,22 @@ import CryptoKit
|
||||
|
||||
// MARK: Identity Listing
|
||||
|
||||
|
||||
// let testProvenance = SigningRequestProvenance(root: .init(pid: 0, processName: "Test", appName: "Test", iconURL: nil, path: /, validSignature: true, parentPID: nil))
|
||||
|
||||
@Test func emptyStores() async throws {
|
||||
let agent = Agent(storeList: SecretStoreList())
|
||||
let response = try await agent.handle(data: Constants.Requests.requestIdentities, provenance: .test)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestIdentitiesEmpty)
|
||||
}
|
||||
|
||||
@Test func identitiesList() async throws {
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
let response = try await agent.handle(data: Constants.Requests.requestIdentities, provenance: .test)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
|
||||
let actual = OpenSSHReader(data: response)
|
||||
let expected = OpenSSHReader(data: Constants.Responses.requestIdentitiesMultiple)
|
||||
print(actual, expected)
|
||||
#expect(response == Constants.Responses.requestIdentitiesMultiple)
|
||||
}
|
||||
|
||||
@@ -29,40 +32,42 @@ import CryptoKit
|
||||
@Test func noMatchingIdentities() async throws {
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
let response = try await agent.handle(data: Constants.Requests.requestSignatureWithNoneMatching, provenance: .test)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
// @Test func ecdsaSignature() async throws {
|
||||
// let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
// let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...])
|
||||
// _ = requestReader.readNextChunk()
|
||||
// let dataToSign = requestReader.readNextChunk()
|
||||
// let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
// let agent = Agent(storeList: list)
|
||||
// await agent.handle(reader: stubReader, writer: stubWriter)
|
||||
// let outer = OpenSSHReader(data: stubWriter.data[5...])
|
||||
// let payload = outer.readNextChunk()
|
||||
// let inner = OpenSSHReader(data: payload)
|
||||
// _ = inner.readNextChunk()
|
||||
// let signedData = inner.readNextChunk()
|
||||
// let rsData = OpenSSHReader(data: signedData)
|
||||
// var r = rsData.readNextChunk()
|
||||
// var s = rsData.readNextChunk()
|
||||
// // This is fine IRL, but it freaks out CryptoKit
|
||||
// if r[0] == 0 {
|
||||
// r.removeFirst()
|
||||
// }
|
||||
// if s[0] == 0 {
|
||||
// s.removeFirst()
|
||||
// }
|
||||
// var rs = r
|
||||
// rs.append(s)
|
||||
// let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||
// // Correct signature
|
||||
// #expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
||||
// .isValidSignature(signature, for: dataToSign))
|
||||
// }
|
||||
@Test func ecdsaSignature() async throws {
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
guard case SSHAgent.Request.signRequest(let context) = request else { return }
|
||||
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||
let agent = Agent(storeList: list)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
let responseReader = OpenSSHReader(data: response)
|
||||
let length = try responseReader.readNextBytes(as: UInt32.self).bigEndian
|
||||
let type = try responseReader.readNextBytes(as: UInt8.self).bigEndian
|
||||
#expect(length == response.count - MemoryLayout<UInt32>.size)
|
||||
#expect(type == SSHAgent.Response.agentSignResponse.rawValue)
|
||||
let outer = OpenSSHReader(data: responseReader.remaining)
|
||||
let inner = try outer.readNextChunkAsSubReader()
|
||||
_ = try inner.readNextChunk()
|
||||
let rsData = try inner.readNextChunkAsSubReader()
|
||||
var r = try rsData.readNextChunk()
|
||||
var s = try rsData.readNextChunk()
|
||||
// This is fine IRL, but it freaks out CryptoKit
|
||||
if r[0] == 0 {
|
||||
r.removeFirst()
|
||||
}
|
||||
if s[0] == 0 {
|
||||
s.removeFirst()
|
||||
}
|
||||
var rs = r
|
||||
rs.append(s)
|
||||
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||
// Correct signature
|
||||
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
||||
.isValidSignature(signature, for: context.dataToSign))
|
||||
}
|
||||
|
||||
// MARK: Witness protocol
|
||||
|
||||
@@ -72,7 +77,7 @@ import CryptoKit
|
||||
return true
|
||||
}, witness: { _, _ in })
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
let response = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
|
||||
let response = await agent.handle(request: .signRequest(.empty), provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
@@ -85,7 +90,8 @@ import CryptoKit
|
||||
witnessed = true
|
||||
})
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
_ = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
_ = await agent.handle(request: request, provenance: .test)
|
||||
#expect(witnessed)
|
||||
}
|
||||
|
||||
@@ -100,7 +106,8 @@ import CryptoKit
|
||||
witnessTrace = trace
|
||||
})
|
||||
let agent = Agent(storeList: list, witness: witness)
|
||||
_ = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
_ = await agent.handle(request: request, provenance: .test)
|
||||
#expect(witnessTrace == speakNowTrace)
|
||||
#expect(witnessTrace == .test)
|
||||
}
|
||||
@@ -112,7 +119,8 @@ import CryptoKit
|
||||
let store = await list.stores.first?.base as! Stub.Store
|
||||
store.shouldThrow = true
|
||||
let agent = Agent(storeList: list)
|
||||
let response = try await agent.handle(data: Constants.Requests.requestSignature, provenance: .test)
|
||||
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
|
||||
let response = await agent.handle(request: request, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
@@ -120,7 +128,7 @@ import CryptoKit
|
||||
|
||||
@Test func unhandledAdd() async throws {
|
||||
let agent = Agent(storeList: SecretStoreList())
|
||||
let response = try await agent.handle(data: Constants.Requests.addIdentity, provenance: .test)
|
||||
let response = await agent.handle(request: .addIdentity, provenance: .test)
|
||||
#expect(response == Constants.Responses.requestFailure)
|
||||
}
|
||||
|
||||
@@ -146,14 +154,13 @@ extension AgentTests {
|
||||
|
||||
enum Requests {
|
||||
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")!
|
||||
static let addIdentity = Data(base64Encoded: "AAAAARE=")!
|
||||
static let requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
|
||||
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
|
||||
}
|
||||
|
||||
enum Responses {
|
||||
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")!
|
||||
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABKwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0")!
|
||||
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABLwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAFWVjZHNhLTI1NkBleGFtcGxlLmNvbQAAAIgAAAATZWNkc2Etc2hhMi1uaXN0cDM4NAAAAAhuaXN0cDM4NAAAAGEEspLMDmreMJverQkqKC9zF9ZUasn5uSWkbRlz1jNTCtuyH1KKm+VImL6wdAj47SbzwM6lEEC24AdfrR64P9i/bnS2i83v/4wQVtcZn+Et13QGgWlZst8lxCPzTookaVwMAAAAFWVjZHNhLTM4NEBleGFtcGxlLmNvbQ==")!
|
||||
static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import SecretKit
|
||||
@testable import SecretAgentKit
|
||||
@testable import SecureEnclaveSecretKit
|
||||
@testable import SmartCardSecretKit
|
||||
|
||||
@@ -82,7 +82,7 @@ extension Stub {
|
||||
let privateKey: Data
|
||||
|
||||
init(keySize: Int, publicKey: Data, privateKey: Data) {
|
||||
self.attributes = Attributes(keyType: .init(algorithm: .ecdsa, size: keySize), authentication: .notRequired)
|
||||
self.attributes = Attributes(keyType: .init(algorithm: .ecdsa, size: keySize), authentication: .notRequired, publicKeyAttribution: "ecdsa-\(keySize)@example.com")
|
||||
self.publicKey = publicKey
|
||||
self.privateKey = privateKey
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user