This commit is contained in:
Max Goedjen
2026-03-12 12:46:45 -07:00
parent f848eb659e
commit 6b1f5bbb7c
3 changed files with 107 additions and 59 deletions

View File

@@ -62,5 +62,6 @@ public final class OpenSSHReader {
} }
public enum OpenSSHReaderError: Error, Codable { public enum OpenSSHReaderError: Error, Codable {
case incorrectFormat
case beyondBounds case beyondBounds
} }

View File

@@ -15,6 +15,8 @@ public final class Agent: Sendable {
private let certificateHandler = OpenSSHCertificateHandler() private let certificateHandler = OpenSSHCertificateHandler()
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.
@@ -44,14 +46,33 @@ 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):
if let boundSession = await sessionID {
switch context.dataToSign.decoded {
case .sshConnection(let payload):
guard payload.hostKey == boundSession.hostKey else {
logger.error("Agent received bind request, but host key does not match signature reqeust host key.")
throw BindingFailure()
}
case .sshSig:
// SSHSIG does not have a host binding payload.
break
default:
break
}
}
response.append(SSHAgent.Response.agentSignResponse.data) response.append(SSHAgent.Response.agentSignResponse.data)
response.append(try await sign(data: context.dataToSign.raw, 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))): case .protocolExtension(.openSSH(.sessionBind(let bind))):
response = SSHAgent.Response.agentSuccess.data response = try await MainActor.run {
_ = bind guard sessionID == nil else {
// FIXME: STORE BIND IN KEYCHAIN logger.error("Agent received bind request, but already bound.")
// FIXME: CLEAR OUT BINDS BASED ON EXPIRATION? throw BindingFailure()
}
logger.debug("Agent bound")
sessionID = bind
return SSHAgent.Response.agentSuccess.data
}
logger.debug("Agent returned \(SSHAgent.Response.agentSuccess.debugDescription)") 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).")
@@ -152,6 +173,7 @@ extension Agent {
struct NoMatchingKeyError: Error {} struct NoMatchingKeyError: Error {}
struct UnhandledRequestError: Error {} struct UnhandledRequestError: Error {}
struct BindingFailure: Error {}
} }

View File

@@ -57,8 +57,6 @@ public struct SSHAgentInputParser: SSHAgentInputParserProtocol {
return .addSmartcardKeyConstrained return .addSmartcardKeyConstrained
case SSHAgent.Request.protocolExtension(.empty).protocolID: case SSHAgent.Request.protocolExtension(.empty).protocolID:
return .protocolExtension(try protocolExtension(from: body)) return .protocolExtension(try protocolExtension(from: body))
// case SSHAgent.Request.constrainExtension(.empty).protocolID:
// return .constrainExtension(try constrainExtension(from: body))
default: default:
return .unknown(rawRequestInt) return .unknown(rawRequestInt)
} }
@@ -79,62 +77,58 @@ extension SSHAgentInputParser {
let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob let keyBlob = certificatePublicKeyBlob(from: rawKeyBlob) ?? rawKeyBlob
let rawPayload = try reader.readNextChunk() let rawPayload = try reader.readNextChunk()
let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload let payload: SSHAgent.Request.SignatureRequestContext.SignaturePayload
if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic { do {
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L79 if rawPayload.count > 6 && rawPayload[0..<6] == Constants.sshSigMagic {
let payloadReader = OpenSSHReader(data: rawPayload[6...]) payload = .init(raw: rawPayload, decoded: .sshSig(try sshSigPayload(from: rawPayload[6...])))
let namespace = try payloadReader.readNextChunkAsString() } else {
_ = try payloadReader.readNextChunk() // reserved payload = .init(raw: rawPayload, decoded: .sshConnection(try sshConnectionPayload(from: rawPayload)))
let hashAlgorithm = try payloadReader.readNextChunkAsString()
let hash = try payloadReader.readNextChunk()
payload = .init(
raw: data,
decoded: .init(
.sshSig(
.init(
namespace: namespace,
hashAlgorithm: hashAlgorithm,
hash: hash
)
)
)
)
} else {
let payloadReader = OpenSSHReader(data: rawPayload)
do {
_ = try payloadReader.readNextChunk()
let magic = try payloadReader.readNextBytes(as: UInt8.self, convertEndianness: false)
if magic == Constants.userAuthMagic {
let username = try payloadReader.readNextChunkAsString()
_ = try payloadReader.readNextChunkAsString() // "ssh-connection"
_ = try payloadReader.readNextChunkAsString() // "publickey-hostbound-v00@openssh.com"
let hasSignature = try payloadReader.readNextByteAsBool()
let pkAlg = try payloadReader.readNextChunkAsString()
let pk = try payloadReader.readNextChunk()
let hostKey = try payloadReader.readNextChunk()
payload = .init(
raw: rawPayload,
decoded: .init(
.sshConnection(
.init(
username: username,
hasSignature: hasSignature,
publicKeyAlgorithm: pkAlg,
publicKey: pk,
hostKey: hostKey
)
)
)
)
} else {
throw AgentParsingError.unknownRequest
}
} catch {
payload = .init(raw: rawPayload, decoded: nil)
} }
} catch {
payload = .init(raw: rawPayload, decoded: nil)
} }
return SSHAgent.Request.SignatureRequestContext(keyBlob: keyBlob, dataToSign: payload) 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 { func protocolExtension(from data: Data) throws(AgentParsingError) -> SSHAgent.ProtocolExtension {
do { do {
let reader = OpenSSHReader(data: data) let reader = OpenSSHReader(data: data)
@@ -158,12 +152,42 @@ extension SSHAgentInputParser {
let forwarding = try reader.readNextByteAsBool() let forwarding = try reader.readNextByteAsBool()
switch hostKeyType { switch hostKeyType {
// FIXME: FACTOR OUT? // FIXME: FACTOR OUT?
// FIXME: HANDLE OTHER KEYS
case "ssh-ed25519": case "ssh-ed25519":
let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData) let hostKey = try CryptoKit.Curve25519.Signing.PublicKey(rawRepresentation: hostKeyData)
guard hostKey.isValidSignature(signature, for: sessionID) else { guard hostKey.isValidSignature(signature, for: sessionID) else {
throw AgentParsingError.invalidData 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":
// FIXME: HANDLE
throw AgentParsingError.unhandledRequest
default: default:
throw AgentParsingError.unhandledRequest throw AgentParsingError.unhandledRequest
} }
@@ -222,6 +246,7 @@ extension SSHAgentInputParser {
case unknownRequest case unknownRequest
case unhandledRequest case unhandledRequest
case invalidData case invalidData
case incorrectSignature
case openSSHReader(OpenSSHReaderError) case openSSHReader(OpenSSHReaderError)
} }