mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-04-10 17:47:19 +00:00
First pass
This commit is contained in:
parent
158afb210e
commit
c0e9c2d39a
@ -4,25 +4,6 @@ import OSLog
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
enum OpenSSHCertificateError: Error {
|
|
||||||
case unsupportedType
|
|
||||||
case parsingFailed
|
|
||||||
case doesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
extension OpenSSHCertificateError: CustomStringConvertible {
|
|
||||||
public var description: String {
|
|
||||||
switch self {
|
|
||||||
case .unsupportedType:
|
|
||||||
return "The key type was unsupported"
|
|
||||||
case .parsingFailed:
|
|
||||||
return "Failed to properly parse the SSH certificate"
|
|
||||||
case .doesNotExist:
|
|
||||||
return "Certificate does not exist"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
/// 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 {
|
public class Agent {
|
||||||
|
|
||||||
@ -30,14 +11,15 @@ public class Agent {
|
|||||||
private let witness: SigningWitness?
|
private let witness: SigningWitness?
|
||||||
private let writer = OpenSSHKeyWriter()
|
private let writer = OpenSSHKeyWriter()
|
||||||
private let requestTracer = SigningRequestTracer()
|
private let requestTracer = SigningRequestTracer()
|
||||||
private let certsPath = (NSHomeDirectory() as NSString).appendingPathComponent("PublicKeys") as String
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||||
|
private let logger = Logger()
|
||||||
|
|
||||||
/// 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.
|
||||||
/// - witness: A witness to notify of requests.
|
/// - witness: A witness to notify of requests.
|
||||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||||
Logger().debug("Agent is running")
|
logger.debug("Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
self.witness = witness
|
self.witness = witness
|
||||||
}
|
}
|
||||||
@ -53,16 +35,16 @@ extension Agent {
|
|||||||
/// - Return value:
|
/// - Return value:
|
||||||
/// - Boolean if data could be read
|
/// - Boolean if data could be read
|
||||||
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
|
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
|
||||||
Logger().debug("Agent handling new data")
|
logger.debug("Agent handling new data")
|
||||||
let data = Data(reader.availableData)
|
let data = Data(reader.availableData)
|
||||||
guard data.count > 4 else { return false}
|
guard data.count > 4 else { return false}
|
||||||
let requestTypeInt = data[4]
|
let requestTypeInt = data[4]
|
||||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||||
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
|
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
||||||
let subData = Data(data[5...])
|
let subData = Data(data[5...])
|
||||||
let response = handle(requestType: requestType, data: subData, reader: reader)
|
let response = handle(requestType: requestType, data: subData, reader: reader)
|
||||||
writer.write(response)
|
writer.write(response)
|
||||||
@ -76,17 +58,17 @@ extension Agent {
|
|||||||
case .requestIdentities:
|
case .requestIdentities:
|
||||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||||
response.append(identities())
|
response.append(identities())
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
||||||
case .signRequest:
|
case .signRequest:
|
||||||
let provenance = requestTracer.provenance(from: reader)
|
let provenance = requestTracer.provenance(from: reader)
|
||||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||||
response.append(try sign(data: data, provenance: provenance))
|
response.append(try sign(data: data, provenance: provenance))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
response.removeAll()
|
response.removeAll()
|
||||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||||
}
|
}
|
||||||
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
||||||
return full
|
return full
|
||||||
@ -108,9 +90,9 @@ extension Agent {
|
|||||||
let keyBlob: Data
|
let keyBlob: Data
|
||||||
let curveData: Data
|
let curveData: Data
|
||||||
|
|
||||||
if let (certBlob, certName) = try? checkForCert(secret: secret) {
|
if let (certificateData, name) = try? sshCertificateKeyBlobAndName(for: secret) {
|
||||||
keyBlob = certBlob
|
keyBlob = certificateData
|
||||||
curveData = certName
|
curveData = name
|
||||||
} else {
|
} else {
|
||||||
keyBlob = writer.data(secret: secret)
|
keyBlob = writer.data(secret: secret)
|
||||||
curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||||
@ -120,7 +102,7 @@ extension Agent {
|
|||||||
keyData.append(writer.lengthAndData(of: curveData))
|
keyData.append(writer.lengthAndData(of: curveData))
|
||||||
|
|
||||||
}
|
}
|
||||||
Logger().debug("Agent enumerated \(secrets.count) identities")
|
logger.debug("Agent enumerated \(secrets.count) identities")
|
||||||
return countData + keyData
|
return countData + keyData
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,15 +113,17 @@ extension Agent {
|
|||||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||||
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
||||||
let reader = OpenSSHReader(data: data)
|
let reader = OpenSSHReader(data: data)
|
||||||
var hash = reader.readNextChunk()
|
let payloadHash = reader.readNextChunk()
|
||||||
|
let hash: Data
|
||||||
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
||||||
if let certPublicKey = try? getPublicKeyFromCert(certBlob: hash) {
|
if let certificatePublicKey = publicKeyHashFromSSHCertificateHash(payloadHash) {
|
||||||
hash = certPublicKey
|
hash = certificatePublicKey
|
||||||
|
} else {
|
||||||
|
hash = payloadHash
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let (store, secret) = secret(matching: hash) else {
|
guard let (store, secret) = secret(matching: hash) else {
|
||||||
Logger().debug("Agent did not have a key matching \(hash as NSData)")
|
logger.debug("Agent did not have a key matching \(hash as NSData)")
|
||||||
throw AgentError.noMatchingKey
|
throw AgentError.noMatchingKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,77 +177,66 @@ extension Agent {
|
|||||||
try witness.witness(accessTo: secret, from: store, by: provenance)
|
try witness.witness(accessTo: secret, from: store, by: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger().debug("Agent signed request")
|
logger.debug("Agent signed request")
|
||||||
|
|
||||||
return signedData
|
return signedData
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconstructs a public key from a ``Data`` object that contains an OpenSSH certificate. Currently only ecdsa certificates are supported
|
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
|
||||||
/// - Parameter certBlock: The openssh certificate to extract the public key from
|
/// - Parameter certBlock: The openssh certificate to extract the public key from
|
||||||
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format
|
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
|
||||||
func getPublicKeyFromCert(certBlob: Data) throws -> Data {
|
func publicKeyHashFromSSHCertificateHash(_ hash: Data) -> Data? {
|
||||||
let reader = OpenSSHReader(data: certBlob)
|
let reader = OpenSSHReader(data: hash)
|
||||||
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
|
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
|
||||||
|
|
||||||
switch certType {
|
switch certType {
|
||||||
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||||
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||||
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
|
||||||
|
|
||||||
_ = reader.readNextChunk() // nonce
|
_ = reader.readNextChunk() // nonce
|
||||||
let curveIdentifier = reader.readNextChunk()
|
let curveIdentifier = reader.readNextChunk()
|
||||||
let publicKey = reader.readNextChunk()
|
let publicKey = reader.readNextChunk()
|
||||||
|
|
||||||
if let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8) {
|
let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8)!
|
||||||
return writer.lengthAndData(of: curveType) +
|
return writer.lengthAndData(of: curveType) +
|
||||||
writer.lengthAndData(of: curveIdentifier) +
|
writer.lengthAndData(of: curveIdentifier) +
|
||||||
writer.lengthAndData(of: publicKey)
|
writer.lengthAndData(of: publicKey)
|
||||||
} else {
|
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw OpenSSHCertificateError.unsupportedType
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||||
/// - Parameter secret: The secret to search for a certificate with
|
/// - Parameter secret: The secret to search for a certificate with
|
||||||
/// - Returns: Two ``Data`` objects containing the certificate and certificate name respectively
|
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||||
func checkForCert(secret: AnySecret) throws -> (Data, Data) {
|
func sshCertificateKeyBlobAndName(for secret: AnySecret) throws -> (Data, Data) {
|
||||||
let minimalHex = writer.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret)
|
||||||
let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub")
|
guard FileManager.default.fileExists(atPath: certificatePath) else {
|
||||||
|
throw OpenSSHCertificateError.doesNotExist
|
||||||
if FileManager.default.fileExists(atPath: certificatePath) {
|
}
|
||||||
Logger().debug("Found certificate for \(secret.name)")
|
|
||||||
do {
|
logger.debug("Found certificate for \(secret.name)")
|
||||||
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
||||||
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
||||||
|
|
||||||
if certElements.count >= 2 {
|
guard certElements.count >= 3 else {
|
||||||
if let certDecoded = Data(base64Encoded: certElements[1] as String) {
|
logger.warning("Certificate found for \(secret.name) but failed to load")
|
||||||
if certElements.count >= 3 {
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
if let certName = certElements[2].data(using: .utf8) {
|
}
|
||||||
return (certDecoded, certName)
|
guard let certDecoded = Data(base64Encoded: certElements[1] as String) else {
|
||||||
} else if let certName = secret.name.data(using: .utf8) {
|
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
|
||||||
Logger().info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
return (certDecoded, certName)
|
}
|
||||||
} else {
|
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
if let certName = certElements[2].data(using: .utf8) {
|
||||||
}
|
return (certDecoded, certName)
|
||||||
}
|
} else if let certName = secret.name.data(using: .utf8) {
|
||||||
} else {
|
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
||||||
Logger().warning("Certificate found for \(secret.name) but failed to decode base64 key")
|
return (certDecoded, certName)
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
} else {
|
||||||
}
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Logger().warning("Certificate found for \(secret.name) but failed to load")
|
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw OpenSSHCertificateError.doesNotExist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -297,6 +270,23 @@ extension Agent {
|
|||||||
case unsupportedKeyType
|
case unsupportedKeyType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OpenSSHCertificateError: LocalizedError {
|
||||||
|
case unsupportedType
|
||||||
|
case parsingFailed
|
||||||
|
case doesNotExist
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .unsupportedType:
|
||||||
|
return "The key type was unsupported"
|
||||||
|
case .parsingFailed:
|
||||||
|
return "Failed to properly parse the SSH certificate"
|
||||||
|
case .doesNotExist:
|
||||||
|
return "Certificate does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SSHAgent.ResponseType {
|
extension SSHAgent.ResponseType {
|
||||||
|
Loading…
Reference in New Issue
Block a user