aboutsummaryrefslogtreecommitdiffstats
path: root/tool
diff options
context:
space:
mode:
authorLibravatar Rutger Broekhoff2021-05-25 20:32:42 +0200
committerLibravatar Rutger Broekhoff2021-05-25 20:32:42 +0200
commit5fc11d694ad86bdb08fe61a3007aa0c44ba894aa (patch)
tree216c08cdbcfc28e0886c85c4568a36cae2c9aa57 /tool
parent3b6b7b5eefe6270f126048d7a1ff5919f1da18cf (diff)
downloadzig-nkeys-5fc11d694ad86bdb08fe61a3007aa0c44ba894aa.tar.gz
zig-nkeys-5fc11d694ad86bdb08fe61a3007aa0c44ba894aa.zip
Put the znk tool into a different directory
Diffstat (limited to 'tool')
-rw-r--r--tool/znk.zig507
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 @@
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");
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
39pub 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
52pub 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
76const 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
98pub 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
163const 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
176pub 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
245const 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
259pub 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
358const 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
408fn 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
418fn 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
424pub 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
484pub 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
503test {
504 testing.refAllDecls(@This());
505 testing.refAllDecls(Nkey);
506 testing.refAllDecls(PrefixKeyGenerator);
507}