diff --git a/lib/ctap/auth/Authenticator.zig b/lib/ctap/auth/Authenticator.zig index d375cef..97ffdb3 100644 --- a/lib/ctap/auth/Authenticator.zig +++ b/lib/ctap/auth/Authenticator.zig @@ -28,6 +28,18 @@ token: struct { two: ?fido.ctap.pinuv.PinUvAuth = null, }, +credential_list: ?struct { + list: []const fido.ctap.crypto.Id, + credentialCounter: usize = 0, + time_stamp: i64, + authData: fido.common.AuthenticatorData = undefined, + clientDataHash: fido.ctap.crypto.ClientDataHash = undefined, + + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { + allocator.free(self.list); + } +} = null, + allocator: std.mem.Allocator, pub fn init(self: *@This()) !void { @@ -52,6 +64,13 @@ pub fn init(self: *@This()) !void { try self.callbacks.persist(); } +pub fn deinit(self: *@This()) void { + if (self.credential_list != null) { + self.credential_list.?.deinit(self.allocator); + self.credential_list = null; + } +} + pub fn handle(self: *@This(), command: []const u8) Response { // Buffer for the response message var res = std.ArrayList(u8).init(self.allocator); @@ -175,6 +194,21 @@ pub fn handle(self: *@This(), command: []const u8) Response { self.callbacks.reset(); }, + .authenticatorGetNextAssertion => { + // Execute command + const status = fido.ctap.commands.authenticator.authenticatorGetNextAssertion( + self, + response, + ) catch { + res.deinit(); + return Response{ .err = @intFromEnum(StatusCodes.ctap1_err_other) }; + }; + + if (status != .ctap1_err_success) { + res.deinit(); + return Response{ .err = @intFromEnum(status) }; + } + }, .authenticatorSelection => { const status = fido.ctap.commands.authenticator.authenticatorSelection(self); diff --git a/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig b/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig index a4b93df..efaa5a4 100644 --- a/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig +++ b/lib/ctap/commands/authenticator/authenticatorGetAssertion.zig @@ -9,6 +9,13 @@ pub fn authenticatorGetAssertion( gap: *const fido.ctap.request.GetAssertion, out: anytype, ) !fido.ctap.StatusCodes { + // Remove the credential list form the previous getAssertion + // call if one exists. + if (auth.credential_list != null) { + auth.credential_list.?.deinit(auth.allocator); + auth.credential_list = null; + } + // ++++++++++++++++++++++++++++++++++++++++++++++++ // 1. and 2. Verify pinUvAuthParam // ++++++++++++++++++++++++++++++++++++++++++++++++ @@ -296,6 +303,14 @@ pub fn authenticatorGetAssertion( std.log.warn("UserId field missing for id {s}. Returning the user id is mandatory for resident keys so expect errors.", .{std.fmt.fmtSliceHexUpper(cred.raw[0..])}); } } + + if (credentials.items.len >= 1) { + // Copy the remaining credential Ids for later use by authenticatorGetNextAssertion + auth.credential_list = .{ + .list = try auth.allocator.dupe(fido.ctap.crypto.Id, credentials.items), + .time_stamp = auth.callbacks.millis(), + }; + } } else { settings.times.usageCount += 1; } @@ -384,5 +399,11 @@ pub fn authenticatorGetAssertion( try cbor.stringify(gar, .{ .allocator = auth.allocator }, out); + if (auth.credential_list) |*cl| { + // We remember authData and clientDataHash for authenticatorGetNextAssertion + cl.authData = auth_data; + cl.clientDataHash = gap.clientDataHash; + } + return status; } diff --git a/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig b/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig new file mode 100644 index 0000000..3650189 --- /dev/null +++ b/lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const cbor = @import("zbor"); +const cks = @import("cks"); +const fido = @import("../../../main.zig"); +const helper = @import("helper.zig"); + +pub fn authenticatorGetNextAssertion( + auth: *fido.ctap.authenticator.Authenticator, + out: anytype, +) !fido.ctap.StatusCodes { + if (auth.credential_list == null) { + return fido.ctap.StatusCodes.ctap2_err_not_allowed; + } + + if (auth.credential_list.?.credentialCounter >= auth.credential_list.?.list.len or + (auth.callbacks.millis() - auth.credential_list.?.time_stamp) >= 30000) + { + auth.allocator.free(auth.credential_list.?.list); + auth.credential_list = null; + return fido.ctap.StatusCodes.ctap2_err_not_allowed; + } + + // Fetch authenticator settings to get master secret + var settings = if (auth.callbacks.getEntry("Settings")) |settings| settings else { + std.log.err("Unable to fetch Settings", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + + var _ms = if (settings.getField("Secret", auth.callbacks.millis())) |ms| ms else { + std.log.err("Secret field missing in Settings", .{}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + + const ms: fido.ctap.crypto.master_secret.MasterSecret = _ms[0..fido.ctap.crypto.master_secret.MS_LEN].*; + + // Fetch next credential id + const id = auth.credential_list.?.list[auth.credential_list.?.credentialCounter]; + auth.credential_list.?.credentialCounter += 1; + + // Fetch the credential based on credential id and update the return data + var user: ?fido.common.User = null; + if (auth.callbacks.getEntry(id.raw[0..])) |entry| { + // Seems like this is a discoverable credential, because we + // just discovered it :) + auth.credential_list.?.authData.signCount = @as(u32, @intCast(entry.times.usageCount)); + entry.times.usageCount += 1; + + if (auth.credential_list.?.authData.flags.uv == 1) { + // publicKeyCredentialUserEntity MUST NOT be returned if user verification + // was not done by the authenticator in the original authenticatorGetAssertion call + const user_id = entry.getField("UserId", auth.callbacks.millis()); + if (user_id) |uid| { + // User identifiable information (name, DisplayName, icon) + // inside the publicKeyCredentialUserEntity MUST NOT be returned + // if user verification is not done by the authenticator + user = .{ .id = uid, .name = null, .displayName = null }; + } else { + std.log.warn( + "UserId field missing for id: {s}", + .{std.fmt.fmtSliceHexUpper(id.raw[0..])}, + ); + } + } + } else { + std.log.warn( + "Unable to load credential with id: {s}", + .{std.fmt.fmtSliceHexUpper(id.raw[0..])}, + ); + return fido.ctap.StatusCodes.ctap1_err_other; + } + + // select algorithm based on credential + const algorithm = id.getAlg(); + var alg: ?fido.ctap.crypto.SigAlg = null; + for (auth.algorithms) |_alg| blk: { + if (algorithm == _alg.alg) { + alg = _alg; + break :blk; + } + } + + if (alg == null) { + std.log.err("Unknown algorithm for credential with id: {s}", .{std.fmt.fmtSliceHexLower(&id.raw)}); + return fido.ctap.StatusCodes.ctap1_err_other; + } + + const seed = id.deriveSeed(ms); + const key_pair = if (alg.?.create_det( + &seed, + auth.allocator, + )) |kp| kp else return fido.ctap.StatusCodes.ctap1_err_other; + defer { + auth.allocator.free(key_pair.cose_public_key); + auth.allocator.free(key_pair.raw_private_key); + } + + // Sign the data + var authData = std.ArrayList(u8).init(auth.allocator); + defer authData.deinit(); + try auth.credential_list.?.authData.encode(authData.writer()); + + const sig = if (alg.?.sign( + key_pair.raw_private_key, + &.{ authData.items, &auth.credential_list.?.clientDataHash }, + auth.allocator, + )) |signature| signature else { + std.log.err("signature creation failed for credential with id: {s}", .{std.fmt.fmtSliceHexLower(&id.raw)}); + return fido.ctap.StatusCodes.ctap1_err_other; + }; + defer auth.allocator.free(sig); + + const gar = fido.ctap.response.GetAssertion{ + .credential = .{ + .type = .@"public-key", + .id = &id.raw, + }, + .authData = authData.items, + .signature = sig, + .user = user, + }; + + try auth.callbacks.persist(); + + try cbor.stringify(gar, .{ .allocator = auth.allocator }, out); + + auth.credential_list.?.time_stamp = auth.callbacks.millis(); + + return .ctap1_err_success; +} diff --git a/lib/main.zig b/lib/main.zig index 3b74035..e97bff8 100644 --- a/lib/main.zig +++ b/lib/main.zig @@ -174,6 +174,7 @@ pub const ctap = struct { pub const authenticatorGetInfo = @import("ctap/commands/authenticator/get_info.zig").authenticatorGetInfo; pub const authenticatorMakeCredential = @import("ctap/commands/authenticator/authenticatorMakeCredential.zig").authenticatorMakeCredential; pub const authenticatorGetAssertion = @import("ctap/commands/authenticator/authenticatorGetAssertion.zig").authenticatorGetAssertion; + pub const authenticatorGetNextAssertion = @import("ctap/commands/authenticator/authenticatorGetNextAssertion.zig").authenticatorGetNextAssertion; pub const authenticatorClientPin = @import("ctap/commands/authenticator/authenticatorClientPin.zig").authenticatorClientPin; pub const authenticatorSelection = @import("ctap/commands/authenticator/authenticatorSelection.zig").authenticatorSelection; pub const authenticatorCredentialManagement = @import("ctap/commands/authenticator/authenticatorCredentialManagement.zig").authenticatorCredentialManagement; diff --git a/platform-auth/main.zig b/platform-auth/main.zig index 1058981..3be5d1d 100644 --- a/platform-auth/main.zig +++ b/platform-auth/main.zig @@ -200,6 +200,7 @@ pub fn main() !void { } try authenticator.init(); + defer authenticator.deinit(); // -------------------------------------------------------- notify.g_main_loop_run(loop);