diff --git a/README.md b/README.md index fd2abba..312b547 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ brew install zvm Now add this line to your `~/.bashrc`, `~/.profile`, or `~/.zshrc` file. ```bash -export PATH="$HOME/.zm/current:$PATH" +export PATH="$HOME/.zm/current/zig:$PATH" ``` ### Windows diff --git a/src/alias.zig b/src/alias.zig index a57e33b..999ae6a 100644 --- a/src/alias.zig +++ b/src/alias.zig @@ -60,16 +60,14 @@ fn update_current(zig_path: []const u8, symlink_path: []const u8) !void { return; } - // when platform is not windows, this is execute here - - // when file exist(it is a systemlink), delete it + // Remove existing symlink if it exists if (util_tool.does_path_exist(symlink_path)) try std.fs.deleteFileAbsolute(symlink_path); - // system link it + // Create symlink to the version directory try std.posix.symlink(zig_path, symlink_path); } -/// verify current zig version +/// Verify current zig version fn verify_zig_version(expected_version: []const u8) !void { const allocator = util_data.get_allocator(); diff --git a/src/command.zig b/src/command.zig index 78802bb..5cd70aa 100644 --- a/src/command.zig +++ b/src/command.zig @@ -205,7 +205,7 @@ fn install_version(subcmd: ?[]const u8, param: ?[]const u8) !void { // set zig version try install.install(version, false); // set zls version - try install.install(version, true); + //try install.install(version, true); } else { std.debug.print("Please specify a version to install: 'install zig/zls ' or 'install '.\n", .{}); } @@ -234,7 +234,7 @@ fn use_version(subcmd: ?[]const u8, param: ?[]const u8) !void { // set zig version try alias.set_version(version, false); // set zls version - try alias.set_version(version, true); + // try alias.set_version(version, true); } else { std.debug.print("Please specify a version to use: 'use zig/zls ' or 'use '.\n", .{}); } diff --git a/src/config.zig b/src/config.zig index 77580eb..38bcdfc 100644 --- a/src/config.zig +++ b/src/config.zig @@ -12,6 +12,10 @@ pub var progress_root: std.Progress.Node = undefined; /// zig meta data url pub const zig_meta_url: []const u8 = "https://ziglang.org/download/index.json"; + +/// zig minisign public key +pub const ZIG_MINISIGN_PUBLIC_KEY = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"; + /// zls meta data url pub const zls_meta_url: []const u8 = "https://api.github.com/repos/zigtools/zls/releases"; diff --git a/src/install.zig b/src/install.zig index 8d21553..90d6fb3 100644 --- a/src/install.zig +++ b/src/install.zig @@ -9,6 +9,7 @@ const util_data = @import("util/data.zig"); const util_extract = @import("util/extract.zig"); const util_tool = @import("util/tool.zig"); const util_http = @import("util/http.zig"); +const util_minisign = @import("util/minisign.zig"); const Version = struct { name: []const u8, @@ -28,7 +29,7 @@ pub fn install(version: []const u8, is_zls: bool) !void { /// Try to install the specified version of zig fn install_zig(version: []const u8) !void { - const allocator = util_data.get_allocator(); + var allocator = util_data.get_allocator(); const platform_str = try util_arch.platform_str(.{ .os = builtin.os.tag, @@ -41,17 +42,12 @@ fn install_zig(version: []const u8) !void { const arena_allocator = arena.allocator(); - // get version path + // Get version path const version_path = try util_data.get_zvm_zig_version(arena_allocator); - // get extract path + // Get extract path const extract_path = try std.fs.path.join(arena_allocator, &.{ version_path, version }); - if (util_tool.does_path_exist(extract_path)) { - try alias.set_version(version, false); - return; - } - - // get version data + // Get version data const version_data: meta.Zig.VersionData = blk: { const res = try util_http.http_get(arena_allocator, config.zig_url); var zig_meta = try meta.Zig.init(res, arena_allocator); @@ -59,38 +55,70 @@ fn install_zig(version: []const u8) !void { break :blk tmp_val orelse return error.UnsupportedVersion; }; - const reverse_platform_str = try util_arch.platform_str(.{ - .os = builtin.os.tag, - .arch = builtin.cpu.arch, - .reverse = false, - }) orelse unreachable; + if (util_tool.does_path_exist(extract_path)) { + try alias.set_version(version, false); + return; + } - const file_name = try std.mem.concat( + const file_name = std.fs.path.basename(version_data.tarball); + + const parsed_uri = std.Uri.parse(version_data.tarball) catch unreachable; + + // Download the tarball + const tarball_file = try util_http.download(parsed_uri, file_name, version_data.shasum, version_data.size); + defer tarball_file.close(); + + // Derive signature URI by appending ".minisig" to the tarball URL + var signature_uri_buffer: [1024]u8 = undefined; + const signature_uri_buf = try std.fmt.bufPrint( + &signature_uri_buffer, + "{s}.minisig", + .{version_data.tarball}, // Use the original tarball URL + ); + + const signature_uri = try std.Uri.parse(signature_uri_buffer[0..signature_uri_buf.len]); + + // Define signature file name + const signature_file_name = try std.mem.concat( arena_allocator, u8, - &.{ "zig-", reverse_platform_str, "-", version, ".", config.archive_ext }, + &.{ file_name, ".minisig" }, ); - const parsed_uri = std.Uri.parse(version_data.tarball) catch unreachable; - const new_file = try util_http.download(parsed_uri, file_name, version_data.shasum, version_data.size); - defer new_file.close(); + // Download the signature file + const minisig_file = try util_http.download(signature_uri, signature_file_name, null, null); + defer minisig_file.close(); + + // Get paths to the tarball and signature files + const zvm_store_path = try util_data.get_zvm_path_segment(allocator, "store"); + defer allocator.free(zvm_store_path); + const tarball_path = try std.fs.path.join(arena_allocator, &.{ zvm_store_path, file_name }); + const sig_path = try std.fs.path.join(arena_allocator, &.{ zvm_store_path, signature_file_name }); + + // Perform Minisign Verification + try util_minisign.verify( + &allocator, + sig_path, + config.ZIG_MINISIGN_PUBLIC_KEY, + tarball_path, + ); + // Proceed with extraction after successful verification try util_tool.try_create_path(extract_path); const extract_dir = try std.fs.openDirAbsolute(extract_path, .{}); - try util_extract.extract(extract_dir, new_file, if (builtin.os.tag == .windows) .zip else .tarxz, false); - - // mv - const sub_path = try std.fs.path.join(arena_allocator, &.{ - extract_path, try std.mem.concat( - arena_allocator, - u8, - &.{ "zig-", reverse_platform_str, "-", version }, - ), - }); - defer std.fs.deleteTreeAbsolute(sub_path) catch unreachable; - - try util_tool.copy_dir(sub_path, extract_path); + try util_extract.extract(extract_dir, tarball_file, if (builtin.os.tag == .windows) .zip else .tarxz, false); + + //TODO: not needed (macOS) still needed for unix and windows? + // const sub_path = try std.fs.path.join(arena_allocator, &.{ + // extract_path, try std.mem.concat( + // arena_allocator, + // u8, + // &.{}, + // ), + // }); + // + //try util_tool.copy_dir(sub_path, extract_path); try alias.set_version(version, false); } diff --git a/src/util/minisign.zig b/src/util/minisign.zig new file mode 100644 index 0000000..4afd001 --- /dev/null +++ b/src/util/minisign.zig @@ -0,0 +1,247 @@ +const std = @import("std"); +const base64 = std.base64; +const crypto = std.crypto; +const fs = std.fs; +const mem = std.mem; +const fmt = std.fmt; +const heap = std.heap; +const io = std.io; +const Endian = std.builtin.Endian; +const Ed25519 = crypto.sign.Ed25519; +const Blake2b512 = crypto.hash.blake2.Blake2b512; + +// Error Definitions +const Error = error{ + invalid_encoding, + unsupported_algorithm, + key_id_mismatch, + signature_verification_failed, + file_read_error, + public_key_format_error, +}; + +// Algorithm Enumeration +pub const Algorithm = enum { + Prehash, + Legacy, +}; + +// Signature Structure +pub const Signature = struct { + signature_algorithm: [2]u8, + key_id: [8]u8, + signature: [64]u8, + trusted_comment: []const u8, + global_signature: [64]u8, + + pub fn deinit(self: *Signature, allocator: *std.mem.Allocator) void { + allocator.free(self.trusted_comment); + } + + pub fn get_algorithm(self: Signature) !Algorithm { + const signature_algorithm = self.signature_algorithm; + const prehashed = if (signature_algorithm[0] == 0x45 and signature_algorithm[1] == 0x64) false else if (signature_algorithm[0] == 0x45 and signature_algorithm[1] == 0x44) true else return error.UnsupportedAlgorithm; + return if (prehashed) .Prehash else .Legacy; + } + + pub fn decode(allocator: *std.mem.Allocator, lines: []const u8) !Signature { + var tokenizer = mem.tokenizeScalar(u8, lines, '\n'); + + // Skip untrusted comment lines + var line: []const u8 = undefined; + while (true) { + line = tokenizer.next() orelse { + std.debug.print("No more lines to read. Invalid encoding.\n", .{}); + return Error.invalid_encoding; + }; + const trimmed_line = mem.trim(u8, line, " \t\r\n"); + + if (!mem.startsWith(u8, trimmed_line, "untrusted comment:")) { + break; + } + // Optionally, store or process the untrusted comment if needed + } + + // Now 'line' should be the Base64 encoded signature + const sig_line_trimmed = mem.trim(u8, line, " \t\r\n"); + + var sig_bin: [74]u8 = undefined; + try base64.standard.Decoder.decode(&sig_bin, sig_line_trimmed); + + // Decode trusted comment + const comment_line = tokenizer.next() orelse { + std.debug.print("Expected trusted comment line but none found.\n", .{}); + return Error.invalid_encoding; + }; + const comment_line_trimmed = mem.trim(u8, comment_line, " \t\r\n"); + + const trusted_comment_prefix = "trusted comment: "; + if (!mem.startsWith(u8, comment_line_trimmed, trusted_comment_prefix)) { + std.debug.print("Trusted comment line does not start with the expected prefix.\n", .{}); + return Error.invalid_encoding; + } + const trusted_comment_slice = comment_line_trimmed[trusted_comment_prefix.len..]; + // Allocate a copy of the trusted_comment + const trusted_comment = try allocator.alloc(u8, trusted_comment_slice.len); + mem.copyForwards(u8, trusted_comment, trusted_comment_slice); + + // Decode global signature + const global_sig_line = tokenizer.next() orelse { + std.debug.print("Expected global signature line but none found.\n", .{}); + return Error.invalid_encoding; + }; + const global_sig_line_trimmed = mem.trim(u8, global_sig_line, " \t\r\n"); + + var global_sig_bin: [64]u8 = undefined; + try base64.standard.Decoder.decode(&global_sig_bin, global_sig_line_trimmed); + + return Signature{ + .signature_algorithm = sig_bin[0..2].*, + .key_id = sig_bin[2..10].*, + .signature = sig_bin[10..74].*, + .trusted_comment = trusted_comment, + .global_signature = global_sig_bin, + }; + } + + pub fn from_file(allocator: *std.mem.Allocator, path: []const u8) !Signature { + const file = try fs.cwd().openFile(path, .{ .mode = .read_only }); + defer file.close(); + + const sig_str = try file.readToEndAlloc(allocator.*, 4096); + const signature = try decode(allocator, sig_str); + allocator.free(sig_str); + + return signature; + } +}; + +// PublicKey Structure +pub const PublicKey = struct { + signature_algorithm: [2]u8 = "Ed".*, + key_id: [8]u8, + key: [32]u8, + + pub fn decode(str: []const u8) !PublicKey { + const trimmed_str = std.mem.trim(u8, str, " \t\r\n"); + + if (trimmed_str.len != 56) { // Base64 for 42-byte key + std.debug.print("Error: Public key string length is {d}, expected 56.\n", .{trimmed_str.len}); + return Error.public_key_format_error; + } + + var bin: [42]u8 = undefined; + try base64.standard.Decoder.decode(&bin, trimmed_str); + + const signature_algorithm = bin[0..2]; + + if (bin[0] != 0x45 or (bin[1] != 0x64 and bin[1] != 0x44)) { + std.debug.print("Unsupported signature algorithm: {x}\n", .{signature_algorithm}); + return Error.unsupported_algorithm; + } + + const key_id = bin[2..10]; + const public_key = bin[10..42]; + + return PublicKey{ + .signature_algorithm = signature_algorithm.*, + .key_id = key_id.*, + .key = public_key.*, + }; + } +}; + +// Verifier Structure +pub const Verifier = struct { + public_key: PublicKey, + signature: Signature, + hasher: union(Algorithm) { + Prehash: Blake2b512, + Legacy: Ed25519.Verifier, + }, + + pub fn init(public_key: PublicKey, signature: Signature) !Verifier { + const algorithm = try signature.get_algorithm(); + const ed25519_pk = try Ed25519.PublicKey.fromBytes(public_key.key); + return Verifier{ + .public_key = public_key, + .signature = signature, + .hasher = switch (algorithm) { + .Prehash => .{ .Prehash = Blake2b512.init(.{}) }, + .Legacy => .{ .Legacy = try Ed25519.Signature.fromBytes(signature.signature).verifier(ed25519_pk) }, + }, + }; + } + + pub fn update(self: *Verifier, data: []const u8) void { + switch (self.hasher) { + .Prehash => |*prehash| prehash.update(data), + .Legacy => |*legacy| legacy.update(data), + } + } + + pub fn finalize(self: *Verifier, allocator: *std.mem.Allocator) !void { + const public_key = try Ed25519.PublicKey.fromBytes(self.public_key.key); + switch (self.hasher) { + .Prehash => |*prehash| { + var digest: [64]u8 = undefined; + prehash.final(&digest); + try Ed25519.Signature.fromBytes(self.signature.signature).verify(digest[0..], public_key); + }, + .Legacy => |*legacy| { + try legacy.verify(); + }, + } + + // Verify Global Signature + const global_data = try self.build_global_signature_data(allocator); + defer allocator.free(global_data); + + try Ed25519.Signature.fromBytes(self.signature.global_signature).verify(global_data, public_key); + } + + fn build_global_signature_data(self: *Verifier, allocator: *std.mem.Allocator) ![]const u8 { + const signature_len = self.signature.signature.len; + const trusted_comment_len = self.signature.trusted_comment.len; + + var global_data = try allocator.alloc(u8, signature_len + trusted_comment_len); + + std.mem.copyForwards(u8, global_data[0..signature_len], self.signature.signature[0..]); + std.mem.copyForwards(u8, global_data[signature_len..], self.signature.trusted_comment[0..]); + + return global_data[0 .. signature_len + trusted_comment_len]; + } +}; + +// Verification Function +pub fn verify( + allocator: *std.mem.Allocator, + signature_path: []const u8, + public_key_str: []const u8, + file_path: []const u8, +) !void { + // Load Signature + var signature = try Signature.from_file(allocator, signature_path); + defer signature.deinit(allocator); // Ensure we free the allocated memory + + // Load Public Key from String + const public_key = try PublicKey.decode(public_key_str); + + // Initialize Verifier + var verifier = try Verifier.init(public_key, signature); + + // Open File to Verify + const file = try fs.cwd().openFile(file_path, .{ .mode = .read_only }); + defer file.close(); + + // Read and Update Verifier with File Data + var buffer: [4096]u8 = undefined; + while (true) { + const bytes_read = try file.read(&buffer); + if (bytes_read == 0) break; + verifier.update(buffer[0..bytes_read]); + } + + // Finalize Verification + try verifier.finalize(allocator); +}