From 033685e91faad8f5913a9edb1b2998470c7433e2 Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Wed, 5 Nov 2025 16:41:53 +0100 Subject: [PATCH 1/2] feat: Add C bindings support and Rust bindings with keylib-sys --- bindings/c/include/keylib.h | 264 ++++++- bindings/c/src/credential_management.zig | 645 +++++++++++++++++ bindings/c/src/keylib.zig | 854 +++++++++++++++++++---- build.zig | 61 +- example/client.zig | 4 +- 5 files changed, 1680 insertions(+), 148 deletions(-) create mode 100644 bindings/c/src/credential_management.zig diff --git a/bindings/c/include/keylib.h b/bindings/c/include/keylib.h index c3a1fd0..67e80c9 100644 --- a/bindings/c/include/keylib.h +++ b/bindings/c/include/keylib.h @@ -44,22 +44,32 @@ typedef enum{ Transports_ble = 4, } Transports; +typedef struct { + uint8_t id[64]; + uint8_t id_len; + uint8_t rp_id[128]; + uint8_t rp_id_len; + uint8_t rp_name[64]; + uint8_t rp_name_len; + uint8_t user_id[64]; + uint8_t user_id_len; + uint32_t sign_count; + int32_t alg; + uint8_t private_key[32]; + int64_t created; + uint8_t discoverable; + uint8_t cred_protect; +} FfiCredential; + typedef struct{ - // User presence request; user and rp might be NULL! UpResult (*up)(const char* info, const char* user, const char* rp); - // User verification request; user and rp might be NULL! UvResult (*uv)(const char* info, const char* user, const char* rp); - // Callback for selecting a user account. - // The platform is expected to return the index of the selected user or an error. int (*select)(const char* rpId, char** users); - // Read the payload specified by id and rp into out. - // The allocated memory is owned by the caller and he is responsible for freeing it. - // Returns either the length of the string assigned to out or an error. int (*read)(const char* id, const char* rp, char*** out); - // Persist the given data; the id is considered unique. - int (*write)(const char* id, const char* rp, const char* data); - // Delete the entry with the given id. + int (*write)(const FfiCredential* credential); int (*del)(const char* id); + int (*read_first)(const char* id, const char* rp, const char* hash, FfiCredential* out); + int (*read_next)(FfiCredential* out); } Callbacks; typedef struct{ @@ -69,11 +79,243 @@ typedef struct{ void* auth_init(Callbacks); void auth_deinit(void*); -void auth_handle(void*, void*); +// Process CTAP request and write response to buffer +// Returns the length of the response written to response_buffer +size_t auth_handle(void* auth, const uint8_t* request_data, size_t request_len, + uint8_t* response_buffer, size_t response_buffer_size); +// CTAPHID protocol handler functions void* ctaphid_init(); void ctaphid_deinit(void*); void* ctaphid_handle(void*, const char*, size_t); void* ctaphid_iterator(void*); int ctaphid_iterator_next(void*, char*); void ctaphid_iterator_deinit(void*); + +int ctaphid_response_get_cmd(void* response); +size_t ctaphid_response_get_data(void* response, char* out, size_t max_len); +int ctaphid_response_set_data(void* response, const char* data, size_t len); + +int uhid_open(); +int uhid_read_packet(int, char*); +int uhid_write_packet(int, char*, size_t); +void uhid_close(int); + +// Client-side APIs for device enumeration and communication + +// Transport types +typedef enum { + TransportType_USB = 0, + TransportType_NFC = 1, + TransportType_BLE = 2, +} TransportType; + +// Transport operations +typedef struct { + void* handle; + TransportType type; + char* description; +} Transport; + +// Transport enumeration +typedef struct { + Transport** transports; + size_t count; +} TransportList; + +TransportList* transport_enumerate(); +void transport_list_free(TransportList*); + +// Transport operations +int transport_open(Transport* transport); +void transport_close(Transport* transport); +int transport_write(Transport* transport, const char* data, size_t len); +int transport_read(Transport* transport, char* buffer, size_t max_len, int timeout_ms); +TransportType transport_get_type(Transport* transport); +const char* transport_get_description(Transport* transport); +void transport_free(Transport* transport); + +// CBOR command operations +typedef struct { + void* internal; +} CborCommand; + +typedef enum { + CborCommandStatus_Pending = 0, + CborCommandStatus_Fulfilled = 1, + CborCommandStatus_Rejected = 2, +} CborCommandStatus; + +typedef struct { + CborCommandStatus status; + union { + char* data; // for fulfilled + int error_code; // for rejected + } result; + size_t data_len; +} CborCommandResult; + +// AuthenticatorGetInfo +CborCommand* cbor_authenticator_get_info(Transport* transport); + +// Credential operations +typedef struct { + const char* challenge; + size_t challenge_len; + const char* rp_id; + const char* rp_name; + const char* user_id; + size_t user_id_len; + const char* user_name; + const char* user_display_name; + uint32_t timeout_ms; + int require_resident_key; + int require_user_verification; + const char* attestation_preference; // "none", "direct", "enterprise", "indirect" + const char* exclude_credentials_json; // JSON array of credential descriptors + const char* extensions_json; // JSON object of extensions +} CredentialCreationOptions; + +typedef struct { + const char* rp_id; + const char* challenge; + size_t challenge_len; + uint32_t timeout_ms; + const char* user_verification; // "discouraged", "preferred", "required" + const char* allow_credentials_json; // JSON array of credential descriptors +} CredentialAssertionOptions; + +CborCommand* cbor_credentials_create(Transport* transport, CredentialCreationOptions* options); +CborCommand* cbor_credentials_get(Transport* transport, CredentialAssertionOptions* options); + +CborCommandResult* cbor_command_get_result(CborCommand* cmd, int timeout_ms); +void cbor_command_free(CborCommand* cmd); +void cbor_command_result_free(CborCommandResult* result); + +// Credential Management operations +typedef enum { + CredentialManagementError_SUCCESS = 0, + CredentialManagementError_INVALID_COMMAND = 1, + CredentialManagementError_INVALID_PARAMETER = 2, + CredentialManagementError_INVALID_LENGTH = 3, + CredentialManagementError_INVALID_SEQ = 4, + CredentialManagementError_TIMEOUT = 5, + CredentialManagementError_CHANNEL_BUSY = 6, + CredentialManagementError_LOCK_REQUIRED = 7, + CredentialManagementError_INVALID_CHANNEL = 8, + CredentialManagementError_CBOR_UNEXPECTED_TYPE = 9, + CredentialManagementError_INVALID_CBOR = 10, + CredentialManagementError_MISSING_PARAMETER = 11, + CredentialManagementError_LIMIT_EXCEEDED = 12, + CredentialManagementError_UNSUPPORTED_EXTENSION = 13, + CredentialManagementError_CREDENTIAL_EXCLUDED = 14, + CredentialManagementError_PROCESSING = 15, + CredentialManagementError_INVALID_CREDENTIAL = 16, + CredentialManagementError_USER_ACTION_PENDING = 17, + CredentialManagementError_OPERATION_PENDING = 18, + CredentialManagementError_NO_OPERATIONS = 19, + CredentialManagementError_UNSUPPORTED_ALGORITHM = 20, + CredentialManagementError_OPERATION_DENIED = 21, + CredentialManagementError_KEY_STORE_FULL = 22, + CredentialManagementError_NOT_BUSY = 23, + CredentialManagementError_NO_OPERATION_PENDING = 24, + CredentialManagementError_UNSUPPORTED_OPTION = 25, + CredentialManagementError_INVALID_OPTION = 26, + CredentialManagementError_KEEPALIVE_CANCEL = 27, + CredentialManagementError_NO_CREDENTIALS = 28, + CredentialManagementError_USER_ACTION_TIMEOUT = 29, + CredentialManagementError_NOT_ALLOWED = 30, + CredentialManagementError_PIN_INVALID = 31, + CredentialManagementError_PIN_BLOCKED = 32, + CredentialManagementError_PIN_AUTH_INVALID = 33, + CredentialManagementError_PIN_AUTH_BLOCKED = 34, + CredentialManagementError_PIN_NOT_SET = 35, + CredentialManagementError_PIN_REQUIRED = 36, + CredentialManagementError_PIN_POLICY_VIOLATION = 37, + CredentialManagementError_PIN_TOKEN_EXPIRED = 38, + CredentialManagementError_REQUEST_TOO_LARGE = 39, + CredentialManagementError_ACTION_TIMEOUT = 40, + CredentialManagementError_UP_REQUIRED = 41, + CredentialManagementError_UV_BLOCKED = 42, + CredentialManagementError_INTEGRITY_FAILURE = 43, + CredentialManagementError_INVALID_SUBCOMMAND = 44, + CredentialManagementError_UV_INVALID = 45, + CredentialManagementError_UNAUTHORIZED_PERMISSION = 46, + CredentialManagementError_OTHER = -1, +} CredentialManagementError; + +// Get credentials metadata (total count) +int credential_management_get_metadata( + void* transport, + const uint8_t* pin_token, + size_t pin_token_len, + uint8_t protocol, + uint32_t* existing_count_out, + uint32_t* max_remaining_out +); + +// Begin RP enumeration - returns total count +int credential_management_enumerate_rps_begin( + void* transport, + const uint8_t* pin_token, + size_t pin_token_len, + uint8_t protocol, + uint32_t* total_rps_out, + uint8_t* rp_id_hash_out, + char** rp_id_out, + size_t* rp_id_len_out +); + +// Get next RP in enumeration +int credential_management_enumerate_rps_next( + void* transport, + uint8_t* rp_id_hash_out, + char** rp_id_out, + size_t* rp_id_len_out +); + +// Begin credential enumeration for an RP +int credential_management_enumerate_credentials_begin( + void* transport, + const uint8_t* rp_id_hash, + const uint8_t* pin_token, + size_t pin_token_len, + uint8_t protocol, + uint32_t* total_credentials_out, + FfiCredential* credential_out +); + +// Get next credential in enumeration +int credential_management_enumerate_credentials_next( + void* transport, + FfiCredential* credential_out +); + +// Delete a credential by ID +int credential_management_delete_credential( + void* transport, + const uint8_t* credential_id, + size_t credential_id_len, + const uint8_t* pin_token, + size_t pin_token_len, + uint8_t protocol +); + +// Update user information for a credential +int credential_management_update_user_information( + void* transport, + const uint8_t* credential_id, + size_t credential_id_len, + const uint8_t* user_id, + size_t user_id_len, + const uint8_t* user_name, + size_t user_name_len, + const uint8_t* user_display_name, + size_t user_display_name_len, + const uint8_t* pin_token, + size_t pin_token_len, + uint8_t protocol +); + +// Free allocated strings +void credential_management_free_string(char* str); diff --git a/bindings/c/src/credential_management.zig b/bindings/c/src/credential_management.zig new file mode 100644 index 0000000..8c15961 --- /dev/null +++ b/bindings/c/src/credential_management.zig @@ -0,0 +1,645 @@ +const std = @import("std"); +const allocator = std.heap.c_allocator; +const client = @import("clientlib"); +const fido = @import("keylib"); +const cbor = @import("zbor"); +const client_err = client.err; +const ClientTransport = client.Transports.Transport; +const CredentialManagementRequest = fido.ctap.request.CredentialManagement; +const CredentialManagementResponse = fido.ctap.response.CredentialManagement; +const FfiCredential = @import("keylib.zig").FfiCredential; +const Transport = @import("keylib.zig").Transport; + +pub const CredentialManagementError = enum(c_int) { + SUCCESS = 0, + INVALID_COMMAND = 1, + INVALID_PARAMETER = 2, + INVALID_LENGTH = 3, + INVALID_SEQ = 4, + TIMEOUT = 5, + CHANNEL_BUSY = 6, + LOCK_REQUIRED = 7, + INVALID_CHANNEL = 8, + CBOR_UNEXPECTED_TYPE = 9, + INVALID_CBOR = 10, + MISSING_PARAMETER = 11, + LIMIT_EXCEEDED = 12, + UNSUPPORTED_EXTENSION = 13, + CREDENTIAL_EXCLUDED = 14, + PROCESSING = 15, + INVALID_CREDENTIAL = 16, + USER_ACTION_PENDING = 17, + OPERATION_PENDING = 18, + NO_OPERATIONS = 19, + UNSUPPORTED_ALGORITHM = 20, + OPERATION_DENIED = 21, + KEY_STORE_FULL = 22, + NOT_BUSY = 23, + NO_OPERATION_PENDING = 24, + UNSUPPORTED_OPTION = 25, + INVALID_OPTION = 26, + KEEPALIVE_CANCEL = 27, + NO_CREDENTIALS = 28, + USER_ACTION_TIMEOUT = 29, + NOT_ALLOWED = 30, + PIN_INVALID = 31, + PIN_BLOCKED = 32, + PIN_AUTH_INVALID = 33, + PIN_AUTH_BLOCKED = 34, + PIN_NOT_SET = 35, + PIN_REQUIRED = 36, + PIN_POLICY_VIOLATION = 37, + PIN_TOKEN_EXPIRED = 38, + REQUEST_TOO_LARGE = 39, + ACTION_TIMEOUT = 40, + UP_REQUIRED = 41, + UV_BLOCKED = 42, + INTEGRITY_FAILURE = 43, + INVALID_SUBCOMMAND = 44, + UV_INVALID = 45, + UNAUTHORIZED_PERMISSION = 46, + OTHER = -1, +}; + +fn cborErrorToCredentialManagementError(e: anyerror) CredentialManagementError { + return switch (e) { + error.InvalidCommand => .INVALID_COMMAND, + error.InvalidParameter => .INVALID_PARAMETER, + error.InvalidLength => .INVALID_LENGTH, + error.InvalidSeq => .INVALID_SEQ, + error.Timeout => .TIMEOUT, + error.ChannelBusy => .CHANNEL_BUSY, + error.LockRequired => .LOCK_REQUIRED, + error.InvalidChannel => .INVALID_CHANNEL, + error.CborUnexpectedType => .CBOR_UNEXPECTED_TYPE, + error.InvalidCbor => .INVALID_CBOR, + error.MissingParameter => .MISSING_PARAMETER, + error.LimitExceeded => .LIMIT_EXCEEDED, + error.UnsupportedExtension => .UNSUPPORTED_EXTENSION, + error.CredentialExcluded => .CREDENTIAL_EXCLUDED, + error.Processing => .PROCESSING, + error.InvalidCredential => .INVALID_CREDENTIAL, + error.UserActionPending => .USER_ACTION_PENDING, + error.OperationPending => .OPERATION_PENDING, + error.NoOperations => .NO_OPERATIONS, + error.UnsupportedAlgorithm => .UNSUPPORTED_ALGORITHM, + error.OperationDenied => .OPERATION_DENIED, + error.KeyStoreFull => .KEY_STORE_FULL, + error.NotBusy => .NOT_BUSY, + error.NoOperationPending => .NO_OPERATION_PENDING, + error.UnsupportedOption => .UNSUPPORTED_OPTION, + error.InvalidOption => .INVALID_OPTION, + error.KeepaliveCancel => .KEEPALIVE_CANCEL, + error.NoCredentials => .NO_CREDENTIALS, + error.UserActionTimeout => .USER_ACTION_TIMEOUT, + error.NotAllowed => .NOT_ALLOWED, + error.PinInvalid => .PIN_INVALID, + error.PinBlocked => .PIN_BLOCKED, + error.PinAuthInvalid => .PIN_AUTH_INVALID, + error.PinAuthBlocked => .PIN_AUTH_BLOCKED, + error.PinNotSet => .PIN_NOT_SET, + error.PinRequired => .PIN_REQUIRED, + error.PinPolicyViolation => .PIN_POLICY_VIOLATION, + error.PinTokenExpired => .PIN_TOKEN_EXPIRED, + error.RequestTooLarge => .REQUEST_TOO_LARGE, + error.ActionTimeout => .ACTION_TIMEOUT, + error.UpRequired => .UP_REQUIRED, + error.UvBlocked => .UV_BLOCKED, + error.IntegrityFailure => .INTEGRITY_FAILURE, + error.InvalidSubcommand => .INVALID_SUBCOMMAND, + error.UvInvalid => .UV_INVALID, + error.UnauthorizedPermission => .UNAUTHORIZED_PERMISSION, + else => .OTHER, + }; +} + +fn executeCredentialManagementCommand( + transport: *ClientTransport, + sub_command: CredentialManagementRequest.SubCommand, + params: ?CredentialManagementRequest.SubCommandParams, + pin_token: []const u8, + protocol: u8, +) !CredentialManagementResponse { + // Convert protocol to enum + const pin_protocol: fido.ctap.pinuv.common.PinProtocol = if (protocol == 1) .V1 else .V2; + + // Calculate pinUvAuthParam based on subCommand + const pin_uv_auth_param = blk: { + // Skip PIN auth if token is all zeros (placeholder) + if (pin_token.len == 0 or std.mem.allEqual(u8, pin_token, 0)) { + break :blk null; + } + + const PinUvAuth = fido.ctap.pinuv.PinUvAuth; + const sub_cmd_byte: []const u8 = switch (sub_command) { + .getCredsMetadata => "\x01", + .enumerateRPsBegin => "\x02", + .enumerateRPsGetNextRP => break :blk null, // No auth for continuation + .enumerateCredentialsBegin => "\x04", + .enumerateCredentialsGetNextCredential => break :blk null, // No auth for continuation + .deleteCredential => "\x05", + .updateUserInformation => "\x06", + }; + + const auth_param = switch (pin_protocol) { + .V1 => PinUvAuth.authenticate_v1(pin_token, sub_cmd_byte), + .V2 => PinUvAuth.authenticate_v2(pin_token, sub_cmd_byte), + }; + break :blk auth_param.get(); + }; + + // Build the request + const request = CredentialManagementRequest{ + .subCommand = sub_command, + .subCommandParams = params, + .pinUvAuthProtocol = if (pin_uv_auth_param != null) pin_protocol else null, + .pinUvAuthParam = pin_uv_auth_param, + }; + + // Serialize the request - use the module-level allocator constant + var arr = std.Io.Writer.Allocating.init(allocator); + defer arr.deinit(); + + try arr.writer.writeByte(0x0a); // authenticatorCredentialManagement command + try cbor.stringify(request, .{}, &arr.writer); + + // Send the request + try transport.write(arr.written()); + + // Read the response + const response = try transport.read(allocator) orelse return error.MissingResponse; + defer allocator.free(response); + + // Check status code + if (response.len == 0) return error.InvalidResponse; + + if (response[0] != 0) { + return client_err.errorFromInt(response[0]); + } + + // Parse the response + if (response.len < 2) { + // Empty success response (e.g., for delete operations) + return CredentialManagementResponse{}; + } + + var parsed = try cbor.parse(CredentialManagementResponse, try cbor.DataItem.new(response[1..]), .{ .allocator = allocator }); + defer parsed.deinit(allocator); + + // Create a copy of the response that owns its data + return CredentialManagementResponse{ + .existingResidentCredentialsCount = parsed.existingResidentCredentialsCount, + .maxPossibleRemainingResidentCredentialsCount = parsed.maxPossibleRemainingResidentCredentialsCount, + .rp = if (parsed.rp) |rp| .{ + .id = (try fido.common.dt.ABS128T.fromSlice(rp.id.get())) orelse return error.InvalidData, + .name = if (rp.name) |n| (try fido.common.dt.ABS64T.fromSlice(n.get())) else null, + } else null, + .rpIDHash = if (parsed.rpIDHash) |hash| hash else null, + .totalRPs = parsed.totalRPs, + .user = if (parsed.user) |u| .{ + .id = (try fido.common.dt.ABS64B.fromSlice(u.id.get())) orelse return error.InvalidData, + .name = if (u.name) |n| (try fido.common.dt.ABS64T.fromSlice(n.get())) else null, + .displayName = if (u.displayName) |d| (try fido.common.dt.ABS64T.fromSlice(d.get())) else null, + } else null, + .credentialID = if (parsed.credentialID) |cred_id| .{ + .id = (try fido.common.dt.ABS64B.fromSlice(cred_id.id.get())) orelse return error.InvalidData, + .type = cred_id.type, + .transports = cred_id.transports, + } else null, + .publicKey = parsed.publicKey, + .totalCredentials = parsed.totalCredentials, + .credProtect = parsed.credProtect, + .largeBlobKey = parsed.largeBlobKey, + }; +} + +fn credentialToFfi(cred: fido.ctap.authenticator.Credential) FfiCredential { + var ffi: FfiCredential = undefined; + + const id_slice = cred.id.get(); + @memcpy(ffi.id[0..id_slice.len], id_slice); + ffi.id_len = @intCast(id_slice.len); + + const rp_id_slice = cred.rp.id.get(); + @memcpy(ffi.rp_id[0..rp_id_slice.len], rp_id_slice); + ffi.rp_id_len = @intCast(rp_id_slice.len); + + if (cred.rp.name) |name| { + const rp_name_slice = name.get(); + @memcpy(ffi.rp_name[0..rp_name_slice.len], rp_name_slice); + ffi.rp_name_len = @intCast(rp_name_slice.len); + } else { + ffi.rp_name_len = 0; + } + + const user_id_slice = cred.user.id.get(); + @memcpy(ffi.user_id[0..user_id_slice.len], user_id_slice); + ffi.user_id_len = @intCast(user_id_slice.len); + + ffi.sign_count = @intCast(cred.sign_count); + ffi.alg = @intFromEnum(cred.key.P256.alg); + ffi.private_key = cred.key.P256.d orelse [_]u8{0} ** 32; + ffi.created = cred.created; + ffi.discoverable = if (cred.discoverable) 1 else 0; + ffi.cred_protect = @intFromEnum(cred.policy); + + return ffi; +} + +// Get credentials metadata (total count) +export fn credential_management_get_metadata( + transport: ?*anyopaque, + pin_token: [*c]const u8, + pin_token_len: usize, + protocol: u8, + existing_count_out: *u32, + max_remaining_out: *u32, +) callconv(.c) c_int { + if (transport == null or pin_token == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + const pin_token_slice = pin_token[0..pin_token_len]; + + const response = executeCredentialManagementCommand( + t, + .getCredsMetadata, + null, + pin_token_slice, + protocol, + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + existing_count_out.* = response.existingResidentCredentialsCount orelse 0; + max_remaining_out.* = response.maxPossibleRemainingResidentCredentialsCount orelse 0; + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Begin RP enumeration - returns total count +export fn credential_management_enumerate_rps_begin( + transport: ?*anyopaque, + pin_token: [*c]const u8, + pin_token_len: usize, + protocol: u8, + total_rps_out: ?*u32, + rp_id_hash_out: ?*[32]u8, + rp_id_out: ?*[*c]u8, + rp_id_len_out: ?*usize, +) callconv(.c) c_int { + if (transport == null or pin_token == null or total_rps_out == null or rp_id_hash_out == null or rp_id_out == null or rp_id_len_out == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + const pin_token_slice = pin_token[0..pin_token_len]; + + const response = executeCredentialManagementCommand( + t, + .enumerateRPsBegin, + null, + pin_token_slice, + protocol, + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + total_rps_out.?.* = response.totalRPs orelse 0; + + if (response.rpIDHash) |hash| { + @memcpy(rp_id_hash_out.?, &hash); + } else { + @memset(rp_id_hash_out.?, 0); + } + + if (response.rp) |rp| { + const rp_id_slice = rp.id.get(); + const rp_id_copy = allocator.dupeZ(u8, rp_id_slice) catch return @intFromEnum(CredentialManagementError.OTHER); + rp_id_out.?.* = rp_id_copy.ptr; + rp_id_len_out.?.* = rp_id_slice.len; + } else { + rp_id_out.?.* = null; + rp_id_len_out.?.* = 0; + } + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Get next RP in enumeration +export fn credential_management_enumerate_rps_next( + transport: ?*anyopaque, + rp_id_hash_out: ?*[32]u8, + rp_id_out: ?*[*c]u8, + rp_id_len_out: ?*usize, +) callconv(.c) c_int { + if (transport == null or rp_id_hash_out == null or rp_id_out == null or rp_id_len_out == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + + const response = executeCredentialManagementCommand( + t, + .enumerateRPsGetNextRP, + null, + &[_]u8{}, // Empty pin token for continuation + 0, // Protocol not needed for continuation + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + if (response.rpIDHash) |hash| { + @memcpy(rp_id_hash_out.?, &hash); + } else { + @memset(rp_id_hash_out.?, 0); + } + + if (response.rp) |rp| { + const rp_id_slice = rp.id.get(); + const rp_id_copy = allocator.dupeZ(u8, rp_id_slice) catch return @intFromEnum(CredentialManagementError.OTHER); + rp_id_out.?.* = rp_id_copy.ptr; + rp_id_len_out.?.* = rp_id_slice.len; + } else { + rp_id_out.?.* = null; + rp_id_len_out.?.* = 0; + } + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Begin credential enumeration for an RP +export fn credential_management_enumerate_credentials_begin( + transport: ?*anyopaque, + rp_id_hash: ?*const [32]u8, + pin_token: [*c]const u8, + pin_token_len: usize, + protocol: u8, + total_credentials_out: ?*u32, + credential_out: ?*FfiCredential, +) callconv(.c) c_int { + if (transport == null or rp_id_hash == null or pin_token == null or total_credentials_out == null or credential_out == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + const pin_token_slice = pin_token[0..pin_token_len]; + + const params = CredentialManagementRequest.SubCommandParams{ + .rpIDHash = rp_id_hash.?.*, + }; + + const response = executeCredentialManagementCommand( + t, + .enumerateCredentialsBegin, + params, + pin_token_slice, + protocol, + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + total_credentials_out.?.* = response.totalCredentials orelse 0; + + if (response.user != null or response.credentialID != null or response.publicKey != null) { + // Create a synthetic credential from the response + var cred: fido.ctap.authenticator.Credential = undefined; + + if (response.credentialID) |cred_id| { + cred.id = cred_id.id; + } else { + return @intFromEnum(CredentialManagementError.INVALID_CREDENTIAL); + } + + // We need RP ID hash, but we don't have the full RP info here + // This is a limitation - we might need to store RP context + cred.rp = .{ + .id = (fido.common.dt.ABS128T.fromSlice("placeholder") catch null) orelse return @intFromEnum(CredentialManagementError.OTHER), + .name = null, + }; + + if (response.user) |user| { + cred.user = user; + } else { + cred.user = .{ + .id = (fido.common.dt.ABS64B.fromSlice("placeholder") catch null) orelse return @intFromEnum(CredentialManagementError.OTHER), + .name = null, + .displayName = null, + }; + } + + cred.sign_count = 0; // Not provided in enumeration + cred.key = .{ + .P256 = .{ + .alg = .Es256, + .x = undefined, + .y = undefined, + .d = [_]u8{0} ** 32, + }, + }; + cred.created = 0; + cred.discoverable = true; + cred.policy = .userVerificationOptional; + + credential_out.?.* = credentialToFfi(cred); + } + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Get next credential in enumeration +export fn credential_management_enumerate_credentials_next( + transport: ?*anyopaque, + credential_out: ?*FfiCredential, +) callconv(.c) c_int { + if (transport == null or credential_out == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + + const response = executeCredentialManagementCommand( + t, + .enumerateCredentialsGetNextCredential, + null, + &[_]u8{}, // Empty pin token for continuation + 0, // Protocol not needed for continuation + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + if (response.user != null or response.credentialID != null or response.publicKey != null) { + // Create a synthetic credential from the response + var cred: fido.ctap.authenticator.Credential = undefined; + + if (response.credentialID) |cred_id| { + cred.id = cred_id.id; + } else { + return @intFromEnum(CredentialManagementError.INVALID_CREDENTIAL); + } + + // We need RP ID hash, but we don't have the full RP info from next call + // This is a limitation - using placeholder + cred.rp = .{ + .id = (fido.common.dt.ABS128T.fromSlice("placeholder") catch null) orelse return @intFromEnum(CredentialManagementError.OTHER), + .name = null, + }; + + if (response.user) |user| { + cred.user = user; + } else { + cred.user = .{ + .id = (fido.common.dt.ABS64B.fromSlice("placeholder") catch null) orelse return @intFromEnum(CredentialManagementError.OTHER), + .name = null, + .displayName = null, + }; + } + + cred.sign_count = 0; + cred.key = .{ + .P256 = .{ + .alg = .Es256, + .x = undefined, + .y = undefined, + .d = [_]u8{0} ** 32, + }, + }; + cred.created = 0; + cred.discoverable = true; + cred.policy = .userVerificationOptional; + + credential_out.?.* = credentialToFfi(cred); + } + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Delete a credential by ID +export fn credential_management_delete_credential( + transport: ?*anyopaque, + credential_id: [*c]const u8, + credential_id_len: usize, + pin_token: [*c]const u8, + pin_token_len: usize, + protocol: u8, +) callconv(.c) c_int { + if (transport == null or credential_id == null or pin_token == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + const cred_id_slice = credential_id[0..credential_id_len]; + const pin_token_slice = pin_token[0..pin_token_len]; + + const cred_id_abs = (fido.common.dt.ABS64B.fromSlice(cred_id_slice) catch return @intFromEnum(CredentialManagementError.INVALID_CREDENTIAL)) orelse return @intFromEnum(CredentialManagementError.INVALID_CREDENTIAL); + + const cred_desc = fido.common.PublicKeyCredentialDescriptor{ + .id = cred_id_abs, + .type = .@"public-key", + .transports = null, + }; + + const params = CredentialManagementRequest.SubCommandParams{ + .credentialID = cred_desc, + }; + + _ = executeCredentialManagementCommand( + t, + .deleteCredential, + params, + pin_token_slice, + protocol, + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Update user information for a credential +export fn credential_management_update_user_information( + transport: ?*anyopaque, + credential_id: [*c]const u8, + credential_id_len: usize, + user_id: [*c]const u8, + user_id_len: usize, + user_name: [*c]const u8, + user_name_len: usize, + user_display_name: [*c]const u8, + user_display_name_len: usize, + pin_token: [*c]const u8, + pin_token_len: usize, + protocol: u8, +) callconv(.c) c_int { + if (transport == null or credential_id == null or user_id == null or pin_token == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + + const transport_ptr = @as(*Transport, @ptrCast(@alignCast(transport.?))); + if (transport_ptr.handle == null) { + return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + } + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport_ptr.handle.?))); + const cred_id_slice = credential_id[0..credential_id_len]; + const user_id_slice = user_id[0..user_id_len]; + const pin_token_slice = pin_token[0..pin_token_len]; + + const cred_id_abs = (fido.common.dt.ABS64B.fromSlice(cred_id_slice) catch return @intFromEnum(CredentialManagementError.INVALID_CREDENTIAL)) orelse return @intFromEnum(CredentialManagementError.INVALID_CREDENTIAL); + const uid_abs = (fido.common.dt.ABS64B.fromSlice(user_id_slice) catch return @intFromEnum(CredentialManagementError.INVALID_PARAMETER)) orelse return @intFromEnum(CredentialManagementError.INVALID_PARAMETER); + + const cred_desc = fido.common.PublicKeyCredentialDescriptor{ + .id = cred_id_abs, + .type = .@"public-key", + .transports = null, + }; + + var user = fido.common.User{ + .id = uid_abs, + .name = null, + .displayName = null, + }; + + if (user_name_len > 0) { + user.name = (fido.common.dt.ABS64T.fromSlice(user_name[0..user_name_len]) catch null) orelse null; + } + + if (user_display_name_len > 0) { + user.displayName = (fido.common.dt.ABS64T.fromSlice(user_display_name[0..user_display_name_len]) catch null) orelse null; + } + + const params = CredentialManagementRequest.SubCommandParams{ + .credentialID = cred_desc, + .user = user, + }; + + _ = executeCredentialManagementCommand( + t, + .updateUserInformation, + params, + pin_token_slice, + protocol, + ) catch |err| return @intFromEnum(cborErrorToCredentialManagementError(err)); + + return @intFromEnum(CredentialManagementError.SUCCESS); +} + +// Free allocated strings +export fn credential_management_free_string(str: [*c]u8) void { + if (str != null) { + allocator.free(std.mem.span(str)); + } +} diff --git a/bindings/c/src/keylib.zig b/bindings/c/src/keylib.zig index 6360803..8009d17 100644 --- a/bindings/c/src/keylib.zig +++ b/bindings/c/src/keylib.zig @@ -1,33 +1,336 @@ const std = @import("std"); const allocator = std.heap.c_allocator; - const keylib = @import("keylib"); +const cbor = @import("zbor"); +const uhid = @import("uhid"); const Auth = keylib.ctap.authenticator.Auth; +const User = keylib.common.User; +const RelyingParty = keylib.common.RelyingParty; +const PinUvAuth = keylib.ctap.pinuv.PinUvAuth; +const ctaphid = keylib.ctap.transports.ctaphid; +const CtapHid = ctaphid.authenticator.CtapHid; +const CtapHidMsg = ctaphid.authenticator.CtapHidMsg; +const CtapHidMessageIterator = ctaphid.authenticator.CtapHidMessageIterator; + +// Import credential management functions to ensure they're compiled +const credential_management = @import("credential_management.zig"); + +// Force credential management functions to be compiled by referencing them +comptime { + _ = credential_management; +} + +pub const Error = enum(i32) { + SUCCESS = 0, + DoesAlreadyExist = -1, + DoesNotExist = -2, + KeyStoreFull = -3, + OutOfMemory = -4, + Timeout = -5, + Other = -6, +}; + +pub const UpResult = enum(c_int) { + Denied = 0, + Accepted = 1, + Timeout = 2, +}; -const cb = keylib.ctap.authenticator.callbacks; -pub const Error = cb.Error; -pub const UpResult = cb.UpResult; -pub const Callbacks = cb.Callbacks; +pub const UvResult = enum(c_int) { + Denied = 0, + Accepted = 1, + AcceptedWithUp = 2, + Timeout = 3, +}; -const CtapHid = keylib.ctap.transports.ctaphid.authenticator.CtapHid; -const CtapHidMessageIterator = keylib.ctap.transports.ctaphid.authenticator.CtapHidMessageIterator; -const CtapHidMsg = keylib.ctap.transports.ctaphid.authenticator.CtapHidMsg; +pub const Callbacks = extern struct { + up: ?*const fn ([*c]const u8, [*c]const u8, [*c]const u8) callconv(.c) UpResult, + uv: ?*const fn ([*c]const u8, [*c]const u8, [*c]const u8) callconv(.c) UvResult, + select: ?*const fn ([*c]const u8, [*c][*c]u8) callconv(.c) c_int, + read: ?*const fn ([*c]const u8, [*c]const u8, [*c][*c][*c]u8) callconv(.c) c_int, + write: ?*const fn ([*c]const FfiCredential) callconv(.c) c_int, + del: ?*const fn ([*c]const u8) callconv(.c) c_int, + read_first: ?*const fn ([*c]const u8, [*c]const u8, [*c]const u8, [*c]FfiCredential) callconv(.c) c_int, + read_next: ?*const fn ([*c]FfiCredential) callconv(.c) c_int, +}; pub const AuthSettings = extern struct { aaguid: [16]u8 = "\x6f\x15\x82\x74\xaa\xb6\x44\x3d\x9b\xcf\x8a\x3f\x69\x29\x7c\x88".*, }; -export fn auth_init(callbacks: Callbacks, settings: AuthSettings) ?*anyopaque { - var a = allocator.create(Auth) catch { - return null; +pub const FfiCredential = extern struct { + id: [64]u8, + id_len: u8, + rp_id: [128]u8, + rp_id_len: u8, + rp_name: [64]u8, + rp_name_len: u8, + user_id: [64]u8, + user_id_len: u8, + sign_count: u32, + alg: i32, + private_key: [32]u8, + created: i64, + discoverable: u8, + cred_protect: u8, +}; + +var c_callbacks_storage: Callbacks = undefined; +var c_callbacks: *Callbacks = &c_callbacks_storage; + +fn ffiCredentialToZig(ffi: FfiCredential) keylib.ctap.authenticator.callbacks.CallbackError!keylib.ctap.authenticator.Credential { + var cred: keylib.ctap.authenticator.Credential = undefined; + + cred.id = (keylib.common.dt.ABS64B.fromSlice(ffi.id[0..ffi.id_len]) catch return error.Other) orelse return error.Other; + + cred.rp = .{ + .id = (keylib.common.dt.ABS128T.fromSlice(ffi.rp_id[0..ffi.rp_id_len]) catch return error.Other) orelse return error.Other, + .name = if (ffi.rp_name_len > 0) + (keylib.common.dt.ABS64T.fromSlice(ffi.rp_name[0..ffi.rp_name_len]) catch return error.Other) + else + null, + }; + + cred.user = .{ + .id = (keylib.common.dt.ABS64B.fromSlice(ffi.user_id[0..ffi.user_id_len]) catch return error.Other) orelse return error.Other, + .name = null, + .displayName = null, + }; + + cred.sign_count = ffi.sign_count; + + cred.key = .{ + .P256 = .{ + .alg = @enumFromInt(ffi.alg), + .x = undefined, + .y = undefined, + .d = ffi.private_key, + }, + }; + + cred.created = ffi.created; + cred.discoverable = ffi.discoverable != 0; + cred.policy = @enumFromInt(ffi.cred_protect); + + return cred; +} + +fn zigCredentialToFfi(cred: keylib.ctap.authenticator.Credential) FfiCredential { + var ffi: FfiCredential = undefined; + + const id_slice = cred.id.get(); + @memcpy(ffi.id[0..id_slice.len], id_slice); + ffi.id_len = @intCast(id_slice.len); + + const rp_id_slice = cred.rp.id.get(); + @memcpy(ffi.rp_id[0..rp_id_slice.len], rp_id_slice); + ffi.rp_id_len = @intCast(rp_id_slice.len); + + if (cred.rp.name) |name| { + const rp_name_slice = name.get(); + @memcpy(ffi.rp_name[0..rp_name_slice.len], rp_name_slice); + ffi.rp_name_len = @intCast(rp_name_slice.len); + } else { + ffi.rp_name_len = 0; + } + + const user_id_slice = cred.user.id.get(); + @memcpy(ffi.user_id[0..user_id_slice.len], user_id_slice); + ffi.user_id_len = @intCast(user_id_slice.len); + + ffi.sign_count = @intCast(cred.sign_count); + ffi.alg = @intFromEnum(cred.key.P256.alg); + ffi.private_key = cred.key.P256.d orelse [_]u8{0} ** 32; + ffi.created = cred.created; + ffi.discoverable = if (cred.discoverable) 1 else 0; + ffi.cred_protect = @intFromEnum(cred.policy); + + return ffi; +} + +fn wrapper_up(info: []const u8, user: ?User, rp: ?RelyingParty) keylib.ctap.authenticator.callbacks.UpResult { + if (c_callbacks.up == null) return .Denied; + var info_buf: [256]u8 = undefined; + @memcpy(info_buf[0..info.len], info); + info_buf[info.len] = 0; + const c_info: [*c]const u8 = @ptrCast(&info_buf); + + var user_buf: [256]u8 = undefined; + const c_user: ?[*c]const u8 = if (user) |u| blk: { + const name = u.getName(); + @memcpy(user_buf[0..name.len], name); + user_buf[name.len] = 0; + break :blk @ptrCast(&user_buf); + } else null; + + var rp_buf: [256]u8 = undefined; + const c_rp: ?[*c]const u8 = if (rp) |r| blk: { + const id = r.id.get(); + @memcpy(rp_buf[0..id.len], id); + rp_buf[id.len] = 0; + break :blk @ptrCast(&rp_buf); + } else null; + + const result = c_callbacks.up.?(c_info, c_user orelse null, c_rp orelse null); + return switch (result) { + .Denied => .Denied, + .Accepted => .Accepted, + .Timeout => .Timeout, + }; +} + +fn wrapper_uv(info: []const u8, user: ?User, rp: ?RelyingParty) keylib.ctap.authenticator.callbacks.UvResult { + if (c_callbacks.uv == null) return .Denied; + var info_buf: [256]u8 = undefined; + @memcpy(info_buf[0..info.len], info); + info_buf[info.len] = 0; + const c_info: [*c]const u8 = @ptrCast(&info_buf); + + var user_buf: [256]u8 = undefined; + const c_user: ?[*c]const u8 = if (user) |u| blk: { + const name = u.getName(); + @memcpy(user_buf[0..name.len], name); + user_buf[name.len] = 0; + break :blk @ptrCast(&user_buf); + } else null; + + var rp_buf: [256]u8 = undefined; + const c_rp: ?[*c]const u8 = if (rp) |r| blk: { + const id = r.id.get(); + @memcpy(rp_buf[0..id.len], id); + rp_buf[id.len] = 0; + break :blk @ptrCast(&rp_buf); + } else null; + + const result = c_callbacks.uv.?(c_info, c_user orelse null, c_rp orelse null); + return switch (result) { + .Denied => .Denied, + .Accepted => .Accepted, + .AcceptedWithUp => .AcceptedWithUp, + .Timeout => .Timeout, + }; +} + +fn wrapper_read_first(id: ?keylib.common.dt.ABS64B, rp: ?keylib.common.dt.ABS128T, hash: ?[32]u8) keylib.ctap.authenticator.callbacks.CallbackError!keylib.ctap.authenticator.Credential { + if (c_callbacks.read_first == null) return error.DoesNotExist; + + var id_buf: [64]u8 = undefined; + const c_id: ?[*c]const u8 = if (id) |i| blk: { + @memcpy(id_buf[0..i.get().len], i.get()); + id_buf[i.get().len] = 0; + break :blk @ptrCast(&id_buf); + } else null; + + var rp_buf: [128]u8 = undefined; + const c_rp: ?[*c]const u8 = if (rp) |r| blk: { + @memcpy(rp_buf[0..r.get().len], r.get()); + rp_buf[r.get().len] = 0; + break :blk @ptrCast(&rp_buf); + } else null; + + const c_hash: ?[*c]const u8 = if (hash) |*h| @ptrCast(h) else null; + + var ffi_cred: FfiCredential = undefined; + const result = c_callbacks.read_first.?(c_id orelse null, c_rp orelse null, c_hash orelse null, @ptrCast(&ffi_cred)); + + if (result != 0) { + return switch (result) { + -1 => error.DoesAlreadyExist, + -2 => error.DoesNotExist, + -3 => error.KeyStoreFull, + -4 => error.OutOfMemory, + -5 => error.Timeout, + else => error.Other, + }; + } + + return ffiCredentialToZig(ffi_cred); +} + +fn wrapper_read_next() keylib.ctap.authenticator.callbacks.CallbackError!keylib.ctap.authenticator.Credential { + if (c_callbacks.read_next == null) return error.DoesNotExist; + + var ffi_cred: FfiCredential = undefined; + const result = c_callbacks.read_next.?(@ptrCast(&ffi_cred)); + + if (result != 0) { + return switch (result) { + -1 => error.DoesAlreadyExist, + -2 => error.DoesNotExist, + -3 => error.KeyStoreFull, + -4 => error.OutOfMemory, + -5 => error.Timeout, + else => error.Other, + }; + } + + return ffiCredentialToZig(ffi_cred); +} + +fn wrapper_write(data: keylib.ctap.authenticator.Credential) keylib.ctap.authenticator.callbacks.CallbackError!void { + if (c_callbacks.write == null) return error.KeyStoreFull; + + const ffi_cred = zigCredentialToFfi(data); + const result = c_callbacks.write.?(@ptrCast(&ffi_cred)); + + if (result != 0) { + return switch (result) { + -1 => error.DoesAlreadyExist, + -2 => error.DoesNotExist, + -3 => error.KeyStoreFull, + -4 => error.OutOfMemory, + -5 => error.Timeout, + else => error.Other, + }; + } +} + +fn wrapper_delete(id: [*c]const u8) callconv(.c) keylib.ctap.authenticator.callbacks.Error { + if (c_callbacks.del == null) return .DoesNotExist; + + const result = c_callbacks.del.?(id); + return switch (result) { + 0 => .SUCCESS, + -1 => .DoesAlreadyExist, + -2 => .DoesNotExist, + -3 => .KeyStoreFull, + -4 => .OutOfMemory, + -5 => .Timeout, + else => .Other, }; +} + +// Settings callbacks (these remain as stubs since Rust doesn't have them) +fn stub_read_settings() keylib.ctap.authenticator.Meta { + return .{}; +} + +fn stub_write_settings(data: keylib.ctap.authenticator.Meta) void { + _ = data; +} + +var ctaphid_instance: ?CtapHid = null; +var current_iterator: ?CtapHidMessageIterator = null; +var uhid_instance: ?uhid.Uhid = null; - a.* = keylib.ctap.authenticator.Auth{ - // The callbacks are the interface between the authenticator and the rest of the application (see below). - .callbacks = callbacks, - // The commands map from a command code to a command function. All functions have the - // same interface and you can implement your own to extend the authenticator beyond - // the official spec, e.g. add a command to store passwords. +export fn auth_init(callbacks: Callbacks, settings: AuthSettings) ?*anyopaque { + c_callbacks_storage = callbacks; + c_callbacks = &c_callbacks_storage; + + const a = allocator.create(Auth) catch return null; + + a.* = Auth{ + .callbacks = .{ + .up = wrapper_up, + .uv = wrapper_uv, + .read_first = wrapper_read_first, + .read_next = wrapper_read_next, + .write = wrapper_write, + .delete = wrapper_delete, + .read_settings = stub_read_settings, + .write_settings = stub_write_settings, + .processPinHash = null, + }, .commands = &.{ .{ .cmd = 0x01, .cb = keylib.ctap.commands.authenticator.authenticatorMakeCredential }, .{ .cmd = 0x02, .cb = keylib.ctap.commands.authenticator.authenticatorGetAssertion }, @@ -35,66 +338,26 @@ export fn auth_init(callbacks: Callbacks, settings: AuthSettings) ?*anyopaque { .{ .cmd = 0x06, .cb = keylib.ctap.commands.authenticator.authenticatorClientPin }, .{ .cmd = 0x0b, .cb = keylib.ctap.commands.authenticator.authenticatorSelection }, }, - // The settings are returned by a getInfo request and describe the capabilities - // of your authenticator. Make sure your configuration is valid based on the - // CTAP2 spec! .settings = .{ - // Those are the FIDO2 spec you support .versions = &.{ .FIDO_2_0, .FIDO_2_1 }, - // The extensions are defined as strings which should make it easy to extend - // the authenticator (in combination with a new command). .extensions = &.{"credProtect"}, - // This should be unique for all models of the same authenticator. .aaguid = settings.aaguid, + .remainingDiscoverableCredentials = 9999, .options = .{ - // We don't support the credential management command. If you want to - // then you need to implement it yourself and add it to commands and - // set this flag to true. - .credMgmt = false, - // We support discoverable credentials, a.k.a resident keys, a.k.a passkeys .rk = true, - // We support built in user verification (see the callback below) - .uv = true, - // This is a platform authenticator even if we use usb for ipc - .plat = true, - // We don't support client pin but you could also add the command - // yourself and set this to false (not initialized) or true (initialized). - .clientPin = null, - // We support pinUvAuthToken - .pinUvAuthToken = true, - // If you want to enforce alwaysUv you also have to set this to true. - .alwaysUv = false, + .up = true, + .uv = if (c_callbacks.uv != null) true else false, + .plat = false, }, - // The pinUvAuth protocol to support. This library implements V1 and V2. - .pinUvAuthProtocols = &.{.V2}, - // The transports your authenticator supports. - .transports = &.{.usb}, - // The algorithms you support. - .algorithms = &.{.{ .alg = .Es256 }}, - .firmwareVersion = 0xcafe, - .remainingDiscoverableCredentials = 100, }, - // Here we initialize the pinUvAuth token data structure wich handles the generation - // and management of pinUvAuthTokens. - .token = keylib.ctap.pinuv.PinUvAuth.v2(std.crypto.random), - // Here we set the supported algorithm. You can also implement your - // own and add them here. - .algorithms = &.{ - keylib.ctap.crypto.algorithms.Es256, - }, - // This allocator is used to allocate memory and has to be the same - // used for the callbacks. - .allocator = allocator, - // A function to get the epoch time as i64. - .milliTimestamp = std.time.milliTimestamp, - // A cryptographically secure random number generator + .token = PinUvAuth.v2(std.crypto.random), + .algorithms = &.{keylib.ctap.crypto.algorithms.Es256}, .random = std.crypto.random, - // If you don't want to increment the sign counts - // of credentials (e.g. because you sync them between devices) - // set this to true. - .constSignCount = true, + .milliTimestamp = std.time.milliTimestamp, }; + a.init() catch { + allocator.destroy(a); return null; }; @@ -103,87 +366,440 @@ export fn auth_init(callbacks: Callbacks, settings: AuthSettings) ?*anyopaque { export fn auth_deinit(a: *anyopaque) void { const auth = @as(*Auth, @ptrCast(@alignCast(a))); - auth.allocator.destroy(auth); + allocator.destroy(auth); } export fn auth_handle( a: *anyopaque, - m: ?*anyopaque, -) void { - if (m == null) return; + request_data: [*c]const u8, + request_len: usize, + response_buffer: [*c]u8, + response_buffer_size: usize, +) usize { + if (request_data == null or response_buffer == null) return 0; + if (request_len == 0 or request_len > 7609) return 0; + if (response_buffer_size < 7609) return 0; const auth = @as(*Auth, @ptrCast(@alignCast(a))); - const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(m.?))); - - switch (msg.cmd) { - .cbor => { - var out: [7609]u8 = undefined; // TODO: we have to make this configurable - const r = auth.handle(&out, msg.getData()); - @memcpy(msg._data[0..r.len], r); - msg.len = r.len; - }, - else => {}, - } + const request = request_data[0..request_len]; + const response_buf_ptr = @as(*[7609]u8, @ptrCast(@alignCast(response_buffer))); + const response = auth.handle(response_buf_ptr, request); + + return response.len; } export fn ctaphid_init() ?*anyopaque { - const c = allocator.create(CtapHid) catch { - return null; + ctaphid_instance = CtapHid.init(allocator, std.crypto.random); + return @as(*anyopaque, @ptrCast(&ctaphid_instance.?)); +} + +export fn ctaphid_deinit(a: *anyopaque) void { + _ = a; + if (ctaphid_instance) |*instance| { + instance.deinit(); + ctaphid_instance = null; + } +} + +export fn ctaphid_handle(a: *anyopaque, data: [*c]const u8, len: usize) ?*anyopaque { + _ = a; + if (ctaphid_instance == null) return null; + + const packet = data[0..len]; + const response = ctaphid_instance.?.handle(packet) orelse return null; + + const response_copy = allocator.create(CtapHidMsg) catch return null; + response_copy.* = response; + return @as(*anyopaque, @ptrCast(response_copy)); +} + +export fn ctaphid_iterator(a: ?*anyopaque) ?*anyopaque { + if (a == null) return null; + + const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(a.?))); + current_iterator = msg.iterator(); + + return @as(*anyopaque, @ptrCast(¤t_iterator.?)); +} + +export fn ctaphid_iterator_next(a: ?*anyopaque, out: [*c]u8) c_int { + _ = a; + if (current_iterator == null) return 0; + + const packet = current_iterator.?.next() orelse { + current_iterator = null; + return 0; }; - c.* = CtapHid.init(allocator, std.crypto.random); + @memcpy(out[0..packet.len], packet); + return @intCast(packet.len); +} - return @as(*anyopaque, @ptrCast(c)); +export fn ctaphid_iterator_deinit(a: ?*anyopaque) void { + _ = a; + if (current_iterator) |*iter| { + iter.deinit(); + current_iterator = null; + } } -export fn ctaphid_deinit(a: *anyopaque) void { - const c = @as(*CtapHid, @ptrCast(@alignCast(a))); - c.deinit(); - allocator.destroy(c); +export fn ctaphid_response_get_cmd(response: ?*anyopaque) c_int { + if (response == null) return -1; + + const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(response.?))); + return @intFromEnum(msg.cmd); +} + +export fn ctaphid_response_get_data(response: ?*anyopaque, out: [*c]u8, max_len: usize) usize { + if (response == null) return 0; + + const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(response.?))); + const data = msg.getData(); + const len = @min(data.len, max_len); + @memcpy(out[0..len], data[0..len]); + return len; } -/// This function either returns null or a pointer to a CtapHidMsg. -export fn ctaphid_handle( - ctap: *anyopaque, - packet: [*c]const u8, - len: usize, -) ?*anyopaque { - const ctaphid = @as(*CtapHid, @ptrCast(@alignCast(ctap))); +export fn ctaphid_response_set_data(response: ?*anyopaque, data: [*c]const u8, len: usize) c_int { + if (response == null) return -1; + + const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(response.?))); + if (len > msg._data.len) return -1; + + @memcpy(msg._data[0..len], data[0..len]); + msg.len = len; + return 0; +} + +pub const TransportType = enum(c_int) { + USB = 0, + NFC = 1, + BLE = 2, +}; - if (ctaphid.handle(packet[0..len])) |res| { - const msg = allocator.create(CtapHidMsg) catch { +pub const Transport = extern struct { + handle: ?*anyopaque, + type: TransportType, + description: [*c]u8, +}; + +pub const TransportList = extern struct { + transports: [*c]?*Transport, + count: usize, + _internal: ?*anyopaque = null, +}; + +const client = @import("clientlib"); +const ClientTransport = client.Transports.Transport; +const ClientTransports = client.Transports; + +export fn transport_enumerate() ?*TransportList { + const transports_ptr = allocator.create(ClientTransports) catch return null; + errdefer allocator.destroy(transports_ptr); + + transports_ptr.* = ClientTransports.enumerate(allocator, .{}) catch { + allocator.destroy(transports_ptr); + return null; + }; + + if (transports_ptr.devices.len == 0) { + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + return null; + } + + const list = allocator.create(TransportList) catch { + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + return null; + }; + errdefer allocator.destroy(list); + + const transport_array = allocator.alloc(?*Transport, transports_ptr.devices.len) catch { + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + allocator.destroy(list); + return null; + }; + errdefer allocator.free(transport_array); + + for (transports_ptr.devices, 0..) |*device, i| { + const c_transport = allocator.create(Transport) catch { + for (0..i) |j| { + if (transport_array[j]) |t| { + allocator.free(std.mem.span(@as([*:0]const u8, @ptrCast(t.description)))); + allocator.destroy(t); + } + } + allocator.free(transport_array); + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + allocator.destroy(list); return null; }; - msg.* = res; - return @as(*anyopaque, @ptrCast(msg)); - } else { - return null; + + const desc = device.allocPrint(allocator) catch { + allocator.destroy(c_transport); + for (0..i) |j| { + if (transport_array[j]) |t| { + allocator.free(std.mem.span(@as([*:0]const u8, @ptrCast(t.description)))); + allocator.destroy(t); + } + } + allocator.free(transport_array); + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + allocator.destroy(list); + return null; + }; + + const desc_c = allocator.dupeZ(u8, desc) catch { + allocator.free(desc); + allocator.destroy(c_transport); + for (0..i) |j| { + if (transport_array[j]) |t| { + allocator.free(std.mem.span(@as([*:0]const u8, @ptrCast(t.description)))); + allocator.destroy(t); + } + } + allocator.free(transport_array); + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + allocator.destroy(list); + return null; + }; + allocator.free(desc); + + c_transport.* = Transport{ + .handle = @ptrCast(device), + .type = .USB, + .description = @constCast(desc_c.ptr), + }; + + transport_array[i] = c_transport; } + + list.* = TransportList{ + .transports = @ptrCast(transport_array.ptr), + .count = transports_ptr.devices.len, + ._internal = @ptrCast(transports_ptr), + }; + + return list; } -export fn ctaphid_iterator(m: *anyopaque) ?*anyopaque { - const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(m))); - const iter = allocator.create(CtapHidMessageIterator) catch { - return null; +export fn transport_list_free(list: ?*TransportList) void { + if (list == null) return; + + const l = list.?; + for (0..l.count) |i| { + if (l.transports[i]) |transport| { + allocator.free(std.mem.span(@as([*:0]const u8, @ptrCast(transport.description)))); + allocator.destroy(transport); + } + } + allocator.free(@as([*]?*Transport, @ptrCast(l.transports))[0..l.count]); + + if (l._internal) |internal| { + const transports_ptr = @as(*ClientTransports, @ptrCast(@alignCast(internal))); + transports_ptr.deinit(); + allocator.destroy(transports_ptr); + } + + allocator.destroy(l); +} + +export fn transport_open(transport: ?*Transport) c_int { + if (transport == null or transport.?.handle == null) return -1; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.?.handle.?))); + t.open() catch return -1; + return 0; +} + +export fn transport_close(transport: ?*Transport) void { + if (transport == null) return; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.?.handle.?))); + t.close(); +} + +export fn transport_write(transport: ?*Transport, data: [*c]const u8, len: usize) c_int { + if (transport == null) return -1; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.?.handle.?))); + const slice = data[0..len]; + t.write(slice) catch return -1; + return 0; +} + +export fn transport_read(transport: ?*Transport, buffer: [*c]u8, max_len: usize, timeout_ms: c_int) c_int { + _ = timeout_ms; + if (transport == null) return -1; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.?.handle.?))); + const result = t.read(allocator) catch return -1; + if (result) |data| { + defer allocator.free(data); + const copy_len = @min(data.len, max_len); + @memcpy(buffer[0..copy_len], data[0..copy_len]); + return @intCast(copy_len); + } + return 0; +} + +export fn transport_get_type(transport: ?*Transport) TransportType { + if (transport == null) return .USB; + return transport.?.type; +} + +export fn transport_get_description(transport: ?*Transport) [*c]const u8 { + if (transport == null) return ""; + return transport.?.description; +} + +export fn transport_free(transport: ?*Transport) void { + _ = transport; +} + +pub const CborCommandStatus = enum(c_int) { + Pending = 0, + Fulfilled = 1, + Rejected = 2, +}; + +pub const CborCommand = extern struct { + promise: ?*anyopaque, + transport: ?*anyopaque, +}; + +pub const CborCommandResult = extern struct { + status: c_int, + data: [*c]u8, + data_len: usize, + error_code: c_int, +}; + +export fn cbor_authenticator_get_info(transport: ?*Transport) ?*CborCommand { + if (transport == null) return null; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.?.handle.?))); + const promise = client.cbor_commands.authenticatorGetInfo(t) catch return null; + + const cmd = allocator.create(CborCommand) catch return null; + cmd.* = CborCommand{ + .promise = @ptrCast(@constCast(&promise)), + .transport = @ptrCast(t), }; - iter.* = msg.iterator(); - return @as(*anyopaque, @ptrCast(iter)); + return cmd; } -export fn ctaphid_iterator_next(iter: *anyopaque, out: [*c]u8) c_int { - const iterator = @as(*CtapHidMessageIterator, @ptrCast(@alignCast(iter))); +const CredentialCreationOptions = extern struct { + challenge: [*c]const u8, + challenge_len: usize, + rp_id: [*c]const u8, + rp_name: [*c]const u8, + user_id: [*c]const u8, + user_id_len: usize, + user_name: [*c]const u8, + user_display_name: [*c]const u8, + timeout_ms: u32, + require_resident_key: c_int, + require_user_verification: c_int, + attestation_preference: [*c]const u8, + exclude_credentials_json: [*c]const u8, + extensions_json: [*c]const u8, +}; - if (iterator.next()) |packet| { - @memcpy(out[0..packet.len], packet); // 64 bytes - return 1; - } else { - return 0; +const CredentialAssertionOptions = extern struct { + rp_id: [*c]const u8, + challenge: [*c]const u8, + challenge_len: usize, + timeout_ms: u32, + user_verification: [*c]const u8, + allow_credentials_json: [*c]const u8, +}; + +export fn cbor_credentials_create(transport: ?*Transport, options: ?*CredentialCreationOptions) ?*CborCommand { + _ = transport; + _ = options; + + const cmd = allocator.create(CborCommand) catch return null; + cmd.* = CborCommand{ + .promise = null, + .transport = null, + }; + return cmd; +} + +export fn cbor_credentials_get(transport: ?*Transport, options: ?*CredentialAssertionOptions) ?*CborCommand { + _ = transport; + _ = options; + + const cmd = allocator.create(CborCommand) catch return null; + cmd.* = CborCommand{ + .promise = null, + .transport = null, + }; + return cmd; +} + +export fn cbor_command_get_result(cmd: ?*CborCommand, timeout_ms: c_int) ?*CborCommandResult { + _ = timeout_ms; + if (cmd == null) return null; + + const c = cmd.?; + const promise = @as(*client.cbor_commands.Promise, @ptrCast(@alignCast(c.promise.?))); + const state = promise.get(allocator); + + const result: *CborCommandResult = allocator.create(CborCommandResult) catch return null; + + switch (state) { + .pending => { + result.* = CborCommandResult{ + .status = @intFromEnum(CborCommandStatus.Pending), + .data = null, + .data_len = 0, + .error_code = 0, + }; + }, + .fulfilled => |data| { + const data_copy = allocator.dupe(u8, data) catch { + allocator.destroy(result); + return null; + }; + result.* = CborCommandResult{ + .status = @intFromEnum(CborCommandStatus.Fulfilled), + .data = @ptrCast(data_copy.ptr), + .data_len = data_copy.len, + .error_code = 0, + }; + }, + .rejected => |err| { + result.* = CborCommandResult{ + .status = @intFromEnum(CborCommandStatus.Rejected), + .data = null, + .data_len = 0, + .error_code = @intFromError(err), + }; + }, } + + return result; +} + +export fn cbor_command_free(cmd: ?*CborCommand) void { + if (cmd == null) return; + allocator.destroy(cmd.?); } -export fn ctaphid_iterator_deinit(iter: *anyopaque) void { - const iterator = @as(*CtapHidMessageIterator, @ptrCast(@alignCast(iter))); - iterator.deinit(); - allocator.destroy(iterator); +export fn cbor_command_result_free(result: ?*CborCommandResult) void { + if (result == null) return; + + const r = result.?; + if (r.data != null) { + allocator.free(std.mem.span(r.data)); + } + allocator.destroy(r); } diff --git a/build.zig b/build.zig index 57e5343..94eafc3 100644 --- a/build.zig +++ b/build.zig @@ -18,6 +18,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }); + hidapi_dep.artifact("hidapi").linkSystemLibrary("udev"); const uuid_dep = b.dependency("uuid", .{ .target = target, @@ -83,6 +84,7 @@ pub fn build(b: *std.Build) !void { .root_module = client_example_mod, }); client_example.root_module.addImport("client", client_module); + client_example.linkSystemLibrary("libudev"); const client_example_step = b.step("client-example", "Build the client application example"); client_example_step.dependOn(&b.addInstallArtifact(client_example, .{}).step); @@ -108,23 +110,48 @@ pub fn build(b: *std.Build) !void { // C bindings // ------------------------------------------------ - //const c_bindings = b.addStaticLibrary(.{ - // .name = "keylib", - // .root_source_file = .{ .path = "bindings/c/src/keylib.zig" }, - // .target = target, - // .optimize = optimize, - //}); - //c_bindings.root_module.addImport("keylib", keylib_module); - //c_bindings.linkLibC(); - //c_bindings.installHeadersDirectory( - // b.path("bindings/c/include"), - // "keylib", - // .{ - // .exclude_extensions = &.{}, - // .include_extensions = &.{".h"}, - // }, - //); - //b.installArtifact(c_bindings); + const c_bindings_mod = b.createModule(.{ + .root_source_file = b.path("bindings/c/src/keylib.zig"), + .target = target, + .optimize = optimize, + }); + + const c_bindings = b.addLibrary(.{ + .name = "keylib", + .root_module = c_bindings_mod, + .linkage = .static, + }); + c_bindings.root_module.addImport("keylib", keylib_module); + c_bindings.root_module.addImport("uhid", uhid_module); + c_bindings.root_module.addImport("clientlib", client_module); + c_bindings.root_module.addImport("zbor", zbor_module); + c_bindings.linkLibC(); + c_bindings.linkLibrary(hidapi_dep.artifact("hidapi")); + c_bindings.linkSystemLibrary("udev"); + c_bindings.installHeadersDirectory( + b.path("bindings/c/include"), + "keylib", + .{ + .exclude_extensions = &.{}, + .include_extensions = &.{".h"}, + }, + ); + b.installArtifact(c_bindings); + + // Static libraries for Zig API + const keylib_lib = b.addLibrary(.{ + .name = "keylib", + .root_module = keylib_module, + .linkage = .static, + }); + b.installArtifact(keylib_lib); + + const zbor_lib = b.addLibrary(.{ + .name = "zbor", + .root_module = zbor_module, + .linkage = .static, + }); + b.installArtifact(zbor_lib); const uhid_mod = b.createModule(.{ .root_source_file = b.path("bindings/linux/src/uhid-c.zig"), diff --git a/example/client.zig b/example/client.zig index 723791b..136b00c 100644 --- a/example/client.zig +++ b/example/client.zig @@ -73,7 +73,9 @@ pub fn main() !void { return; } - if (!info.options.clientPin.? and !info.options.uv.?) { + if ((info.options.clientPin == null or !info.options.clientPin.?) and + (info.options.uv == null or !info.options.uv.?)) + { std.log.err("No user verification set up for device", .{}); return; } From af83808455170d5c3cc922330724ca5381aca10b Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Sun, 9 Nov 2025 00:27:39 +0100 Subject: [PATCH 2/2] feat: Expose client pin functions and add `getPinUvAuthTokenUsingPinWithPermissions` --- bindings/c/include/keylib.h | 71 +++++++ bindings/c/src/client_pin.zig | 190 ++++++++++++++++++ bindings/c/src/keylib.zig | 23 ++- lib/client/cbor_commands.zig | 25 +-- .../authenticator/authenticatorClientPin.zig | 114 +++++++++++ 5 files changed, 405 insertions(+), 18 deletions(-) create mode 100644 bindings/c/src/client_pin.zig diff --git a/bindings/c/include/keylib.h b/bindings/c/include/keylib.h index 67e80c9..824f5ab 100644 --- a/bindings/c/include/keylib.h +++ b/bindings/c/include/keylib.h @@ -84,6 +84,10 @@ void auth_deinit(void*); size_t auth_handle(void* auth, const uint8_t* request_data, size_t request_len, uint8_t* response_buffer, size_t response_buffer_size); +// Set PIN hash for the authenticator (SHA-256 hash of the PIN, up to 63 bytes) +// This must be called before auth_init if you want the authenticator to support PIN +void auth_set_pin_hash(const uint8_t* pin_hash, size_t len); + // CTAPHID protocol handler functions void* ctaphid_init(); void ctaphid_deinit(void*); @@ -319,3 +323,70 @@ int credential_management_update_user_information( // Free allocated strings void credential_management_free_string(char* str); + +// Client PIN Protocol operations + +/// Establish PIN encapsulation with the authenticator +/// Returns opaque handle to encapsulation on success, null on failure +/// Must be freed with client_pin_encapsulation_free +void* client_pin_encapsulation_new( + void* transport, + uint8_t protocol +); + +/// Get the platform's public key from an encapsulation +/// Returns 0 on success, negative on failure +/// public_key_out must be at least 65 bytes (uncompressed P-256 point: 0x04 || x || y) +int client_pin_encapsulation_get_platform_key( + const void* encapsulation, + uint8_t* public_key_out +); + +/// Free PIN encapsulation +void client_pin_encapsulation_free(void* enc); + +/// Get PIN token from authenticator +/// Returns 0 on success, negative on failure +/// token_out and token_len_out will be set to allocated buffer and its length +/// Caller must free with client_pin_free_token +int client_pin_get_pin_token( + void* transport, + void* enc, + const uint8_t* pin, + size_t pin_len, + uint8_t** token_out, + size_t* token_len_out +); + +/// Get PIN/UV auth token with permissions (CTAP 2.1+) +/// Returns 0 on success, negative on failure +/// permissions: bitmap (mc=1, ga=2, cm=4, be=8, lbw=16, acfg=32) +/// Caller must free token with client_pin_free_token +int client_pin_get_pin_uv_auth_token_using_pin_with_permissions( + void* transport, + void* enc, + const uint8_t* pin, + size_t pin_len, + uint8_t permissions, + const uint8_t* rp_id, + size_t rp_id_len, + uint8_t** token_out, + size_t* token_len_out +); + +/// Get PIN/UV auth token using UV with permissions (CTAP 2.1+) +/// Returns 0 on success, negative on failure +/// Caller must free token with client_pin_free_token +int client_pin_get_pin_uv_auth_token_using_uv_with_permissions( + void* transport, + void* enc, + uint8_t permissions, + const uint8_t* rp_id, + size_t rp_id_len, + uint8_t** token_out, + size_t* token_len_out +); + +/// Free PIN token buffer +void client_pin_free_token(uint8_t* token, size_t len); + diff --git a/bindings/c/src/client_pin.zig b/bindings/c/src/client_pin.zig new file mode 100644 index 0000000..085a709 --- /dev/null +++ b/bindings/c/src/client_pin.zig @@ -0,0 +1,190 @@ +const std = @import("std"); +const allocator = std.heap.c_allocator; +const keylib = @import("keylib"); +const client = @import("clientlib"); +const cbor = @import("zbor"); + +// Types from the library +const Encapsulation = client.cbor_commands.client_pin.Encapsulation; +const ClientTransport = client.Transports.Transport; +const PinProtocol = keylib.ctap.pinuv.common.PinProtocol; +const Permissions = client.cbor_commands.client_pin.Permissions; + +// C wrapper types from keylib.zig +const CTransport = extern struct { + handle: ?*anyopaque, + type: u32, // TransportType + description: [*c]u8, +}; + +/// Create a new PIN encapsulation for key agreement with the authenticator +/// Returns opaque pointer to Encapsulation on success, null on error +export fn client_pin_encapsulation_new( + transport: *CTransport, + protocol: u8, +) callconv(.c) ?*Encapsulation { + if (transport.handle == null) return null; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.handle.?))); + const proto: PinProtocol = if (protocol == 1) .V1 else .V2; + + const enc = client.cbor_commands.client_pin.getKeyAgreement( + t, + proto, + allocator, + ) catch |e| { + std.log.err("getKeyAgreement failed: {}", .{e}); + return null; + }; + + const enc_ptr = allocator.create(Encapsulation) catch return null; + enc_ptr.* = enc; + return enc_ptr; +} + +/// Get the platform public key from an encapsulation (65 bytes: 0x04 || x || y) +/// public_key_out must point to a buffer of at least 65 bytes +/// Returns 0 on success, -1 on error +export fn client_pin_encapsulation_get_platform_key( + encapsulation: *const Encapsulation, + public_key_out: [*]u8, +) callconv(.c) i32 { + const pub_key = encapsulation.platform_key_agreement_key.public_key.toUncompressedSec1(); + @memcpy(public_key_out[0..65], &pub_key); + return 0; +} + +/// Free a PIN encapsulation +export fn client_pin_encapsulation_free( + encapsulation: *Encapsulation, +) callconv(.c) void { + allocator.destroy(encapsulation); +} + +/// Get PIN token from authenticator (CTAP 2.0) +/// Returns 0 on success with allocated token, -1 on failure +/// Caller must free the returned buffer with client_pin_free_token +export fn client_pin_get_pin_token( + transport: *CTransport, + enc: *Encapsulation, + pin: [*]const u8, + pin_len: usize, + token_out: *[*]u8, + token_len_out: *usize, +) callconv(.c) i32 { + if (transport.handle == null) return -1; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.handle.?))); + const pin_slice = pin[0..pin_len]; + + const token = client.cbor_commands.client_pin.getPinToken( + t, + enc, + pin_slice, + allocator, + ) catch |e| { + std.log.err("getPinToken failed: {}", .{e}); + return -1; + }; + + // Return the token directly - caller takes ownership + token_out.* = @constCast(token.ptr); + token_len_out.* = token.len; + + return 0; +} + +/// Convert permissions byte to Permissions struct +fn permissionsFromByte(byte: u8) Permissions { + return .{ + .mc = @truncate(byte & 0x01), + .ga = @truncate((byte >> 1) & 0x01), + .cm = @truncate((byte >> 2) & 0x01), + .be = @truncate((byte >> 3) & 0x01), + .lbw = @truncate((byte >> 4) & 0x01), + .acfg = @truncate((byte >> 5) & 0x01), + }; +} + +/// Get PIN/UV auth token with permissions (CTAP 2.1+) +/// Returns 0 on success with allocated token, -1 on failure +/// rp_id can be null if not needed for the requested permissions +export fn client_pin_get_pin_uv_auth_token_using_pin_with_permissions( + transport: *CTransport, + enc: *Encapsulation, + pin: [*]const u8, + pin_len: usize, + permissions: u8, + rp_id: ?[*]const u8, + rp_id_len: usize, + token_out: *[*]u8, + token_len_out: *usize, +) callconv(.c) i32 { + if (transport.handle == null) return -1; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.handle.?))); + const pin_slice = pin[0..pin_len]; + const rp_id_slice = if (rp_id) |r| r[0..rp_id_len] else null; + const perms = permissionsFromByte(permissions); + + // Actual order is: transport, enc, permissions, rpId, pin, allocator + const token = client.cbor_commands.client_pin.getPinUvAuthTokenUsingPinWithPermissions( + t, + enc, + perms, + rp_id_slice, + pin_slice, + allocator, + ) catch |e| { + std.log.err("getPinUvAuthTokenUsingPinWithPermissions failed: {}", .{e}); + return -1; + }; + + // Return the token directly - caller takes ownership + token_out.* = @constCast(token.ptr); + token_len_out.* = token.len; + + return 0; +} + +/// Get PIN/UV auth token using UV with permissions (CTAP 2.1+) +/// Returns 0 on success with allocated token, -1 on failure +/// rp_id can be null if not needed for the requested permissions +export fn client_pin_get_pin_uv_auth_token_using_uv_with_permissions( + transport: *CTransport, + enc: *Encapsulation, + permissions: u8, + rp_id: ?[*]const u8, + rp_id_len: usize, + token_out: *[*]u8, + token_len_out: *usize, +) callconv(.c) i32 { + if (transport.handle == null) return -1; + + const t = @as(*ClientTransport, @ptrCast(@alignCast(transport.handle.?))); + const rp_id_slice = if (rp_id) |r| r[0..rp_id_len] else null; + const perms = permissionsFromByte(permissions); + + const token = client.cbor_commands.client_pin.getPinUvAuthTokenUsingUvWithPermissions( + t, + enc, + perms, + rp_id_slice, + allocator, + ) catch |e| { + std.log.err("getPinUvAuthTokenUsingUvWithPermissions failed: {}", .{e}); + return -1; + }; + + // Return the token directly - caller takes ownership + token_out.* = @constCast(token.ptr); + token_len_out.* = token.len; + + return 0; +} + +/// Free PIN token buffer allocated by the library +export fn client_pin_free_token(token: [*]u8, len: usize) callconv(.c) void { + const slice = token[0..len]; + allocator.free(slice); +} diff --git a/bindings/c/src/keylib.zig b/bindings/c/src/keylib.zig index 8009d17..71edba5 100644 --- a/bindings/c/src/keylib.zig +++ b/bindings/c/src/keylib.zig @@ -14,10 +14,12 @@ const CtapHidMessageIterator = ctaphid.authenticator.CtapHidMessageIterator; // Import credential management functions to ensure they're compiled const credential_management = @import("credential_management.zig"); +const client_pin = @import("client_pin.zig"); -// Force credential management functions to be compiled by referencing them +// Force credential management and client_pin functions to be compiled by referencing them comptime { _ = credential_management; + _ = client_pin; } pub const Error = enum(i32) { @@ -300,13 +302,26 @@ fn wrapper_delete(id: [*c]const u8) callconv(.c) keylib.ctap.authenticator.callb }; } -// Settings callbacks (these remain as stubs since Rust doesn't have them) +// Settings callbacks - now supports PIN configuration +var pin_hash_storage: ?[63]u8 = null; + fn stub_read_settings() keylib.ctap.authenticator.Meta { - return .{}; + return .{ + .pin = pin_hash_storage, + }; } fn stub_write_settings(data: keylib.ctap.authenticator.Meta) void { - _ = data; + pin_hash_storage = data.pin; +} + +export fn auth_set_pin_hash(pin_hash: [*]const u8, len: usize) void { + if (len > 63) return; + + var new_pin: [63]u8 = undefined; + @memset(&new_pin, 0); + @memcpy(new_pin[0..len], pin_hash[0..len]); + pin_hash_storage = new_pin; } var ctaphid_instance: ?CtapHid = null; diff --git a/lib/client/cbor_commands.zig b/lib/client/cbor_commands.zig index 74a4ffc..ae7327e 100644 --- a/lib/client/cbor_commands.zig +++ b/lib/client/cbor_commands.zig @@ -567,7 +567,7 @@ pub const client_pin = struct { }; if (rpId) |id| { - request.rpId = id; + request.rpId = try keylib.common.dt.ABS128T.fromSlice(id); } var pin_hash: [Sha256.digest_length]u8 = undefined; @@ -581,7 +581,7 @@ pub const client_pin = struct { const iv: [16]u8 = .{0} ** 16; PinUvAuth._encrypt( iv, - e.shared_secret[0..32].*, + e.shared_secret.get()[0..32].*, _pinHashEnc[0..16], pin_hash_left, ); @@ -591,14 +591,14 @@ pub const client_pin = struct { std.crypto.random.bytes(_pinHashEnc[0..16]); PinUvAuth._encrypt( _pinHashEnc[0..16].*, - e.shared_secret[32..64].*, + e.shared_secret.get()[32..64].*, _pinHashEnc[16..32], pin_hash_left, ); pinHashEnc = _pinHashEnc[0..32]; }, } - request.pinHashEnc = pinHashEnc; + request.pinHashEnc = try keylib.common.dt.ABS32B.fromSlice(pinHashEnc); var arr = std.Io.Writer.Allocating.init(a); defer arr.deinit(); @@ -615,8 +615,7 @@ pub const client_pin = struct { return err.errorFromInt(response[0]); } - var cpr = try cbor.parse(ClientPinResponse, try cbor.DataItem.new(response[1..]), .{ .allocator = a }); - defer cpr.deinit(a); + const cpr = try cbor.parse(ClientPinResponse, try cbor.DataItem.new(response[1..]), .{}); if (cpr.pinUvAuthToken == null) return error.MissingPar; @@ -624,11 +623,11 @@ pub const client_pin = struct { switch (e.version) { .V1 => { token = try a.alloc(u8, cpr.pinUvAuthToken.?.len); - PinUvAuth.decrypt_v1(e.shared_secret, token, cpr.pinUvAuthToken.?); + PinUvAuth.decrypt_v1(e.shared_secret.get(), token, cpr.pinUvAuthToken.?.get()); }, .V2 => { token = try a.alloc(u8, cpr.pinUvAuthToken.?.len - 16); - PinUvAuth.decrypt_v2(e.shared_secret, token, cpr.pinUvAuthToken.?); + PinUvAuth.decrypt_v2(e.shared_secret.get(), token, cpr.pinUvAuthToken.?.get()); }, } return token; @@ -656,7 +655,7 @@ pub const client_pin = struct { }; if (rpId) |id| { - request.rpId = id; + request.rpId = try keylib.common.dt.ABS128T.fromSlice(id); } var arr = std.Io.Writer.Allocating.init(a); @@ -674,20 +673,18 @@ pub const client_pin = struct { return err.errorFromInt(response[0]); } - var cpr = try cbor.parse(ClientPinResponse, try cbor.DataItem.new(response[1..]), .{ .allocator = a }); - defer cpr.deinit(a); - + const cpr = try cbor.parse(ClientPinResponse, try cbor.DataItem.new(response[1..]), .{}); if (cpr.pinUvAuthToken == null) return error.MissingPar; var token: []u8 = undefined; switch (e.version) { .V1 => { token = try a.alloc(u8, cpr.pinUvAuthToken.?.len); - PinUvAuth.decrypt_v1(e.shared_secret, token, cpr.pinUvAuthToken.?); + PinUvAuth.decrypt_v1(e.shared_secret.get(), token, cpr.pinUvAuthToken.?.get()); }, .V2 => { token = try a.alloc(u8, cpr.pinUvAuthToken.?.len - 16); - PinUvAuth.decrypt_v2(e.shared_secret, token, cpr.pinUvAuthToken.?); + PinUvAuth.decrypt_v2(e.shared_secret.get(), token, cpr.pinUvAuthToken.?.get()); }, } return token; diff --git a/lib/ctap/commands/authenticator/authenticatorClientPin.zig b/lib/ctap/commands/authenticator/authenticatorClientPin.zig index 6119257..ef238e2 100644 --- a/lib/ctap/commands/authenticator/authenticatorClientPin.zig +++ b/lib/ctap/commands/authenticator/authenticatorClientPin.zig @@ -152,6 +152,120 @@ pub fn authenticatorClientPin( .pinUvAuthToken = (dt.ABS48B.fromSlice(&enc_shared_secret) catch unreachable).?, }; }, + .getPinUvAuthTokenUsingPinWithPermissions => { + // CTAP 2.1 - Get PIN token with permissions + if (client_pin_param.pinUvAuthProtocol == null or + client_pin_param.keyAgreement == null or + client_pin_param.pinHashEnc == null or + client_pin_param.permissions == null) + { + return fido.ctap.StatusCodes.ctap2_err_missing_parameter; + } + + if (client_pin_param.pinUvAuthProtocol.? != auth.token.version) { + return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; + } + + if (client_pin_param.permissions.? == 0) { + return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; + } + + const settings = auth.callbacks.read_settings(); + + // Check if PIN is set + if (settings.pin == null) { + return fido.ctap.StatusCodes.ctap2_err_pin_not_set; + } + + // Check PIN retries + if (settings.pinRetries == 0) { + return fido.ctap.StatusCodes.ctap2_err_pin_blocked; + } + + // Check if all requested permissions are valid + const options = auth.settings.options; + const cm = client_pin_param.cmPermissionSet() and (options.credMgmt == null or options.credMgmt.? == false); + const be = client_pin_param.bePermissionSet() and (options.bioEnroll == null); + const lbw = client_pin_param.lbwPermissionSet() and (options.largeBlobs == null or options.largeBlobs.? == false); + const acfg = client_pin_param.acfgPermissionSet() and (options.authnrCfg == null or options.authnrCfg.? == false); + + if (cm or be or lbw or acfg) { + return fido.ctap.StatusCodes.ctap2_err_unauthorized_permission; + } + + // Obtain the shared secret + const shared_secret = auth.token.ecdh( + client_pin_param.keyAgreement.?, + ) catch { + return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; + }; + + // Decrypt the PIN hash + var decrypted_pin_hash: [16]u8 = undefined; + const pin_hash_enc = client_pin_param.pinHashEnc.?.get(); + const shared_secret_bytes = shared_secret.get(); + + // Create mutable key buffer (decrypt function pointer incorrectly requires mutable slice) + var key_buffer_v1: [32]u8 = undefined; + var key_buffer_v2: [64]u8 = undefined; + + switch (auth.token.version) { + .V1 => { + // V1 uses first 32 bytes of shared secret, pinHashEnc is just 16 bytes + @memcpy(&key_buffer_v1, shared_secret_bytes[0..32]); + auth.token.decrypt( + &key_buffer_v1, + &decrypted_pin_hash, + pin_hash_enc[0..16], + ); + }, + .V2 => { + // V2 needs full 64-byte shared secret + // decrypt_v2 extracts IV from demCiphertext[0..16] and decrypts from [16..] + // So we pass the full 32-byte pinHashEnc (IV + encrypted data) + @memcpy(&key_buffer_v2, shared_secret_bytes[0..64]); + auth.token.decrypt( + &key_buffer_v2, + &decrypted_pin_hash, + pin_hash_enc[0..32], + ); + }, + } + + // Verify PIN by comparing hashes + // Note: settings.pin already contains the SHA-256 hash of the PIN, + // so we compare the first 16 bytes directly with the decrypted PIN hash + const stored_pin_hash = settings.pin.?[0..16]; + + if (!std.mem.eql(u8, &decrypted_pin_hash, stored_pin_hash)) { + std.log.err("PIN verification failed - hashes don't match", .{}); + return fido.ctap.StatusCodes.ctap2_err_pin_invalid; + } + + // PIN correct - generate and encrypt token with permissions + auth.token.resetPinUvAuthToken(); + auth.token.beginUsingPinUvAuthToken(false, auth.milliTimestamp()); + auth.token.permissions = client_pin_param.permissions.?; + + // If the rpId parameter is present, associate it with the token + if (client_pin_param.rpId) |rpId| { + auth.token.setRpId(rpId.get()) catch { + return fido.ctap.StatusCodes.ctap1_err_other; + }; + } + + var enc_shared_secret: [48]u8 = undefined; + auth.token.encrypt( + &auth.token, + shared_secret_bytes, + enc_shared_secret[0..], + auth.token.pin_token[0..], + ); + + client_pin_response = .{ + .pinUvAuthToken = (dt.ABS48B.fromSlice(&enc_shared_secret) catch unreachable).?, + }; + }, else => { return fido.ctap.StatusCodes.ctap2_err_invalid_subcommand; },