mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-10-31 07:20:57 +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,78 +177,67 @@ 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)") | ||||||
|             Logger().debug("Found certificate for \(secret.name)") |  | ||||||
|             do { |  | ||||||
|         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 | ||||||
|  |         } | ||||||
|  |         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 let certName = certElements[2].data(using: .utf8) { |         if let certName = certElements[2].data(using: .utf8) { | ||||||
|             return (certDecoded, certName) |             return (certDecoded, certName) | ||||||
|         } else if let certName = secret.name.data(using: .utf8) { |         } 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") |             logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead") | ||||||
|             return (certDecoded, certName) |             return (certDecoded, certName) | ||||||
|         } else { |         } else { | ||||||
|             throw OpenSSHCertificateError.parsingFailed |             throw OpenSSHCertificateError.parsingFailed | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|                     } else { |  | ||||||
|                         Logger().warning("Certificate found for \(secret.name) but failed to decode base64 key") |  | ||||||
|                         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