diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 8851127..783b209 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -43,7 +43,7 @@ extension Agent { } let requestTypeInt = data[4] 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 } logger.debug("Agent handling request of type \(requestType.debugDescription)") @@ -66,10 +66,25 @@ extension Agent { response.append(SSHAgent.ResponseType.agentSignResponse.data) response.append(try await sign(data: data, provenance: provenance)) logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)") + case .protocolExtension: + response.append(SSHAgent.ResponseType.agentExtensionResponse.data) + try await handleExtension(data) + default: + let reader = OpenSSHReader(data: data) + while true { + do { + let payloadHash = try reader.readNextChunk() + print(String(String(decoding: payloadHash, as: UTF8.self))) + print(payloadHash) + } catch { + break + } + } + logger.debug("Agent received valid request of type \(requestType.debugDescription), but not currently supported.") + response.append(SSHAgent.ResponseType.agentFailure.data) } } catch { - response.removeAll() - response.append(SSHAgent.ResponseType.agentFailure.data) + response = SSHAgent.ResponseType.agentFailure.data logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)") } return response.lengthAndData @@ -77,6 +92,28 @@ extension Agent { } +// PROTOCOL EXTENSIONS +extension Agent { + + func handleExtension(_ data: Data) async throws { + let reader = OpenSSHReader(data: data) + guard try reader.readNextChunkAsString() == "session-bind@openssh.com" else { throw UnsupportedExtensionError() } + let hostKey = try reader.readNextChunk() + let khReader = OpenSSHReader(data: hostKey) + print(try khReader.readNextChunkAsString()) + let keyData = try khReader.readNextChunk() + let sessionID = try reader.readNextChunk() + let signatureData = try reader.readNextChunk() + let forwarding = try reader.readNextBytes(count: 1, as: Bool.self) + print(forwarding) + let signatureReader = OpenSSHSignatureReader() + guard try signatureReader.verify(signatureData, for: sessionID, with: keyData) else { throw SignatureVerificationFailedError() } + } + + struct UnsupportedExtensionError: Error {} + struct SignatureVerificationFailedError: Error {} +} + extension Agent { /// Lists the identities available for signing operations @@ -112,7 +149,7 @@ extension Agent { /// - Returns: An OpenSSH formatted Data payload containing the signed data response. func sign(data: Data, provenance: SigningRequestProvenance) async throws -> Data { let reader = OpenSSHReader(data: data) - let payloadHash = reader.readNextChunk() + let payloadHash = try reader.readNextChunk() let hash: Data // Check if hash is actually an openssh certificate and reconstruct the public key if it is @@ -129,7 +166,7 @@ extension Agent { 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 signedData = signatureWriter.data(secret: secret, signature: rawRepresentation) diff --git a/Sources/Packages/Sources/SecretAgentKit/SSHAgentProtocol.swift b/Sources/Packages/Sources/SecretAgentKit/SSHAgentProtocol.swift index 4c45616..30b4747 100644 --- a/Sources/Packages/Sources/SecretAgentKit/SSHAgentProtocol.swift +++ b/Sources/Packages/Sources/SecretAgentKit/SSHAgentProtocol.swift @@ -10,13 +10,32 @@ extension SSHAgent { case requestIdentities = 11 case signRequest = 13 + case addIdentity = 17 + case removeIdentity = 18 + case removeAllIdentities = 19 + case addIDConstrained = 25 + case addSmartcardKey = 20 + case removeSmartcardKey = 21 + case lock = 22 + case unlock = 23 + case addSmartcardKeyConstrained = 26 + case protocolExtension = 27 + public var debugDescription: String { switch self { - case .requestIdentities: - return "RequestIdentities" - case .signRequest: - return "SignRequest" + case .requestIdentities: "SSH_AGENTC_REQUEST_IDENTITIES" + case .signRequest: "SSH_AGENTC_SIGN_REQUEST" + case .addIdentity: "SSH_AGENTC_ADD_IDENTITY" + 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 agentIdentitiesAnswer = 12 case agentSignResponse = 14 + case agentExtensionFailure = 28 + case agentExtensionResponse = 29 public var debugDescription: String { switch self { - case .agentFailure: - return "AgentFailure" - case .agentSuccess: - return "AgentSuccess" - case .agentIdentitiesAnswer: - return "AgentIdentitiesAnswer" - case .agentSignResponse: - return "AgentSignResponse" + case .agentFailure: "SSH_AGENT_FAILURE" + case .agentSuccess: "SSH_AGENT_SUCCESS" + case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER" + case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE" + case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE" + case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE" } } } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 23d64ce..2b16938 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -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. public func publicKeyHash(from hash: Data) -> Data? { let reader = OpenSSHReader(data: hash) - let certType = String(decoding: reader.readNextChunk(), as: UTF8.self) - switch certType { - case "ecdsa-sha2-nistp256-cert-v01@openssh.com", - "ecdsa-sha2-nistp384-cert-v01@openssh.com", - "ecdsa-sha2-nistp521-cert-v01@openssh.com": - _ = reader.readNextChunk() // nonce - let curveIdentifier = reader.readNextChunk() - let publicKey = reader.readNextChunk() + do { + let certType = String(decoding: try reader.readNextChunk(), as: UTF8.self) + switch certType { + case "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com": + _ = try reader.readNextChunk() // nonce + let curveIdentifier = try reader.readNextChunk() + let publicKey = try reader.readNextChunk() - let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "") - return openSSHIdentifier.lengthAndData + - curveIdentifier.lengthAndData + - publicKey.lengthAndData - default: + let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "") + return openSSHIdentifier.lengthAndData + + curveIdentifier.lengthAndData + + publicKey.lengthAndData + default: + return nil + } + } catch { return nil } } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift index 99713e0..30249e0 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHPublicKeyWriter.swift @@ -97,7 +97,7 @@ extension OpenSSHPublicKeyWriter { extension OpenSSHPublicKeyWriter { - public func rsaPublicKeyBlob(secret: SecretType) -> Data { + func rsaPublicKeyBlob(secret: SecretType) -> Data { // 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: // [4 byte prefix][2 byte prefix][n][2 byte prefix][e] diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift index e3ef8aa..422b6e3 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift @@ -13,7 +13,8 @@ public final class OpenSSHReader { /// Reads the next chunk of data from the playload. /// - Returns: The next chunk of data. - public func readNextChunk() -> Data { + public func readNextChunk() throws -> Data { + guard remaining.count > UInt32.bitWidth/8 else { throw EndOfData() } let lengthRange = 0..<(UInt32.bitWidth/8) let lengthChunk = remaining[lengthRange] remaining.removeSubrange(lengthRange) @@ -25,4 +26,18 @@ public final class OpenSSHReader { return ret } + public func readNextBytes(count: Int = 0, as: T.Type) throws -> T { + let lengthRange = 0.. String { + try String(decoding: readNextChunk(), as: UTF8.self) + } + + public struct EndOfData: Error {} + } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureReader.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureReader.swift new file mode 100644 index 0000000..b2d59e3 --- /dev/null +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureReader.swift @@ -0,0 +1,57 @@ +import Foundation +import CryptoKit +import Security + +/// Reads OpenSSH representations of Secrets. +public struct OpenSSHSignatureReader: Sendable { + + /// Initializes the reader. + public init() { + } + + public func verify(_ signatureData: Data, for signedData: Data, with publicKey: Data) throws -> Bool { + let reader = OpenSSHReader(data: signatureData) + let signatureType = try reader.readNextChunkAsString() + let signatureData = try reader.readNextChunk() + switch signatureType { + case "ssh-rsa": + let attributes = KeychainDictionary([ + kSecAttrKeyType: kSecAttrKeyTypeRSA, + kSecAttrKeySizeInBits: 2048, + kSecAttrKeyClass: kSecAttrKeyClassPublic + ]) + var verifyError: SecurityError? + let untyped: CFTypeRef? = SecKeyCreateWithData(publicKey as CFData, attributes, &verifyError) + guard let untypedSafe = untyped else { + throw KeychainError(statusCode: errSecSuccess) + } + let key = untypedSafe as! SecKey + return SecKeyVerifySignature(key, .rsaSignatureMessagePKCS1v15SHA512, signedData as CFData, signatureData as CFData, nil) + case "ecdsa-sha2-nistp256": + return try P256.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData) + case "ecdsa-sha2-nistp384": + return try P384.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData) + case "ecdsa-sha2-nistp521": + return try P521.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(.init(rawRepresentation: signatureData), for: signedData) + case "ssh-ed25519": + return try Curve25519.Signing.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData) + case "ssh-mldsa-65": + if #available(macOS 26.0, *) { + return try MLDSA65.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData) + } else { + throw UnsupportedSignatureType() + } + case "ssh-mldsa-87": + if #available(macOS 26.0, *) { + return try MLDSA87.PublicKey(rawRepresentation: publicKey).isValidSignature(signatureData, for: signedData) + } else { + throw UnsupportedSignatureType() + } + default: + throw UnsupportedSignatureType() + } + } + + public struct UnsupportedSignatureType: Error {} + +}