-
-
Notifications
You must be signed in to change notification settings - Fork 159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial openssh certificate support #416
Initial openssh certificate support #416
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Damn, this is looking awesome, thanks for doing this! I want to take some time to review this properly and make sure I understand everything that's going on here, but gimme a couple days and we can get this merged. Exciting! 🥳
Love to see it, welcome to the fold. I'll probably have a few minor "this is a swiftier way to do it" type things but it looks pretty solid for someone newish to the language! |
@@ -101,7 +135,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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can avoid having this be a var
by doing something like this:
let myInstance: MyType
if condition {
myInstance = someFuncX()
} else {
myInstance = someFuncY()
}
Basically it can still be a let so long as it's 1) only written once and 2) set before read.
@@ -83,12 +85,44 @@ extension Agent { | |||
var count = UInt32(secrets.count).bigEndian | |||
let countData = Data(bytes: &count, count: UInt32.bitWidth/8) | |||
var keyData = Data() | |||
let writer = OpenSSHKeyWriter() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
for secret in secrets { | ||
let keyBlob = writer.data(secret: secret) | ||
let minimalHex = writer.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") | ||
let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of those areas where I need to defer to someone with experience using the thing (ie, you): is this a reasonable ask for people? To look up what the fingerprint will be for the key, and to put their public key in that location?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's also some code in the agent that cleans out the public keys folder periodically (to make sure any keys the user deleted don't have a public key hanging out there still).
We might be better off having that be in some sibling path: ie, if my public key is
/Users/max/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/PublicKeys/e1da7c30f1fe9bebb924fbf62fb443b5.pub
we could request the user place their key in
/Users/max/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/SSHCertificates/e1da7c30f1fe9bebb924fbf62fb443b5.pub
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason i went with this path is that generally the openssh tooling will write the certificate out in that path by default, ie if you pass it in id_ecdsa.pub
it generates id_ecdsa-cert.pub
so i went with that since the public key path is easy to copy/paste from the interface...
A sibling path would be fine as well though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Precisely why I asked, didn't realize the tool spit it out there, makes perfect sense. I'll take a look at the cleanup code and make sure it doesn't incorrectly cleanup up any cert files.
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) { | ||
certsMap[certDecoded] = keyBlob // Probably a better way to store the certificate/key association than this... | ||
keyBlob = certDecoded | ||
|
||
if certElements.count >= 3 { | ||
if let certName = certElements[2].data(using: .utf8) { | ||
curveData = certName | ||
} else { | ||
Logger().info("Certificate for \(secret.name) does not have a name tag, using curveData instead") | ||
} | ||
} | ||
} else { | ||
Logger().warning("Certificate found for \(secret.name) but failed to decode base64 key, using public key instead") | ||
} | ||
} | ||
} catch { | ||
Logger().warning("Certificate found for \(secret.name) but failed to load, using public key instead") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be cleaner to chunk this all out into a separate function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved this into its own function now
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8) | ||
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ") | ||
|
||
if certElements.count >= 2 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you define an error type outside the function
public struct SSHCertificateError: Error {
}
You can switch to something like this:
guard certElements.count >= 2 else { Logger().warning("Certificate found for \(secret.name) but failed to decode base64 key, using public key instead") throw SSHCertificateError() }
(and likewise anywhere else you have a terminating if like that.
|
||
if certElements.count >= 2 { | ||
if let certDecoded = Data(base64Encoded: certElements[1] as String) { | ||
certsMap[certDecoded] = keyBlob // Probably a better way to store the certificate/key association than this... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might move this to be loaded up at the same point we load in the keys/etc, but this is good for now, I can take care of that later :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ive removed certsMap
and now reconstruct the public key from the cert when a signing request comes in, should be a bit neater
@maxgoedjen I've committed most of the requested changes, let me know if anything else needs changing :) |
All merged. Thanks again, definitely put it down in the "probably wouldn't have happened without your direct efforts" column :) There should be a nightly build with this up soon under (you can grab from the Actions tab), I'll probably plan on cutting a new release with this once I wrap up #370. |
This change adds initial OpenSSH certificate support and closes #269
If a a cert file exists for a public key in the 'PublicKeys' directory, SecretAgent will use it instead of the PublicKey.
For example if Secretive creates a key with the path
~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/PublicKeys/266b0718b08bc8653c885ae41534e1c0.pub
it will check if~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/PublicKeys/266b0718b08bc8653c885ae41534e1c0-cert.pub
exists, and return the contents as an available identity.Upon a signing request it will attempt to check if the signing request is actually for a certificate, resolve it and sign using the correct secret instead.
This is probably pretty rough, im not a Swift dev normally, i just really wanted this feature in :)
Please let me know what changes need to occur to get this merged in.