This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

implement webauthn ceremonies

+185
+44
src/assertion.zig
··· 1 + const std = @import("std"); 2 + const authenticator_data = @import("authenticator_data.zig"); 3 + const base64url = @import("base64url.zig"); 4 + const client_data = @import("client_data.zig"); 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 + 22 + pub const Response = struct { 23 + id: []const u8, 24 + raw_id: []const u8, 25 + client_data_json: []const u8, 26 + authenticator_data: []const u8, 27 + signature: []const u8, 28 + }; 29 + 30 + pub fn verify(allocator: std.mem.Allocator, response: Response, options: VerifyOptions) !VerifiedAssertion { 31 + var cd = try client_data.parseBase64Url(allocator, response.client_data_json); 32 + defer cd.deinit(allocator); 33 + try client_data.verify(cd, .{ .ceremony = .get, .challenge = options.challenge, .origin = options.origin }); 34 + 35 + const auth_bytes = try base64url.decodeAlloc(allocator, response.authenticator_data); 36 + defer allocator.free(auth_bytes); 37 + const parsed_auth = try authenticator_data.parse(auth_bytes); 38 + try authenticator_data.verifyRpIdHash(parsed_auth, options.rp_id); 39 + try authenticator_data.requireUserPresence(parsed_auth); 40 + if (options.user_verification == .required) try authenticator_data.requireUserVerification(parsed_auth); 41 + 42 + const sig = try base64url.decodeAlloc(allocator, response.signature); 43 + defer allocator.free(sig); 44 + var client_hash: [32]u8 = undefined; 45 + std.crypto.hash.sha2.Sha256.hash(cd.raw_json, &client_hash, .{}); 46 + const message = try allocator.alloc(u8, auth_bytes.len + client_hash.len); 47 + defer allocator.free(message); 48 + @memcpy(message[0..auth_bytes.len], auth_bytes); 49 + @memcpy(message[auth_bytes.len..], &client_hash); 50 + try crypto.verifyWebAuthnEs256(options.credential_public_key, sig, message); 51 + 52 + if (options.known_sign_count != 0 and parsed_auth.sign_count != 0 and parsed_auth.sign_count <= options.known_sign_count) { 53 + return types.Error.SignCountMismatch; 54 + } 55 + return .{ 56 + .sign_count = parsed_auth.sign_count, 57 + .recommended_sign_count = @max(options.known_sign_count, parsed_auth.sign_count), 58 + }; 59 + }
+138
src/registration.zig
··· 1 + const std = @import("std"); 2 + const authenticator_data = @import("authenticator_data.zig"); 3 + const base64url = @import("base64url.zig"); 4 + const client_data = @import("client_data.zig"); 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 + 21 + pub const Response = struct { 22 + id: []const u8, 23 + raw_id: []const u8, 24 + client_data_json: []const u8, 25 + attestation_object: []const u8, 26 + }; 27 + 28 + const AttestationObject = struct { 29 + fmt: []const u8, 30 + auth_data: []const u8, 31 + }; 32 + 33 + const CborParser = struct { 34 + input: []const u8, 35 + index: usize = 0, 36 + 37 + fn read(self: *CborParser) types.Error!u8 { 38 + if (self.index >= self.input.len) return types.Error.InvalidAttestationObject; 39 + const byte = self.input[self.index]; 40 + self.index += 1; 41 + return byte; 42 + } 43 + 44 + fn readSlice(self: *CborParser, len: usize) types.Error![]const u8 { 45 + if (len > self.input.len - self.index) return types.Error.InvalidAttestationObject; 46 + const out = self.input[self.index .. self.index + len]; 47 + self.index += len; 48 + return out; 49 + } 50 + 51 + fn readLen(self: *CborParser, additional: u5) types.Error!usize { 52 + return switch (additional) { 53 + 0...23 => additional, 54 + 24 => try self.read(), 55 + 25 => std.mem.readInt(u16, (try self.readSlice(2))[0..2], .big), 56 + 26 => std.mem.readInt(u32, (try self.readSlice(4))[0..4], .big), 57 + 27 => std.math.cast(usize, std.mem.readInt(u64, (try self.readSlice(8))[0..8], .big)) orelse types.Error.InvalidAttestationObject, 58 + else => types.Error.InvalidAttestationObject, 59 + }; 60 + } 61 + 62 + fn readText(self: *CborParser, head: u8) types.Error![]const u8 { 63 + if (head >> 5 != 3) return types.Error.InvalidAttestationObject; 64 + return self.readSlice(try self.readLen(@intCast(head & 0x1f))); 65 + } 66 + 67 + fn readBytes(self: *CborParser, head: u8) types.Error![]const u8 { 68 + if (head >> 5 != 2) return types.Error.InvalidAttestationObject; 69 + return self.readSlice(try self.readLen(@intCast(head & 0x1f))); 70 + } 71 + 72 + fn skip(self: *CborParser, head: u8) types.Error!void { 73 + const major = head >> 5; 74 + const additional: u5 = @intCast(head & 0x1f); 75 + switch (major) { 76 + 0, 1 => _ = try self.readLen(additional), 77 + 2, 3 => _ = try self.readSlice(try self.readLen(additional)), 78 + 4 => for (0..try self.readLen(additional)) |_| try self.skip(try self.read()), 79 + 5 => for (0..try self.readLen(additional)) |_| { 80 + try self.skip(try self.read()); 81 + try self.skip(try self.read()); 82 + }, 83 + 6 => { 84 + _ = try self.readLen(additional); 85 + try self.skip(try self.read()); 86 + }, 87 + 7 => switch (additional) { 88 + 20...23 => {}, 89 + 24 => _ = try self.read(), 90 + 25 => _ = try self.readSlice(2), 91 + 26 => _ = try self.readSlice(4), 92 + 27 => _ = try self.readSlice(8), 93 + else => return types.Error.InvalidAttestationObject, 94 + }, 95 + else => return types.Error.InvalidAttestationObject, 96 + } 97 + } 98 + }; 99 + 100 + fn parseAttestationObject(bytes: []const u8) types.Error!AttestationObject { 101 + var parser: CborParser = .{ .input = bytes }; 102 + const head = try parser.read(); 103 + if (head >> 5 != 5) return types.Error.InvalidAttestationObject; 104 + const len = try parser.readLen(@intCast(head & 0x1f)); 105 + 106 + var fmt: ?[]const u8 = null; 107 + var auth_data: ?[]const u8 = null; 108 + for (0..len) |_| { 109 + const key = try parser.readText(try parser.read()); 110 + const value_head = try parser.read(); 111 + if (std.mem.eql(u8, key, "fmt")) { 112 + fmt = try parser.readText(value_head); 113 + } else if (std.mem.eql(u8, key, "authData")) { 114 + auth_data = try parser.readBytes(value_head); 115 + } else { 116 + try parser.skip(value_head); 117 + } 118 + } 119 + if (parser.index != bytes.len) return types.Error.InvalidAttestationObject; 120 + return .{ 121 + .fmt = fmt orelse return types.Error.InvalidAttestationObject, 122 + .auth_data = auth_data orelse return types.Error.InvalidAttestationObject, 123 + }; 124 + } 125 + 126 + pub fn verify(allocator: std.mem.Allocator, response: Response, options: VerifyOptions) !VerifiedCredential { 127 + var cd = try client_data.parseBase64Url(allocator, response.client_data_json); 128 + defer cd.deinit(allocator); 129 + try client_data.verify(cd, .{ .ceremony = .create, .challenge = options.challenge, .origin = options.origin }); 130 + 131 + const attestation_bytes = try base64url.decodeAlloc(allocator, response.attestation_object); 132 + defer allocator.free(attestation_bytes); 133 + const attestation = try parseAttestationObject(attestation_bytes); 134 + if (!std.mem.eql(u8, attestation.fmt, "none")) return types.Error.UnsupportedAttestationFormat; 135 + 136 + const parsed_auth = try authenticator_data.parse(attestation.auth_data); 137 + try authenticator_data.verifyRpIdHash(parsed_auth, options.rp_id); 138 + try authenticator_data.requireUserPresence(parsed_auth); 139 + if (options.user_verification == .required) try authenticator_data.requireUserVerification(parsed_auth); 140 + 141 + const attested = parsed_auth.attested_credential_data orelse return types.Error.AttestedCredentialDataMissing; 142 + const raw_id = try base64url.decodeAlloc(allocator, response.raw_id); 143 + defer allocator.free(raw_id); 144 + if (!std.mem.eql(u8, raw_id, attested.credential_id)) return types.Error.CredentialIdMismatch; 145 + _ = try cose.parseEc2PublicKey(attested.credential_public_key); 146 + 147 + return .{ 148 + .credential_id = try allocator.dupe(u8, attested.credential_id), 149 + .credential_public_key = try allocator.dupe(u8, attested.credential_public_key), 150 + .sign_count = parsed_auth.sign_count, 151 + }; 152 + }
+3
src/types.zig
··· 34 34 InvalidSignature, 35 35 SignatureVerificationFailed, 36 36 SignCountMismatch, 37 + InvalidAttestationObject, 38 + UnsupportedAttestationFormat, 39 + CredentialIdMismatch, 37 40 };