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