Thêm hỗ trợ RSA vào swift-nio-ssh bằng một fork

Phần mềm13 tháng 11, 2026·6 phút đọc

Một nhà phát triển đã tạo bản fork của swift-nio-ssh để bổ sung hỗ trợ khóa RSA .pem, phục vụ việc kết nối SSH tới các máy chủ Amazon EC2 cũ chỉ dùng khóa RSA. Bài viết mô tả những thay đổi cần thiết và các cạm bẫy kỹ thuật gặp phải khi vá một thư viện bảo mật.

Thêm hỗ trợ RSA vào swift-nio-ssh bằng một fork

Một nhà phát triển đứng sau dự án LogRaker đã công bố bản fork của swift-nio-ssh nhằm bổ sung hỗ trợ khóa RSA dạng .pem, vốn bị thư viện gốc cố ý bỏ qua. Nhu cầu xuất phát từ việc Amazon EC2 phát hành cặp khóa PKCS#1 RSA và chỉ bổ sung khóa ed25519 từ năm 2021, nên mọi cặp khóa cũ hơn đều là RSA và các instance mà chúng mở vẫn đang chạy.

Vấn đề nằm ở chỗ LogRaker cần đọc log qua SSH bằng chính khóa nằm trong thư mục ~/.ssh/. Một trình xem log không dùng được khóa sẵn có thì không có giá trị sử dụng.

Không có điểm mở rộng nào để bám vào

Cách làm tưởng như hiển nhiên là thêm kiểu khóa từ bên ngoài module. Nhưng không thể: NIOSSHPrivateKey lưu khóa trong một enum nội bộ, với backingKey là internal, hàm khởi tạo init(backingKey:) là private, và chỉ có các hàm khởi tạo công khai cho từng thuật toán được hỗ trợ.

Không có protocol nào để tuân theo và cũng không có case nào để thêm. Muốn bổ sung thuật toán thì phải sửa chính module, nghĩa là phải tạo fork.

Bản fork cuối cùng đụng tới 8 file: Package.swift để kéo _RSA.Signing từ sản phẩm CryptoExtras của swift-crypto (không nằm trong Crypto), 5 file nguồn dưới Sources/NIOSSH/, và một test gốc phải chỉnh vì nó dùng ssh-rsa làm ví dụ về thuật toán không ai nhận ra.

Cạm bẫy: hai chuỗi khác nhau

Đây là phần đáng đọc kể cả khi không bao giờ đụng tới nio-ssh. Một yêu cầu publickey userauth mang theo một blob khóa công khai và một tên thuật toán. Với ed25519 và các đường cong ECDSA, hai chuỗi này giống hệt nhau, nên thư viện gốc ghi luôn tag của khóa vào trường tên thuật toán.

RSA là thuật toán duy nhất mà chúng khác nhau. Blob khóa vẫn mang tag ssh-rsa, nhưng tên thuật toán phải nêu thuật toán chữ ký: rsa-sha2-256 hoặc rsa-sha2-512, theo RFC 8332. Tên ssh-rsa trần có nghĩa là RSA với SHA-1, thứ mà OpenSSH 8.8 đã tắt mặc định vào năm 2021. Gửi tên đó tới bất kỳ máy chủ hiện đại nào cũng bị từ chối thẳng, bất kể khóa tốt đến đâu.

Bản fork thêm thuộc tính signatureAlgorithmName bên cạnh keyPrefix để trả về tên chữ ký phù hợp. Điểm tinh tế không nằm ở bản thân thuộc tính, mà ở chỗ tên thuật toán được ghi ở hai nơi và chúng phải khớp nhau đến từng byte: trong SSHMessages.swift để đưa vào thông điệp trên đường truyền, và trong UserAuthSignablePayload.swift để đưa vào buffer được ký.

Một bên là thứ bạn tuyên bố, bên kia là thứ bạn ký. Nếu chúng lệch nhau, mọi byte gửi đi đều đúng định dạng, khóa đúng, chữ ký RSA hợp lệ, nhưng máy chủ vẫn từ chối. Không có thông báo lỗi nào chỉ ra sự lệch nhau, vì từ phía máy chủ nó không khác gì khóa sai. Đây là kiểu lỗi chỉ phát hiện được bằng cách ghi cả hai buffer ra rồi so sánh.

Hiện một test đã gắn chặt hai vị trí ghi này với nhau, để người tiếp theo sửa một trong hai sẽ nhận lỗi thay vì một ẩn số.

Chọn tên nào để gửi

Bản fork luôn gửi rsa-sha2-512 và không thương lượng. Nghe có vẻ lười, nhưng thực ra là bắt buộc: nio-ssh không phân tích thông điệp SSH_MSG_EXT_INFO, nên danh sách server-sig-algs của máy chủ không bao giờ tới được đoạn mã có thể xử lý nó. Không có gì để thương lượng.

Dù sao đây cũng là mặc định an toàn. rsa-sha2-512 đã được chấp nhận từ OpenSSH 7.2 năm 2016, và mọi thứ mà một trình xem log thực tế kết nối tới đều mới hơn mốc đó.

Một chi tiết dễ bỏ sót: danh sách thuật toán đã biết của bản fork phải chứa cả tag của khóa lẫn cả hai tên thuật toán chữ ký, vì danh sách này được đối chiếu với trường tên thuật toán trong các yêu cầu userauth đến và trong phản hồi PK_OK. Một tag khóa, nhiều tên hợp lệ.

Giữ cho bản fork không mục ruỗng

Một fork tự vá sẽ lặng lẽ hỏng dần. Ba nguyên tắc giúp bản này còn dễ đọc:

  • Mọi chỉnh sửa đều được đánh dấu. Mỗi đoạn thay đổi mang một chú thích LOGRAKER FORK:, nên toàn bộ diff so với bản gốc chỉ cần một lệnh grep. Đây cũng là danh sách kiểm tra khi rebase: lấy bản gốc, rebase lên tag mới, và các dấu này chỉ ra xung đột nào là của mình.
  • Một test mang tính sống còn. Test tuần tự hóa so sánh từng byte blob khóa công khai được tạo ra với kết quả mà ssh-keygen -y in ra cho cùng khóa đó. Định dạng đường truyền SSH tự viết tay chính là nơi một sai sót trông có vẻ hợp lý vẫn sống sót qua bước review, và OpenSSH là ý kiến duy nhất đáng tin.
  • Cảnh báo bảo mật sẽ không tìm đến bạn. Đây là cái giá thực sự của việc tự vá, không liên quan gì tới RSA. Một gói SwiftPM cục bộ không bao giờ báo phiên bản mới, nên không công cụ nào cho bạn biết bản gốc vừa phát hành bản sửa. Bạn phải tự đi tra: gh api repos/apple/swift-nio-ssh/security-advisories --jq '.[] | "\(.severity) \(.ghsa_id) \(.summary)"'.

Có nên làm theo?

Nếu có lựa chọn khác thì có lẽ không nên. Fork một thư viện liên quan tới bảo mật nghĩa là bạn sở hữu mãi mãi mọi bản sửa lỗi từ upstream, và mô tả trung thực về fork này là "chúng tôi thêm một thuật toán mà tác giả đã chọn bỏ ra".

Nhưng ràng buộc là có thật: những khóa đó tồn tại, chúng là RSA, và chúng mở các máy mà con người cần theo dõi. Bản fork tác động 8 file trên nền upstream 0.15.0, mọi chỉnh sửa đều được đánh dấu, và có thể xóa đi ngay khi nio-ssh tự hỗ trợ RSA.

Bản fork được công khai, giấy phép Apache 2.0 như thư viện gốc. Tác giả lưu ý đọc README trước khi dựa vào: nó tồn tại để phục vụ một ứng dụng duy nhất, ghim vào phiên bản 0.15.0, và chưa ai kiểm định.

Góc nhìn

Câu chuyện này là bài học kinh điển về xung đột giữa thực tiễn và nguyên tắc bảo mật. Các thư viện hiện đại cố ý loại bỏ thuật toán cũ là đúng, nhưng hạ tầng đã triển khai không biến mất theo quyết định đó. Lập trình viên Việt Nam làm việc với hệ thống cũ, đặc biệt là khi kết nối tới máy chủ đám mây thuê từ nhiều năm trước, sẽ gặp đúng tình huống này: khóa trong tay là RSA, còn thư viện thì từ chối.

Bài viết cũng gợi ý một mẫu tư duy nên áp dụng rộng hơn: khi buộc phải fork hay vá một thư viện bảo mật, hãy đánh dấu rõ từng thay đổi, viết test đối chiếu với công cụ chuẩn, và đặc biệt là chủ động theo dõi cảnh báo bảo mật của upstream, bởi không ai làm việc đó thay bạn. Với các đội đang tự duy trì bản vá riêng, một lịch kiểm tra advisory định kỳ nên là phần bắt buộc của quy trình.

Nguồn: lograker.app · Quy trình biên tập

Chia sẻ:FacebookX