secretive/Sources/Packages/Sources/SecretAgentKit/Agent.swift

206 lines
7.8 KiB
Swift
Raw Normal View History

2020-03-04 07:14:38 +00:00
import Foundation
import CryptoKit
import OSLog
import SecretKit
2020-03-17 07:56:55 +00:00
import AppKit
2020-03-04 07:14:38 +00:00
/// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores.
public class Agent {
2020-03-04 07:14:38 +00:00
2020-05-16 06:19:00 +00:00
private let storeList: SecretStoreList
private let witness: SigningWitness?
private let writer = OpenSSHKeyWriter()
private let requestTracer = SigningRequestTracer()
2020-03-04 07:14:38 +00:00
/// Initializes an agent with a store list and a witness.
/// - Parameters:
/// - storeList: The `SecretStoreList` to make available.
/// - witness: A witness to notify of requests.
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
Logger().debug("Agent is running")
2020-03-09 04:11:59 +00:00
self.storeList = storeList
self.witness = witness
2020-03-04 07:14:38 +00:00
}
}
extension Agent {
/// Handles an incoming request.
/// - Parameters:
/// - reader: A ``FileHandleReader`` to read the content of the request.
/// - writer: A ``FileHandleWriter`` to write the response to.
/// - Return value:
/// - Boolean if data could be read
2022-02-25 06:57:29 +00:00
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
Logger().debug("Agent handling new data")
let data = Data(reader.availableData)
guard data.count > 4 else { return false}
2020-03-04 07:14:38 +00:00
let requestTypeInt = data[4]
2020-03-24 06:22:22 +00:00
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
return true
2020-03-24 06:22:22 +00:00
}
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
2020-03-04 07:14:38 +00:00
let subData = Data(data[5...])
2020-03-24 06:22:22 +00:00
let response = handle(requestType: requestType, data: subData, reader: reader)
writer.write(response)
return true
2020-03-04 07:14:38 +00:00
}
2020-03-24 06:22:22 +00:00
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
2020-03-04 07:14:38 +00:00
var response = Data()
do {
switch requestType {
case .requestIdentities:
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
2020-03-24 06:22:22 +00:00
response.append(identities())
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
2020-03-04 07:14:38 +00:00
case .signRequest:
2020-03-24 06:22:22 +00:00
let provenance = requestTracer.provenance(from: reader)
2020-03-04 07:14:38 +00:00
response.append(SSHAgent.ResponseType.agentSignResponse.data)
2020-03-24 06:22:22 +00:00
response.append(try sign(data: data, provenance: provenance))
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
2020-03-04 07:14:38 +00:00
}
} catch {
response.removeAll()
response.append(SSHAgent.ResponseType.agentFailure.data)
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
2020-03-04 07:14:38 +00:00
}
let full = OpenSSHKeyWriter().lengthAndData(of: response)
2020-03-24 06:22:22 +00:00
return full
2020-03-04 07:14:38 +00:00
}
}
extension Agent {
/// Lists the identities available for signing operations
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
2020-03-24 06:22:22 +00:00
func identities() -> Data {
2020-04-05 23:05:45 +00:00
let secrets = storeList.stores.flatMap(\.secrets)
2020-03-09 04:11:59 +00:00
var count = UInt32(secrets.count).bigEndian
2020-03-04 07:14:38 +00:00
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
var keyData = Data()
let writer = OpenSSHKeyWriter()
2020-03-09 04:11:59 +00:00
for secret in secrets {
2020-03-04 07:14:38 +00:00
let keyBlob = writer.data(secret: secret)
keyData.append(writer.lengthAndData(of: keyBlob))
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
2020-03-04 07:14:38 +00:00
keyData.append(writer.lengthAndData(of: curveData))
}
Logger().debug("Agent enumerated \(secrets.count) identities")
2020-03-04 07:14:38 +00:00
return countData + keyData
}
/// Notifies witnesses of a pending signature request, and performs the signing operation if none object.
/// - Parameters:
/// - data: The data to sign.
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
2020-03-24 06:22:22 +00:00
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
2020-03-04 07:14:38 +00:00
let reader = OpenSSHReader(data: data)
2020-03-22 02:28:08 +00:00
let hash = reader.readNextChunk()
guard let (store, secret) = secret(matching: hash) else {
Logger().debug("Agent did not have a key matching \(hash as NSData)")
2020-03-04 07:14:38 +00:00
throw AgentError.noMatchingKey
}
if let witness = witness {
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
}
2020-03-22 02:28:08 +00:00
let dataToSign = reader.readNextChunk()
let signed = try store.sign(data: dataToSign, with: secret, for: provenance)
let derSignature = signed
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
2020-03-04 07:14:38 +00:00
// Convert from DER formatted rep to raw (r||s)
let rawRepresentation: Data
switch (secret.algorithm, secret.keySize) {
case (.ellipticCurve, 256):
rawRepresentation = try CryptoKit.P256.Signing.ECDSASignature(derRepresentation: derSignature).rawRepresentation
case (.ellipticCurve, 384):
rawRepresentation = try CryptoKit.P384.Signing.ECDSASignature(derRepresentation: derSignature).rawRepresentation
default:
2020-03-24 06:22:22 +00:00
throw AgentError.unsupportedKeyType
}
let rawLength = rawRepresentation.count/2
2020-04-02 07:43:45 +00:00
// Check if we need to pad with 0x00 to prevent certain
// ssh servers from thinking r or s is negative
let paddingRange: ClosedRange<UInt8> = 0x80...0xFF
var r = Data(rawRepresentation[0..<rawLength])
if paddingRange ~= r.first! {
r.insert(0x00, at: 0)
}
var s = Data(rawRepresentation[rawLength...])
if paddingRange ~= s.first! {
s.insert(0x00, at: 0)
}
2020-03-04 07:14:38 +00:00
var signatureChunk = Data()
signatureChunk.append(writer.lengthAndData(of: r))
signatureChunk.append(writer.lengthAndData(of: s))
var signedData = Data()
var sub = Data()
sub.append(writer.lengthAndData(of: curveData))
sub.append(writer.lengthAndData(of: signatureChunk))
signedData.append(writer.lengthAndData(of: sub))
2020-03-19 03:04:24 +00:00
if let witness = witness {
try witness.witness(accessTo: secret, from: store, by: provenance)
2020-03-19 03:04:24 +00:00
}
Logger().debug("Agent signed request")
2020-03-04 07:14:38 +00:00
return signedData
}
}
extension Agent {
/// Finds a ``Secret`` matching a specified hash whos signature was requested.
/// - Parameter hash: The hash to match against.
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
let allMatching = store.secrets.filter { secret in
hash == writer.data(secret: secret)
}
if let matching = allMatching.first {
return (store, matching)
}
return nil
}.first
}
}
2020-03-04 07:14:38 +00:00
extension Agent {
/// An error involving agent operations..
2020-03-04 07:14:38 +00:00
enum AgentError: Error {
case unhandledType
case noMatchingKey
2020-03-24 06:22:22 +00:00
case unsupportedKeyType
2020-03-04 07:14:38 +00:00
}
}
extension SSHAgent.ResponseType {
var data: Data {
var raw = self.rawValue
return Data(bytes: &raw, count: UInt8.bitWidth/8)
}
}