aboutsummaryrefslogtreecommitdiffstats
path: root/src/znk.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/znk.zig')
-rw-r--r--src/znk.zig462
1 files changed, 462 insertions, 0 deletions
diff --git a/src/znk.zig b/src/znk.zig
new file mode 100644
index 0000000..ab36c96
--- /dev/null
+++ b/src/znk.zig
@@ -0,0 +1,462 @@
1const std = @import("std");
2const Allocator = mem.Allocator;
3const ascii = std.ascii;
4const build_options = @import("build_options");
5const builtin = std.builtin;
6const fs = std.fs;
7const io = std.io;
8const mem = std.mem;
9const nkeys = @import("nkeys.zig");
10const process = std.process;
11const testing = std.testing;
12
13pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
14 std.debug.print("error: " ++ format ++ "\n", args);
15 process.exit(1);
16}
17
18pub fn info(comptime format: []const u8, args: anytype) void {
19 std.debug.print(format ++ "\n", args);
20}
21
22const usage =
23 \\Usage: znk [command] [options]
24 \\
25 \\Commands:
26 \\
27 \\ gen Generate a new key pair
28 \\ help Print this help and exit
29 \\ sign Sign a file
30 \\ verify Verify a file with a signature
31 \\ version Print version number and exit
32 \\
33 \\General Options:
34 \\
35 \\ -h, --help Print this help and exit
36 \\
37;
38
39var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
40
41pub fn main() anyerror!void {
42 // Stolen from the Zig compiler
43 var gpa_need_deinit = false;
44 const gpa = gpa: {
45 if (!std.builtin.link_libc) {
46 gpa_need_deinit = true;
47 break :gpa &general_purpose_allocator.allocator;
48 }
49 // We would prefer to use raw libc allocator here, but cannot
50 // use it if it won't support the alignment we need.
51 if (@alignOf(std.c.max_align_t) < @alignOf(i128)) {
52 break :gpa std.heap.c_allocator;
53 }
54 break :gpa std.heap.raw_c_allocator;
55 };
56 defer if (gpa_need_deinit) {
57 std.debug.assert(!general_purpose_allocator.deinit());
58 };
59 var arena_instance = std.heap.ArenaAllocator.init(gpa);
60 defer arena_instance.deinit();
61 const arena = &arena_instance.allocator;
62
63 const args = try process.argsAlloc(arena);
64 return mainArgs(gpa, arena, args);
65}
66
67pub fn mainArgs(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
68 if (args.len <= 1) {
69 info("{s}", .{usage});
70 fatal("expected command argument", .{});
71 }
72
73 const cmd = args[1];
74 const cmd_args = args[2..];
75 if (mem.eql(u8, cmd, "gen")) {
76 return cmdGen(gpa, arena, cmd_args);
77 } else if (mem.eql(u8, cmd, "sign")) {
78 return cmdSign(gpa, arena, cmd_args);
79 } else if (mem.eql(u8, cmd, "verify")) {
80 return cmdVerify(gpa, arena, cmd_args);
81 } else if (mem.eql(u8, cmd, "version")) {
82 return io.getStdOut().writeAll(build_options.version ++ "\n");
83 } else if (mem.eql(u8, cmd, "help") or mem.eql(u8, cmd, "-h") or mem.eql(u8, cmd, "--help")) {
84 return io.getStdOut().writeAll(usage);
85 } else {
86 info("{s}", .{usage});
87 fatal("unknown command: {s}", .{cmd});
88 }
89}
90
91const usage_gen =
92 \\Usage: znk gen [options] <type>
93 \\
94 \\Supported Types:
95 \\
96 \\ account
97 \\ cluster
98 \\ operator
99 \\ server
100 \\ user
101 \\
102 \\General Options:
103 \\
104 \\ -h, --help Print this help and exit
105 \\
106 \\Generate Options:
107 \\
108 \\ -o, --pub-out Print the public key to stdout
109 \\ -p, --prefix Vanity public key prefix, turns -o on
110 \\
111;
112
113pub fn cmdGen(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
114 const stdout = io.getStdOut();
115
116 var ty: ?nkeys.PublicPrefixByte = null;
117 var pub_out: bool = false;
118 var prefix: ?[]const u8 = null;
119
120 var i: usize = 0;
121 while (i < args.len) : (i += 1) {
122 const arg = args[i];
123 if (mem.startsWith(u8, arg, "-")) {
124 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
125 return stdout.writeAll(usage_gen);
126 } else if (mem.eql(u8, arg, "-o") or mem.eql(u8, arg, "--pub-out")) {
127 pub_out = true;
128 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
129 if (i + 1 >= args.len)
130 fatal("expected argument after '{s}'", .{arg});
131 i += 1;
132 if (args[i].len > nkeys.text_public_len - 1)
133 fatal("public key prefix '{s}' is too long", .{arg});
134 prefix = args[i];
135 } else {
136 fatal("unrecognized parameter: '{s}'", .{arg});
137 }
138 } else if (ty != null) {
139 fatal("more than one type to generate provided", .{});
140 } else if (mem.eql(u8, arg, "account")) {
141 ty = .account;
142 } else if (mem.eql(u8, arg, "cluster")) {
143 ty = .cluster;
144 } else if (mem.eql(u8, arg, "operator")) {
145 ty = .operator;
146 } else if (mem.eql(u8, arg, "server")) {
147 ty = .server;
148 } else if (mem.eql(u8, arg, "user")) {
149 ty = .user;
150 } else {
151 fatal("unrecognized extra parameter: '{s}'", .{arg});
152 }
153 }
154
155 if (ty == null) {
156 info("{s}", .{usage_gen});
157 fatal("no type to generate seed for provided", .{});
158 }
159
160 if (prefix != null) {
161 const capitalized_prefix = try toUpper(arena, prefix.?);
162
163 try PrefixKeyGenerator.init(arena, ty.?, capitalized_prefix).generate();
164 } else {
165 var kp = nkeys.SeedKeyPair.init(ty.?) catch |e| fatal("could not generate key pair: {e}", .{e});
166 defer kp.wipe();
167 try stdout.writeAll(&kp.seed);
168 try stdout.writeAll("\n");
169
170 var public_key = kp.publicKey() catch |e| fatal("could not generate public key: {e}", .{e});
171 if (pub_out) {
172 try stdout.writeAll(&public_key);
173 try stdout.writeAll("\n");
174 }
175 }
176}
177
178const usage_sign =
179 \\Usage: znk sign -k <file> [options] <file>
180 \\
181 \\General Options:
182 \\
183 \\ -h, --help Print this help and exit
184 \\
185 \\Sign Options:
186 \\
187 \\ -k, --key Path of private key/seed to sign with
188 \\
189;
190
191pub fn cmdSign(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
192 // TODO(rutgerbrf): support setting a custom entropy file?
193 const stdin = io.getStdIn();
194 const stdout = io.getStdOut();
195
196 var file_stdin = false;
197 var key_stdin = false;
198 var file: ?fs.File = null;
199 var key: ?fs.File = null;
200 defer if (!key_stdin) if (file) |f| f.close();
201 defer if (!file_stdin) if (key) |f| f.close();
202
203 var i: usize = 0;
204 while (i < args.len) : (i += 1) {
205 const arg = args[i];
206 if (mem.startsWith(u8, arg, "-") and arg.len > 1) {
207 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
208 return stdout.writeAll(usage_sign);
209 } else if (mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "--key")) {
210 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
211 i += 1;
212 if (key != null) fatal("parameter '{s}' provided more than once", .{arg});
213 if (std.mem.eql(u8, args[i], "-")) {
214 key = stdin;
215 key_stdin = true;
216 } else {
217 key = try fs.cwd().openFile(args[i], .{});
218 }
219 } else {
220 fatal("unrecognized parameter: '{s}'", .{arg});
221 }
222 } else if (file != null) {
223 fatal("more than one file to generate a signature for provided", .{});
224 } else if (mem.eql(u8, args[i], "-")) {
225 file = stdin;
226 file_stdin = true;
227 } else {
228 file = try fs.cwd().openFile(args[i], .{});
229 }
230 }
231
232 if (file == null) {
233 info("{s}", .{usage_sign});
234 fatal("no file to generate a signature for provided", .{});
235 }
236
237 if (key == null) {
238 info("{s}", .{usage_sign});
239 fatal("no key to sign with provided", .{});
240 }
241
242 if (file_stdin and key_stdin) {
243 fatal("can't use stdin for reading multiple files", .{});
244 }
245
246 const content = file.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
247 fatal("could not read file to generate signature for", .{});
248 };
249 var kp = switch (readKeyFile(arena, key.?)) {
250 .seed_key_pair => |kp| kp,
251 else => |*k| {
252 k.wipe();
253 fatal("key was provided but is not a seed", .{});
254 },
255 };
256 defer kp.wipe();
257
258 const sig = kp.sign(content) catch fatal("could not generate signature", .{});
259 var encoded_sig = try arena.alloc(u8, std.base64.standard.Encoder.calcSize(sig.len));
260 _ = std.base64.standard.Encoder.encode(encoded_sig, &sig);
261 try stdout.writeAll(encoded_sig);
262 try stdout.writeAll("\n");
263}
264
265const usage_verify =
266 \\Usage: znk verify [options] <file>
267 \\
268 \\General Options:
269 \\
270 \\ -h, --help Print this help and exit
271 \\
272 \\Verify Options:
273 \\
274 \\ -k, --key Path of key to verify with
275 \\ -s, --sig Path of signature to verify
276 \\
277;
278
279pub fn cmdVerify(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
280 const stdin = io.getStdIn();
281 const stdout = io.getStdOut();
282
283 var file_stdin = false;
284 var key_stdin = false;
285 var sig_stdin = false;
286 var key: ?fs.File = null;
287 var file: ?fs.File = null;
288 var sig: ?fs.File = null;
289 defer if (!file_stdin) if (file) |f| f.close();
290 defer if (!key_stdin) if (key) |f| f.close();
291 defer if (!sig_stdin) if (sig) |f| f.close();
292
293 var i: usize = 0;
294 while (i < args.len) : (i += 1) {
295 const arg = args[i];
296 if (mem.startsWith(u8, arg, "-") and arg.len > 1) {
297 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
298 return stdout.writeAll(usage_verify);
299 } else if (mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "--key")) {
300 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
301 i += 1;
302 if (key != null) fatal("parameter '{s}' provided more than once", .{arg});
303 if (std.mem.eql(u8, args[i], "-")) {
304 key = stdin;
305 key_stdin = true;
306 } else {
307 key = try fs.cwd().openFile(args[i], .{});
308 }
309 } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--sig")) {
310 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
311 i += 1;
312 if (sig != null) fatal("parameter '{s}' provided more than once", .{arg});
313 if (std.mem.eql(u8, args[i], "-")) {
314 sig = stdin;
315 sig_stdin = true;
316 } else {
317 sig = try fs.cwd().openFile(args[i], .{});
318 }
319 } else {
320 fatal("unrecognized parameter: '{s}'", .{arg});
321 }
322 } else if (file != null) {
323 fatal("more than one file to verify signature of provided", .{});
324 } else if (mem.eql(u8, args[i], "-")) {
325 file = stdin;
326 file_stdin = true;
327 } else {
328 file = try fs.cwd().openFile(args[i], .{});
329 }
330 }
331
332 if (file == null) {
333 info("{s}", .{usage_verify});
334 fatal("no file to verify signature of provided", .{});
335 }
336
337 if (key == null) {
338 info("{s}", .{usage_verify});
339 fatal("no key to verify signature with provided", .{});
340 }
341
342 if (sig == null) {
343 info("{s}", .{usage_verify});
344 fatal("no file to generate a signature for provided", .{});
345 }
346
347 if (two(&.{ file_stdin, key_stdin, sig_stdin })) {
348 fatal("can't use stdin for reading multiple files", .{});
349 }
350
351 const content = file.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
352 fatal("could not read file to generate signature for", .{});
353 };
354 const signature_b64 = sig.?.readToEndAlloc(arena, std.math.maxInt(usize)) catch {
355 fatal("could not read signature", .{});
356 };
357 var k = readKeyFile(arena, key.?);
358 defer k.wipe();
359
360 const trimmed_signature_b64 = mem.trim(u8, signature_b64, " \n\t\r");
361 const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(trimmed_signature_b64) catch {
362 fatal("invalid signature encoding", .{});
363 };
364 if (decoded_len != std.crypto.sign.Ed25519.signature_length)
365 fatal("invalid signature length", .{});
366 const signature = try arena.alloc(u8, decoded_len);
367
368 _ = std.base64.standard.Decoder.decode(signature, trimmed_signature_b64) catch {
369 fatal("invalid signature encoding", .{});
370 };
371 k.verify(content, signature[0..std.crypto.sign.Ed25519.signature_length].*) catch {
372 fatal("bad signature", .{});
373 };
374
375 try stdout.writeAll("good signature\n");
376}
377
378const PrefixKeyGenerator = struct {
379 ty: nkeys.PublicPrefixByte,
380 prefix: []const u8,
381 allocator: *Allocator,
382 done: std.atomic.Bool,
383
384 const Self = @This();
385
386 pub fn init(allocator: *Allocator, ty: nkeys.PublicPrefixByte, prefix: []const u8) Self {
387 return .{
388 .ty = ty,
389 .prefix = prefix,
390 .allocator = allocator,
391 .done = std.atomic.Bool.init(false),
392 };
393 }
394
395 fn generatePrivate(self: *Self) void {
396 while (true) {
397 if (self.done.load(.SeqCst)) return;
398
399 var kp = nkeys.SeedKeyPair.init(self.ty) catch |e| fatal("could not generate key pair: {e}", .{e});
400 defer kp.wipe();
401 var public_key = kp.publicKey() catch |e| fatal("could not generate public key: {e}", .{e});
402 if (!mem.startsWith(u8, public_key[1..], self.prefix)) continue;
403
404 if (self.done.xchg(true, .SeqCst)) return; // another thread is already done
405
406 info("{s}", .{kp.seed});
407 info("{s}", .{public_key});
408
409 return;
410 }
411 }
412
413 pub usingnamespace if (builtin.single_threaded) struct {
414 pub fn generate(self: *Self) !void {
415 return self.generatePrivate();
416 }
417 } else struct {
418 pub fn generate(self: *Self) !void {
419 var cpu_count = try std.Thread.cpuCount();
420 var threads = try self.allocator.alloc(*std.Thread, cpu_count);
421 defer self.allocator.free(threads);
422 for (threads) |*thread| thread.* = try std.Thread.spawn(Self.generatePrivate, self);
423 for (threads) |thread| thread.wait();
424 }
425 };
426};
427
428pub fn readKeyFile(allocator: *Allocator, file: fs.File) nkeys.Key {
429 var bytes = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch fatal("could not read key file", .{});
430
431 var iterator = mem.split(bytes, "\n");
432 while (iterator.next()) |line| {
433 if (nkeys.isValidEncoding(line) and line.len == nkeys.text_seed_len) {
434 var k = nkeys.fromText(line) catch continue;
435 defer k.wipe();
436 allocator.free(bytes);
437 return k;
438 }
439 }
440
441 fatal("could not find a valid key", .{});
442}
443
444fn two(slice: []const bool) bool {
445 var one = false;
446 for (slice) |x| if (x and one) {
447 return true;
448 } else {
449 one = true;
450 };
451 return false;
452}
453
454fn toUpper(allocator: *Allocator, slice: []const u8) ![]u8 {
455 const result = try allocator.alloc(u8, slice.len);
456 for (slice) |c, i| result[i] = ascii.toUpper(c);
457 return result;
458}
459
460test {
461 testing.refAllDecls(@This());
462}