Merge branch 'main' into xpc_updater

This commit is contained in:
Max Goedjen 2025-09-06 12:52:07 -07:00
commit 9c8810cc56
No known key found for this signature in database
7 changed files with 83 additions and 43 deletions

View File

@ -43,7 +43,7 @@ extension Agent {
} }
let requestTypeInt = data[4] let requestTypeInt = data[4]
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else { guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription) for unknown request type \(requestTypeInt)")
return SSHAgent.ResponseType.agentFailure.data.lengthAndData return SSHAgent.ResponseType.agentFailure.data.lengthAndData
} }
logger.debug("Agent handling request of type \(requestType.debugDescription)") logger.debug("Agent handling request of type \(requestType.debugDescription)")
@ -66,10 +66,13 @@ extension Agent {
response.append(SSHAgent.ResponseType.agentSignResponse.data) response.append(SSHAgent.ResponseType.agentSignResponse.data)
response.append(try await sign(data: data, provenance: provenance)) response.append(try await sign(data: data, provenance: provenance))
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)") logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
default:
logger.debug("Agent received valid request of type \(requestType.debugDescription), but not currently supported.")
response.append(SSHAgent.ResponseType.agentFailure.data)
} }
} catch { } catch {
response.removeAll() response = SSHAgent.ResponseType.agentFailure.data
response.append(SSHAgent.ResponseType.agentFailure.data)
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
} }
return response.lengthAndData return response.lengthAndData
@ -101,7 +104,7 @@ extension Agent {
} }
logger.log("Agent enumerated \(count) identities") logger.log("Agent enumerated \(count) identities")
var countBigEndian = UInt32(count).bigEndian var countBigEndian = UInt32(count).bigEndian
let countData = Data(bytes: &countBigEndian, count: UInt32.bitWidth/8) let countData = Data(bytes: &countBigEndian, count: MemoryLayout<UInt32>.size)
return countData + keyData return countData + keyData
} }
@ -112,7 +115,7 @@ extension Agent {
/// - Returns: An OpenSSH formatted Data payload containing the signed data response. /// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data { func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data {
let reader = OpenSSHReader(data: data) let reader = OpenSSHReader(data: data)
let payloadHash = reader.readNextChunk() let payloadHash = try reader.readNextChunk()
let hash: Data let hash: Data
// Check if hash is actually an openssh certificate and reconstruct the public key if it is // Check if hash is actually an openssh certificate and reconstruct the public key if it is
@ -129,7 +132,7 @@ extension Agent {
try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance) try await witness?.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
let dataToSign = reader.readNextChunk() let dataToSign = try reader.readNextChunk()
let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance) let rawRepresentation = try await store.sign(data: dataToSign, with: secret, for: provenance)
let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation) let signedData = signatureWriter.data(secret: secret, signature: rawRepresentation)

View File

@ -10,13 +10,32 @@ extension SSHAgent {
case requestIdentities = 11 case requestIdentities = 11
case signRequest = 13 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
public var debugDescription: String { public var debugDescription: String {
switch self { switch self {
case .requestIdentities: case .requestIdentities: "SSH_AGENTC_REQUEST_IDENTITIES"
return "RequestIdentities" case .signRequest: "SSH_AGENTC_SIGN_REQUEST"
case .signRequest: case .addIdentity: "SSH_AGENTC_ADD_IDENTITY"
return "SignRequest" case .removeIdentity: "SSH_AGENTC_REMOVE_IDENTITY"
case .removeAllIdentities: "SSH_AGENTC_REMOVE_ALL_IDENTITIES"
case .addIDConstrained: "SSH_AGENTC_ADD_ID_CONSTRAINED"
case .addSmartcardKey: "SSH_AGENTC_ADD_SMARTCARD_KEY"
case .removeSmartcardKey: "SSH_AGENTC_REMOVE_SMARTCARD_KEY"
case .lock: "SSH_AGENTC_LOCK"
case .unlock: "SSH_AGENTC_UNLOCK"
case .addSmartcardKeyConstrained: "SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED"
case .protocolExtension: "SSH_AGENTC_EXTENSION"
} }
} }
} }
@ -28,17 +47,17 @@ extension SSHAgent {
case agentSuccess = 6 case agentSuccess = 6
case agentIdentitiesAnswer = 12 case agentIdentitiesAnswer = 12
case agentSignResponse = 14 case agentSignResponse = 14
case agentExtensionFailure = 28
case agentExtensionResponse = 29
public var debugDescription: String { public var debugDescription: String {
switch self { switch self {
case .agentFailure: case .agentFailure: "SSH_AGENT_FAILURE"
return "AgentFailure" case .agentSuccess: "SSH_AGENT_SUCCESS"
case .agentSuccess: case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
return "AgentSuccess" case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
case .agentIdentitiesAnswer: case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
return "AgentIdentitiesAnswer" case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
case .agentSignResponse:
return "AgentSignResponse"
} }
} }
} }

View File

@ -7,7 +7,7 @@ extension Data {
package var lengthAndData: Data { package var lengthAndData: Data {
let rawLength = UInt32(count) let rawLength = UInt32(count)
var endian = rawLength.bigEndian var endian = rawLength.bigEndian
return Data(bytes: &endian, count: UInt32.bitWidth/8) + self return Data(bytes: &endian, count: MemoryLayout<UInt32>.size) + self
} }
} }

View File

@ -30,20 +30,24 @@ public actor OpenSSHCertificateHandler: Sendable {
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil. /// - 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? { public func publicKeyHash(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash) let reader = OpenSSHReader(data: hash)
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self) do {
switch certType { let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self)
case "ecdsa-sha2-nistp256-cert-v01@openssh.com", switch certType {
"ecdsa-sha2-nistp384-cert-v01@openssh.com", case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com": "ecdsa-sha2-nistp384-cert-v01@openssh.com",
_ = reader.readNextChunk() // nonce "ecdsa-sha2-nistp521-cert-v01@openssh.com":
let curveIdentifier = reader.readNextChunk() _ = try reader.readNextChunk() // nonce
let publicKey = reader.readNextChunk() let curveIdentifier = try reader.readNextChunk()
let publicKey = try reader.readNextChunk()
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "") let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
return openSSHIdentifier.lengthAndData + return openSSHIdentifier.lengthAndData +
curveIdentifier.lengthAndData + curveIdentifier.lengthAndData +
publicKey.lengthAndData publicKey.lengthAndData
default: default:
return nil
}
} catch {
return nil return nil
} }
} }

View File

@ -97,7 +97,7 @@ extension OpenSSHPublicKeyWriter {
extension OpenSSHPublicKeyWriter { extension OpenSSHPublicKeyWriter {
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data { func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253 // Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
// Keychain stores it as a thin ASN.1 wrapper with this format: // Keychain stores it as a thin ASN.1 wrapper with this format:
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e] // [4 byte prefix][2 byte prefix][n][2 byte prefix][e]

View File

@ -13,16 +13,30 @@ public final class OpenSSHReader {
/// Reads the next chunk of data from the playload. /// Reads the next chunk of data from the playload.
/// - Returns: The next chunk of data. /// - Returns: The next chunk of data.
public func readNextChunk() -> Data { public func readNextChunk(convertEndianness: Bool = true) throws -> Data {
let lengthRange = 0..<(UInt32.bitWidth/8) let littleEndianLength = try readNextBytes(as: UInt32.self)
let lengthChunk = remaining[lengthRange] let length = convertEndianness ? Int(littleEndianLength.bigEndian) : Int(littleEndianLength)
remaining.removeSubrange(lengthRange) guard remaining.count >= length else { throw EndOfData() }
let littleEndianLength = lengthChunk.bytes.unsafeLoad(as: UInt32.self)
let length = Int(littleEndianLength.bigEndian)
let dataRange = 0..<length let dataRange = 0..<length
let ret = Data(remaining[dataRange]) let ret = Data(remaining[dataRange])
remaining.removeSubrange(dataRange) remaining.removeSubrange(dataRange)
return ret return ret
} }
public func readNextBytes<T>(as: T.Type) throws -> T {
let size = MemoryLayout<T>.size
guard remaining.count >= size else { throw EndOfData() }
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)
}
public struct EndOfData: Error {}
} }

View File

@ -6,13 +6,13 @@ import Testing
@Suite struct OpenSSHReaderTests { @Suite struct OpenSSHReaderTests {
@Test func signatureRequest() { @Test func signatureRequest() throws {
let reader = OpenSSHReader(data: Constants.signatureRequest) let reader = OpenSSHReader(data: Constants.signatureRequest)
let hash = reader.readNextChunk() let hash = try reader.readNextChunk()
#expect(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ==")) #expect(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ=="))
let dataToSign = reader.readNextChunk() let dataToSign = try reader.readNextChunk()
#expect(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU=")) #expect(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU="))
let empty = reader.readNextChunk() let empty = try reader.readNextChunk()
#expect(empty.isEmpty) #expect(empty.isEmpty)
} }