From 87ab1b37cae7c9974124e21a965d2db81ab5e829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20Niel=C3=A4nder?= Date: Sun, 17 Nov 2024 20:07:05 +0100 Subject: [PATCH 1/4] wip: add minisign --- src/config.zig | 4 + src/install.zig | 67 +++++++++++--- src/util/minisign.zig | 210 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 src/util/minisign.zig 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..1012e7f 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,9 +42,9 @@ 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)) { @@ -51,7 +52,7 @@ fn install_zig(version: []const u8) !void { 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); @@ -72,22 +73,58 @@ fn install_zig(version: []const u8) !void { ); 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 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 ".minisign" to the tarball URL + var signature_uri_buffer: [1024]u8 = undefined; + const signature_uri_buf = try std.fmt.bufPrint( + &signature_uri_buffer, + "{s}.minisign", + .{version_data.tarball}, + ); + 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, + &.{ file_name, ".minisign" }, + ); + + // Download the signature file using the corrected signature_uri + const signature_data = try util_http.http_get(arena_allocator, signature_uri); + defer allocator.free(signature_data); + + // Save the signature file to disk + const store_path = try std.fs.path.join(arena_allocator, &.{ "store", signature_file_name }); + const signature_file = try std.fs.cwd().createFile(store_path, .{ .truncate = true }); + + defer signature_file.close(); + try signature_file.writeAll(signature_data); + + // Perform Minisign Verification + try util_minisign.verify( + &allocator, + config.ZIG_MINISIGN_PUBLIC_KEY, + file_name, + store_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); + try util_extract.extract(extract_dir, tarball_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 }, - ), - }); + // Move extracted files if necessary + 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); diff --git a/src/util/minisign.zig b/src/util/minisign.zig new file mode 100644 index 0000000..ade5286 --- /dev/null +++ b/src/util/minisign.zig @@ -0,0 +1,210 @@ +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 get_algorithm(self: Signature) !Algorithm { + const legacy_alg = "Ed"; + const prehash_alg = "ED"; + + if (self.signature_algorithm == prehash_alg) { + return .prehash; + } else if (self.signature_algorithm == legacy_alg) { + return .legacy; + } else { + return Error.unsupported_algorithm; + } + } + + pub fn decode(_: *std.mem.Allocator, lines: []const u8) !Signature { + var tokenizer = mem.tokenizeScalar(u8, lines, '\n'); + + // Decode first line: signature_algorithm + key_id + signature + const sig_line = tokenizer.next() orelse return Error.invalid_encoding; + var sig_bin: [74]u8 = undefined; + try base64.standard.Decoder.decode(&sig_bin, sig_line); + + // Decode trusted comment + const trusted_comment_prefix = "trusted comment: "; + const comment_line = tokenizer.next() orelse return Error.invalid_encoding; + if (!mem.startsWith(u8, comment_line, trusted_comment_prefix)) { + return Error.invalid_encoding; + } + const trusted_comment = comment_line[trusted_comment_prefix.len..]; + + // Decode global signature + const global_sig_line = tokenizer.next() orelse return Error.invalid_encoding; + var global_sig_bin: [64]u8 = undefined; + try base64.standard.Decoder.decode(&global_sig_bin, global_sig_line); + + 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); + defer allocator.free(sig_str); + + return decode(allocator, sig_str); + } +}; + +// 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 { + if (str.len != 44) { // Base64 for 32-byte key + return error.public_key_format_error; + } + + var bin: [42]u8 = undefined; + try base64.standard.Decoder.decode(&bin, str); + const signature_algorithm = bin[0..2]; + if (bin[0] != 0x45 or (bin[1] != 0x64 and bin[1] != 0x44)) { + return error.unsupported_algorithm; + } + + return PublicKey{ + .signature_algorithm = signature_algorithm.*, + .key_id = bin[2..10].*, + .key = bin[10..42].*, + }; + } + + pub fn from_file(allocator: *std.mem.Allocator, path: []const u8) !PublicKey { + const file = try fs.cwd().openFile(path, .{ .mode = .read_only }); + defer file.close(); + + const pk_str = try file.readToEndAlloc(allocator.*, 4096); + defer allocator.free(pk_str); + + const trimmed_pk = mem.trim(u8, pk_str, " \t\r\n"); + return decode(trimmed_pk); + } +}; + +// 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(); + return Verifier{ + .public_key = public_key, + .signature = signature, + .hasher = switch (algorithm) { + .prehash => Blake2b512.init(.{}), + .legacy => try Ed25519.Verifier.fromBytes(signature.signature), + }, + }; + } + + 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) !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); + const signature = try Ed25519.Signature.fromBytes(self.signature.signature); + try signature.verify(&digest, public_key); + }, + .legacy => |legacy| { + try legacy.verify(); + }, + } + + // Verify Global Signature + var global_data: [128]u8 = undefined; + mem.copy(u8, global_data[0..64], self.signature.signature); + mem.copy(u8, global_data[64..128], self.signature.trusted_comment); + const global_signature = try Ed25519.Signature.fromBytes(self.signature.global_signature); + try global_signature.verify(&global_data, public_key); + } +}; + +// Verification Function +pub fn verify( + allocator: *std.mem.Allocator, + signature_path: []const u8, + public_key_path: []const u8, + file_path: []const u8, +) !void { + // Load Signature + const signature = try Signature.from_file(allocator, signature_path); + defer allocator.free(signature.trusted_comment); + + // Load Public Key + const public_key = try PublicKey.from_file(allocator, public_key_path); + + // 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(); +} From b70e49ea79546028c69dd40504a11089cbbf2bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20Niel=C3=A4nder?= Date: Mon, 18 Nov 2024 20:01:58 +0100 Subject: [PATCH 2/4] fix: minisign impl --- src/util/minisign.zig | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/util/minisign.zig b/src/util/minisign.zig index ade5286..56a9fa8 100644 --- a/src/util/minisign.zig +++ b/src/util/minisign.zig @@ -35,16 +35,9 @@ pub const Signature = struct { global_signature: [64]u8, pub fn get_algorithm(self: Signature) !Algorithm { - const legacy_alg = "Ed"; - const prehash_alg = "ED"; - - if (self.signature_algorithm == prehash_alg) { - return .prehash; - } else if (self.signature_algorithm == legacy_alg) { - return .legacy; - } else { - return Error.unsupported_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(_: *std.mem.Allocator, lines: []const u8) !Signature { @@ -59,7 +52,7 @@ pub const Signature = struct { const trusted_comment_prefix = "trusted comment: "; const comment_line = tokenizer.next() orelse return Error.invalid_encoding; if (!mem.startsWith(u8, comment_line, trusted_comment_prefix)) { - return Error.invalid_encoding; + return error.invalid_encoding; } const trusted_comment = comment_line[trusted_comment_prefix.len..]; @@ -136,43 +129,42 @@ pub const Verifier = struct { 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 => Blake2b512.init(.{}), - .legacy => try Ed25519.Verifier.fromBytes(signature.signature), + .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), + .Prehash => |*prehash| prehash.update(data), + .Legacy => |*legacy| legacy.update(data), } } pub fn finalize(self: *Verifier) !void { const public_key = try Ed25519.PublicKey.fromBytes(self.public_key.key); switch (self.hasher) { - .prehash => |prehash| { + .Prehash => |*prehash| { var digest: [64]u8 = undefined; prehash.final(&digest); - const signature = try Ed25519.Signature.fromBytes(self.signature.signature); - try signature.verify(&digest, public_key); + try Ed25519.Signature.fromBytes(self.signature.signature).verify(&digest, public_key); }, - .legacy => |legacy| { + .Legacy => |*legacy| { try legacy.verify(); }, } // Verify Global Signature var global_data: [128]u8 = undefined; - mem.copy(u8, global_data[0..64], self.signature.signature); - mem.copy(u8, global_data[64..128], self.signature.trusted_comment); - const global_signature = try Ed25519.Signature.fromBytes(self.signature.global_signature); - try global_signature.verify(&global_data, public_key); + mem.copyForwards(u8, global_data[0..64], &self.signature.signature); + mem.copyForwards(u8, global_data[64..128], self.signature.trusted_comment); + try Ed25519.Signature.fromBytes(self.signature.global_signature).verify(&global_data, public_key); } }; From 1519da609bd634d7e428908dc1b9a0b9e05121a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20Niel=C3=A4nder?= Date: Tue, 19 Nov 2024 21:24:34 +0100 Subject: [PATCH 3/4] wip: minisign --- src/install.zig | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/install.zig b/src/install.zig index 1012e7f..306705f 100644 --- a/src/install.zig +++ b/src/install.zig @@ -29,6 +29,8 @@ 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 { + std.debug.print("install start", .{}); + var allocator = util_data.get_allocator(); const platform_str = try util_arch.platform_str(.{ @@ -74,29 +76,37 @@ fn install_zig(version: []const u8) !void { const parsed_uri = std.Uri.parse(version_data.tarball) catch unreachable; + std.debug.print("parsed url: {any}", .{parsed_uri}); + // 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 ".minisign" to the tarball URL + std.debug.print("download done", .{}); + + // Derive signature URI by appending ".minisig" to the tarball URL + // https://ziglang.org/builds/zig-0.14.0-dev.2261+fbcb00fbb.tar.xz.minisig + const minisig_url = try remove_arch_from_url(arena_allocator, version_data.tarball); var signature_uri_buffer: [1024]u8 = undefined; const signature_uri_buf = try std.fmt.bufPrint( &signature_uri_buffer, - "{s}.minisign", - .{version_data.tarball}, + "{s}.minisig", + .{minisig_url}, ); + const signature_uri = try std.Uri.parse(signature_uri_buffer[0..signature_uri_buf.len]); + std.debug.print("signature url {any}", .{signature_uri}); + // Define signature file name const signature_file_name = try std.mem.concat( arena_allocator, u8, - &.{ file_name, ".minisign" }, + &.{ file_name, ".minisig" }, ); // Download the signature file using the corrected signature_uri const signature_data = try util_http.http_get(arena_allocator, signature_uri); - defer allocator.free(signature_data); // Save the signature file to disk const store_path = try std.fs.path.join(arena_allocator, &.{ "store", signature_file_name }); @@ -199,3 +209,24 @@ fn install_zls(version: []const u8) !void { } pub fn build_zls() !void {} + +fn remove_arch_from_url(allocator: std.mem.Allocator, url: []const u8) ![]const u8 { + const prefix = "zig-"; + const version_marker = "-0."; // A reliable marker for version information + + const prefix_index = std.mem.indexOf(u8, url, prefix) orelse return url; + const version_start = std.mem.indexOf(u8, url, version_marker) orelse return url; + + // Ensure proper removal of redundant dashes + const prefix_end = prefix_index + prefix.len; + + // Rebuild the URL without platform/architecture + return try std.mem.concat( + allocator, + u8, + &.{ + url[0..prefix_end], // Keep everything up to and including `zig-` + url[version_start + 1 ..], + }, + ); +} From 1019d6b018af93a232ee2ab5749ee8587fcfac68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20Niel=C3=A4nder?= Date: Thu, 21 Nov 2024 23:26:57 +0100 Subject: [PATCH 4/4] wip: add minisign --- src/install.zig | 17 +++++++---------- src/util/minisign.zig | 1 + 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/install.zig b/src/install.zig index 306705f..10aefe5 100644 --- a/src/install.zig +++ b/src/install.zig @@ -97,7 +97,6 @@ fn install_zig(version: []const u8) !void { const signature_uri = try std.Uri.parse(signature_uri_buffer[0..signature_uri_buf.len]); std.debug.print("signature url {any}", .{signature_uri}); - // Define signature file name const signature_file_name = try std.mem.concat( arena_allocator, @@ -106,21 +105,19 @@ fn install_zig(version: []const u8) !void { ); // Download the signature file using the corrected signature_uri - const signature_data = try util_http.http_get(arena_allocator, signature_uri); - - // Save the signature file to disk - const store_path = try std.fs.path.join(arena_allocator, &.{ "store", signature_file_name }); - const signature_file = try std.fs.cwd().createFile(store_path, .{ .truncate = true }); + const minisig_file = try util_http.download(signature_uri, signature_file_name, null, null); + defer minisig_file.close(); - defer signature_file.close(); - try signature_file.writeAll(signature_data); + const zvm_path = try util_data.get_zvm_path_segment(allocator, "store"); + defer allocator.free(zvm_path); + const sig_path = try std.fs.path.join(arena_allocator, &.{ zvm_path, signature_file_name }); // Perform Minisign Verification try util_minisign.verify( &allocator, + sig_path, config.ZIG_MINISIGN_PUBLIC_KEY, - file_name, - store_path, + extract_path, ); // Proceed with extraction after successful verification diff --git a/src/util/minisign.zig b/src/util/minisign.zig index 56a9fa8..8d86e78 100644 --- a/src/util/minisign.zig +++ b/src/util/minisign.zig @@ -176,6 +176,7 @@ pub fn verify( file_path: []const u8, ) !void { // Load Signature + std.debug.print("signature_path: {s}", .{signature_path}); const signature = try Signature.from_file(allocator, signature_path); defer allocator.free(signature.trusted_comment);