Merge branch 'xpc_updater' into xpc_services

This commit is contained in:
Max Goedjen 2025-09-06 23:07:59 -07:00
commit 06a10d57ce
No known key found for this signature in database
10 changed files with 126 additions and 58 deletions

View File

@ -24,6 +24,8 @@ jobs:
- name: Set Environment - name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
- name: Test - name: Test
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
# SPM doesn't seem to pick up on the tests currently?
run: swift test --build-system swiftbuild --package-path Sources/Packages run: swift test --build-system swiftbuild --package-path Sources/Packages
build: build:
permissions: permissions:

View File

@ -12,6 +12,8 @@ jobs:
- name: Set Environment - name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
- name: Test Main Packages - name: Test Main Packages
run: swift test --build-system swiftbuild --package-path Sources/Packages run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme PackageTests test
# SPM doesn't seem to pick up on the tests currently?
# run: swift test --build-system swiftbuild --package-path Sources/Packages
- name: Test SecretKit Packages - name: Test SecretKit Packages
run: swift test --build-system swiftbuild run: swift test --build-system swiftbuild

View File

@ -13,12 +13,24 @@
}, },
"testTargets" : [ "testTargets" : [
{ {
"enabled" : false,
"parallelizable" : true,
"target" : { "target" : {
"containerPath" : "container:Secretive.xcodeproj", "containerPath" : "container:Packages",
"identifier" : "50617D9323FCE48E0099B055", "identifier" : "BriefTests",
"name" : "SecretiveTests" "name" : "BriefTests"
}
},
{
"target" : {
"containerPath" : "container:Packages",
"identifier" : "SecretKitTests",
"name" : "SecretKitTests"
}
},
{
"target" : {
"containerPath" : "container:Packages",
"identifier" : "SecretAgentKitTests",
"name" : "SecretAgentKitTests"
} }
} }
], ],

View File

@ -1,9 +1,5 @@
import Foundation import Foundation
@objc public protocol ReleaseProtocol: Sendable {
}
/// A release is a representation of a downloadable update. /// A release is a representation of a downloadable update.
public struct Release: Codable, Sendable { public struct Release: Codable, Sendable {

View File

@ -150,7 +150,7 @@ extension SSHAgent.Response {
var data: Data { var data: Data {
var raw = self.rawValue var raw = self.rawValue
return Data(bytes: &raw, count: MemoryLayout<UInt8>.size) return Data(bytes: &raw, count: MemoryLayout<UInt8>.size)
} }
} }

View File

@ -32,9 +32,12 @@ final class OpenSSHReader {
return lengthChunk.bytes.unsafeLoad(as: T.self) return lengthChunk.bytes.unsafeLoad(as: T.self)
} }
func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
}
func readNextChunkAsString() throws(OpenSSHReaderError) -> String { func readNextChunkAsSubReader(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> OpenSSHReader {
try String(decoding: readNextChunk(), as: UTF8.self) OpenSSHReader(data: try readNextChunk(convertEndianness: convertEndianness))
} }
} }

View File

@ -8,19 +8,22 @@ import CryptoKit
// MARK: Identity Listing // 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 { @Test func emptyStores() async throws {
let agent = Agent(storeList: SecretStoreList()) 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) #expect(response == Constants.Responses.requestIdentitiesEmpty)
} }
@Test func identitiesList() async throws { @Test func identitiesList() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list) 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) #expect(response == Constants.Responses.requestIdentitiesMultiple)
} }
@ -29,40 +32,42 @@ import CryptoKit
@Test func noMatchingIdentities() async throws { @Test func noMatchingIdentities() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list) 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) #expect(response == Constants.Responses.requestFailure)
} }
// @Test func ecdsaSignature() async throws { @Test func ecdsaSignature() async throws {
// let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature) let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
// let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...]) guard case SSHAgent.Request.signRequest(let context) = request else { return }
// _ = requestReader.readNextChunk() let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
// let dataToSign = requestReader.readNextChunk() let agent = Agent(storeList: list)
// let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret]) let response = await agent.handle(request: request, provenance: .test)
// let agent = Agent(storeList: list) let responseReader = OpenSSHReader(data: response)
// await agent.handle(reader: stubReader, writer: stubWriter) let length = try responseReader.readNextBytes(as: UInt32.self).bigEndian
// let outer = OpenSSHReader(data: stubWriter.data[5...]) let type = try responseReader.readNextBytes(as: UInt8.self).bigEndian
// let payload = outer.readNextChunk() #expect(length == response.count - MemoryLayout<UInt32>.size)
// let inner = OpenSSHReader(data: payload) #expect(type == SSHAgent.Response.agentSignResponse.rawValue)
// _ = inner.readNextChunk() let outer = OpenSSHReader(data: responseReader.remaining)
// let signedData = inner.readNextChunk() let inner = try outer.readNextChunkAsSubReader()
// let rsData = OpenSSHReader(data: signedData) _ = try inner.readNextChunk()
// var r = rsData.readNextChunk() let rsData = try inner.readNextChunkAsSubReader()
// var s = rsData.readNextChunk() var r = try rsData.readNextChunk()
// // This is fine IRL, but it freaks out CryptoKit var s = try rsData.readNextChunk()
// if r[0] == 0 { // This is fine IRL, but it freaks out CryptoKit
// r.removeFirst() if r[0] == 0 {
// } r.removeFirst()
// if s[0] == 0 { }
// s.removeFirst() if s[0] == 0 {
// } s.removeFirst()
// var rs = r }
// rs.append(s) var rs = r
// let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs) rs.append(s)
// // Correct signature let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
// #expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey) // Correct signature
// .isValidSignature(signature, for: dataToSign)) #expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
// } .isValidSignature(signature, for: context.dataToSign))
}
// MARK: Witness protocol // MARK: Witness protocol
@ -72,7 +77,7 @@ import CryptoKit
return true return true
}, witness: { _, _ in }) }, witness: { _, _ in })
let agent = Agent(storeList: list, witness: witness) 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) #expect(response == Constants.Responses.requestFailure)
} }
@ -85,7 +90,8 @@ import CryptoKit
witnessed = true witnessed = true
}) })
let agent = Agent(storeList: list, witness: witness) 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) #expect(witnessed)
} }
@ -100,7 +106,8 @@ import CryptoKit
witnessTrace = trace witnessTrace = trace
}) })
let agent = Agent(storeList: list, witness: witness) 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 == speakNowTrace)
#expect(witnessTrace == .test) #expect(witnessTrace == .test)
} }
@ -112,7 +119,8 @@ import CryptoKit
let store = await list.stores.first?.base as! Stub.Store let store = await list.stores.first?.base as! Stub.Store
store.shouldThrow = true store.shouldThrow = true
let agent = Agent(storeList: list) 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) #expect(response == Constants.Responses.requestFailure)
} }
@ -120,7 +128,7 @@ import CryptoKit
@Test func unhandledAdd() async throws { @Test func unhandledAdd() async throws {
let agent = Agent(storeList: SecretStoreList()) 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) #expect(response == Constants.Responses.requestFailure)
} }
@ -146,14 +154,13 @@ extension AgentTests {
enum Requests { enum Requests {
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")! 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 requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")! static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
} }
enum Responses { enum Responses {
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")! 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=")! static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
} }

View File

@ -1,6 +1,6 @@
import Foundation import Foundation
import Testing import Testing
@testable import SecretKit @testable import SecretAgentKit
@testable import SecureEnclaveSecretKit @testable import SecureEnclaveSecretKit
@testable import SmartCardSecretKit @testable import SmartCardSecretKit

View File

@ -82,7 +82,7 @@ extension Stub {
let privateKey: Data let privateKey: Data
init(keySize: Int, publicKey: Data, 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.publicKey = publicKey
self.privateKey = privateKey self.privateKey = privateKey
} }

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Config/Secretive.xctestplan">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>