Merge
This commit is contained in:
commit
34703060b8
|
@ -5,7 +5,7 @@ on:
|
|||
- cron: "0 8 * * *"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-12.0
|
||||
runs-on: macOS-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -50,4 +50,4 @@ jobs:
|
|||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: Secretive.zip
|
||||
path: Secretive.zip
|
||||
path: Secretive.zip
|
||||
|
|
|
@ -26,6 +26,7 @@ jobs:
|
|||
# pushd Sources/Packages
|
||||
# swift test
|
||||
# popd
|
||||
|
||||
build:
|
||||
runs-on: macOS-latest
|
||||
timeout-minutes: 10
|
||||
|
|
|
@ -3,12 +3,12 @@ name: Test
|
|||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-12.0
|
||||
runs-on: macOS-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_14.1.app
|
||||
- name: Test
|
||||
run: |
|
||||
pushd Sources/Packages
|
||||
|
|
4
FAQ.md
4
FAQ.md
|
@ -12,6 +12,10 @@ Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. Th
|
|||
|
||||
Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [new GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) with a description of your issue.
|
||||
|
||||
### Secretive was working for me, but now it has stopped
|
||||
|
||||
Try running the "Setup Secretive" process by clicking on "Help", then "Setup Secretive." If that doesn't work, follow the process above.
|
||||
|
||||
### Secretive prompts me to type my password instead of using my Apple Watch
|
||||
|
||||
1) Make sure you have enabled "Use your Apple Watch to unlock apps and your Mac" in System Preferences --> Security & Privacy:
|
||||
|
|
|
@ -4,6 +4,25 @@ import OSLog
|
|||
import SecretKit
|
||||
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.
|
||||
public class Agent {
|
||||
|
||||
|
@ -11,6 +30,7 @@ public class Agent {
|
|||
private let witness: SigningWitness?
|
||||
private let writer = OpenSSHKeyWriter()
|
||||
private let requestTracer = SigningRequestTracer()
|
||||
private let certsPath = (NSHomeDirectory() as NSString).appendingPathComponent("PublicKeys") as String
|
||||
|
||||
/// Initializes an agent with a store list and a witness.
|
||||
/// - Parameters:
|
||||
|
@ -83,12 +103,22 @@ extension Agent {
|
|||
var count = UInt32(secrets.count).bigEndian
|
||||
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
|
||||
var keyData = Data()
|
||||
let writer = OpenSSHKeyWriter()
|
||||
|
||||
for secret in secrets {
|
||||
let keyBlob = writer.data(secret: secret)
|
||||
let keyBlob: Data
|
||||
let curveData: Data
|
||||
|
||||
if let (certBlob, certName) = try? checkForCert(secret: secret) {
|
||||
keyBlob = certBlob
|
||||
curveData = certName
|
||||
} else {
|
||||
keyBlob = writer.data(secret: secret)
|
||||
curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||
}
|
||||
|
||||
keyData.append(writer.lengthAndData(of: keyBlob))
|
||||
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||
keyData.append(writer.lengthAndData(of: curveData))
|
||||
|
||||
}
|
||||
Logger().debug("Agent enumerated \(secrets.count) identities")
|
||||
return countData + keyData
|
||||
|
@ -101,7 +131,13 @@ extension Agent {
|
|||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
||||
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
let hash = reader.readNextChunk()
|
||||
var hash = reader.readNextChunk()
|
||||
|
||||
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
|
||||
if let certPublicKey = try? getPublicKeyFromCert(certBlob: hash) {
|
||||
hash = certPublicKey
|
||||
}
|
||||
|
||||
guard let (store, secret) = secret(matching: hash) else {
|
||||
Logger().debug("Agent did not have a key matching \(hash as NSData)")
|
||||
throw AgentError.noMatchingKey
|
||||
|
@ -161,6 +197,74 @@ extension Agent {
|
|||
|
||||
return signedData
|
||||
}
|
||||
|
||||
/// Reconstructs a public key from a ``Data`` object that contains an OpenSSH certificate. 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
|
||||
func getPublicKeyFromCert(certBlob: Data) throws -> Data {
|
||||
let reader = OpenSSHReader(data: certBlob)
|
||||
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()
|
||||
|
||||
if 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)
|
||||
} else {
|
||||
throw OpenSSHCertificateError.parsingFailed
|
||||
}
|
||||
default:
|
||||
throw OpenSSHCertificateError.unsupportedType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||
/// - Parameter secret: The secret to search for a certificate with
|
||||
/// - Returns: Two ``Data`` objects containing the certificate and certificate name respectively
|
||||
func checkForCert(secret: AnySecret) throws -> (Data, Data) {
|
||||
let minimalHex = writer.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub")
|
||||
|
||||
if FileManager.default.fileExists(atPath: certificatePath) {
|
||||
Logger().debug("Found certificate for \(secret.name)")
|
||||
do {
|
||||
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
||||
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
||||
|
||||
if certElements.count >= 2 {
|
||||
if let certDecoded = Data(base64Encoded: certElements[1] as String) {
|
||||
if certElements.count >= 3 {
|
||||
if 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
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,10 @@ extension SigningRequestTracer {
|
|||
func process(from pid: Int32) -> SigningRequestProvenance.Process {
|
||||
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
|
||||
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
|
||||
let procName = String(cString: &pidAndNameInfo.kp_proc.p_comm.0)
|
||||
let procName = withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in
|
||||
String(cString: pointer)
|
||||
}
|
||||
|
||||
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
|
||||
_ = proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
|
||||
let path = String(cString: pathPointer)
|
||||
|
|
|
@ -15,15 +15,21 @@ public class PublicKeyFileStoreController {
|
|||
|
||||
/// Writes out the keys specified to disk.
|
||||
/// - Parameter secrets: The Secrets to generate keys for.
|
||||
/// - Parameter clear: Whether or not the directory should be erased before writing keys.
|
||||
/// - Parameter clear: Whether or not any untracked files in the directory should be removed.
|
||||
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
|
||||
logger.log("Writing public keys to disk")
|
||||
if clear {
|
||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory))
|
||||
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||
let untracked = Set(try FileManager.default.contentsOfDirectory(atPath: directory)
|
||||
.map { "\(directory)/\($0)" })
|
||||
.subtracting(validPaths)
|
||||
for path in untracked {
|
||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
|
||||
}
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
|
||||
for secret in secrets {
|
||||
let path = path(for: secret)
|
||||
let path = publicKeyPath(for: secret)
|
||||
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
|
||||
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
|
||||
}
|
||||
|
@ -34,9 +40,18 @@ public class PublicKeyFileStoreController {
|
|||
/// - Parameter secret: The Secret to return the path for.
|
||||
/// - Returns: The path to the Secret's public key.
|
||||
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
|
||||
public func path<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
return directory.appending("/").appending("\(minimalHex).pub")
|
||||
}
|
||||
|
||||
/// The path for a Secret's SSH Certificate public key.
|
||||
/// - Parameter secret: The Secret to return the path for.
|
||||
/// - Returns: The path to the SSH Certificate public key.
|
||||
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
||||
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
return directory.appending("/").appending("\(minimalHex)-cert.pub")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.path(for: secret))
|
||||
CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue