mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-21 16:47:24 +01:00
WIP
This commit is contained in:
@@ -62,5 +62,6 @@ public final class OpenSSHReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum OpenSSHReaderError: Error, Codable {
|
public enum OpenSSHReaderError: Error, Codable {
|
||||||
|
case incorrectFormat
|
||||||
case beyondBounds
|
case beyondBounds
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user