mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-09-09 09:59:03 +02:00
Parsing OpenSSH extensions (#823)
* Agent extension parsing * Temporarily disable binding * Fix test * TODO Cleanup * TODO Cleanup * Put back assertion
This commit is contained in:
@@ -19,7 +19,7 @@ public struct OpenSSHPublicKeyWriter: Sendable {
|
|||||||
("nistp" + String(describing: secret.keyType.size)).lengthAndData +
|
("nistp" + String(describing: secret.keyType.size)).lengthAndData +
|
||||||
secret.publicKey.lengthAndData
|
secret.publicKey.lengthAndData
|
||||||
case .mldsa:
|
case .mldsa:
|
||||||
// https://www.ietf.org/archive/id/draft-sfluhrer-ssh-mldsa-04.txt
|
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
|
||||||
openSSHIdentifier(for: secret.keyType).lengthAndData +
|
openSSHIdentifier(for: secret.keyType).lengthAndData +
|
||||||
secret.publicKey.lengthAndData
|
secret.publicKey.lengthAndData
|
||||||
case .rsa:
|
case .rsa:
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ public final class OpenSSHReader {
|
|||||||
return convertEndianness ? T(value.bigEndian) : T(value)
|
return convertEndianness ? T(value.bigEndian) : T(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func readNextByteAsBool() throws(OpenSSHReaderError) -> Bool {
|
||||||
|
let size = MemoryLayout<Bool>.size
|
||||||
|
guard remaining.count >= size else { throw .beyondBounds }
|
||||||
|
let lengthRange = 0..<size
|
||||||
|
let lengthChunk = remaining[lengthRange]
|
||||||
|
remaining.removeSubrange(lengthRange)
|
||||||
|
if remaining.isEmpty {
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
return unsafe lengthChunk.bytes.unsafeLoad(as: Bool.self)
|
||||||
|
}
|
||||||
|
|
||||||
public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
|
public func readNextChunkAsString(convertEndianness: Bool = true) throws(OpenSSHReaderError) -> String {
|
||||||
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
|
try String(decoding: readNextChunk(convertEndianness: convertEndianness), as: UTF8.self)
|
||||||
}
|
}
|
||||||
@@ -53,5 +65,6 @@ public final class OpenSSHReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum OpenSSHReaderError: Error, Codable {
|
public enum OpenSSHReaderError: Error, Codable {
|
||||||
|
case incorrectFormat
|
||||||
case beyondBounds
|
case beyondBounds
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public struct OpenSSHSignatureWriter: Sendable {
|
|||||||
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
||||||
ecdsaSignature(signature, keyType: secret.keyType)
|
ecdsaSignature(signature, keyType: secret.keyType)
|
||||||
case .mldsa:
|
case .mldsa:
|
||||||
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-00#name-public-key-algorithms
|
// https://datatracker.ietf.org/doc/html/draft-sfluhrer-ssh-mldsa-05
|
||||||
mldsaSignature(signature, keyType: secret.keyType)
|
mldsaSignature(signature, keyType: secret.keyType)
|
||||||
case .rsa:
|
case .rsa:
|
||||||
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import OSLog
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import CertificateKit
|
import CertificateKit
|
||||||
|
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
public protocol SSHAgentInputParserProtocol {
|
public protocol SSHAgentInputParserProtocol {
|
||||||
|
|
||||||
func parse(data: Data) async throws -> SSHAgent.Request
|
func parse(data: Data) async throws -> SSHAgent.Request
|
||||||
@@ -53,8 +55,8 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
|
|||||||
return .unlock
|
return .unlock
|
||||||
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
|
case SSHAgent.Request.addSmartcardKeyConstrained.protocolID:
|
||||||
return .addSmartcardKeyConstrained
|
return .addSmartcardKeyConstrained
|
||||||
case SSHAgent.Request.protocolExtension.protocolID:
|
case SSHAgent.Request.protocolExtension(.empty).protocolID:
|
||||||
return .protocolExtension
|
return .protocolExtension(try protocolExtension(from: body))
|
||||||
default:
|
default:
|
||||||
return .unknown(rawRequestInt)
|
return .unknown(rawRequestInt)
|
||||||
}
|
}
|
||||||
@@ -64,12 +66,150 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
|
|||||||
|
|
||||||
extension SSHAgentInputParser {
|
extension SSHAgentInputParser {
|
||||||
|
|
||||||
|
private enum Constants {
|
||||||
|
static let userAuthMagic: UInt8 = 50 // SSH2_MSG_USERAUTH_REQUEST
|
||||||
|
static let sshSigMagic = Data("SSHSIG".utf8)
|
||||||
|
}
|
||||||
|
|
||||||
func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext {
|
func signatureRequestContext(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext {
|
||||||
let reader = OpenSSHReader(data: data)
|
let reader = OpenSSHReader(data: data)
|
||||||
let rawKeyBlob = try reader.readNextChunk()
|
let rawKeyBlob = try reader.readNextChunk()
|
||||||
let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob
|
let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob
|
||||||
let dataToSign = try reader.readNextChunk()
|
let rawPayload = try reader.readNextChunk()
|
||||||
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: dataToSign)
|
let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload
|
||||||
|
do {
|
||||||
|
if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic {
|
||||||
|
payload = .init(raw: rawPayload, decoded: .sshSig(try sshSigPayload(from: rawPayload[6...])))
|
||||||
|
} else {
|
||||||
|
payload = .init(raw: rawPayload, decoded: .sshConnection(try sshConnectionPayload(from: rawPayload)))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
payload = .init(raw: rawPayload, decoded: nil)
|
||||||
|
}
|
||||||
|
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshSigPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHSigPayload {
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L79
|
||||||
|
let payloadReader = OpenSSHReader(data: data)
|
||||||
|
let namespace = try payloadReader.readNextChunkAsString()
|
||||||
|
_ = try payloadReader.readNextChunk() // reserved
|
||||||
|
let hashAlgorithm = try payloadReader.readNextChunkAsString()
|
||||||
|
let hash = try payloadReader.readNextChunk()
|
||||||
|
return .init(
|
||||||
|
namespace: namespace,
|
||||||
|
hashAlgorithm: hashAlgorithm,
|
||||||
|
hash: hash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshConnectionPayload(from data: Data) throws(OpenSSHReaderError) -> SSHAgent.Request.SignatureRequestContext.SignaturePayload.DecodedPayload.SSHConnectionPayload {
|
||||||
|
let payloadReader = OpenSSHReader(data: data)
|
||||||
|
_ = try payloadReader.readNextChunk()
|
||||||
|
let magic = try payloadReader.readNextBytes(as: UInt8.self, convertEndianness: false)
|
||||||
|
guard magic == Constants.userAuthMagic else { throw .incorrectFormat }
|
||||||
|
let username = try payloadReader.readNextChunkAsString()
|
||||||
|
_ = try payloadReader.readNextChunkAsString() // "ssh-connection"
|
||||||
|
_ = try payloadReader.readNextChunkAsString() // "publickey-hostbound-v00@openssh.com"
|
||||||
|
let hasSignature = try payloadReader.readNextByteAsBool()
|
||||||
|
let algorithm = try payloadReader.readNextChunkAsString()
|
||||||
|
let publicKeyReader = try payloadReader.readNextChunkAsSubReader()
|
||||||
|
_ = try publicKeyReader.readNextChunk()
|
||||||
|
_ = try publicKeyReader.readNextChunk()
|
||||||
|
let publicKey = try publicKeyReader.readNextChunk()
|
||||||
|
let hostKeyReader = try payloadReader.readNextChunkAsSubReader()
|
||||||
|
_ = try hostKeyReader.readNextChunk()
|
||||||
|
let hostKey = try hostKeyReader.readNextChunk()
|
||||||
|
return .init(
|
||||||
|
username: username,
|
||||||
|
hasSignature: hasSignature,
|
||||||
|
publicKeyAlgorithm: algorithm,
|
||||||
|
publicKey: publicKey,
|
||||||
|
hostKey: hostKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolExtension(from data: Data) throws(AgentParsingError) -> SSHAgent.ProtocolExtension {
|
||||||
|
do {
|
||||||
|
let reader = OpenSSHReader(data: data)
|
||||||
|
let nameRaw = try reader.readNextChunkAsString()
|
||||||
|
let nameSplit = nameRaw.split(separator: "@")
|
||||||
|
guard nameSplit.count == 2 else {
|
||||||
|
throw AgentParsingError.invalidData
|
||||||
|
}
|
||||||
|
let (name, domain) = (nameSplit[0], nameSplit[1])
|
||||||
|
switch domain {
|
||||||
|
case SSHAgent.ProtocolExtension.OpenSSHExtension.domain:
|
||||||
|
switch name {
|
||||||
|
case SSHAgent.ProtocolExtension.OpenSSHExtension.sessionBind(.empty).name:
|
||||||
|
let hostkeyBlob = try reader.readNextChunkAsSubReader()
|
||||||
|
let hostKeyType = try hostkeyBlob.readNextChunkAsString()
|
||||||
|
let hostKeyData = try hostkeyBlob.readNextChunk()
|
||||||
|
let sessionID = try reader.readNextChunk()
|
||||||
|
let signatureBlob = try reader.readNextChunkAsSubReader()
|
||||||
|
_ = try signatureBlob.readNextChunk() // key type again
|
||||||
|
let signature = try signatureBlob.readNextChunk()
|
||||||
|
let forwarding = try reader.readNextByteAsBool()
|
||||||
|
switch hostKeyType {
|
||||||
|
case "ssh-ed25519":
|
||||||
|
let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData)
|
||||||
|
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
||||||
|
throw AgentParsingError.incorrectSignature
|
||||||
|
}
|
||||||
|
case "ecdsa-sha2-nistp256":
|
||||||
|
let hostKey = try CryptoKit.P256.Signing.PublicKey(rawRepresentation: hostKeyData)
|
||||||
|
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
|
||||||
|
throw AgentParsingError.incorrectSignature
|
||||||
|
}
|
||||||
|
case "ecdsa-sha2-nistp384":
|
||||||
|
let hostKey = try CryptoKit.P384.Signing.PublicKey(rawRepresentation: hostKeyData)
|
||||||
|
guard hostKey.isValidSignature(try .init(rawRepresentation: signature), for: sessionID) else {
|
||||||
|
throw AgentParsingError.incorrectSignature
|
||||||
|
}
|
||||||
|
case "ssh-mldsa-65":
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
|
||||||
|
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
||||||
|
throw AgentParsingError.incorrectSignature
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw AgentParsingError.unhandledRequest
|
||||||
|
}
|
||||||
|
case "ssh-mldsa-87":
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
let hostKey = try CryptoKit.MLDSA65.PublicKey(rawRepresentation: hostKeyData)
|
||||||
|
guard hostKey.isValidSignature(signature, for: sessionID) else {
|
||||||
|
throw AgentParsingError.incorrectSignature
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw AgentParsingError.unhandledRequest
|
||||||
|
}
|
||||||
|
case "ssh-rsa":
|
||||||
|
throw AgentParsingError.unhandledRequest
|
||||||
|
default:
|
||||||
|
throw AgentParsingError.unhandledRequest
|
||||||
|
}
|
||||||
|
let context = SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext(
|
||||||
|
hostKey: hostKeyData,
|
||||||
|
sessionID: sessionID,
|
||||||
|
signature: signature,
|
||||||
|
forwarding: forwarding
|
||||||
|
)
|
||||||
|
return .openSSH(.sessionBind(context))
|
||||||
|
default:
|
||||||
|
return .openSSH(.unknown(String(name)))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return .unknown(nameRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch let error as OpenSSHReaderError {
|
||||||
|
throw .openSSHReader(error)
|
||||||
|
} catch let error as AgentParsingError {
|
||||||
|
throw error
|
||||||
|
} catch {
|
||||||
|
throw .unknownRequest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func certificatePublicKeyBlob(from hash: Data) -> Data? {
|
func certificatePublicKeyBlob(from hash: Data) -> Data? {
|
||||||
@@ -99,6 +239,7 @@ extension SSHAgentInputParser {
|
|||||||
case unknownRequest
|
case unknownRequest
|
||||||
case unhandledRequest
|
case unhandledRequest
|
||||||
case invalidData
|
case invalidData
|
||||||
|
case incorrectSignature
|
||||||
case openSSHReader(OpenSSHReaderError)
|
case openSSHReader(OpenSSHReaderError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ extension SSHAgent {
|
|||||||
case lock
|
case lock
|
||||||
case unlock
|
case unlock
|
||||||
case addSmartcardKeyConstrained
|
case addSmartcardKeyConstrained
|
||||||
case protocolExtension
|
case protocolExtension(ProtocolExtension)
|
||||||
case unknown(UInt8)
|
case unknown(UInt8)
|
||||||
|
|
||||||
public var protocolID: UInt8 {
|
public var protocolID: UInt8 {
|
||||||
@@ -60,18 +60,82 @@ extension SSHAgent {
|
|||||||
|
|
||||||
public struct SignatureRequestContext: Sendable, Codable {
|
public struct SignatureRequestContext: Sendable, Codable {
|
||||||
public let keyBlob: Data
|
public let keyBlob: Data
|
||||||
public let dataToSign: Data
|
public let dataToSign: SignaturePayload
|
||||||
|
|
||||||
public init(keyBlob: Data, dataToSign: Data) {
|
public init(keyBlob: Data, dataToSign: SignaturePayload) {
|
||||||
self.keyBlob = keyBlob
|
self.keyBlob = keyBlob
|
||||||
self.dataToSign = dataToSign
|
self.dataToSign = dataToSign
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var empty: SignatureRequestContext {
|
public static var empty: SignatureRequestContext {
|
||||||
SignatureRequestContext(keyBlob: Data(), dataToSign: Data())
|
SignatureRequestContext(keyBlob: Data(), dataToSign: SignaturePayload(raw: Data(), decoded: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SignaturePayload: Sendable, Codable {
|
||||||
|
|
||||||
|
public let raw: Data
|
||||||
|
public let decoded: DecodedPayload?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
raw: Data,
|
||||||
|
decoded: DecodedPayload?
|
||||||
|
) {
|
||||||
|
self.raw = raw
|
||||||
|
self.decoded = decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DecodedPayload: Sendable, Codable {
|
||||||
|
case sshConnection(SSHConnectionPayload)
|
||||||
|
case sshSig(SSHSigPayload)
|
||||||
|
|
||||||
|
public struct SSHConnectionPayload: Sendable, Codable {
|
||||||
|
|
||||||
|
public let username: String
|
||||||
|
public let hasSignature: Bool
|
||||||
|
public let publicKeyAlgorithm: String
|
||||||
|
public let publicKey: Data
|
||||||
|
public let hostKey: Data
|
||||||
|
|
||||||
|
public init(
|
||||||
|
username: String,
|
||||||
|
hasSignature: Bool,
|
||||||
|
publicKeyAlgorithm: String,
|
||||||
|
publicKey: Data,
|
||||||
|
hostKey: Data
|
||||||
|
) {
|
||||||
|
self.username = username
|
||||||
|
self.hasSignature = hasSignature
|
||||||
|
self.publicKeyAlgorithm = publicKeyAlgorithm
|
||||||
|
self.publicKey = publicKey
|
||||||
|
self.hostKey = hostKey
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SSHSigPayload: Sendable, Codable {
|
||||||
|
|
||||||
|
public let namespace: String
|
||||||
|
public let hashAlgorithm: String
|
||||||
|
public let hash: Data
|
||||||
|
|
||||||
|
public init(
|
||||||
|
namespace: String,
|
||||||
|
hashAlgorithm: String,
|
||||||
|
hash: Data,
|
||||||
|
) {
|
||||||
|
self.namespace = namespace
|
||||||
|
self.hashAlgorithm = hashAlgorithm
|
||||||
|
self.hash = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
||||||
@@ -88,8 +152,8 @@ extension SSHAgent {
|
|||||||
switch self {
|
switch self {
|
||||||
case .agentFailure: "SSH_AGENT_FAILURE"
|
case .agentFailure: "SSH_AGENT_FAILURE"
|
||||||
case .agentSuccess: "SSH_AGENT_SUCCESS"
|
case .agentSuccess: "SSH_AGENT_SUCCESS"
|
||||||
case .agentIdentitiesAnswer: "SSH_AGENT_IDENTITIES_ANSWER"
|
case .agentIdentitiesAnswer: "SSH2_AGENT_IDENTITIES_ANSWER"
|
||||||
case .agentSignResponse: "SSH_AGENT_SIGN_RESPONSE"
|
case .agentSignResponse: "SSH2_AGENT_SIGN_RESPONSE"
|
||||||
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
|
case .agentExtensionFailure: "SSH_AGENT_EXTENSION_FAILURE"
|
||||||
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
|
case .agentExtensionResponse: "SSH_AGENT_EXTENSION_RESPONSE"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Extensions, as defined in https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent
|
||||||
|
|
||||||
|
extension SSHAgent {
|
||||||
|
|
||||||
|
public enum ProtocolExtension: CustomDebugStringConvertible, Codable, Sendable {
|
||||||
|
case openSSH(OpenSSHExtension)
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
public var debugDescription: String {
|
||||||
|
switch self {
|
||||||
|
case let .openSSH(protocolExtension):
|
||||||
|
protocolExtension.debugDescription
|
||||||
|
case .unknown(let string):
|
||||||
|
"Unknown (\(string))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var empty: ProtocolExtension {
|
||||||
|
.unknown("empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ProtocolExtensionParsingError: Error {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SSHAgent.ProtocolExtension {
|
||||||
|
|
||||||
|
public enum OpenSSHExtension: CustomDebugStringConvertible, Codable, Sendable {
|
||||||
|
case sessionBind(SessionBindContext)
|
||||||
|
case unknown(String)
|
||||||
|
|
||||||
|
public static var domain: String {
|
||||||
|
"openssh.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String {
|
||||||
|
switch self {
|
||||||
|
case .sessionBind:
|
||||||
|
"session-bind"
|
||||||
|
case .unknown(let name):
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var debugDescription: String {
|
||||||
|
"\(name)@\(OpenSSHExtension.domain)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SSHAgent.ProtocolExtension.OpenSSHExtension {
|
||||||
|
|
||||||
|
public struct SessionBindContext: Codable, Sendable {
|
||||||
|
|
||||||
|
public let hostKey: Data
|
||||||
|
public let sessionID: Data
|
||||||
|
public let signature: Data
|
||||||
|
public let forwarding: Bool
|
||||||
|
|
||||||
|
public init(hostKey: Data, sessionID: Data, signature: Data, forwarding: Bool) {
|
||||||
|
self.hostKey = hostKey
|
||||||
|
self.sessionID = sessionID
|
||||||
|
self.signature = signature
|
||||||
|
self.forwarding = forwarding
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let empty = SessionBindContext(hostKey: Data(), sessionID: Data(), signature: Data(), forwarding: false)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ public final class Agent: Sendable {
|
|||||||
private let signatureWriter = OpenSSHSignatureWriter()
|
private let signatureWriter = OpenSSHSignatureWriter()
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
||||||
|
|
||||||
|
@MainActor private var sessionID: SSHAgent.ProtocolExtension.OpenSSHExtension.SessionBindContext?
|
||||||
|
|
||||||
/// Initializes an agent with a store list and a witness.
|
/// Initializes an agent with a store list and a witness.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - storeList: The `SecretStoreList` to make available.
|
/// - storeList: The `SecretStoreList` to make available.
|
||||||
@@ -31,7 +33,11 @@ public final class Agent: Sendable {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
public func handle(request: SSHAgent.Request, provenance: SigningRequestProvenance) async -> Data {
|
public func handle(
|
||||||
|
request: SSHAgent.Request,
|
||||||
|
provenance: SigningRequestProvenance
|
||||||
|
) async -> Data {
|
||||||
|
logger.debug("Agent received request of type \(request.debugDescription)")
|
||||||
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
||||||
await reloadSecretsIfNeccessary()
|
await reloadSecretsIfNeccessary()
|
||||||
var response = Data()
|
var response = Data()
|
||||||
@@ -42,9 +48,54 @@ extension Agent {
|
|||||||
response.append(await identities())
|
response.append(await identities())
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.Response.agentIdentitiesAnswer.debugDescription)")
|
||||||
case .signRequest(let context):
|
case .signRequest(let context):
|
||||||
|
let target: SigningRequestTarget?
|
||||||
|
switch context.dataToSign.decoded {
|
||||||
|
case .sshConnection(let payload):
|
||||||
|
target = .connection(
|
||||||
|
.init(
|
||||||
|
username: payload.username,
|
||||||
|
hasSignature: payload.hasSignature,
|
||||||
|
publicKeyAlgorithm: payload.publicKeyAlgorithm,
|
||||||
|
publicKey: payload.publicKey,
|
||||||
|
hostKey: payload.hostKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if let boundSession = await sessionID {
|
||||||
|
guard payload.hostKey == boundSession.hostKey else {
|
||||||
|
logger.error("Agent received bind request, but host key does not match signature request host key.")
|
||||||
|
throw BindingFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .sshSig(let payload):
|
||||||
|
target = .signature(
|
||||||
|
.init(
|
||||||
|
namespace: payload.namespace,
|
||||||
|
hashAlgorithm: payload.hashAlgorithm,
|
||||||
|
hash: payload.hash
|
||||||
|
)
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
target = nil
|
||||||
|
}
|
||||||
|
_ = target
|
||||||
response.append(SSHAgent.Response.agentSignResponse.data)
|
response.append(SSHAgent.Response.agentSignResponse.data)
|
||||||
response.append(try await sign(data: context.dataToSign, keyBlob: context.keyBlob, provenance: provenance))
|
response.append(try await sign(data: context.dataToSign.raw, keyBlob: context.keyBlob, provenance: provenance))
|
||||||
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
|
||||||
|
case .protocolExtension(.openSSH(.sessionBind(let bind))):
|
||||||
|
// This is disabled until forward enforcement is handled.
|
||||||
|
_ = bind
|
||||||
|
response = try await MainActor.run {
|
||||||
|
logger.debug("Agent received bind request but not currently supported.")
|
||||||
|
throw UnhandledRequestError()
|
||||||
|
// guard sessionID == nil else {
|
||||||
|
// logger.error("Agent received bind request, but already bound.")
|
||||||
|
// throw BindingFailure()
|
||||||
|
// }
|
||||||
|
// logger.debug("Agent bound")
|
||||||
|
// sessionID = bind
|
||||||
|
// return SSHAgent.Response.agentSuccess.data
|
||||||
|
}
|
||||||
|
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)")
|
||||||
case .unknown(let value):
|
case .unknown(let value):
|
||||||
logger.error("Agent received unknown request of type \(value).")
|
logger.error("Agent received unknown request of type \(value).")
|
||||||
throw UnhandledRequestError()
|
throw UnhandledRequestError()
|
||||||
@@ -142,6 +193,7 @@ extension Agent {
|
|||||||
|
|
||||||
struct NoMatchingKeyError: Error {}
|
struct NoMatchingKeyError: Error {}
|
||||||
struct UnhandledRequestError: Error {}
|
struct UnhandledRequestError: Error {}
|
||||||
|
struct BindingFailure: Error {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
/// Describes the target of the signature operation.
|
||||||
|
public enum SigningRequestTarget: Sendable {
|
||||||
|
|
||||||
|
case connection(ConnectionPayload)
|
||||||
|
case signature(SignaturePayload)
|
||||||
|
|
||||||
|
public struct ConnectionPayload: Sendable, Codable{
|
||||||
|
|
||||||
|
public let username: String
|
||||||
|
public let hasSignature: Bool
|
||||||
|
public let publicKeyAlgorithm: String
|
||||||
|
public let publicKey: Data
|
||||||
|
public let hostKey: Data
|
||||||
|
|
||||||
|
public init(
|
||||||
|
username: String,
|
||||||
|
hasSignature: Bool,
|
||||||
|
publicKeyAlgorithm: String,
|
||||||
|
publicKey: Data,
|
||||||
|
hostKey: Data,
|
||||||
|
) {
|
||||||
|
self.username = username
|
||||||
|
self.hasSignature = hasSignature
|
||||||
|
self.publicKeyAlgorithm = publicKeyAlgorithm
|
||||||
|
self.publicKey = publicKey
|
||||||
|
self.hostKey = hostKey
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SignaturePayload: Sendable, Codable {
|
||||||
|
|
||||||
|
public let namespace: String
|
||||||
|
public let hashAlgorithm: String
|
||||||
|
public let hash: Data
|
||||||
|
|
||||||
|
public init(
|
||||||
|
namespace: String,
|
||||||
|
hashAlgorithm: String,
|
||||||
|
hash: Data,
|
||||||
|
) {
|
||||||
|
self.namespace = namespace
|
||||||
|
self.hashAlgorithm = hashAlgorithm
|
||||||
|
self.hash = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ import CertificateKit
|
|||||||
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||||
// Correct signature
|
// Correct signature
|
||||||
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
||||||
.isValidSignature(signature, for: context.dataToSign))
|
.isValidSignature(signature, for: context.dataToSign.raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Witness protocol
|
// MARK: Witness protocol
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>com.apple.security.hardened-process.hardened-heap</key>
|
<key>com.apple.security.hardened-process.hardened-heap</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.smartcard</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
<key>com.apple.security.hardened-process.platform-restrictions-string</key>
|
||||||
<string>2</string>
|
<string>2</string>
|
||||||
|
<key>com.apple.security.smartcard</key>
|
||||||
|
<true/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
||||||
|
|||||||
@@ -699,7 +699,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 2650;
|
LastSwiftUpdateCheck = 2700;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2640;
|
||||||
ORGANIZATIONNAME = "Max Goedjen";
|
ORGANIZATIONNAME = "Max Goedjen";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -744,8 +744,8 @@
|
|||||||
50617D7E23FCE48D0099B055 /* Secretive */,
|
50617D7E23FCE48D0099B055 /* Secretive */,
|
||||||
50A3B78924026B7500D209EA /* SecretAgent */,
|
50A3B78924026B7500D209EA /* SecretAgent */,
|
||||||
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
|
50692D112E6FDB880043C7BB /* SecretiveUpdater */,
|
||||||
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
|
|
||||||
50E205132FAAB81C00402380 /* SecretiveCertificateParser */,
|
50E205132FAAB81C00402380 /* SecretiveCertificateParser */,
|
||||||
|
50692E4F2E6FF9D20043C7BB /* SecretAgentInputParser */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -1515,6 +1515,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
AUTOMATION_APPLE_EVENTS = NO;
|
||||||
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
@@ -1533,6 +1534,7 @@
|
|||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||||
@@ -1544,6 +1546,12 @@
|
|||||||
MARKETING_VERSION = 1;
|
MARKETING_VERSION = 1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
|
||||||
|
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
};
|
};
|
||||||
name = Test;
|
name = Test;
|
||||||
};
|
};
|
||||||
@@ -1551,6 +1559,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
AUTOMATION_APPLE_EVENTS = NO;
|
||||||
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
@@ -1570,6 +1579,7 @@
|
|||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||||
@@ -1581,6 +1591,12 @@
|
|||||||
MARKETING_VERSION = 1;
|
MARKETING_VERSION = 1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
|
||||||
|
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -1588,6 +1604,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
AUTOMATION_APPLE_EVENTS = NO;
|
||||||
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
@@ -1608,6 +1625,7 @@
|
|||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||||
|
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||||
@@ -1620,6 +1638,12 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(SECRETIVE_BASE_BUNDLE_ID).SecretAgent";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";
|
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
|
||||||
|
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
|
||||||
|
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
|
||||||
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
ReferencedContainer = "container:Secretive.xcodeproj">
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<MetalAPIValidationSettings
|
||||||
|
isEnabled = "No">
|
||||||
|
</MetalAPIValidationSettings>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|||||||
Reference in New Issue
Block a user