From 667c01b20b829fef1b398e28efc8f3c066222a6a Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Thu, 27 Oct 2022 00:36:07 -0700 Subject: [PATCH] Split out into dedicated handler --- .../Sources/SecretAgentKit/Agent.swift | 81 +----------- .../OpenSSH/OpenSSHCertificateHandler.swift | 116 ++++++++++++++++++ 2 files changed, 120 insertions(+), 77 deletions(-) create mode 100644 Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift diff --git a/Sources/Packages/Sources/SecretAgentKit/Agent.swift b/Sources/Packages/Sources/SecretAgentKit/Agent.swift index 5f26f09..667b3e6 100644 --- a/Sources/Packages/Sources/SecretAgentKit/Agent.swift +++ b/Sources/Packages/Sources/SecretAgentKit/Agent.swift @@ -11,7 +11,7 @@ public class Agent { private let witness: SigningWitness? private let writer = OpenSSHKeyWriter() private let requestTracer = SigningRequestTracer() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let certificateHandler = OpenSSHCertificateHandler() private let logger = Logger() /// Initializes an agent with a store list and a witness. @@ -22,6 +22,7 @@ public class Agent { logger.debug("Agent is running") self.storeList = storeList self.witness = witness + certificateHandler.reloadCertificates(for: storeList.stores.flatMap(\.secrets)) } } @@ -90,7 +91,7 @@ extension Agent { let keyBlob: Data let curveData: Data - if let (certificateData, name) = try? sshCertificateKeyBlobAndName(for: secret) { + if let (certificateData, name) = try? certificateHandler.keyBlobAndName(for: secret) { keyBlob = certificateData curveData = name } else { @@ -116,7 +117,7 @@ extension Agent { let payloadHash = reader.readNextChunk() let hash: Data // Check if hash is actually an openssh certificate and reconstruct the public key if it is - if let certificatePublicKey = publicKeyHashFromSSHCertificateHash(payloadHash) { + if let certificatePublicKey = certificateHandler.publicKeyHash(from: payloadHash) { hash = certificatePublicKey } else { hash = payloadHash @@ -181,63 +182,6 @@ extension Agent { return signedData } - - /// 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 - /// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil. - func publicKeyHashFromSSHCertificateHash(_ hash: Data) -> Data? { - let reader = OpenSSHReader(data: hash) - let certType = String(decoding: reader.readNextChunk(), as: UTF8.self) - - switch certType { - case "ecdsa-sha2-nistp256-cert-v01@openssh.com", - "ecdsa-sha2-nistp384-cert-v01@openssh.com", - "ecdsa-sha2-nistp521-cert-v01@openssh.com": - _ = reader.readNextChunk() // nonce - let curveIdentifier = reader.readNextChunk() - let publicKey = reader.readNextChunk() - - let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8)! - return writer.lengthAndData(of: curveType) + - writer.lengthAndData(of: curveIdentifier) + - writer.lengthAndData(of: publicKey) - default: - return nil - } - } - - - /// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret`` - /// - Parameter secret: The secret to search for a certificate with - /// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively. - func sshCertificateKeyBlobAndName(for secret: AnySecret) throws -> (Data, Data) { - let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret) - guard FileManager.default.fileExists(atPath: certificatePath) else { - throw OpenSSHCertificateError.doesNotExist - } - - logger.debug("Found certificate for \(secret.name)") - let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8) - let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ") - - guard certElements.count >= 2 else { - logger.warning("Certificate found for \(secret.name) but failed to load") - throw OpenSSHCertificateError.parsingFailed - } - guard let certDecoded = Data(base64Encoded: certElements[1] as String) else { - logger.warning("Certificate found for \(secret.name) but failed to decode base64 key") - throw OpenSSHCertificateError.parsingFailed - } - - if certElements.count >= 3, let certName = certElements[2].data(using: .utf8) { - return (certDecoded, certName) - } else if let certName = secret.name.data(using: .utf8) { - logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead") - return (certDecoded, certName) - } else { - throw OpenSSHCertificateError.parsingFailed - } - } } @@ -270,23 +214,6 @@ extension Agent { 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 { diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift new file mode 100644 index 0000000..6734e08 --- /dev/null +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -0,0 +1,116 @@ +import Foundation +import OSLog + +/// Manages storage and lookup for OpenSSH certificates. +public class OpenSSHCertificateHandler { + + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let logger = Logger() + private let writer = OpenSSHKeyWriter() + private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] + + /// Initializes an OpenSSHCertificateHandler. + public init() { + } + + /// Reloads any + /// - Parameter secrets: the secrets to look up corresponding certificates for. + public func reloadCertificates(for secrets: [AnySecret]) { + keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in + partialResult[next] = try? loadKeyblobAndName(for: next) + } + } + + /// Whether or not the certificate handler has a certifiicate associated with a given secret. + /// - Parameter secret: The secret to check for a certificate. + /// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret + public func hasCertificate(for secret: AnySecret) -> Bool { + keyBlobsAndNames[secret] != nil + } + + + /// 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 + /// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil. + public func publicKeyHash(from hash: Data) -> Data? { + let reader = OpenSSHReader(data: hash) + let certType = String(decoding: reader.readNextChunk(), as: UTF8.self) + + switch certType { + case "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com": + _ = reader.readNextChunk() // nonce + let curveIdentifier = reader.readNextChunk() + let publicKey = reader.readNextChunk() + + let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8)! + return writer.lengthAndData(of: curveType) + + writer.lengthAndData(of: curveIdentifier) + + writer.lengthAndData(of: publicKey) + default: + return nil + } + } + + /// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret`` + /// - Parameter secret: The secret to search for a certificate with + /// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively. + public func keyBlobAndName(for secret: AnySecret) throws -> (Data, Data)? { + keyBlobsAndNames[secret] + } + + /// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret`` + /// - Parameter secret: The secret to search for a certificate with + /// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively. + private func loadKeyblobAndName(for secret: AnySecret) throws -> (Data, Data)? { + let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret) + guard FileManager.default.fileExists(atPath: certificatePath) else { + return nil + } + + logger.debug("Found certificate for \(secret.name)") + let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8) + let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ") + + guard certElements.count >= 2 else { + logger.warning("Certificate found for \(secret.name) but failed to load") + throw OpenSSHCertificateError.parsingFailed + } + guard let certDecoded = Data(base64Encoded: certElements[1] as String) else { + logger.warning("Certificate found for \(secret.name) but failed to decode base64 key") + throw OpenSSHCertificateError.parsingFailed + } + + if certElements.count >= 3, let certName = certElements[2].data(using: .utf8) { + return (certDecoded, certName) + } else if let certName = secret.name.data(using: .utf8) { + logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead") + return (certDecoded, certName) + } else { + throw OpenSSHCertificateError.parsingFailed + } + } + +} + +extension OpenSSHCertificateHandler { + + 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" + } + } + } + +}