╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2453 const fingerprint = User.fingerprintOfKey(key) orelse return error.InvalidKey; │ │2454 // the key types the ssh server verifies │ │2455 const key_type = std.mem.sliceTo(key, ' '); │ │2456 - if (!std.mem.eql(u8, key_type, "ssh-ed25519") and !std.mem.eql(u8, key_type, "ssh-rsa")) return error.UnsupportedKey;│ │2456 + const supported = [_][]const u8{ "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384" }; │ │2457 + for (supported) |name| { │ │2458 + if (std.mem.eql(u8, key_type, name)) break; │ │2459 + } else return error.UnsupportedKey; │ │2460 try writeUserKeys(io, allocator, users_dir, admin_repo, user_id, &fingerprint, key); │ │2461 } │ │2462 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │26 //! cipher: chacha20-poly1305@openssh.com (RFC 4253 + openssh extension)│ │27 //! MAC: none (implicit in AEAD) │ │28 //! compress: none │ │29 -//! user auth: publickey + ssh-ed25519, rsa-sha2-512, rsa-sha2-256 │ │29 +//! user auth: publickey + ssh-ed25519, rsa-sha2-512, rsa-sha2-256, │ │30 +//! ecdsa-sha2-nistp256, ecdsa-sha2-nistp384 (RFC 5656) │ │31 │ │32 const std = @import("std"); │ │33 const builtin = @import("builtin"); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │36 const Sha256 = std.crypto.hash.sha2.Sha256; │ │37 const Sha512 = std.crypto.hash.sha2.Sha512; │ │38 const rsa = std.crypto.Certificate.rsa; │ │39 +const ecdsa = std.crypto.sign.ecdsa; │ │40 const ChaCha20 = std.crypto.stream.chacha.ChaCha20With64BitNonce;│ │41 const Poly1305 = std.crypto.onetimeauth.Poly1305; │ │42 │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1047 const hybrid_client_blob_len = MLKem768.PublicKey.encoded_length + X25519.public_length; // 1216 │ │1048 const hybrid_server_blob_len = MLKem768.ciphertext_length + X25519.public_length; // 1120 │ │1049 pub const our_host_key_algos = [_][]const u8{"ssh-ed25519"}; │ │1048 -pub const our_userauth_algos = [_][]const u8{ "ssh-ed25519", "rsa-sha2-512", "rsa-sha2-256" }; │ │1050 +pub const our_userauth_algos = [_][]const u8{ "ssh-ed25519", "rsa-sha2-512", "rsa-sha2-256", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384" };│ │1051 pub const our_ciphers = [_][]const u8{"chacha20-poly1305@openssh.com"}; │ │1052 const our_macs = [_][]const u8{}; // none — implicit in the AEAD cipher │ │1053 const our_compression = [_][]const u8{"none"}; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1840 if (std.mem.eql(u8, algo, "ssh-ed25519")) return verifyEd25519(pubkey_blob, signature_blob, signed.items); │ │1841 if (std.mem.eql(u8, algo, "rsa-sha2-512")) return verifyRsa(Sha512, algo, pubkey_blob, signature_blob, signed.items); │ │1842 if (std.mem.eql(u8, algo, "rsa-sha2-256")) return verifyRsa(Sha256, algo, pubkey_blob, signature_blob, signed.items); │ │1843 + if (std.mem.eql(u8, algo, "ecdsa-sha2-nistp256")) return verifyEcdsa(ecdsa.EcdsaP256Sha256, "nistp256", pubkey_blob, signature_blob, signed.items);│ │1844 + if (std.mem.eql(u8, algo, "ecdsa-sha2-nistp384")) return verifyEcdsa(ecdsa.EcdsaP384Sha384, "nistp384", pubkey_blob, signature_blob, signed.items);│ │1845 return false; │ │1846 } │ │1847 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1896 }, │ │1897 else => return false, │ │1898 } │ │1899 +} │ │1900 + │ │1901 +fn verifyEcdsa(comptime Ecdsa: type, comptime curve: []const u8, pubkey_blob: []const u8, signature_blob: []const u8, signed: []const u8) !bool {│ │1902 + const algo = "ecdsa-sha2-" ++ curve; │ │1903 + // parse pubkey_blob: string algo || string curve || string point │ │1904 + var pubkey_reader = std.Io.Reader.fixed(pubkey_blob); │ │1905 + if (!std.mem.eql(u8, try takeString(&pubkey_reader, 64), algo)) return false; │ │1906 + if (!std.mem.eql(u8, try takeString(&pubkey_reader, 64), curve)) return false; │ │1907 + const point = try takeString(&pubkey_reader, 256); │ │1908 + // the fingerprint hashes the whole blob, so it must hold nothing else │ │1909 + if (pubkey_reader.bufferedLen() != 0) return false; │ │1910 + │ │1911 + // parse signature_blob: string algo || string (mpint r || mpint s) │ │1912 + var sig_reader = std.Io.Reader.fixed(signature_blob); │ │1913 + if (!std.mem.eql(u8, try takeString(&sig_reader, 64), algo)) return false; │ │1914 + var rs_reader = std.Io.Reader.fixed(try takeString(&sig_reader, 256)); │ │1915 + const r = canonicalMpintMagnitude(try takeString(&rs_reader, 128)) orelse return false; │ │1916 + const s = canonicalMpintMagnitude(try takeString(&rs_reader, 128)) orelse return false; │ │1917 + if (rs_reader.bufferedLen() != 0) return false; │ │1918 + │ │1919 + // r and s become fixed-width big-endian halves of the signature │ │1920 + const half = Ecdsa.Signature.encoded_length / 2; │ │1921 + if (r.len > half or s.len > half) return false; │ │1922 + var sig_bytes: [Ecdsa.Signature.encoded_length]u8 = @splat(0); │ │1923 + @memcpy(sig_bytes[half - r.len .. half], r); │ │1924 + @memcpy(sig_bytes[2 * half - s.len ..], s); │ │1925 + │ │1926 + const pk = Ecdsa.PublicKey.fromSec1(point) catch return false; │ │1927 + Ecdsa.Signature.fromBytes(sig_bytes).verify(signed, pk) catch return false; │ │1928 + return true; │ │1929 } │ │1930 │ │1931 // the unsigned bytes of a positive mpint in its shortest encoding (RFC 4251 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯