检查SSH私钥是否加密

5
在 macOS 上使用 `ssh-keygen` 生成的密钥对可以有不同的格式:
  • 标准的 PEM ASN.1 对象格式,可由 macOS 的 `SecKey` API 读取
  • 带文本头的 PEM 格式
  • OpenSSH 密钥格式
  • OpenSSH 加密密钥格式

这里非正式规范中 OpenSSH/BSD 使用了一种非标准化格式。

现在我只需要能够检查私钥是否设置了密码,以便提示用户输入,而无需处理不同密钥格式的复杂性。

在 macOS 上,是否有一种快速的方法(通过 Swift 或 C API)来检查密钥是否设置了密码?


你有检查过这个页面吗?https://stackoverflow.com/questions/49926324/seckeycreatefromdata-fails-on-10-9-with-2147415792 - jvarela
那个答案与加密私钥无关。 - user187676
libmagic 对你有用吗? - S.S. Anne
3个回答

4
未加密和加密私钥之间的区别在于密钥数据块是经过加密的。在使用私钥数据块之前,您需要解密私钥数据块。因此,一旦解码加密的私钥数据,您可以像处理未加密的私钥数据一样处理它。
未加密的私钥数据块PEM文件如下所示:
—–BEGIN PRIVATE KEY—–
{base64 private key blob)
—–END PRIVATE KEY—–

RSA加密的私钥PEM文件长这样:
—–BEGIN RSA PRIVATE KEY—–
Proc-Type: 4,ENCRYPTED
DEK-Info: {encryption algorithm},{salt}
{base64 encrypted private key blob)
—–END RSA PRIVATE KEY—–

例如

—–BEGIN RSA PRIVATE KEY—–
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,AB8E2B5B2D989271273F6730B6F9C687
{base64 encrypted private key blob)
—–END RSA PRIVATE KEY—–

为了解码私钥数据,您需要:
  1. 解析DEK-Info加密算法和盐(最好确认第一行为:"Proc-Type: 4,ENCRYPTED")。
  2. 解码base64加密的私钥块。
  3. 基于盐和口令生成加密算法“key”和“IV”。
  4. 解码加密的私钥块。
完成上述步骤后,解密后的私钥块可以像未编码的私钥块一样处理。
支持的加密算法数量相当大,因此您可能希望支持子集算法。例如“DES-EDE3-CBC”,“AES-xxx-CBC”等。
要生成IV,您需要将salt字符串转换为二进制。盐字符串是一个十六进制编码的字符串,因此使用十六进制字符串字符到字节转换器将每两个字符串字符转换为一个字节。
生成加密算法密钥需要密钥大小(例如,DES-EDE3-CBC为192位,AES-256-CBC为256位)。使用循环将MD5哈希结果附加到密钥上,直到生成所需的所有密钥位数。
MD5哈希循环生成包括:
1. 第一个MD5哈希:IV和密码的前8个字节的MD5哈希值 2. 所有其他MD5哈希都是上一个MD5哈希结果和IV以及密码的前8个字节的MD5哈希
请参见openssl源代码中的EVP_BytesToKey方法,了解密钥位生成的示例。
现在可以使用上面构建的IV和KEY使用所选的加密算法解码加密私钥块。

我所描述的是openSSL加密的PEM私钥格式。而你指向的是openSSH私钥格式,它不是PEM格式。你确定它甚至是ASN.1格式吗?文件中并没有这样说明。 - Shane Powell
看起来 openssh 公钥/私钥是我不知道的一个世界,它们的格式与 openssl pem 文件不同。 - Shane Powell
也许你应该将标题更改为“SSH-RSA”,以更明确地说明你所讨论的格式是什么? - Shane Powell
你可以根据头部和尾部来确定正在处理的文件。你可以测试 "BEGIN OPENSSH PRIVATE KEY" 是否在 ssh 格式中。 - Shane Powell
是的,但这并不能告诉我它是否已加密。但我可以尝试将其解码为常规私钥,该私钥具有指定的ASN1格式,并且苹果的安全框架可以读取这些格式。 - user187676
显示剩余6条评论

0

我自己实现了针对两种最常见格式的OpenSSH检查

  • 首先,我会检查Linux样式的SSH PEM的PEM头是否包含DEK-Info
  • 对于OpenSSH样式的密钥,我会使用下面的类手动解析格式
import Foundation

private let opensshMagic = "openssh-key-v1"

public class SSHPrivateKey {

    public struct OpenSSHKey {
        let cipherName: String
        let kdfName: String
        let kdfOptions: Data
        let numKeys: Int

        var isEncrypted: Bool {
            return cipherName != "none"
        }
    }

    public let data: Data

    public init(data: Data) {
        self.data = data
    }

    public func openSSHKey() -> OpenSSHKey? {
        // #define AUTH_MAGIC      "openssh-key-v1"
        //
        // byte[]  AUTH_MAGIC
        // string  ciphername
        // string  kdfname
        // string  kdfoptions
        // int     number of keys N
        // string  publickey1
        // string  publickey2
        // ...
        // string  publickeyN
        // string  encrypted, padded list of private keys

        guard let magic = opensshMagic.data(using: .utf8) else {
            return nil
        }

        if data.prefix(magic.count) != magic {
            return nil
        }

        var offset = magic.count + 1

        guard let cipherName = data.consumeString(offset: &offset),
            let kdfName = data.consumeString(offset: &offset) else {
                return nil
        }

        let kdfOptions = data.consumeBytes(offset: &offset)
        let numKeys = data.consumeUInt32(offset: &offset)

        return OpenSSHKey(cipherName: cipherName,
                          kdfName: kdfName,
                          kdfOptions: kdfOptions,
                          numKeys: Int(numKeys))
    }
}

private extension Data {

    func consumeBytes(offset: inout Int) -> Data {
        let n = Int(consumeUInt32(offset: &offset))
        let b = Data(self[offset..<offset + n])
        offset += n
        return b
    }

    func consumeString(offset: inout Int) -> String? {
        return consumeBytes(offset: &offset).utf8String
    }

    func consumeUInt8(offset: inout Int) -> UInt8 {
        let v = self[offset] & 0xFF
        offset += 1

        return v
    }

    func consumeUInt32(offset: inout Int) -> UInt32 {
        var i: UInt32 = 0

        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))
        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))
        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))
        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))

        return i
    }
}


0

我建议有两种方法。一种是使用readLine()读取命令行输出,并检查是否要求输入密码,然后根据情况执行相应的操作。

import Foundation

func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {

    var output : [String] = []
    var error : [String] = []

    let task = Process()
    task.launchPath = cmd
    task.arguments = args

    let outpipe = Pipe()
    task.standardOutput = outpipe
    let errpipe = Pipe()
    task.standardError = errpipe

    task.launch()

    let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
    if var string = String(data: outdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        output = string.components(separatedBy: "\n")
    }

    let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
    if var string = String(data: errdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        error = string.components(separatedBy: "\n")
    }

    task.waitUntilExit()
    let status = task.terminationStatus

    return (output, error, status)
}

//Sample usage

let (output, error, status) = runCommand(cmd: "/usr/local/bin/node", args: "--version")
print("program exited with status \(status)")
if output.count > 0 {
    print("program output:")
    print(output)
    //HERE YOU CAN CHECK IF PASSWORD REQUEST HAS BEEN MADE
}
if error.count > 0 {
    print("error output:")
    print(error)
}

示例代码将返回已安装的节点版本(如果有),但您可以使用它来检查系统是否为RSA密钥进行了密码提示。

另一种方法可能是使用第三方库,例如SwiftyRSABlueRSA,这可能有助于验证。


无法从沙盒应用程序中使用此功能。 - user187676
@ErikAigner 我已经尝试了上述代码,编译了一个简单的应用程序,实际上它可以工作,因为它基于标准终端命令行,允许执行shell命令,我假设你一直在你的应用程序中使用这些命令。然而,你并没有说明这一点。 - AD Progress
此外,node 不是标准的系统二进制文件。 - user187676
@ErikAigner 这只是一个命令行执行和响应读取的示例。由于我正在开发一个Node项目,所以这是我想到的第一个二进制文件。尝试执行该命令并尝试读取受密码保护的SSH密钥,您应该会在程序输出中收到密码提示:....... 祝您拥有愉快的一天。 - AD Progress

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接