mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-04-10 17:47:19 +00:00
Split out into dedicated handler
This commit is contained in:
parent
fb6179391e
commit
667c01b20b
@ -11,7 +11,7 @@ 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 publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
private let certificateHandler = OpenSSHCertificateHandler()
|
||||||
private let logger = Logger()
|
private let logger = Logger()
|
||||||
|
|
||||||
/// Initializes an agent with a store list and a witness.
|
/// Initializes an agent with a store list and a witness.
|
||||||
@ -22,6 +22,7 @@ public class Agent {
|
|||||||
logger.debug("Agent is running")
|
logger.debug("Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
self.witness = witness
|
self.witness = witness
|
||||||
|
certificateHandler.reloadCertificates(for: storeList.stores.flatMap(\.secrets))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -90,7 +91,7 @@ extension Agent {
|
|||||||
let keyBlob: Data
|
let keyBlob: Data
|
||||||
let curveData: Data
|
let curveData: Data
|
||||||
|
|
||||||
if let (certificateData, name) = try? sshCertificateKeyBlobAndName(for: secret) {
|
if let (certificateData, name) = try? certificateHandler.keyBlobAndName(for: secret) {
|
||||||
keyBlob = certificateData
|
keyBlob = certificateData
|
||||||
curveData = name
|
curveData = name
|
||||||
} else {
|
} else {
|
||||||
@ -116,7 +117,7 @@ extension Agent {
|
|||||||
let payloadHash = reader.readNextChunk()
|
let payloadHash = reader.readNextChunk()
|
||||||
let hash: Data
|
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 certificatePublicKey = publicKeyHashFromSSHCertificateHash(payloadHash) {
|
if let certificatePublicKey = certificateHandler.publicKeyHash(from: payloadHash) {
|
||||||
hash = certificatePublicKey
|
hash = certificatePublicKey
|
||||||
} else {
|
} else {
|
||||||
hash = payloadHash
|
hash = payloadHash
|
||||||
@ -181,63 +182,6 @@ extension Agent {
|
|||||||
|
|
||||||
return signedData
|
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
|
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 {
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user