lib/peer/src/identity.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const text = @import("text.zig");
3
4 const Ed25519 = std.crypto.sign.Ed25519;
5
6 pub const key_bytes: usize = Ed25519.PublicKey.encoded_length;
7 pub const text_bytes: usize = text.encodedLength(key_bytes);
8 pub const PeerIdParseError = error{
9 InvalidEncoding,
10 InvalidLength,
11 };
12
13 pub const PeerId = struct {
14 key: [key_bytes]u8,
15
16 pub const ParseError: type = PeerIdParseError;
17
18 pub fn format(self: PeerId, output: []u8) text.EncodeError![]const u8 {
19 return text.encode(&self.key, output);
20 }
21
22 pub fn parse(source: []const u8) ParseError!PeerId {
23 if (source.len != text_bytes) return error.InvalidLength;
24 var key: [key_bytes]u8 = undefined;
25 const decoded = text.decode(source, &key) catch return error.InvalidEncoding;
26 if (decoded != key.len) return error.InvalidLength;
27 return .{ .key = key };
28 }
29
30 pub fn eql(self: PeerId, other: PeerId) bool {
31 return std.mem.eql(u8, &self.key, &other.key);
32 }
33 };
34
35 pub const Identity = struct {
36 key_pair: Ed25519.KeyPair,
37
38 pub const SeedError: type = error{InvalidSeed};
39
40 pub fn generate(io: std.Io) Identity {
41 return .{ .key_pair = Ed25519.KeyPair.generate(io) };
42 }
43
44 pub fn fromSeed(seed: [Ed25519.KeyPair.seed_length]u8) SeedError!Identity {
45 const pair = Ed25519.KeyPair.generateDeterministic(seed) catch
46 return error.InvalidSeed;
47 return .{ .key_pair = pair };
48 }
49
50 pub fn id(self: Identity) PeerId {
51 return .{ .key = self.key_pair.public_key.toBytes() };
52 }
53 };
54
55 test "Identity generation uses caller I/O" {
56 const identity = Identity.generate(std.testing.io);
57 try std.testing.expectEqualSlices(
58 u8,
59 &identity.key_pair.public_key.toBytes(),
60 &identity.id().key,
61 );
62 }
63
64 test "RFC 8032 7.1 Ed25519 seed yields the documented PeerId text" {
65 var seed: [Ed25519.KeyPair.seed_length]u8 = undefined;
66 _ = try std.fmt.hexToBytes(
67 &seed,
68 "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
69 );
70 const identity = try Identity.fromSeed(seed);
71 var output: [text_bytes]u8 = undefined;
72 const actual = try identity.id().format(&output);
73 try std.testing.expectEqualStrings(
74 "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
75 actual,
76 );
77 }
78
79 test "PeerId parse rejects lengths other than 43 characters" {
80 const canonical = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo";
81 try std.testing.expectError(error.InvalidLength, PeerId.parse(canonical[0..42]));
82 try std.testing.expectError(error.InvalidLength, PeerId.parse(canonical ++ "A"));
83 }
84
85 test "RFC 4648 3.5 PeerId parse rejects noncanonical base64url" {
86 const canonical = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo";
87 var invalid = canonical.*;
88 invalid[0] = '+';
89 try std.testing.expectError(error.InvalidEncoding, PeerId.parse(&invalid));
90 invalid[0] = '/';
91 try std.testing.expectError(error.InvalidEncoding, PeerId.parse(&invalid));
92 invalid = canonical.*;
93 invalid[invalid.len - 1] = '=';
94 try std.testing.expectError(error.InvalidEncoding, PeerId.parse(&invalid));
95 invalid[invalid.len - 1] = 'p';
96 try std.testing.expectError(error.InvalidEncoding, PeerId.parse(&invalid));
97 }