使用SSH.NET从配置字符串加载SSH私钥时出现“Renci.SshNet.Common.SshException:Invalid private key file”错误。

38

我正在尝试使用SFTP将文件发送到某个服务器。 在此过程中,我遇到了异常

Renci.SshNet.Common.SshException:无效的私钥文件。 在Renci.SshNet.PrivateKeyFile.Open(Stream privateKey,String passPhrase)中。

我使用PuTTYgen生成密钥,下面显示的是私钥文件的示例格式。 它具有公钥和私钥。

PuTTY-User-Key-File-2: ssh-rsa  
Encryption:none  
comment: rsa-key-20190327  
Public-Lines: 4  
AAAAB.....  
......  
Private-Lines: 8  
AAAAgQ......  
.......  
Private-MAC: 54901783....  

我从上面的文件中复制了私钥部分到配置文件,并在代码中将其作为SftpKey访问。

得到了一个类似于以下格式的OpenSSH格式的密钥

------BEGIN RSA PRIVATE KEY-----  
MIIE....  
.......  
------END RSA PRIVATE KEY-------  

我只从上面的文件中复制了关键部分,并将其粘贴到我的配置文件中并运行了代码。问题没有解决。

以下是我用于SFTP上传的代码

var fileLength = data.Length;

var keyStr = ConfigurationManager.ConnectionStrings["SftpKey"].ConnectionString;
using (var keystrm = new MemoryStream(Convert.FromBase64String(keyStr)))
{
    var privateKey = new PrivateKeyFile(keystrm);
    using (var ftp = new SftpClient(_ftpServer, _ftpUser, new[] { privateKey }))
    {
        ftp.ErrorOccurred += ErrorOccurred;
        ftp.Connect();
        ftp.ChangeDirectory(_ftpPath);
        using (var dataStream = new MemoryStream(Encoding.UTF8.GetBytes(data)))
        {
            ftp.UploadFile(dataStream, Path.GetFileName(message.MessageId), true,
                (length) => result = fileLength == (int)length);
        }
        ftp.Disconnect();
    }
}

这段代码有什么问题或者可能会出现什么问题吗?非常感谢任何帮助。

3个回答

53

由于这个错误信息的顶级答案,我认为值得扩展一下您原来问题中的一个点,即将其转换为OpenSSH格式密钥。

Renci.SshNet无法使用以以下字符开头的PuTTY密钥:

PuTTY-User-Key-File-2: ssh-rsa

你可以使用puttygen.exe将其转换为OpenSSH格式

  1. 在puttygen.exe中加载你的密钥文件
  2. Conversions > Export OpenSSH key(不要选择“force new file format”选项)

这将生成一个以以下内容开头的密钥:

-----BEGIN RSA PRIVATE KEY-----

那将起作用


这可能是对问题有用的评论,但绝对不是这个问题的答案。例如,在使用PPK密钥进行身份验证的SSH.NET中已经涵盖了这一点。 - Martin Prikryl
谢谢Matt,我不知道为什么使用私钥会抛出异常,但是当我将其转换为OpenSSH时就可以工作了。 - anhtv13
@anhtv13 OpenSSH格式是事实上的标准,这就是为什么Renci.SSHNet使用它的原因。请参阅此答案以了解格式之间的差异:https://unix.stackexchange.com/a/184791/370336 - Matt Kemp

17
我只复制了上面文件的关键部分。 你需要在MemoryStream中拥有完整的密钥文件。并且与文件中完全一致(就像使用FileStream和文本密钥文件一样)。所以不要使用Convert.FromBase64String。
var keyStr = @"-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQEAiCYlBq7NITBpCCe48asfXKMpnJJJK+7FQj6wIRJCNuBk76tL
jNooDDPPrnrE9VKxRds4olPftjRj87s9gjm4EirbvijZ9PoDlW9CWFhjJPwCPJpA
onkhaiA7SV+abRDQHm/lst5Fk9tzl+DZcS/EleilGDV7rCYEP692UJRsi3GvzngQ
dpRvVvO4o2rXnEkdp+254KHsah0pSxri23+jqbxPguHKGIMylrswokMI0QKcfm+1
/pjrV64EQCxli3i2yPl4WVh/QaNyHMKoze/WN00Pia99QhE1Rm3YCCarFWFeX+R5
7LgIUhtrE7vZGvimfZN7oBdR2pEq10PIc+8q9QIBJQKCAQAWFAYBFW1fU/VbRLY1
Bv4qsqzNSCeKlWwYlItDohiTRvucfKR3tKyMW23JRFdKYG/GI4yks6e8roy/vX+Y
k7z8BvMzl+v+NmFyLbe7TJp0sz6iCy0TbZa3Q388VLFCHmbwLdI4rmwl0I9JD7SO
5SbMM5BkymcU/z71khMvqV21vym5Ge/ApvX0K0XNJs/N/OLnX46Z8taYEyTmreSR
rxAbma4I5BhqXbH0CMOI5u8zCyycghytl5sYyMr+LIWQKWLzQU+mPNN0qIy0pO5t
r8lGNJh5Lnmu1lQw9yAGo2IPPIERP90X64pVrteIjPtt30n87bWDS8gOiam8S/qk
2ZJVAoGBAPZi6E/KpYpzYGKPAfialu0QN1X7uFio1MUmDum+phk5+xeQb/VvlP6Y
d+/o03EMnhvUsop9p7E2CwLZfT6DO7x3LKtumfceq5dPE5hQSWXi9RkBhcOJaZvZ
z+36c8N8iSZZzlxdA5TeDTUqtuVli4HLrcsXaAaVMxEr/G2JwUgTAoGBAI12Gnoy
k/gsiHz4pDLgxWQE6R8vkBMXfQCWhkzvzKca4twQ8z4ZAb/yt+BCiioJn5g58CVS
dP2zd3Lx8e9kkxggZLcUR3Ao6HceYKeD6mx4vkpHiyCtKJI+qfnkw2A64xwbtvTR
h/O5Aq90SjqP4YcaK9E0W/mWYoL3ctFG8DHXAoGBAKZ6LkPARlag+++RDytvXw7h
cX9JN15/6bWkF+oLMfVehw/r+J7qh0Q9gXiWZVo49TVmM3JU5u1b3e0rKxxmgk7o
vVE85JI3UVhl3M6yyc84fBfQmKa2ytEWoT/uaeTzR+l68zd9Hhh6W/N9udlEnIgh
1kr0I7FruriTV4hIUinHAoGASKRudhn49Q/zD73zdBKO4FWMd8xQ5zWTN6c+C9UW
EJ8ajK7CGPgVp8HUC2BwdnOk+ySrwCNsgkdm2ik3DDqQuVy+GNMP7XzKZq68Av6N
IvHlLQ/7VfgN6jvavpgRTRdSB4Pafbe0hBLltAtItknig6WnzEtR0zGMiHE69dhR
1GcCgYBckoyMXpT0HzOjLXWClSiIaDDfgGcmgEKbYJ7c3mncjLinbCVFdJ0UcrqJ
tiauWBvmecAhnJvQGnmInawNUHetAgJoCbqd7cckjI8VtBgHlQyT93wo9fSDz0Kt
dDHspRvVQkhiR/6IWz1PtCT0QGrHP8fJq/PCbLnJf/EJqJv/xA==
-----END RSA PRIVATE KEY-----
";

using (var keystrm = new MemoryStream(Encoding.ASCII.GetBytes(keyStr)))
{
    var privateKey = new PrivateKeyFile(keystrm);
}

实际上,我之前展示的代码演示了一个现有的实现,其中文件已成功发送。
然后你的连接字符串并不包含你所声称的内容。检查PrivateKeyFile.Open的实现。它明确检查流---- BEGIN ... PRIVATE KEY开始。如果不是,则会抛出“无效的私钥文件”错误。
实际上,将多行内容存储到连接字符串中可能甚至是不可能(或困难的)。如果你的代码曾经起作用,那么它必须是因为你的SftpKey连接字符串包含一个完整的密钥文件(包括BEGIN ... PRIVATE KEY包装)但是编码(再次)使用Base64编码(作为单行)。就像这样:
Convert.ToBase64String(File.ReadAllBytes(@"C:\path\to\key"))

这将为您提供类似于以下字符串:

-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAQKCAQEAiCYlBq7NITBpCpD4e48asfXKMpnJJJ+7FQj6wIRJCNuBk76tL +jNoooDPBrrE9VLxRds4olPftjRj87s9gjm4EirbviZj9PoDlW9CWGhjJPwCPJpA onkhaiA7SUa+bREQmh/lsst5Fk9tzl+DZcS/ElleiLDV7rCYEP692URSi3GvzngQ dpRvVvO4o2rXnEkdp+254KHsah0psri23+jqbxPguHKGIMylrswokMI0QQcfm+1/ prrV64EQCxlsi3i2yPl4WVh/QaNyHMyoze/WN00Pia99QiE1Rm3YCarcFWFeX+R5 7LgIUhtRE7vZGvimfZN7oBdR2pEq10PIc+8q9QIBJQKCAAAQAWFAYBFVFX1fU/Vb RLYL Bv4qsqzNTCeKLwWyLiDotihTRvucfKR3tKymW23JRFDkYG/GI4yks6e8roy/vX+Y k7z8BvMzl+v/NmFyLbe7TJp0sz6iCy0TbZa3Q388VLFCHmbwlDI4rmwl0I9JD7SO 5SbMM5BkymcU/z71khMvqV21vym5Ge/APpvX0K0XNJs/N/OLnX46X8taYeYEtTr esR rxAbma4I5BhqXbH0CMOI5u8zCyycghytl5sYyMr+LIWQKWLLzQU+pMNN0qIy0pO8 r8lGNJh5Lnmu1lwQw9yAGo2IPIDErP90X64pVrteIjPt30n87bWDSC8gOiagm8S/ qk 2ZJVAoGBAPZi6E/KpYpzYGKPAfialuQ1N1X7uFio1MUmuDum/phk5xeqb/VvpP6Y /kz+6c8N8iSZzzlxda5TdTUVqttUl4HLrcsXaAaVMxEr/G2JwUgTAoGABIS12Gno y/kgsiHz4pDLgxWQE6R8vkBMYfQCWhkzvzKca4ttwU8z4ZAb/yt+BCiioJn5g58C VS pP2zd3Lxe9kkgZLcUR3Ao

2
同样的错误也可能会发生在私钥格式不正确的情况下,比如压缩到单行上,这将无法与https://github.com/sshnet/SSH.NET/blob/develop/src/Renci.SshNet/PrivateKeyFile.cs#L156中的正则表达式匹配。
私钥格式必须跨越多行,在80列处换行。 https://github.com/sshnet/SSH.NET/blob/develop/src/Renci.SshNet/PrivateKeyFile.cs#L68 - 表达式包含“{1,80}”。
在我的情况下,私钥没有受到密码保护,所以我能够使用以下代码来正确地重新格式化它;在构造内存流之前,重新引入换行符。
// PEM Format Private Key substituting newlines with a space
var privateKeyString = @"-----BEGIN RSA PRIVATE KEY----- line1 line2 line3 -----END RSA PRIVATE KEY-----";

// Group 1: "-----BEGIN RSA PRIVATE KEY-----"
// Group 2: " line1 line2 line3 "
// Group 3: "-----END RSA PRIVATE KEY-----"
var regex = new Regex(@"^\s*(-+[^-]+-+)([^-]+)(-+[^-]+-+)");
var matches = regex.Match(privateKeyString);
var formatted = string.Concat(
    matches.Groups[1].Value,
    matches.Groups[2].Value.Replace(" ", "\r\n"),
    matches.Groups[3].Value);

// ASCII encoding is fine because we're dealing with the base64 alphabet.
var ms = new MemoryStream(Encoding.ASCII.GetBytes(formatted));
var privateKeyFile = new PrivateKeyFile(ms);

但是你最初是如何得到那样格式化的密钥的? - Martin Prikryl
@MartinPrikryl,感谢您的反馈;我已经编辑了解决方案,以说明它是一个PEM格式的私钥,将换行符替换为一个空格。 - Ryan Williams
那并没有真正回答我的问题。你是怎么得到那个格式的? - Martin Prikryl
如果你在谈论privateKeyString的格式...... SOME_KEY=$(ssh-keygen -t rsa -b 2048 -f temp.key -N "" -q; cat temp.key; rm -f temp.key) SOME_KEY_ON_ONE_LINE=$(echo "${SOME_KEY}" | tr "\n" " ") - Ryan Williams
@MartinPrikryl,我从https://github.com/sshnet/SSH.NET/blob/develop/src/Renci.SshNet/PrivateKeyFile.cs#L68获取了正则表达式`^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k<keyName> PRIVATE KEY *-+`,并使用https://regexr.com/来查找为什么我的私钥无法匹配该表达式。 - Ryan Williams
显示剩余2条评论

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