From bb0b6d8dc327fd82d7db546556c81283d84357cd Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Thu, 27 Nov 2025 12:00:53 -0800 Subject: [PATCH 1/9] Run filehandle listening methods in main actor directly vs jumping (#765) --- .../SecretAgentKit/SocketController.swift | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift index 9de2564..f8aa7cb 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift @@ -36,16 +36,16 @@ public struct SocketController { logger.debug("Socket controller path is clear") port = SocketPort(path: path) fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) - Task { [fileHandle, sessionsContinuation, logger] in + Task { @MainActor [fileHandle, sessionsContinuation, logger] in for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) { logger.debug("Socket controller accepted connection") guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue } let session = Session(fileHandle: new) sessionsContinuation.yield(session) - await fileHandle.acceptConnectionInBackgroundAndNotifyOnMainActor() + fileHandle.acceptConnectionInBackgroundAndNotify() } } - fileHandle.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.Mode.common]) + fileHandle.acceptConnectionInBackgroundAndNotify() logger.debug("Socket listening at \(path)") } @@ -90,16 +90,16 @@ extension SocketController { logger.debug("Socket controller yielded data.") } } - Task { - await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() - } + fileHandle.waitForDataInBackgroundAndNotify() } /// Writes new data to the socket. /// - Parameter data: The data to write. public func write(_ data: Data) async throws { try fileHandle.write(contentsOf: data) - await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor() + await MainActor.run { + fileHandle.waitForDataInBackgroundAndNotify() + } } /// Closes the socket and cleans up resources. @@ -113,22 +113,6 @@ extension SocketController { } -private extension FileHandle { - - /// Ensures waitForDataInBackgroundAndNotify will be called on the main actor. - @MainActor func waitForDataInBackgroundAndNotifyOnMainActor() { - waitForDataInBackgroundAndNotify() - } - - - /// Ensures acceptConnectionInBackgroundAndNotify will be called on the main actor. - /// - Parameter modes: the runloop modes to use. - @MainActor func acceptConnectionInBackgroundAndNotifyOnMainActor(forModes modes: [RunLoop.Mode]? = [RunLoop.Mode.common]) { - acceptConnectionInBackgroundAndNotify(forModes: modes) - } - -} - private extension SocketPort { convenience init(path: String) { From 32a1a0bca963e33875e466d4f6d49d2f94016336 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 29 Nov 2025 13:32:34 -0800 Subject: [PATCH 2/9] Use separate socket for debug builds (#766) --- Sources/Packages/Package.swift | 9 ++++++ .../Sources/Common}/BundleIDs.swift | 0 .../Sources/Common}/URLs.swift | 10 +++++-- Sources/SecretAgent/AppDelegate.swift | 3 +- Sources/Secretive.xcodeproj/project.pbxproj | 30 +++++++++---------- .../Controllers/AgentStatusChecker.swift | 1 + 6 files changed, 33 insertions(+), 20 deletions(-) rename Sources/{Secretive/Helpers => Packages/Sources/Common}/BundleIDs.swift (100%) rename Sources/{Secretive/Controllers => Packages/Sources/Common}/URLs.swift (69%) diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 4dcb725..92dc60d 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -22,6 +22,9 @@ let package = Package( .library( name: "SecretAgentKit", targets: ["SecretAgentKit", "XPCWrappers"]), + .library( + name: "Common", + targets: ["Common"]), .library( name: "Brief", targets: ["Brief"]), @@ -65,6 +68,12 @@ let package = Package( name: "SecretAgentKitTests", dependencies: ["SecretAgentKit"], ), + .target( + name: "Common", + dependencies: [], + resources: [localization], + swiftSettings: swiftSettings, + ), .target( name: "Brief", dependencies: ["XPCWrappers"], diff --git a/Sources/Secretive/Helpers/BundleIDs.swift b/Sources/Packages/Sources/Common/BundleIDs.swift similarity index 100% rename from Sources/Secretive/Helpers/BundleIDs.swift rename to Sources/Packages/Sources/Common/BundleIDs.swift diff --git a/Sources/Secretive/Controllers/URLs.swift b/Sources/Packages/Sources/Common/URLs.swift similarity index 69% rename from Sources/Secretive/Controllers/URLs.swift rename to Sources/Packages/Sources/Common/URLs.swift index 3ea1fe5..a2f37f3 100644 --- a/Sources/Secretive/Controllers/URLs.swift +++ b/Sources/Packages/Sources/Common/URLs.swift @@ -2,19 +2,23 @@ import Foundation extension URL { - static var agentHomeURL: URL { + public static var agentHomeURL: URL { URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) } - static var socketPath: String { + public static var socketPath: String { + #if DEBUG + URL.agentHomeURL.appendingPathComponent("socket-debug.ssh").path() + #else URL.agentHomeURL.appendingPathComponent("socket.ssh").path() + #endif } } extension String { - var normalizedPathAndFolder: (String, String) { + public var normalizedPathAndFolder: (String, String) { // All foundation-based normalization methods replace this with the container directly. let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())") let url = URL(filePath: processedPath) diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 28bef7e..660830b 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -6,6 +6,7 @@ import SmartCardSecretKit import SecretAgentKit import Brief import Observation +import Common @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -26,7 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { Agent(storeList: storeList, witness: notifier) }() private lazy var socketController: SocketController = { - let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String + let path = URL.socketPath as String return SocketController(path: path) }() private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate") diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 89cb503..bcd4b37 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; - 50033AC327813F1700253856 /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; }; 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; }; 5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; }; 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3E278005F300DF2006 /* SecretAgentKit */; }; @@ -26,7 +25,6 @@ 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501578122E6C0479004A37D0 /* XPCInputParser.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; - 504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; }; 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; @@ -69,6 +67,8 @@ 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; + 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; }; + 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; }; 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4522E73C78900C73783 /* WindowBackgroundStyle.swift */; }; 50E4C4C32E7765DF00C73783 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E4C4C22E7765DF00C73783 /* AboutView.swift */; }; 50E4C4C82E777E4200C73783 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 50E4C4C72E777E4200C73783 /* AppIcon.icon */; }; @@ -180,14 +180,12 @@ /* Begin PBXFileReference section */ 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSecretView.swift; sourceTree = ""; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = ""; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = ""; }; 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 501578122E6C0479004A37D0 /* XPCInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCInputParser.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; - 504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = ""; }; 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = ""; }; @@ -246,6 +244,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */, 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */, 501421622781262300BBAA70 /* Brief in Frameworks */, 5003EF5F2780081600DF2006 /* SecureEnclaveSecretKit in Frameworks */, @@ -279,20 +278,13 @@ 5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */, 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */, 5003EF41278005FA00DF2006 /* SecretKit in Frameworks */, + 50E0145E2EDB9CE400B121F1 /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 50033AC427813F1C00253856 /* Helpers */ = { - isa = PBXGroup; - children = ( - 50033AC227813F1700253856 /* BundleIDs.swift */, - ); - path = Helpers; - sourceTree = ""; - }; 504788ED2E681EB200B4556F /* Modifiers */ = { isa = PBXGroup; children = ( @@ -376,7 +368,6 @@ 50617D8223FCE48E0099B055 /* App.swift */, 508A58B0241ED1C40069DC07 /* Views */, 508A58B1241ED1EA0069DC07 /* Controllers */, - 50033AC427813F1C00253856 /* Helpers */, 50617D8E23FCE48E0099B055 /* Info.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, 50E4C4C72E777E4200C73783 /* AppIcon.icon */, @@ -442,7 +433,6 @@ 508A58B1241ED1EA0069DC07 /* Controllers */ = { isa = PBXGroup; children = ( - 504788EB2E680DC400B4556F /* URLs.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, @@ -507,6 +497,7 @@ 5003EF5E2780081600DF2006 /* SecureEnclaveSecretKit */, 5003EF602780081600DF2006 /* SmartCardSecretKit */, 501421612781262300BBAA70 /* Brief */, + 50E0145B2EDB9CDF00B121F1 /* Common */, ); productName = Secretive; productReference = 50617D7F23FCE48E0099B055 /* Secretive.app */; @@ -577,6 +568,7 @@ 5003EF40278005FA00DF2006 /* SecretKit */, 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */, 5003EF642780081B00DF2006 /* SmartCardSecretKit */, + 50E0145D2EDB9CE400B121F1 /* Common */, ); productName = SecretAgent; productReference = 50A3B78A24026B7500D209EA /* SecretAgent.app */; @@ -687,7 +679,6 @@ 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 50E4C4532E73C78C00C73783 /* WindowBackgroundStyle.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, - 504788EC2E680DC800B4556F /* URLs.swift in Sources */, 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, @@ -697,7 +688,6 @@ 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, - 50033AC327813F1700253856 /* BundleIDs.swift in Sources */, 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, @@ -1557,6 +1547,14 @@ isa = XCSwiftPackageProductDependency; productName = SecretAgentKit; }; + 50E0145B2EDB9CDF00B121F1 /* Common */ = { + isa = XCSwiftPackageProductDependency; + productName = Common; + }; + 50E0145D2EDB9CE400B121F1 /* Common */ = { + isa = XCSwiftPackageProductDependency; + productName = Common; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 50617D7723FCE48D0099B055 /* Project object */; diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 4cb4620..6e6cf4e 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -4,6 +4,7 @@ import SecretKit import Observation import OSLog import ServiceManagement +import Common @MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable { var running: Bool { get } From bba4fb9e7cc3f12981e703d3dd40d38296b4579a Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 29 Nov 2025 13:36:41 -0800 Subject: [PATCH 3/9] Update runners to use Xcode 26.1 (#767) * Update images to 26.1 builders * Try running tests via spm again * Revert "Try running tests via spm again" This reverts commit ec9cb609dc5fa1214f09e9c25a1eb0e8f38d65c8. --- .github/workflows/codeql.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f8afbed..44ac1ab 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: build-mode: ${{ matrix.build-mode }} - if: matrix.build-mode == 'manual' name: "Select Xcode" - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app - if: matrix.build-mode == 'manual' name: "Build" run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3e3c67d..00411e7 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -26,7 +26,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app - name: Update Build Number env: RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba5b220..77aebf4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app - name: Test run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test # SPM doesn't seem to pick up on the tests currently? @@ -47,7 +47,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app - name: Update Build Number env: TAG_NAME: ${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d79f525..8209e9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app - name: Test Main Packages run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test # SPM doesn't seem to pick up on the tests currently? From a3bfcb316c132fd7b1e33ebf18b1f3e7ca0cc4a8 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 29 Nov 2025 13:40:29 -0800 Subject: [PATCH 4/9] Add support for one-off builds instead of using nightly workflow (#768) --- .github/workflows/nightly.yml | 1 - .github/workflows/oneoff.yml | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/oneoff.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 00411e7..f90208a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -3,7 +3,6 @@ name: Nightly on: schedule: - cron: "0 8 * * *" - workflow_dispatch: jobs: build: diff --git a/.github/workflows/oneoff.yml b/.github/workflows/oneoff.yml new file mode 100644 index 0000000..4c5da3c --- /dev/null +++ b/.github/workflows/oneoff.yml @@ -0,0 +1,64 @@ +name: One-Off Build + +on: + workflow_dispatch: + +jobs: + build: + runs-on: macos-26 + permissions: + id-token: write + contents: write + attestations: write + actions: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + - name: Setup Signing + env: + SIGNING_DATA: ${{ secrets.SIGNING_DATA }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }} + AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }} + APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + run: ./.github/scripts/signing.sh + - name: Set Environment + run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + - name: Update Build Number + env: + RUN_ID: ${{ github.run_id }} + run: | + DATE=$(date "+%Y-%m-%d") + sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_oneoff-$DATE/g" Sources/Config/Config.xcconfig + sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig + sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig + - name: Build + run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive + - name: Move to Artifact Folder + run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact + - name: Upload App to Artifacts + id: upload + uses: actions/upload-artifact@v4 + with: + name: Secretive + path: Artifact + - name: Download Zipped Artifact + id: download + env: + ZIP_ID: ${{ steps.upload.outputs.artifact-id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \ + https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip + - name: Notarize + env: + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip + - name: Attest + id: attest + uses: actions/attest-build-provenance@v2 + with: + subject-name: "Secretive.zip" + subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }} From d82f4041660bad462b74f766eb6c7d7c81dcc98c Mon Sep 17 00:00:00 2001 From: Jamie <2119834+jamieQ@users.noreply.github.com> Date: Sat, 6 Dec 2025 17:27:45 -0600 Subject: [PATCH 5/9] [fix]: eliminate race condition in SocketController (#769) * [fix]: eliminate race condition in SocketController * [refactor]: use sequence- rather than iterator-based iteration * [fix]: remove now-superfluous await --------- Co-authored-by: Max Goedjen --- .../SecretAgentKit/SocketController.swift | 28 ++++++++++++------- Sources/SecretAgent/AppDelegate.swift | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift index f8aa7cb..7839037 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SocketController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SocketController.swift @@ -37,7 +37,13 @@ public struct SocketController { port = SocketPort(path: path) fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true) Task { @MainActor [fileHandle, sessionsContinuation, logger] in - for await notification in NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) { + // Create the sequence before triggering the notification to + // ensure it will not be missed. + let connectionAcceptedNotifications = NotificationCenter.default.notifications(named: .NSFileHandleConnectionAccepted) + + fileHandle.acceptConnectionInBackgroundAndNotify() + + for await notification in connectionAcceptedNotifications { logger.debug("Socket controller accepted connection") guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { continue } let session = Session(fileHandle: new) @@ -45,7 +51,6 @@ public struct SocketController { fileHandle.acceptConnectionInBackgroundAndNotify() } } - fileHandle.acceptConnectionInBackgroundAndNotify() logger.debug("Socket listening at \(path)") } @@ -77,8 +82,14 @@ extension SocketController { self.fileHandle = fileHandle provenance = SigningRequestTracer().provenance(from: fileHandle) (messages, messagesContinuation) = AsyncStream.makeStream() - Task { [messagesContinuation, logger] in - for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) { + Task { @MainActor [messagesContinuation, logger] in + // Create the sequence before triggering the notification to + // ensure it will not be missed. + let dataAvailableNotifications = NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) + + fileHandle.waitForDataInBackgroundAndNotify() + + for await _ in dataAvailableNotifications { let data = fileHandle.availableData guard !data.isEmpty else { logger.debug("Socket controller received empty data, ending continuation.") @@ -90,16 +101,13 @@ extension SocketController { logger.debug("Socket controller yielded data.") } } - fileHandle.waitForDataInBackgroundAndNotify() } /// Writes new data to the socket. /// - Parameter data: The data to write. - public func write(_ data: Data) async throws { - try fileHandle.write(contentsOf: data) - await MainActor.run { - fileHandle.waitForDataInBackgroundAndNotify() - } + @MainActor public func write(_ data: Data) throws { + try fileHandle.write(contentsOf: data) + fileHandle.waitForDataInBackgroundAndNotify() } /// Closes the socket and cleans up resources. diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 660830b..b49cb81 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -42,7 +42,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { for await message in session.messages { let request = try await inputParser.parse(data: message) let agentResponse = await agent.handle(request: request, provenance: session.provenance) - try await session.write(agentResponse) + try session.write(agentResponse) } } catch { try session.close() From 595de41f036182a68740d9c911039fd480dc7f5b Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 13 Dec 2025 15:29:26 -0800 Subject: [PATCH 6/9] Update runners to 26.2 (#773) --- .github/workflows/codeql.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/oneoff.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 2 +- .../Sources/SecretAgentKit/SigningRequestTracer.swift | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 44ac1ab..fedf2de 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: build-mode: ${{ matrix.build-mode }} - if: matrix.build-mode == 'manual' name: "Select Xcode" - run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - if: matrix.build-mode == 'manual' name: "Build" run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f90208a..3a20673 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -25,7 +25,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Update Build Number env: RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/oneoff.yml b/.github/workflows/oneoff.yml index 4c5da3c..1693abb 100644 --- a/.github/workflows/oneoff.yml +++ b/.github/workflows/oneoff.yml @@ -24,7 +24,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Update Build Number env: RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77aebf4..7370f4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Test run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test # SPM doesn't seem to pick up on the tests currently? @@ -47,7 +47,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} run: ./.github/scripts/signing.sh - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Update Build Number env: TAG_NAME: ${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8209e9f..d19c30c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set Environment - run: sudo xcrun xcode-select -s /Applications/Xcode_26.1.app + run: sudo xcrun xcode-select -s /Applications/Xcode_26.2.app - name: Test Main Packages run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test # SPM doesn't seem to pick up on the tests currently? diff --git a/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift b/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift index 8801d6f..1a2226f 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SigningRequestTracer.swift @@ -36,7 +36,7 @@ extension SigningRequestTracer { /// - Parameter pid: The process ID to look up. /// - Returns: A ``SecretKit.SigningRequestProvenance.Process`` describing the process. func process(from pid: Int32) -> SigningRequestProvenance.Process { - var pidAndNameInfo = self.pidAndNameInfo(from: pid) + var pidAndNameInfo = unsafe self.pidAndNameInfo(from: pid) let ppid = unsafe pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil let procName = unsafe withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in unsafe String(cString: pointer) From 845b1ec3133fbb3e7c4ccafb94335db02515861c Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 13 Dec 2025 20:29:06 -0800 Subject: [PATCH 7/9] Reorder modifiers to fix context menus on macOS 14 (#774) --- .../Views/Secrets/SecretListItemView.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Secretive/Views/Secrets/SecretListItemView.swift b/Sources/Secretive/Views/Secrets/SecretListItemView.swift index 6cffb94..ed63006 100644 --- a/Sources/Secretive/Views/Secrets/SecretListItemView.swift +++ b/Sources/Secretive/Views/Secrets/SecretListItemView.swift @@ -24,6 +24,18 @@ struct SecretListItemView: View { Text(secret.name) } } + .sheet(isPresented: $isRenaming, onDismiss: { + renamedSecret(secret) + }, content: { + if let modifiable = store as? AnySecretStoreModifiable { + EditSecretView(store: modifiable, secret: secret) + } + }) + .showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in + if deleted { + deletedSecret(secret) + } + } .contextMenu { if store is AnySecretStoreModifiable { Button(action: { isRenaming = true }) { @@ -36,17 +48,5 @@ struct SecretListItemView: View { } } } - .showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in - if deleted { - deletedSecret(secret) - } - } - .sheet(isPresented: $isRenaming, onDismiss: { - renamedSecret(secret) - }, content: { - if let modifiable = store as? AnySecretStoreModifiable { - EditSecretView(store: modifiable, secret: secret) - } - }) } } From 2b712864d68d7888aa59f293e4853179fb984ef4 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 14 Dec 2025 11:54:56 -0800 Subject: [PATCH 8/9] Pulling out a bunch of openssh stuff to dedicated package. (#775) --- Package.swift | 16 ++++++++ Sources/Packages/Package.swift | 22 +++++++++-- Sources/Packages/Sources/Common/URLs.swift | 17 +++++++++ .../Sources/SSHProtocolKit/Data+Hex.swift | 37 +++++++++++++++++++ .../LengthAndData.swift | 0 .../OpenSSHPublicKeyWriter.swift | 5 +-- .../OpenSSHReader.swift | 27 +++++++++----- .../OpenSSHSignatureWriter.swift | 1 + .../SSHAgentProtocol.swift | 0 .../Sources/SecretAgentKit/Agent.swift | 1 + .../OpenSSHCertificateHandler.swift | 3 +- .../PublicKeyStandinFileController.swift | 19 ++++------ .../SecretAgentKit/SSHAgentInputParser.swift | 5 ++- .../OpenSSHPublicKeyWriterTests.swift | 7 ++-- .../OpenSSHReaderTests.swift | 4 +- .../SSHProtocolKitTests/TestSecret.swift | 11 ++++++ .../SecretAgentKitTests/AgentTests.swift | 5 ++- .../Tests/SecretAgentKitTests/StubStore.swift | 1 + Sources/SecretAgent/AppDelegate.swift | 2 +- Sources/SecretAgent/XPCInputParser.swift | 1 + .../SecretAgentInputParser.swift | 1 + Sources/Secretive.xcodeproj/project.pbxproj | 7 ++++ .../xcschemes/PackageTests.xcscheme | 3 +- .../Configuration/ToolConfigurationView.swift | 5 ++- .../Views/Secrets/SecretDetailView.swift | 5 ++- 25 files changed, 158 insertions(+), 47 deletions(-) create mode 100644 Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift rename Sources/Packages/Sources/{SecretKit/OpenSSH => SSHProtocolKit}/LengthAndData.swift (100%) rename Sources/Packages/Sources/{SecretKit/OpenSSH => SSHProtocolKit}/OpenSSHPublicKeyWriter.swift (96%) rename Sources/Packages/Sources/{SecretAgentKit => SSHProtocolKit}/OpenSSHReader.swift (52%) rename Sources/Packages/Sources/{SecretKit/OpenSSH => SSHProtocolKit}/OpenSSHSignatureWriter.swift (99%) rename Sources/Packages/Sources/{SecretAgentKit => SSHProtocolKit}/SSHAgentProtocol.swift (100%) rename Sources/Packages/Sources/{SecretKit => SecretAgentKit}/PublicKeyStandinFileController.swift (77%) rename Sources/Packages/Tests/{SecretKitTests => SSHProtocolKitTests}/OpenSSHPublicKeyWriterTests.swift (72%) rename Sources/Packages/Tests/{SecretAgentKitTests => SSHProtocolKitTests}/OpenSSHReaderTests.swift (93%) create mode 100644 Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift diff --git a/Package.swift b/Package.swift index 2ba06ef..3ca29ce 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,9 @@ let package = Package( .library( name: "SmartCardSecretKit", targets: ["SmartCardSecretKit"]), + .library( + name: "SSHProtocolKit", + targets: ["SSHProtocolKit"]), ], dependencies: [ ], @@ -53,6 +56,19 @@ let package = Package( resources: [localization], swiftSettings: swiftSettings ), + .target( + name: "SSHProtocolKit", + dependencies: ["SecretKit"], + path: "Sources/Packages/Sources/SSHProtocolKit", + resources: [localization], + swiftSettings: swiftSettings, + ), + .testTarget( + name: "SSHProtocolKitTests", + dependencies: ["SSHProtocolKit"], + path: "Sources/Packages/Tests/SSHProtocolKitTests", + swiftSettings: swiftSettings, + ), ] ) diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 92dc60d..5d48a5e 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -21,7 +21,7 @@ let package = Package( targets: ["SmartCardSecretKit"]), .library( name: "SecretAgentKit", - targets: ["SecretAgentKit", "XPCWrappers"]), + targets: ["SecretAgentKit"]), .library( name: "Common", targets: ["Common"]), @@ -31,6 +31,9 @@ let package = Package( .library( name: "XPCWrappers", targets: ["XPCWrappers"]), + .library( + name: "SSHProtocolKit", + targets: ["SSHProtocolKit"]), ], dependencies: [ ], @@ -60,7 +63,7 @@ let package = Package( ), .target( name: "SecretAgentKit", - dependencies: ["SecretKit"], + dependencies: ["SecretKit", "SSHProtocolKit", "Common"], resources: [localization], swiftSettings: swiftSettings, ), @@ -68,15 +71,26 @@ let package = Package( name: "SecretAgentKitTests", dependencies: ["SecretAgentKit"], ), + .target( + name: "SSHProtocolKit", + dependencies: ["SecretKit"], + resources: [localization], + swiftSettings: swiftSettings, + ), + .testTarget( + name: "SSHProtocolKitTests", + dependencies: ["SSHProtocolKit"], + swiftSettings: swiftSettings, + ), .target( name: "Common", - dependencies: [], + dependencies: ["SSHProtocolKit", "SecretKit"], resources: [localization], swiftSettings: swiftSettings, ), .target( name: "Brief", - dependencies: ["XPCWrappers"], + dependencies: ["XPCWrappers", "SSHProtocolKit"], resources: [localization], swiftSettings: swiftSettings, ), diff --git a/Sources/Packages/Sources/Common/URLs.swift b/Sources/Packages/Sources/Common/URLs.swift index a2f37f3..9dfee59 100644 --- a/Sources/Packages/Sources/Common/URLs.swift +++ b/Sources/Packages/Sources/Common/URLs.swift @@ -1,4 +1,6 @@ import Foundation +import SSHProtocolKit +import SecretKit extension URL { @@ -14,6 +16,20 @@ extension URL { #endif } + public static var publicKeyDirectory: URL { + agentHomeURL.appending(component: "PublicKeys") + } + + /// The path for a Secret's public key. + /// - Parameter secret: The Secret to return the path for. + /// - Returns: The path to the Secret's public key. + /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. + public static func publicKeyPath(for secret: SecretType, in directory: URL) -> String { + let keyWriter = OpenSSHPublicKeyWriter() + let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") + return directory.appending(component: "\(minimalHex).pub").path() + } + } extension String { @@ -27,3 +43,4 @@ extension String { } } + diff --git a/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift b/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift new file mode 100644 index 0000000..bf1bb1d --- /dev/null +++ b/Sources/Packages/Sources/SSHProtocolKit/Data+Hex.swift @@ -0,0 +1,37 @@ +import Foundation +import CryptoKit + +public struct HexDataStyle: Hashable, Codable { + + let separator: String + + public init(separator: String) { + self.separator = separator + } + +} + +extension HexDataStyle: FormatStyle where SequenceType.Element == UInt8 { + + public func format(_ value: SequenceType) -> String { + value + .compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) } + .joined(separator: separator) + } + +} + +extension FormatStyle where Self == HexDataStyle { + + public static func hex(separator: String = "") -> HexDataStyle { + HexDataStyle(separator: separator) + } + +} +extension FormatStyle where Self == HexDataStyle { + + public static func hex(separator: String = ":") -> HexDataStyle { + HexDataStyle(separator: separator) + } + +} diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/LengthAndData.swift b/Sources/Packages/Sources/SSHProtocolKit/LengthAndData.swift similarity index 100% rename from Sources/Packages/Sources/SecretKit/OpenSSH/LengthAndData.swift rename to Sources/Packages/Sources/SSHProtocolKit/LengthAndData.swift diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift similarity index 96% rename from Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift index 30249e0..2c669db 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHPublicKeyWriter.swift @@ -1,5 +1,6 @@ import Foundation import CryptoKit +import SecretKit /// Generates OpenSSH representations of the public key sof secrets. public struct OpenSSHPublicKeyWriter: Sendable { @@ -49,9 +50,7 @@ public struct OpenSSHPublicKeyWriter: Sendable { /// Generates an OpenSSH MD5 fingerprint string. /// - Returns: OpenSSH MD5 fingerprint string. public func openSSHMD5Fingerprint(secret: SecretType) -> String { - Insecure.MD5.hash(data: data(secret: secret)) - .compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) } - .joined(separator: ":") + Insecure.MD5.hash(data: data(secret: secret)).formatted(.hex(separator: ":")) } public func comment(secret: SecretType) -> String { diff --git a/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift similarity index 52% rename from Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift index a3508e3..6378df2 100644 --- a/Sources/Packages/Sources/SecretAgentKit/OpenSSHReader.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHReader.swift @@ -1,42 +1,49 @@ import Foundation /// Reads OpenSSH protocol data. -final class OpenSSHReader { +public final class OpenSSHReader { var remaining: Data + var done = false /// Initialize the reader with an OpenSSH data payload. /// - Parameter data: The data to read. - init(data: Data) { + public init(data: Data) { remaining = Data(data) } /// Reads the next chunk of data from the playload. /// - Returns: The next chunk of data. - func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data { - let littleEndianLength = try readNextBytes(as: UInt32.self) - let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength) + public func readNextChunk(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> Data { + let length = try readNextBytes(as: UInt32.self, convertEndianness: convertEndianness) guard remaining.count >= length else { throw .beyondBounds } - let dataRange = 0..(as: T.Type) throws(OpenSSHReaderError) -> T { + public func readNextBytes(as: T.Type, convertEndianness: Bool = true) throws(OpenSSHReaderError) -> T { let size = MemoryLayout.size guard remaining.count >= size else { throw .beyondBounds } let lengthRange = 0.. String { + public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String { try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self) } - func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader { + public func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader { OpenSSHReader(data: try readNextChunk(convertEndianness: convertEndianness)) } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift similarity index 99% rename from Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift rename to Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift index b713d53..e5c618d 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift @@ -1,5 +1,6 @@ import Foundation import CryptoKit +import SecretKit /// Generates OpenSSH representations of Secrets. public struct OpenSSHSignatureWriter: Sendable { diff --git a/Sources/Packages/Sources/SecretAgentKit/SSHAgentProtocol.swift b/Sources/Packages/Sources/SSHProtocolKit/SSHAgentProtocol.swift similarity index 100% rename from Sources/Packages/Sources/SecretAgentKit/SSHAgentProtocol.swift rename to Sources/Packages/Sources/SSHProtocolKit/SSHAgentProtocol.swift diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 83ce175..ba66603 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -3,6 +3,7 @@ import CryptoKit import OSLog import SecretKit import AppKit +import SSHProtocolKit /// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores. public final class Agent: Sendable { diff --git a/Sources/Packages/Sources/SecretAgentKit/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretAgentKit/OpenSSHCertificateHandler.swift index 5451e49..7fbb0b3 100644 --- a/Sources/Packages/Sources/SecretAgentKit/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretAgentKit/OpenSSHCertificateHandler.swift @@ -1,11 +1,12 @@ import Foundation import OSLog import SecretKit +import SSHProtocolKit /// Manages storage and lookup for OpenSSH certificates. public actor OpenSSHCertificateHandler: Sendable { - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) + private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory) private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let writer = OpenSSHPublicKeyWriter() private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretAgentKit/PublicKeyStandinFileController.swift similarity index 77% rename from Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift rename to Sources/Packages/Sources/SecretAgentKit/PublicKeyStandinFileController.swift index 49e417e..a8aaffd 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretAgentKit/PublicKeyStandinFileController.swift @@ -1,5 +1,8 @@ import Foundation import OSLog +import SecretKit +import SSHProtocolKit +import Common /// Controller responsible for writing public keys to disk, so that they're easily accessible by scripts. public final class PublicKeyFileStoreController: Sendable { @@ -9,8 +12,8 @@ public final class PublicKeyFileStoreController: Sendable { private let keyWriter = OpenSSHPublicKeyWriter() /// Initializes a PublicKeyFileStoreController. - public init(homeDirectory: URL) { - directory = homeDirectory.appending(component: "PublicKeys") + public init(directory: URL) { + self.directory = directory } /// Writes out the keys specified to disk. @@ -19,7 +22,7 @@ public final class PublicKeyFileStoreController: Sendable { public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws { logger.log("Writing public keys to disk") if clear { - let validPaths = Set(secrets.map { publicKeyPath(for: $0) }) + let validPaths = Set(secrets.map { URL.publicKeyPath(for: $0, in: directory) }) .union(Set(secrets.map { sshCertificatePath(for: $0) })) let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? [] let fullPathContents = contentsOfDirectory.map { directory.appending(path: $0).path() } @@ -33,21 +36,13 @@ public final class PublicKeyFileStoreController: Sendable { } try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil) for secret in secrets { - let path = publicKeyPath(for: secret) + let path = URL.publicKeyPath(for: secret, in: directory) let data = Data(keyWriter.openSSHString(secret: secret).utf8) FileManager.default.createFile(atPath: path, contents: data, attributes: nil) } logger.log("Finished writing public keys") } - /// The path for a Secret's public key. - /// - Parameter secret: The Secret to return the path for. - /// - Returns: The path to the Secret's public key. - /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. - public func publicKeyPath(for secret: SecretType) -> String { - let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending(component: "\(minimalHex).pub").path() - } /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. public var hasAnyCertificates: Bool { diff --git a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift index 6e9a2ee..e8c4d61 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SSHAgentInputParser.swift @@ -1,11 +1,12 @@ import Foundation import OSLog import SecretKit +import SSHProtocolKit public protocol SSHAgentInputParserProtocol { func parse(data: Data) async throws -> SSHAgent.Request - + } public struct SSHAgentInputParser: SSHAgentInputParserProtocol { @@ -13,7 +14,7 @@ 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 { diff --git a/Sources/Packages/Tests/SecretKitTests/OpenSSHPublicKeyWriterTests.swift b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHPublicKeyWriterTests.swift similarity index 72% rename from Sources/Packages/Tests/SecretKitTests/OpenSSHPublicKeyWriterTests.swift rename to Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHPublicKeyWriterTests.swift index 92c3132..806d399 100644 --- a/Sources/Packages/Tests/SecretKitTests/OpenSSHPublicKeyWriterTests.swift +++ b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHPublicKeyWriterTests.swift @@ -1,8 +1,7 @@ import Foundation import Testing @testable import SecretKit -@testable import SecureEnclaveSecretKit -@testable import SmartCardSecretKit +import SSHProtocolKit @Suite struct OpenSSHPublicKeyWriterTests { @@ -47,8 +46,8 @@ import Testing extension OpenSSHPublicKeyWriterTests { enum Constants { - static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), authentication: .notRequired, publicKeyAttribution: "test@example.com")) - static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), authentication: .notRequired, publicKeyAttribution: "test@example.com")) + static let ecdsa256Secret = TestSecret(id: Data(), name: "Test Key (ECDSA 256)", publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 256), authentication: .notRequired, publicKeyAttribution: "test@example.com")) + static let ecdsa384Secret = TestSecret(id: Data(), name: "Test Key (ECDSA 384)", publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!, attributes: Attributes(keyType: KeyType(algorithm: .ecdsa, size: 384), authentication: .notRequired, publicKeyAttribution: "test@example.com")) } diff --git a/Sources/Packages/Tests/SecretAgentKitTests/OpenSSHReaderTests.swift b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHReaderTests.swift similarity index 93% rename from Sources/Packages/Tests/SecretAgentKitTests/OpenSSHReaderTests.swift rename to Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHReaderTests.swift index 34201c6..1e3d95e 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/OpenSSHReaderTests.swift +++ b/Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHReaderTests.swift @@ -1,8 +1,6 @@ import Foundation import Testing -@testable import SecretAgentKit -@testable import SecureEnclaveSecretKit -@testable import SmartCardSecretKit +import SSHProtocolKit @Suite struct OpenSSHReaderTests { diff --git a/Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift b/Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift new file mode 100644 index 0000000..7dca504 --- /dev/null +++ b/Sources/Packages/Tests/SSHProtocolKitTests/TestSecret.swift @@ -0,0 +1,11 @@ +import Foundation +import SecretKit + +public struct TestSecret: SecretKit.Secret { + + public let id: Data + public let name: String + public let publicKey: Data + public var attributes: Attributes + +} diff --git a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift index bbef669..f946524 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/AgentTests.swift @@ -1,6 +1,7 @@ import Foundation import Testing import CryptoKit +@testable import SSHProtocolKit @testable import SecretKit @testable import SecretAgentKit @@ -44,8 +45,8 @@ import CryptoKit 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 + let length = try responseReader.readNextBytes(as: UInt32.self) + let type = try responseReader.readNextBytes(as: UInt8.self) #expect(length == response.count - MemoryLayout.size) #expect(type == SSHAgent.Response.agentSignResponse.rawValue) let outer = OpenSSHReader(data: responseReader.remaining) diff --git a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift index c3a01d7..381f14d 100644 --- a/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift +++ b/Sources/Packages/Tests/SecretAgentKitTests/StubStore.swift @@ -1,6 +1,7 @@ import Foundation import SecretKit import CryptoKit +import SSHProtocolKit struct Stub {} diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index b49cb81..49c109a 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -22,7 +22,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let updater = Updater(checkOnLaunch: true) private let notifier = Notifier() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) + private let publicKeyFileStoreController = PublicKeyFileStoreController(directory: URL.publicKeyDirectory) private lazy var agent: Agent = { Agent(storeList: storeList, witness: notifier) }() diff --git a/Sources/SecretAgent/XPCInputParser.swift b/Sources/SecretAgent/XPCInputParser.swift index b78f316..a3d7f28 100644 --- a/Sources/SecretAgent/XPCInputParser.swift +++ b/Sources/SecretAgent/XPCInputParser.swift @@ -3,6 +3,7 @@ import SecretAgentKit import Brief import XPCWrappers import OSLog +import SSHProtocolKit /// Delegates all agent input parsing to an XPC service which wraps OpenSSH public final class XPCAgentInputParser: SSHAgentInputParserProtocol { diff --git a/Sources/SecretAgentInputParser/SecretAgentInputParser.swift b/Sources/SecretAgentInputParser/SecretAgentInputParser.swift index cc0c8fd..6f69047 100644 --- a/Sources/SecretAgentInputParser/SecretAgentInputParser.swift +++ b/Sources/SecretAgentInputParser/SecretAgentInputParser.swift @@ -2,6 +2,7 @@ import Foundation import OSLog import XPCWrappers import SecretAgentKit +import SSHProtocolKit final class SecretAgentInputParser: NSObject, XPCProtocol { diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index bcd4b37..7d70771 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; + 5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 5002C3AA2EEF483300FFAD22 /* XPCWrappers */; }; 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; }; 5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; }; 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3E278005F300DF2006 /* SecretAgentKit */; }; @@ -265,6 +266,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5002C3AB2EEF483300FFAD22 /* XPCWrappers in Frameworks */, 50692E6C2E6FFA510043C7BB /* SecretAgentKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -539,6 +541,7 @@ name = SecretAgentInputParser; packageProductDependencies = ( 50692E6B2E6FFA510043C7BB /* SecretAgentKit */, + 5002C3AA2EEF483300FFAD22 /* XPCWrappers */, ); productName = SecretAgentInputParser; productReference = 50692E502E6FF9D20043C7BB /* SecretAgentInputParser.xpc */; @@ -1499,6 +1502,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 5002C3AA2EEF483300FFAD22 /* XPCWrappers */ = { + isa = XCSwiftPackageProductDependency; + productName = XPCWrappers; + }; 5003EF3A278005E800DF2006 /* SecretKit */ = { isa = XCSwiftPackageProductDependency; productName = SecretKit; diff --git a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme index 500661b..7b1c414 100644 --- a/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme +++ b/Sources/Secretive.xcodeproj/xcshareddata/xcschemes/PackageTests.xcscheme @@ -14,7 +14,8 @@ shouldUseLaunchSchemeArgsEnv = "YES"> + reference = "container:Config/Secretive.xctestplan" + default = "YES"> diff --git a/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift index 8696e06..495254b 100644 --- a/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift +++ b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift @@ -1,5 +1,7 @@ import SwiftUI import SecretKit +import SSHProtocolKit +import Common struct ToolConfigurationView: View { @@ -111,10 +113,9 @@ struct ToolConfigurationView: View { let writer = OpenSSHPublicKeyWriter() let gitAllowedSignersString = [email.isEmpty ? String(localized: .integrationsConfigureUsingEmailPlaceholder) : email, writer.openSSHString(secret: selectedSecret)] .joined(separator: " ") - let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) return text .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: gitAllowedSignersString) - .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret)) + .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: URL.publicKeyPath(for: selectedSecret, in: URL.publicKeyDirectory)) } } diff --git a/Sources/Secretive/Views/Secrets/SecretDetailView.swift b/Sources/Secretive/Views/Secrets/SecretDetailView.swift index b3940ff..da9cf75 100644 --- a/Sources/Secretive/Views/Secrets/SecretDetailView.swift +++ b/Sources/Secretive/Views/Secrets/SecretDetailView.swift @@ -1,12 +1,13 @@ import SwiftUI import SecretKit +import Common +import SSHProtocolKit struct SecretDetailView: View { let secret: SecretType private let keyWriter = OpenSSHPublicKeyWriter() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) var body: some View { ScrollView { @@ -21,7 +22,7 @@ struct SecretDetailView: View { CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString) Spacer() .frame(height: 20) - CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret), showRevealInFinder: true) + CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: URL.publicKeyPath(for: secret, in: URL.publicKeyDirectory), showRevealInFinder: true) Spacer() } } From 6c56039ece2db80777003ff7b340e6c68e7889a7 Mon Sep 17 00:00:00 2001 From: Sergei Razmetov Date: Sun, 14 Dec 2025 23:00:34 +0300 Subject: [PATCH 9/9] Fix SSH ECDSA signature mpint encoding (#772) * Fix SSH ECDSA signature mpint encoding OpenSSHSignatureWriter was emitting non-canonical mpints (keeping fixed-width leading 0x00 bytes), which breaks strict parsers. Canonicalize r/s mpints and add a regression test. * Cleanup and consolidation --------- Co-authored-by: Max Goedjen --- Sources/Packages/Package.swift | 2 +- .../OpenSSHSignatureWriter.swift | 31 ++++--- .../OpenSSHSignatureWriterTests.swift | 83 +++++++++++++++++++ 3 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 Sources/Packages/Tests/SSHProtocolKitTests/OpenSSHSignatureWriterTests.swift diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 5d48a5e..3a8c855 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -46,7 +46,7 @@ let package = Package( ), .testTarget( name: "SecretKitTests", - dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"], + dependencies: ["SecretKit", "SecretAgentKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"], swiftSettings: swiftSettings, ), .target( diff --git a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift index e5c618d..25397db 100644 --- a/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift +++ b/Sources/Packages/Sources/SSHProtocolKit/OpenSSHSignatureWriter.swift @@ -30,19 +30,28 @@ public struct OpenSSHSignatureWriter: Sendable { extension OpenSSHSignatureWriter { + /// Converts a fixed-width big-endian integer (e.g. r/s from CryptoKit rawRepresentation) into an SSH mpint. + /// Strips unnecessary leading zeros and prefixes `0x00` if needed to keep the value positive. + private func mpint(fromFixedWidthPositiveBytes bytes: Data) -> Data { + // mpint zero is encoded as a string with zero bytes of data. + guard let firstNonZeroIndex = bytes.firstIndex(where: { $0 != 0x00 }) else { + return Data() + } + + let trimmed = Data(bytes[firstNonZeroIndex...]) + + if let first = trimmed.first, first >= 0x80 { + var prefixed = Data([0x00]) + prefixed.append(trimmed) + return prefixed + } + return trimmed + } + func ecdsaSignature(_ rawRepresentation: Data, keyType: KeyType) -> Data { let rawLength = rawRepresentation.count/2 - // Check if we need to pad with 0x00 to prevent certain - // ssh servers from thinking r or s is negative - let paddingRange: ClosedRange = 0x80...0xFF - var r = Data(rawRepresentation[0.. (r: Data, s: Data) { + let reader = OpenSSHReader(data: openSSHSignedData) + + // Prefix + _ = try reader.readNextBytes(as: UInt32.self) + + let algorithm = try reader.readNextChunkAsString() + guard algorithm == "ecdsa-sha2-nistp256" else { + throw ParseError.invalidAlgorithm + } + + let sigReader = try reader.readNextChunkAsSubReader() + let r = try sigReader.readNextChunk() + let s = try sigReader.readNextChunk() + return (r, s) + } + +} +