alpha
Login
or
Join now
zzstoatzz.io
/
webauthn
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
implement webauthn ceremonies
author
zzstoatzz
date
2 months ago
(May 27, 2026, 1:56 AM -0500)
commit
3815415a
3815415a0aa4fc280411ef0b9a795a35c02d3b05
parent
252f7f33
252f7f33ba63bfb21911edecb0be4773f6432883
+185
3 changed files
Expand all
Collapse all
Unified
Split
src
assertion.zig
registration.zig
types.zig
+44
src/assertion.zig
View file
Reviewed
···
1
1
+
const std = @import("std");
2
2
+
const authenticator_data = @import("authenticator_data.zig");
3
3
+
const base64url = @import("base64url.zig");
4
4
+
const client_data = @import("client_data.zig");
5
5
+
const crypto = @import("crypto.zig");
1
6
const types = @import("types.zig");
2
7
3
8
pub const VerifyOptions = struct {
···
13
18
sign_count: u32,
14
19
recommended_sign_count: u32,
15
20
};
21
21
+
22
22
+
pub const Response = struct {
23
23
+
id: []const u8,
24
24
+
raw_id: []const u8,
25
25
+
client_data_json: []const u8,
26
26
+
authenticator_data: []const u8,
27
27
+
signature: []const u8,
28
28
+
};
29
29
+
30
30
+
pub fn verify(allocator: std.mem.Allocator, response: Response, options: VerifyOptions) !VerifiedAssertion {
31
31
+
var cd = try client_data.parseBase64Url(allocator, response.client_data_json);
32
32
+
defer cd.deinit(allocator);
33
33
+
try client_data.verify(cd, .{ .ceremony = .get, .challenge = options.challenge, .origin = options.origin });
34
34
+
35
35
+
const auth_bytes = try base64url.decodeAlloc(allocator, response.authenticator_data);
36
36
+
defer allocator.free(auth_bytes);
37
37
+
const parsed_auth = try authenticator_data.parse(auth_bytes);
38
38
+
try authenticator_data.verifyRpIdHash(parsed_auth, options.rp_id);
39
39
+
try authenticator_data.requireUserPresence(parsed_auth);
40
40
+
if (options.user_verification == .required) try authenticator_data.requireUserVerification(parsed_auth);
41
41
+
42
42
+
const sig = try base64url.decodeAlloc(allocator, response.signature);
43
43
+
defer allocator.free(sig);
44
44
+
var client_hash: [32]u8 = undefined;
45
45
+
std.crypto.hash.sha2.Sha256.hash(cd.raw_json, &client_hash, .{});
46
46
+
const message = try allocator.alloc(u8, auth_bytes.len + client_hash.len);
47
47
+
defer allocator.free(message);
48
48
+
@memcpy(message[0..auth_bytes.len], auth_bytes);
49
49
+
@memcpy(message[auth_bytes.len..], &client_hash);
50
50
+
try crypto.verifyWebAuthnEs256(options.credential_public_key, sig, message);
51
51
+
52
52
+
if (options.known_sign_count != 0 and parsed_auth.sign_count != 0 and parsed_auth.sign_count <= options.known_sign_count) {
53
53
+
return types.Error.SignCountMismatch;
54
54
+
}
55
55
+
return .{
56
56
+
.sign_count = parsed_auth.sign_count,
57
57
+
.recommended_sign_count = @max(options.known_sign_count, parsed_auth.sign_count),
58
58
+
};
59
59
+
}
+138
src/registration.zig
View file
Reviewed
···
1
1
+
const std = @import("std");
2
2
+
const authenticator_data = @import("authenticator_data.zig");
3
3
+
const base64url = @import("base64url.zig");
4
4
+
const client_data = @import("client_data.zig");
5
5
+
const cose = @import("cose.zig");
1
6
const types = @import("types.zig");
2
7
3
8
pub const VerifyOptions = struct {
···
12
17
credential_public_key: []const u8,
13
18
sign_count: u32,
14
19
};
20
20
+
21
21
+
pub const Response = struct {
22
22
+
id: []const u8,
23
23
+
raw_id: []const u8,
24
24
+
client_data_json: []const u8,
25
25
+
attestation_object: []const u8,
26
26
+
};
27
27
+
28
28
+
const AttestationObject = struct {
29
29
+
fmt: []const u8,
30
30
+
auth_data: []const u8,
31
31
+
};
32
32
+
33
33
+
const CborParser = struct {
34
34
+
input: []const u8,
35
35
+
index: usize = 0,
36
36
+
37
37
+
fn read(self: *CborParser) types.Error!u8 {
38
38
+
if (self.index >= self.input.len) return types.Error.InvalidAttestationObject;
39
39
+
const byte = self.input[self.index];
40
40
+
self.index += 1;
41
41
+
return byte;
42
42
+
}
43
43
+
44
44
+
fn readSlice(self: *CborParser, len: usize) types.Error![]const u8 {
45
45
+
if (len > self.input.len - self.index) return types.Error.InvalidAttestationObject;
46
46
+
const out = self.input[self.index .. self.index + len];
47
47
+
self.index += len;
48
48
+
return out;
49
49
+
}
50
50
+
51
51
+
fn readLen(self: *CborParser, additional: u5) types.Error!usize {
52
52
+
return switch (additional) {
53
53
+
0...23 => additional,
54
54
+
24 => try self.read(),
55
55
+
25 => std.mem.readInt(u16, (try self.readSlice(2))[0..2], .big),
56
56
+
26 => std.mem.readInt(u32, (try self.readSlice(4))[0..4], .big),
57
57
+
27 => std.math.cast(usize, std.mem.readInt(u64, (try self.readSlice(8))[0..8], .big)) orelse types.Error.InvalidAttestationObject,
58
58
+
else => types.Error.InvalidAttestationObject,
59
59
+
};
60
60
+
}
61
61
+
62
62
+
fn readText(self: *CborParser, head: u8) types.Error![]const u8 {
63
63
+
if (head >> 5 != 3) return types.Error.InvalidAttestationObject;
64
64
+
return self.readSlice(try self.readLen(@intCast(head & 0x1f)));
65
65
+
}
66
66
+
67
67
+
fn readBytes(self: *CborParser, head: u8) types.Error![]const u8 {
68
68
+
if (head >> 5 != 2) return types.Error.InvalidAttestationObject;
69
69
+
return self.readSlice(try self.readLen(@intCast(head & 0x1f)));
70
70
+
}
71
71
+
72
72
+
fn skip(self: *CborParser, head: u8) types.Error!void {
73
73
+
const major = head >> 5;
74
74
+
const additional: u5 = @intCast(head & 0x1f);
75
75
+
switch (major) {
76
76
+
0, 1 => _ = try self.readLen(additional),
77
77
+
2, 3 => _ = try self.readSlice(try self.readLen(additional)),
78
78
+
4 => for (0..try self.readLen(additional)) |_| try self.skip(try self.read()),
79
79
+
5 => for (0..try self.readLen(additional)) |_| {
80
80
+
try self.skip(try self.read());
81
81
+
try self.skip(try self.read());
82
82
+
},
83
83
+
6 => {
84
84
+
_ = try self.readLen(additional);
85
85
+
try self.skip(try self.read());
86
86
+
},
87
87
+
7 => switch (additional) {
88
88
+
20...23 => {},
89
89
+
24 => _ = try self.read(),
90
90
+
25 => _ = try self.readSlice(2),
91
91
+
26 => _ = try self.readSlice(4),
92
92
+
27 => _ = try self.readSlice(8),
93
93
+
else => return types.Error.InvalidAttestationObject,
94
94
+
},
95
95
+
else => return types.Error.InvalidAttestationObject,
96
96
+
}
97
97
+
}
98
98
+
};
99
99
+
100
100
+
fn parseAttestationObject(bytes: []const u8) types.Error!AttestationObject {
101
101
+
var parser: CborParser = .{ .input = bytes };
102
102
+
const head = try parser.read();
103
103
+
if (head >> 5 != 5) return types.Error.InvalidAttestationObject;
104
104
+
const len = try parser.readLen(@intCast(head & 0x1f));
105
105
+
106
106
+
var fmt: ?[]const u8 = null;
107
107
+
var auth_data: ?[]const u8 = null;
108
108
+
for (0..len) |_| {
109
109
+
const key = try parser.readText(try parser.read());
110
110
+
const value_head = try parser.read();
111
111
+
if (std.mem.eql(u8, key, "fmt")) {
112
112
+
fmt = try parser.readText(value_head);
113
113
+
} else if (std.mem.eql(u8, key, "authData")) {
114
114
+
auth_data = try parser.readBytes(value_head);
115
115
+
} else {
116
116
+
try parser.skip(value_head);
117
117
+
}
118
118
+
}
119
119
+
if (parser.index != bytes.len) return types.Error.InvalidAttestationObject;
120
120
+
return .{
121
121
+
.fmt = fmt orelse return types.Error.InvalidAttestationObject,
122
122
+
.auth_data = auth_data orelse return types.Error.InvalidAttestationObject,
123
123
+
};
124
124
+
}
125
125
+
126
126
+
pub fn verify(allocator: std.mem.Allocator, response: Response, options: VerifyOptions) !VerifiedCredential {
127
127
+
var cd = try client_data.parseBase64Url(allocator, response.client_data_json);
128
128
+
defer cd.deinit(allocator);
129
129
+
try client_data.verify(cd, .{ .ceremony = .create, .challenge = options.challenge, .origin = options.origin });
130
130
+
131
131
+
const attestation_bytes = try base64url.decodeAlloc(allocator, response.attestation_object);
132
132
+
defer allocator.free(attestation_bytes);
133
133
+
const attestation = try parseAttestationObject(attestation_bytes);
134
134
+
if (!std.mem.eql(u8, attestation.fmt, "none")) return types.Error.UnsupportedAttestationFormat;
135
135
+
136
136
+
const parsed_auth = try authenticator_data.parse(attestation.auth_data);
137
137
+
try authenticator_data.verifyRpIdHash(parsed_auth, options.rp_id);
138
138
+
try authenticator_data.requireUserPresence(parsed_auth);
139
139
+
if (options.user_verification == .required) try authenticator_data.requireUserVerification(parsed_auth);
140
140
+
141
141
+
const attested = parsed_auth.attested_credential_data orelse return types.Error.AttestedCredentialDataMissing;
142
142
+
const raw_id = try base64url.decodeAlloc(allocator, response.raw_id);
143
143
+
defer allocator.free(raw_id);
144
144
+
if (!std.mem.eql(u8, raw_id, attested.credential_id)) return types.Error.CredentialIdMismatch;
145
145
+
_ = try cose.parseEc2PublicKey(attested.credential_public_key);
146
146
+
147
147
+
return .{
148
148
+
.credential_id = try allocator.dupe(u8, attested.credential_id),
149
149
+
.credential_public_key = try allocator.dupe(u8, attested.credential_public_key),
150
150
+
.sign_count = parsed_auth.sign_count,
151
151
+
};
152
152
+
}
+3
src/types.zig
View file
Reviewed
···
34
34
InvalidSignature,
35
35
SignatureVerificationFailed,
36
36
SignCountMismatch,
37
37
+
InvalidAttestationObject,
38
38
+
UnsupportedAttestationFormat,
39
39
+
CredentialIdMismatch,
37
40
};