为 swift-nio-ssh 添加 RSA 支持
Adding RSA Support to swift-nio-ssh

原始链接: https://lograker.app/blog/swift-nio-ssh-rsa-support

LogRaker 引入了 swift-nio-ssh 的一个 fork,以支持 AWS EC2 常见发行且上游刻意省略的 RSA SSH 密钥。由于 NIOSSHPrivateKey 没有扩展点,添加 RSA 需要在模块的八个文件中编辑。主要难点在于 SSH RSA 的拆分命名:公钥 blob 仍标记为 `ssh-rsa`,而算法名必须是 `rsa-sha2-256` 或 `rsa-sha2-512`;裸 `ssh-rsa` 名称表示 SHA-1,会被现代服务器拒绝。该算法名写在两处——线上传输消息和签名载荷——并且必须完全匹配。由于 nio-ssh 不解析 server-sig-algs,该 fork 总是提供 `rsa-sha2-512` 而不进行协商。为保持 fork 可维护,编辑处被标记,序列化测试将输出与 `ssh-keygen` 进行字节级比较,且安全公告手动检查。该 fork 是公开的,但固定在 0.15.0 且未经审计;若上游添加 RSA 支持,可移除。

Hacker News 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 为 swift-nio-ssh 添加 RSA 支持 (lograker.app) 6 分,作者 moonlighter,1 小时前 | 隐藏 | 过去 | 收藏 | 2 条评论 大家好,我是作者。我需要支持 RSA .pem 密钥,以便通过 SSH 访问较旧的 Amazon EC2 实例,但 swift-nio-ssh 故意不支持它们,所以我只能自己实现,并遇到了一些值得记录和分享的坑。简而言之:现在有一个 swift-no-ssh 的分支,包含该 RSA 支持(并附带最新安全公告),博客文章描述了这些更改。欢迎提问! 帮助 ACABThomas,29 分钟前 [-] 太好了!正是我需要的。 回复 moonlighter,20 分钟前 | 父评论 [-] 这个功能我真的是专门为你们加的 ;) 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

← Blog

~/blog/swift-nio-ssh-rsa-support

LogRaker talks SSH through swift-nio-ssh, which is a great library with one gap: it has no RSA support. That is deliberate. Upstream supports modern primitives only, and for new deployments the newer ed25519 format is the better default.

It is also not a choice you always get to make. Amazon EC2 hands out PKCS#1 RSA .pem key pairs, and only added ed25519 key pairs in 2021. Any key pair older than that is RSA, the instances it opens are still running, and nothing about the situation is negotiable from the client side. A log viewer that cannot use the key sitting in ~/.ssh/ is not useful.

So LogRaker vendored a fork. Here is what that actually took.

There is no extension point

The obvious move would be to add the key type from outside the module. Well, you can’t. NIOSSHPrivateKey stores its key in an internal enum:

extension NIOSSHPrivateKey {
    internal enum BackingKey {
        case ed25519(Curve25519.Signing.PrivateKey)
        case ecdsaP256(P256.Signing.PrivateKey)
        // ...
    }
}

backingKey is internal, the memberwise init(backingKey:) is private, and the only public initializers are one per supported algorithm. There is no protocol to conform to and no case to add. Adding an algorithm means editing the module, which means a new fork.

Eight files, in the end: Package.swift to pull in _RSA.Signing from swift-crypto’s CryptoExtras product (it is not in Crypto), five sources under Sources/NIOSSH/, and one upstream test that had to change, because it used ssh-rsa as its example of an algorithm nobody recognizes.

The trap: two different strings

This is the part worth reading even if you never touch nio-ssh.

A publickey userauth request carries a public key blob and an algorithm name. For ed25519 and the ECDSA curves those two strings are identical, so upstream writes the key’s own tag into the algorithm-name field and never has to think about it:

writtenBytes += self.writeSSHString(key.keyPrefix)

RSA is the one algorithm where they differ. The key blob stays tagged ssh-rsa, but the algorithm name has to name the signature algorithm: rsa-sha2-256 or rsa-sha2-512, from RFC 8332. The bare ssh-rsa name means RSA with SHA-1, which OpenSSH 8.8 disabled by default in 2021. Offer that name to any current server and it is rejected outright, no matter how good your key is.

So the fork adds signatureAlgorithmName next to keyPrefix:

internal var signatureAlgorithmName: String.UTF8View {
    switch self.backingKey {
    case .rsa:
        return Self.rsaSHA512SignatureAlgorithm
    case .ed25519, .ecdsaP256, .ecdsaP384, .ecdsaP521, .certified:
        return self.keyPrefix
    }
}

The subtlety is not the property. It is that the algorithm name gets written in two places, and they have to agree byte for byte:

  • SSHMessages.swift, into the wire message.
  • UserAuthSignablePayload.swift, into the buffer that gets signed.

One is what you claim, the other is what you sign. If they diverge, every byte you send is individually well-formed, the key is correct, the signature is a valid RSA signature, and yet the server rejects it anyway. There is no error message that points at the mismatch, because from the server’s side it is indistinguishable from a wrong key. This is the kind of bug you find by writing out both buffers and diffing them.

A test now pins the two together, so the next person to touch either write site gets a failure instead of a mystery.

Which name to offer

The fork always offers rsa-sha2-512 and does not negotiate. That sounds lazy and is actually forced: nio-ssh does not parse SSH_MSG_EXT_INFO, so the server-sig-algs list never reaches any code that could act on it. There is nothing to negotiate against.

It is a safe default anyway. rsa-sha2-512 has been accepted since OpenSSH 7.2, in 2016. Anything a log viewer realistically connects to is newer than that.

One related detail, easy to miss: the fork’s list of known algorithms has to carry the key tag and both signature algorithm names, because that list is checked against the algorithm-name field of incoming userauth requests and PK_OK replies. One key tag, several legal names.

Keeping the fork honest

A vendored fork rots quietly. Three things keep this one readable:

Every edit is marked. Each hunk carries a LOGRAKER FORK: comment, so the whole diff against upstream is one grep:

grep -rn "LOGRAKER FORK" Sources/ Tests/ Package.swift

That is also the rebase checklist: fetch upstream, rebase onto the new tag, and the markers show you which conflicts are yours.

One test is load-bearing. The serialization test compares the generated public-key blob byte for byte against what ssh-keygen -y prints for the same key. Hand-rolled SSH wire format is exactly where a plausible-looking mistake survives review, and OpenSSH is the only opinion that counts.

Security advisories will not find you. This is the real cost of vendoring, and it has nothing to do with RSA. A local SwiftPM package never reports a new version, so no tool will ever tell you that upstream shipped a fix. You have to go and look:

gh api repos/apple/swift-nio-ssh/security-advisories \
  --jq '.[] | "\(.severity) \(.ghsa_id) \(.summary)"'

Put that somewhere you will actually run it.

Should you do this?

Probably not, if you have a choice. Forking a security-relevant library means you own its upstream fixes forever, and the honest summary of this fork is “we added an algorithm its authors chose to leave out”.

But the constraint was real: the keys exist, they are RSA, and they open machines people need to watch. The fork touches eight files against upstream 0.15.0, every edit marked, and it can be deleted the day nio-ssh adds RSA support of its own.

The fork is public, at lograker/swift-nio-ssh, Apache 2.0 like upstream. Read its README before depending on it: it exists to serve one app, it is pinned to 0.15.0, and nobody has audited it.

联系我们 contact @ memedata.com