使用JSch时出现“无效的私钥”错误

133

我正在使用下面的代码在Java应用程序中处理Git。我有一个有效的密钥(经常使用它),而且这个特定的代码以前已经使用同样的密钥和git仓库为我工作,但现在我收到以下异常:

invalid privatekey: [B@59c40796.

异常出现在这一行:

jSch.addIdentity("<key_path>/private_key.pem");

我的完整代码:

    String remoteURL = "ssh://git@<git_repository>";
    TransportConfigCallback transportConfigCallback = new SshTransportConfigCallback();
    File gitFolder = new File(workingDirectory);
    if (gitFolder.exists()) FileUtils.delete(gitFolder, FileUtils.RECURSIVE);

    Git git = Git.cloneRepository()
            .setURI(remoteURL)
            .setTransportConfigCallback(transportConfigCallback)
            .setDirectory(new File(workingDirectory))
            .call();
}


private static class SshTransportConfigCallback implements TransportConfigCallback {
    private final SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
        @Override
        protected void configure(OpenSshConfig.Host hc, Session session) {
            session.setConfig("StrictHostKeyChecking", "no");
        }

        @Override
        protected JSch createDefaultJSch(FS fs) throws JSchException {
            JSch jSch = super.createDefaultJSch(fs);
            jSch.addIdentity("<key_path>/private_key.pem");

            return jSch;
        }
    };
在搜索网络后,我已将createDefaultJSch更改为使用pemWriter:
@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
    JSch jSch = super.createDefaultJSch(fs);
    byte[] privateKeyPEM = null;

    try {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        List<String> lines = Files.readAllLines(Paths.get("<my_key>.pem"), StandardCharsets.US_ASCII);
        PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(String.join("", lines)));
        RSAPrivateKey privKey = (RSAPrivateKey) keyFactory.generatePrivate(privSpec);

        PKCS8Generator pkcs8 = new PKCS8Generator(privKey);

        StringWriter writer = new StringWriter();
        PemWriter pemWriter = new PemWriter(writer);
        pemWriter.writeObject(pkcs8);

        privateKeyPEM = writer.toString().getBytes("US-ASCII");

    } catch (Exception e) {
        e.printStackTrace();
    }

    jSch.addIdentity("git", privateKeyPEM, null, null);

    return jSch;
}

但仍然收到"invalid privatekey"异常。

12个回答

241

最近版本的OpenSSH(7.8及更新版本)默认生成新的OpenSSH格式的密钥,其开头为:

-----BEGIN OPENSSH PRIVATE KEY-----

JSch不支持这种密钥格式。


您可以使用ssh-keygen将密钥转换为经典的OpenSSH格式:

ssh-keygen -p -f <privateKeyFile> -m pem -P passphrase -N passphrase

这里使用了 -p 命令“滥用”(abuses)。它会使用 -f 选项指定的私钥文件,并以经典的 OpenSSH 格式 (pem) 覆盖该文件,创建一个新的私钥。您可以将当前的密钥短语保留为新的短语。如果密钥未加密,则使用空短语 ""。之后,您可以使用 ssh-keygen -y -e -f <privateKeyFile> >temp.pub 命令来比较公钥并验证现有的公钥是否与新格式的私钥一起工作。

对于 Windows 用户:请注意,ssh-keygen.exe 已内置于 Windows 10/11 中。对于旧版本的 Windows,可以从 Microsoft Win32-OpenSSH 项目中 下载


在 Windows 上,您也可以使用 PuTTYgen (来自 PuTTY 包):

  • 启动 PuTTYgen
  • 加载密钥
  • 选择 转换 > 导出 OpenSSH 密钥.
    对于 RSA 密钥,它将使用经典格式。

如果您使用 ssh-keygen 创建新密钥,只需添加 -m PEM 即可生成新的经典格式密钥:

ssh-keygen -m PEM
事实上,原本的 JSch 看起来已经没有得到积极维护了。因此,特别是如果您正在启动新项目,那么它可能不是最好的库。现在,JSch 遭受着许多兼容性问题。对于一些重要的问题,请参见:

使用相同密钥,JSch 的公钥身份验证失败,但 OpenSSH 可以正常工作
解决 JSch 不支持的 KEX 算法的方法

相反,您可以考虑使用这个 JSch 分支:
https://github.com/mwiede/jsch
除其他之外,它还支持新的 OpenSSH 密钥格式。

34
强调如何转换现有密钥而不仅仅生成新密钥,会额外得到赞扬。 - sryll

148

我也遇到了这个问题。 在Mac上运行Jgit,对于一些用户我们看到了以下异常:

org.eclipse.jgit.transport.JschConfigSessionFactory.getSession(JschConfigSessionFactory.java:160)
    at org.eclipse.jgit.transport.SshTransport.getSession(SshTransport.java:137)
    at org.eclipse.jgit.transport.TransportGitSsh$SshFetchConnection.<init>(TransportGitSsh.java:274)
    at org.eclipse.jgit.transport.TransportGitSsh.openFetch(TransportGitSsh.java:169)
    at org.eclipse.jgit.transport.FetchProcess.executeImp(FetchProcess.java:136)
    at org.eclipse.jgit.transport.FetchProcess.execute(FetchProcess.java:122)
    at org.eclipse.jgit.transport.Transport.fetch(Transport.java:1236)
    at org.eclipse.jgit.api.FetchCommand.call(FetchCommand.java:234)
    ... 17 more
Caused by: com.jcraft.jsch.JSchException: invalid privatekey: [B@e4487af
    at com.jcraft.jsch.KeyPair.load(KeyPair.java:664)
    at com.jcraft.jsch.KeyPair.load(KeyPair.java:561)
    at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
    at com.jcraft.jsch.JSch.addIdentity(JSch.java:407)
    at com.jcraft.jsch.JSch.addIdentity(JSch.java:367)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.getJSch(JschConfigSessionFactory.java:276)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.createSession(JschConfigSessionFactory.java:220)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.createSession(JschConfigSessionFactory.java:176)
    at org.eclipse.jgit.transport.JschConfigSessionFactory.getSession(JschConfigSessionFactory.java:110)

根本原因是发现ssh私钥不匹配。只有使用更新类型ed25519的用户才会出现异常,它输出此密钥头:

-----BEGIN OPENSSH PRIVATE KEY-----

而不是RSA类型:

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

重新生成RSA密钥 (ssh-keygen -t rsa),将异常解决。
编辑以下评论: 如果您使用OpenSSH 7.8及以上版本,则可能需要在生成命令中添加-m PEM: ssh-keygen -t rsa -m PEM

3
此外,JSch似乎会读取~/ssh/config文件,并且如果通过IdentityFile指令将任何非RSA和/或非PEM文件添加到列表中,则会失败。 - Bass

19

不必将 OPENSSH 密钥格式转换为原始 JSch 支持的格式,您也可以切换到 JSch 的一个分支,它位于 https://github.com/mwiede/jsch

您只需要将 JSch Maven 坐标替换为 com.github.mwiede:jsch:0.1.61

该分支支持 OPENSSH 密钥格式和更多算法,在未来可能变得重要,因为 OpenSSH 服务器将限制允许使用的最安全算法。


8

回复有些晚,但我想留下解决此问题的记录。

正如许多人在他们的答案中已经提到的那样,关键点实际上是您生成密钥的方式以及使用 -m PEM 选项解决的方式。

然而,如果像我一样,由于公共部分已安装在多个服务器上,您无法重新生成密钥,则仍然可以将私钥转换为合适的格式。

要这样做,只需执行以下命令:

ssh-keygen -p -m pem -f id_rsa

它会要求输入新的密码短语。如果需要,您可以使用参数-P(旧密码短语)和-N(新密码短语)同时提供它们。


3
没错,但这正是我之前回答中所表达的意思。 - Martin Prikryl

4

JSch 不支持该密钥格式,它仅支持 RSAPrivateKey。这个命令适用于我。尝试这个解决方案。

ssh-keygen -m PEM -t rsa -b 2048

//编辑为使用2048位RSA密钥


1

除了私钥格式问题外,这个错误 "JSchException:invalid privatekey" 也可能在以下情况下发生:

  • 将私钥作为字节数组传递,使用 JSch 库的此重载方法 public void addIdentity(String name, byte[] prvkey, byte[] pubkey, byte[] passphrase)
  • 并从 UNIX 操作系统运行应用程序。例如,在您选择的云服务提供商中部署应用程序到 Linux 实例。

造成这种情况的原因是来自 JSch 源代码的 if 语句,类 KeyPair,方法 parseHeader 在第 803 行: if (buf[i] == 0x0d) {...}: https://github.com/is/jsch/blob/master/src/main/java/com/jcraft/jsch/KeyPair.java#L803

由于这个if语句,只考虑像\r(0x0D == 13)这样编码的换行符(适用于Windows和MacOS)。但UNIX使用\n(0x0A == 10)。编码在此线程中有说明,例如:Java中char文字'\n'和'\r'之间的区别是什么?

如果您的私钥具有正确的结构,但是您从Linux(或任何其他UNIX操作系统)运行应用程序,则对应于您的私钥内容的字节数组将根据您运行应用程序的操作系统而不同

这是一个示例,其中字节数组具有不同的内容:

  • 当应用程序在Linux上运行时:[114、115、97、10、69、110] => 10"\n"
  • 当应用程序在Windows上运行时:[114、115、97、1310、69、110] => 13 10"\r\n"

这张图片展示了将字节数组转换回字符串后不同内容的比较,当应用程序在Linux和Windows上以WAR文件形式运行时,通过远程调试捕获。我使用了目前可用的最新版本的JSch:https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55


因此,如果您从Linux运行应用程序,则解决方案如下:
1. 从您的私钥和公钥获取InputStream(文件应添加到Spring项目的“资源”目录中):
InputStream privateKeyInputStream = new ClassPathResource("private-key.ppk").getInputStream();
InputStream publicKeyInputStream = new ClassPathResource("public-key.ppk").getInputStream();

2. 将InputStream转换为ByteArrays

byte[] privateKeyAsByteArray = IOUtils.toByteArray(privateKeyInputStream);
byte[] publicKeyAsByteArray = IOUtils.toByteArray(publicKeyInputStream);

3. 修复编码问题,通过将10(0x0A)的字节替换为13(0x0D)的字节,在从JSch调用addIdentity方法之前进行修复:

for (int i = 0; i < privateKeyAsByteArray.length; i++) {
    if (privateKeyAsByteArray[i] == 10) {   // if current element is a 10 (\n) (UNIX)
        privateKeyAsByteArray[i] = 13;      // replace it with 13 (\r) (a byte that can be interpreted)
    }
}

for (int i = 0; i < publicKeyAsByteArray.length; i++) {
    if (publicKeyAsByteArray[i] == 10) {    // if current element is a 10 (\n) (UNIX)
        publicKeyAsByteArray[i] = 13;       // replace it with 13 (\r) (a byte that can be interpreted)
    }
}

4. 调用 addIdentity 方法:

jSch.addIdentity("private-key.ppk", privateKeyAsByteArray, publicKeyAsByteArray, passphraseAsString.getBytes());

我认为这种情况可以帮助那些遇到这个错误的人,直到JSch库源代码中的那个if语句得到更新,也能支持UNIX字符。


原始的JSch库似乎已被放弃。但这个GitHub项目继续对原始实现进行改进和更新。从版本0.2.7开始,已解决了与 invalid private-key for UNIX runs 相关的问题:https://github.com/mwiede/jsch - Pop Alexandru

1
  1. 您读取了一个名为.pem的文件,并对其进行了base64解码,然后将结果视为PKCS8未加密格式,显然成功了。这意味着该文件不是PEM格式。至少,PEM格式必须具有破折号-BEGIN和dash-END行才能有效,如果未删除,则会导致de-base64失败或错误。(一些PEM格式还具有必须处理的822样式标题。)

  2. 您似乎正在使用BouncyCastle,但在我的版本中,没有仅接受RSAPrivateKeyPKCS8Generator构造函数。最接近的有效方法是JcaPKCS8Generator(RSAPrivateKey实现PrivateKey,OutputEncryptor = null)(即不同但相关的类,两个参数而不是一个)。

  3. PemWriter是带缓冲的,并且在查看底层StringWriter之前,您没有刷新它。因此,writer.toString().getBytes()是一个空/零长度数组,JSch rightly认为它无效。

在修复了#2和#3并使用我的输入后,直接调用JSch而不是通过JGit,它对我起作用。


0

所以如果你遇到了同样的问题,可以通过PuTTYgen来解决。

enter image description here

1)打开PuTTYgen

  1. 点击“Load”按钮,选择privateKey文件

enter image description here

3) 现在点击 "转换" -> 并选择第一个选项 "导出 OPENSSH 密钥"

enter image description here

  1. 将其保存为文件(不需要任何格式)并使用它

0
从我的角度来看,我遇到了同样的问题。在我的主机文件中,有以下配置,使得jsch通过id_ed25519读取ssh配置。你应该使用id_rsa。
   Host *
     AddKeysToAgent yes
     IdentityFile /Users/xxx/.ssh/id_ed25519

0
由于JSCH不再维护,并且不支持大多数最新的OpenSSH密钥算法,JGIT现在已经转变为Apache MINA SSHD用于SSH连接。为此,JGIT还提供了artifact org.eclipse.jgit.ssh.apache
要使用这个功能,您只需要将依赖从JGit jsch artifact切换到JGit ssh.apache artifact,并将一个新的SshdSessionFactory实例(即apache实现的SshSessionFactory)设置为org.eclipse.jgit.transport.SshTransport。

切换依赖:

从:

<dependency>
        <groupId>org.eclipse.jgit</groupId>
        <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
</dependency>

致:
<dependency>
        <groupId>org.eclipse.jgit</groupId>
        <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
</dependency>

设置SshdSessionFactory以使用带有密码短语的私钥;
TransportCommand<T, C> transportCommand = <Any Transport command in JGIT>;
File sshDir = new File(FS.DETECTED.userHome(), File.separator+SSH_DIR);
      SshdSessionFactory sshSessionFactory = new SshdSessionFactoryBuilder().setPreferredAuthentications("publickey")
        .setHomeDirectory(FS.DETECTED.userHome()).setSshDirectory(sshDir)
        .setKeyPasswordProvider(cp -> new IdentityPasswordProvider(cp)
        {
          @Override
          protected char[] getPassword(URIish uri, String message)
          {
            return passphrase.toCharArray();
          }
        }).build(null);
 transportCommand.setTransportConfigCallback(transport -> ((SshTransport) transport).setSshSessionFactory(sshSessionFactory));

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