├── .gitignore ├── CITATION.cff ├── LICENSE ├── README.md ├── bindings ├── c │ ├── include │ │ └── keylib.h │ └── src │ │ └── keylib.zig ├── linux │ ├── include │ │ └── uhid.h │ └── src │ │ ├── common.zig │ │ ├── hid.zig │ │ ├── uhid-c.zig │ │ └── uhid.zig └── python │ ├── README.md │ ├── setup.py │ ├── uhid.pxd │ └── uhidmodule.pyx ├── build.zig ├── build.zig.zon ├── example ├── authenticator.zig └── client.zig ├── lib ├── client.zig ├── client │ ├── Transport.zig │ ├── Transports.zig │ ├── cbor_commands.zig │ ├── error.zig │ └── transports │ │ ├── ctaphid │ │ └── ctaphid.zig │ │ └── usb.zig ├── common │ ├── AttestationStatement.zig │ ├── AttestationStatementFormatIdentifiers.zig │ ├── AttestationType.zig │ ├── AttestedCredentialData.zig │ ├── AuthenticatorData.zig │ ├── AuthenticatorOptions.zig │ ├── AuthenticatorTransports.zig │ ├── AuthenticatorVersions.zig │ ├── Certifications.zig │ ├── PublicKeyCredentialDescriptor.zig │ ├── PublicKeyCredentialParameters.zig │ ├── PublicKeyCredentialType.zig │ ├── RelyingParty.zig │ ├── User.zig │ └── data_types.zig ├── ctap │ ├── StatusCodes.zig │ ├── auth │ │ ├── Authenticator.zig │ │ ├── Authenticator.zig.old │ │ ├── Callbacks.zig │ │ ├── Credential.zig │ │ ├── Meta.zig │ │ ├── Options.zig │ │ ├── Response.zig │ │ └── Settings.zig │ ├── commands │ │ ├── Commands.zig │ │ └── authenticator │ │ │ ├── authenticatorClientPin.zig │ │ │ ├── authenticatorCredentialManagement.zig │ │ │ ├── authenticatorGetAssertion.zig │ │ │ ├── authenticatorGetNextAssertion.zig │ │ │ ├── authenticatorMakeCredential.zig │ │ │ ├── authenticatorSelection.zig │ │ │ ├── get_info.zig │ │ │ └── helper.zig │ ├── crypto │ │ ├── Id.zig │ │ ├── SigAlg.zig │ │ ├── ecdh.zig │ │ ├── ecdsa.zig │ │ ├── master_secret.zig │ │ └── sigalgs │ │ │ └── Es256.zig │ ├── extensions │ │ ├── CredentialCreationPolicy.zig │ │ ├── Extensions.zig │ │ └── HmacSecret.zig │ ├── pinuv │ │ └── PinUvAuth.zig │ ├── request │ │ ├── ClientPin.zig │ │ ├── CredentialManagement.zig │ │ ├── GetAssertion.zig │ │ └── MakeCredential.zig │ ├── response │ │ ├── ClientPin.zig │ │ ├── CredentialManagement.zig │ │ ├── GetAssertion.zig │ │ └── MakeCredential.zig │ └── transports │ │ └── ctaphid │ │ ├── Cmd.zig │ │ ├── authenticator.zig │ │ ├── message.zig │ │ └── misc.zig └── main.zig └── static ├── design.odg ├── design.pdf ├── design.png └── pinUvAuthToken.odg /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | .zig-cache 3 | zig-out 4 | zbor 5 | *.swp 6 | 7 | # Linux Kernel Modules 8 | *.ko 9 | *.mod* 10 | *.o 11 | *.order 12 | *.symvers 13 | *.cmd 14 | 15 | # Database files 16 | *.db 17 | *.cks 18 | 19 | test.csv 20 | 21 | # Python bindings 22 | *.cpython*.so 23 | build 24 | uhidmodule.c 25 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: keylib 6 | message: >- 7 | If you use this software, please cite it using the 8 | metadata from this file. 9 | type: software 10 | authors: 11 | - given-names: David Pierre 12 | family-names: Sugar 13 | email: david@thesugar.de 14 | orcid: 'https://orcid.org/0009-0007-0056-602X' 15 | repository-code: 'https://github.com/Zig-Sec/keylib' 16 | abstract: A FIDO2 and Passkey compatible authentication library. 17 | keywords: 18 | - passkey 19 | - zig 20 | - package 21 | - library 22 | - authentication 23 | - cryptography 24 | - fido2 25 | license: MIT 26 | version: 0.6.0 27 | date-released: '2025-03-15' 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 - 2023 David P. Sugar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bindings/c/include/keylib.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | typedef enum{ 5 | // The given operation was successful 6 | Error_SUCCESS = 0, 7 | // The given value already exists 8 | Error_DoesAlreadyExist = -1, 9 | // The requested value doesn't exist 10 | Error_DoesNotExist = -2, 11 | // Credentials can't be inserted into the key-store 12 | Error_KeyStoreFull = -3, 13 | // The client ran out of memory 14 | Error_OutOfMemory = -4, 15 | // The operation timed out 16 | Error_Timeout = -5, 17 | // Unspecified operation 18 | Error_Other = -6, 19 | } Error; 20 | 21 | typedef enum{ 22 | // The user has denied the action 23 | UpResult_Denied = 0, 24 | // The user has accepted the action 25 | UpResult_Accepted = 1, 26 | // The user presence check has timed out 27 | UpResult_Timeout = 2, 28 | } UpResult; 29 | 30 | typedef enum{ 31 | // The user has denied the action 32 | UvResult_Denied = 0, 33 | // The user has accepted the action 34 | UvResult_Accepted = 1, 35 | // The user has accepted the action 36 | UvResult_AcceptedWithUp = 2, 37 | // The user presence check has timed out 38 | UvResult_Timeout = 3, 39 | } UvResult; 40 | 41 | typedef enum{ 42 | Transports_usb = 1, 43 | Transports_nfc = 2, 44 | Transports_ble = 4, 45 | } Transports; 46 | 47 | typedef struct{ 48 | // User presence request; user and rp might be NULL! 49 | UpResult (*up)(const char* info, const char* user, const char* rp); 50 | // User verification request; user and rp might be NULL! 51 | UvResult (*uv)(const char* info, const char* user, const char* rp); 52 | // Callback for selecting a user account. 53 | // The platform is expected to return the index of the selected user or an error. 54 | int (*select)(const char* rpId, char** users); 55 | // Read the payload specified by id and rp into out. 56 | // The allocated memory is owned by the caller and he is responsible for freeing it. 57 | // Returns either the length of the string assigned to out or an error. 58 | int (*read)(const char* id, const char* rp, char*** out); 59 | // Persist the given data; the id is considered unique. 60 | int (*write)(const char* id, const char* rp, const char* data); 61 | // Delete the entry with the given id. 62 | int (*del)(const char* id); 63 | } Callbacks; 64 | 65 | typedef struct{ 66 | // A UUID/ String representing the type of authenticator. 67 | char aaguid[16]; 68 | } AuthSettings; 69 | 70 | void* auth_init(Callbacks); 71 | void auth_deinit(void*); 72 | void auth_handle(void*, void*); 73 | 74 | void* ctaphid_init(); 75 | void ctaphid_deinit(void*); 76 | void* ctaphid_handle(void*, const char*, size_t); 77 | void* ctaphid_iterator(void*); 78 | int ctaphid_iterator_next(void*, char*); 79 | void ctaphid_iterator_deinit(void*); 80 | -------------------------------------------------------------------------------- /bindings/c/src/keylib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const allocator = std.heap.c_allocator; 3 | 4 | const keylib = @import("keylib"); 5 | const Auth = keylib.ctap.authenticator.Auth; 6 | 7 | const cb = keylib.ctap.authenticator.callbacks; 8 | pub const Error = cb.Error; 9 | pub const UpResult = cb.UpResult; 10 | pub const Callbacks = cb.Callbacks; 11 | 12 | const CtapHid = keylib.ctap.transports.ctaphid.authenticator.CtapHid; 13 | const CtapHidMessageIterator = keylib.ctap.transports.ctaphid.authenticator.CtapHidMessageIterator; 14 | const CtapHidMsg = keylib.ctap.transports.ctaphid.authenticator.CtapHidMsg; 15 | 16 | pub const AuthSettings = extern struct { 17 | aaguid: [16]u8 = "\x6f\x15\x82\x74\xaa\xb6\x44\x3d\x9b\xcf\x8a\x3f\x69\x29\x7c\x88".*, 18 | }; 19 | 20 | export fn auth_init(callbacks: Callbacks, settings: AuthSettings) ?*anyopaque { 21 | var a = allocator.create(Auth) catch { 22 | return null; 23 | }; 24 | 25 | a.* = keylib.ctap.authenticator.Auth{ 26 | // The callbacks are the interface between the authenticator and the rest of the application (see below). 27 | .callbacks = callbacks, 28 | // The commands map from a command code to a command function. All functions have the 29 | // same interface and you can implement your own to extend the authenticator beyond 30 | // the official spec, e.g. add a command to store passwords. 31 | .commands = &.{ 32 | .{ .cmd = 0x01, .cb = keylib.ctap.commands.authenticator.authenticatorMakeCredential }, 33 | .{ .cmd = 0x02, .cb = keylib.ctap.commands.authenticator.authenticatorGetAssertion }, 34 | .{ .cmd = 0x04, .cb = keylib.ctap.commands.authenticator.authenticatorGetInfo }, 35 | .{ .cmd = 0x06, .cb = keylib.ctap.commands.authenticator.authenticatorClientPin }, 36 | .{ .cmd = 0x0b, .cb = keylib.ctap.commands.authenticator.authenticatorSelection }, 37 | }, 38 | // The settings are returned by a getInfo request and describe the capabilities 39 | // of your authenticator. Make sure your configuration is valid based on the 40 | // CTAP2 spec! 41 | .settings = .{ 42 | // Those are the FIDO2 spec you support 43 | .versions = &.{ .FIDO_2_0, .FIDO_2_1 }, 44 | // The extensions are defined as strings which should make it easy to extend 45 | // the authenticator (in combination with a new command). 46 | .extensions = &.{"credProtect"}, 47 | // This should be unique for all models of the same authenticator. 48 | .aaguid = settings.aaguid, 49 | .options = .{ 50 | // We don't support the credential management command. If you want to 51 | // then you need to implement it yourself and add it to commands and 52 | // set this flag to true. 53 | .credMgmt = false, 54 | // We support discoverable credentials, a.k.a resident keys, a.k.a passkeys 55 | .rk = true, 56 | // We support built in user verification (see the callback below) 57 | .uv = true, 58 | // This is a platform authenticator even if we use usb for ipc 59 | .plat = true, 60 | // We don't support client pin but you could also add the command 61 | // yourself and set this to false (not initialized) or true (initialized). 62 | .clientPin = null, 63 | // We support pinUvAuthToken 64 | .pinUvAuthToken = true, 65 | // If you want to enforce alwaysUv you also have to set this to true. 66 | .alwaysUv = false, 67 | }, 68 | // The pinUvAuth protocol to support. This library implements V1 and V2. 69 | .pinUvAuthProtocols = &.{.V2}, 70 | // The transports your authenticator supports. 71 | .transports = &.{.usb}, 72 | // The algorithms you support. 73 | .algorithms = &.{.{ .alg = .Es256 }}, 74 | .firmwareVersion = 0xcafe, 75 | .remainingDiscoverableCredentials = 100, 76 | }, 77 | // Here we initialize the pinUvAuth token data structure wich handles the generation 78 | // and management of pinUvAuthTokens. 79 | .token = keylib.ctap.pinuv.PinUvAuth.v2(std.crypto.random), 80 | // Here we set the supported algorithm. You can also implement your 81 | // own and add them here. 82 | .algorithms = &.{ 83 | keylib.ctap.crypto.algorithms.Es256, 84 | }, 85 | // This allocator is used to allocate memory and has to be the same 86 | // used for the callbacks. 87 | .allocator = allocator, 88 | // A function to get the epoch time as i64. 89 | .milliTimestamp = std.time.milliTimestamp, 90 | // A cryptographically secure random number generator 91 | .random = std.crypto.random, 92 | // If you don't want to increment the sign counts 93 | // of credentials (e.g. because you sync them between devices) 94 | // set this to true. 95 | .constSignCount = true, 96 | }; 97 | a.init() catch { 98 | return null; 99 | }; 100 | 101 | return @as(*anyopaque, @ptrCast(a)); 102 | } 103 | 104 | export fn auth_deinit(a: *anyopaque) void { 105 | const auth = @as(*Auth, @ptrCast(@alignCast(a))); 106 | auth.allocator.destroy(auth); 107 | } 108 | 109 | export fn auth_handle( 110 | a: *anyopaque, 111 | m: ?*anyopaque, 112 | ) void { 113 | if (m == null) return; 114 | 115 | const auth = @as(*Auth, @ptrCast(@alignCast(a))); 116 | const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(m.?))); 117 | 118 | switch (msg.cmd) { 119 | .cbor => { 120 | var out: [7609]u8 = undefined; // TODO: we have to make this configurable 121 | const r = auth.handle(&out, msg.getData()); 122 | @memcpy(msg._data[0..r.len], r); 123 | msg.len = r.len; 124 | }, 125 | else => {}, 126 | } 127 | } 128 | 129 | export fn ctaphid_init() ?*anyopaque { 130 | const c = allocator.create(CtapHid) catch { 131 | return null; 132 | }; 133 | 134 | c.* = CtapHid.init(allocator, std.crypto.random); 135 | 136 | return @as(*anyopaque, @ptrCast(c)); 137 | } 138 | 139 | export fn ctaphid_deinit(a: *anyopaque) void { 140 | const c = @as(*CtapHid, @ptrCast(@alignCast(a))); 141 | c.deinit(); 142 | allocator.destroy(c); 143 | } 144 | 145 | /// This function either returns null or a pointer to a CtapHidMsg. 146 | export fn ctaphid_handle( 147 | ctap: *anyopaque, 148 | packet: [*c]const u8, 149 | len: usize, 150 | ) ?*anyopaque { 151 | const ctaphid = @as(*CtapHid, @ptrCast(@alignCast(ctap))); 152 | 153 | if (ctaphid.handle(packet[0..len])) |res| { 154 | const msg = allocator.create(CtapHidMsg) catch { 155 | return null; 156 | }; 157 | msg.* = res; 158 | return @as(*anyopaque, @ptrCast(msg)); 159 | } else { 160 | return null; 161 | } 162 | } 163 | 164 | export fn ctaphid_iterator(m: *anyopaque) ?*anyopaque { 165 | const msg = @as(*CtapHidMsg, @ptrCast(@alignCast(m))); 166 | const iter = allocator.create(CtapHidMessageIterator) catch { 167 | return null; 168 | }; 169 | 170 | iter.* = msg.iterator(); 171 | return @as(*anyopaque, @ptrCast(iter)); 172 | } 173 | 174 | export fn ctaphid_iterator_next(iter: *anyopaque, out: [*c]u8) c_int { 175 | const iterator = @as(*CtapHidMessageIterator, @ptrCast(@alignCast(iter))); 176 | 177 | if (iterator.next()) |packet| { 178 | @memcpy(out[0..packet.len], packet); // 64 bytes 179 | return 1; 180 | } else { 181 | return 0; 182 | } 183 | } 184 | 185 | export fn ctaphid_iterator_deinit(iter: *anyopaque) void { 186 | const iterator = @as(*CtapHidMessageIterator, @ptrCast(@alignCast(iter))); 187 | iterator.deinit(); 188 | allocator.destroy(iterator); 189 | } 190 | -------------------------------------------------------------------------------- /bindings/linux/include/uhid.h: -------------------------------------------------------------------------------- 1 | int uhid_open(); 2 | void uhid_close(int fd); 3 | int uhid_read_packet(int fd, char* out); 4 | int uhid_write_packet(int fd, char* in, size_t len); 5 | -------------------------------------------------------------------------------- /bindings/linux/src/common.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const hid = @import("hid.zig"); 3 | 4 | const uhid = @cImport( 5 | @cInclude("linux/uhid.h"), 6 | ); 7 | 8 | pub fn create(fd: std.fs.File) !void { 9 | const device_name = "fido2-device"; 10 | 11 | var event = std.mem.zeroes(uhid.uhid_event); 12 | event.type = uhid.UHID_CREATE2; 13 | @memcpy(event.u.create2.name[0..device_name.len], device_name); 14 | @memcpy( 15 | event.u.create2.rd_data[0..hid.ReportDescriptorFidoU2f[0..].len], 16 | hid.ReportDescriptorFidoU2f[0..], 17 | ); 18 | event.u.create2.rd_size = hid.ReportDescriptorFidoU2f[0..].len; 19 | event.u.create2.bus = uhid.BUS_USB; 20 | event.u.create2.vendor = 0x15d9; 21 | event.u.create2.product = 0x0a37; 22 | event.u.create2.version = 0; 23 | event.u.create2.country = 0; 24 | 25 | try uhid_write(fd, &event); 26 | } 27 | 28 | pub fn uhid_write(fd: std.fs.File, event: *uhid.uhid_event) !void { 29 | fd.writeAll(std.mem.asBytes(event)) catch |e| { 30 | std.log.err("Error writing to uhid: {}\n", .{e}); 31 | return e; 32 | }; 33 | } 34 | 35 | pub fn destroy(fd: std.fs.File) !void { 36 | var event = std.mem.zeroes(uhid.uhid_event); 37 | event.type = uhid.UHID_DESTROY; 38 | return uhid_write(fd, &event); 39 | } 40 | -------------------------------------------------------------------------------- /bindings/linux/src/uhid-c.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const hid = @import("hid.zig"); 3 | const common = @import("common.zig"); 4 | 5 | const uhid = @cImport( 6 | @cInclude("linux/uhid.h"), 7 | ); 8 | 9 | const PATH = "/dev/uhid"; 10 | 11 | const create = common.create; 12 | const uhid_write = common.uhid_write; 13 | const destroy = common.destroy; 14 | 15 | export fn uhid_open() c_int { 16 | var device = std.fs.openFileAbsolute(PATH, .{ 17 | .mode = .read_write, 18 | }) catch { 19 | std.log.err("Can't open uhid-cdev {s}\n", .{PATH}); 20 | return -1; 21 | }; 22 | const flags = std.posix.fcntl(device.handle, 3, 0) catch { 23 | std.log.err("Can't get file stats", .{}); 24 | device.close(); 25 | return -1; 26 | }; 27 | _ = std.posix.fcntl(device.handle, 4, flags | 2048) catch { 28 | std.log.err("Can't set file to non-blocking", .{}); 29 | device.close(); 30 | return -1; 31 | }; 32 | 33 | create(device) catch { 34 | std.log.err("Unabel to create CTAPHID device", .{}); 35 | device.close(); 36 | return -1; 37 | }; 38 | 39 | return @intCast(device.handle); 40 | } 41 | 42 | export fn uhid_read_packet(fd: c_int, out: [*c]u8) c_int { 43 | var device = std.fs.File{ .handle = @intCast(fd) }; 44 | var event = std.mem.zeroes(uhid.uhid_event); 45 | _ = device.read(std.mem.asBytes(&event)) catch { 46 | return 0; 47 | }; 48 | 49 | if (event.u.output.size < 1) return 0; 50 | 51 | @memcpy(out[0 .. event.u.output.size - 1], event.u.output.data[1..event.u.output.size]); 52 | return @intCast(event.u.output.size - 1); 53 | } 54 | 55 | export fn uhid_write_packet(fd: c_int, in: [*c]u8, len: usize) c_int { 56 | const device = std.fs.File{ .handle = @intCast(fd) }; 57 | var rev = std.mem.zeroes(uhid.uhid_event); 58 | rev.type = uhid.UHID_INPUT; 59 | @memcpy(rev.u.input.data[0..len], in[0..len]); 60 | rev.u.input.size = @as(c_ushort, @intCast(len)); 61 | 62 | uhid_write(device, &rev) catch { 63 | std.log.err("failed to send CTAPHID packet\n", .{}); 64 | return 0; 65 | }; 66 | 67 | return @intCast(rev.u.input.size); 68 | } 69 | 70 | export fn uhid_close(fd: c_int) void { 71 | var device = std.fs.File{ .handle = @intCast(fd) }; 72 | destroy(device) catch {}; 73 | device.close(); 74 | } 75 | -------------------------------------------------------------------------------- /bindings/linux/src/uhid.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const hid = @import("hid.zig"); 3 | const common = @import("common.zig"); 4 | 5 | const uhid = @cImport( 6 | @cInclude("linux/uhid.h"), 7 | ); 8 | 9 | const PATH = "/dev/uhid"; 10 | 11 | const create = common.create; 12 | const uhid_write = common.uhid_write; 13 | const destroy = common.destroy; 14 | 15 | pub const Uhid = struct { 16 | device: std.fs.File, 17 | 18 | pub fn open() !@This() { 19 | var device = std.fs.openFileAbsolute(PATH, .{ 20 | .mode = .read_write, 21 | }) catch |e| { 22 | std.log.err("Can't open uhid-cdev {s}\n", .{PATH}); 23 | return e; 24 | }; 25 | 26 | const flags = std.posix.fcntl(device.handle, 3, 0) catch |e| { 27 | std.log.err("Can't get file stats", .{}); 28 | device.close(); 29 | return e; 30 | }; 31 | _ = std.posix.fcntl(device.handle, 4, flags | 2048) catch |e| { 32 | std.log.err("Can't set file to non-blocking", .{}); 33 | device.close(); 34 | return e; 35 | }; 36 | 37 | create(device) catch |e| { 38 | std.log.err("Unabel to create CTAPHID device", .{}); 39 | device.close(); 40 | return e; 41 | }; 42 | 43 | return .{ 44 | .device = device, 45 | }; 46 | } 47 | 48 | pub fn close(self: *const @This()) void { 49 | destroy(self.device) catch { 50 | std.log.err("Unabel to destroy UHID device", .{}); 51 | }; 52 | self.device.close(); 53 | } 54 | 55 | pub fn read(self: *const @This(), out: *[64]u8) ?[]u8 { 56 | var event = std.mem.zeroes(uhid.uhid_event); 57 | _ = self.device.read(std.mem.asBytes(&event)) catch { 58 | return null; 59 | }; 60 | 61 | if (event.type != uhid.UHID_OUTPUT) { 62 | return null; 63 | } 64 | 65 | if (event.u.output.size < 1) return null; 66 | 67 | @memcpy(out[0 .. event.u.output.size - 1], event.u.output.data[1..event.u.output.size]); 68 | return out[0 .. event.u.output.size - 1]; 69 | } 70 | 71 | pub fn write(self: *const @This(), in: []const u8) !void { 72 | if (in.len > 64) return error.InvalidSizedPacket; 73 | 74 | var rev = std.mem.zeroes(uhid.uhid_event); 75 | rev.type = uhid.UHID_INPUT; 76 | @memcpy(rev.u.input.data[0..in.len], in[0..]); 77 | rev.u.input.size = @as(c_ushort, @intCast(in.len)); 78 | 79 | uhid_write(self.device, &rev) catch |e| { 80 | std.log.err("failed to send CTAPHID packet\n", .{}); 81 | return e; 82 | }; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /bindings/python/README.md: -------------------------------------------------------------------------------- 1 | # Python3 bindings 2 | 3 | Currently you have to compile and link the Python3 modules using cpython. 4 | 5 | 1. make sure you have Python3 and [cython](https://cython.org/) installed. 6 | 2. build the `keylib` and `uhid` libraries with optimization (e.g. `zig build -Doptimize=ReleaseSmall`) 7 | 3. run `python3 setup.py build_ext --inplace` to build the python modules. 8 | -------------------------------------------------------------------------------- /bindings/python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | from Cython.Build import cythonize 3 | 4 | uhid_module = Extension( 5 | "uhid", 6 | sources=["./uhidmodule.pyx"], 7 | include_dirs=["../../zig-out/include"], 8 | libraries=["uhid"], 9 | library_dirs=["../../zig-out/lib"], 10 | ) 11 | 12 | setup( 13 | ext_modules = cythonize(uhid_module), 14 | ) 15 | -------------------------------------------------------------------------------- /bindings/python/uhid.pxd: -------------------------------------------------------------------------------- 1 | cdef struct Uhid: 2 | int fd 3 | -------------------------------------------------------------------------------- /bindings/python/uhidmodule.pyx: -------------------------------------------------------------------------------- 1 | cimport uhid 2 | from libc.stdlib cimport malloc, free 3 | from libc.stdint cimport uintptr_t 4 | 5 | cdef extern from "keylib/uhid.h": 6 | int uhid_open() 7 | void uhid_close(int fd) 8 | int uhid_read_packet(int fd, char* out) 9 | int uhid_write_packet(int fd, char* inp, uintptr_t l) 10 | 11 | def open(): 12 | """ 13 | Open a virtual USB HID device that presents itself as FIDO2 authenticator 14 | 15 | Returns: 16 | A struct wrapping the file descriptor of the hid file 17 | """ 18 | cdef uhid.Uhid obj 19 | obj.fd = uhid_open() 20 | return obj 21 | 22 | def close(obj): 23 | uhid_close(obj['fd']) 24 | 25 | def read_packet(obj) -> bytes: 26 | cdef char* buffer = malloc(256) 27 | if buffer is NULL: 28 | raise MemoryError("Failed to allocate memory for uhid packet") 29 | 30 | try: 31 | l = uhid_read_packet(obj['fd'], buffer) 32 | result = bytes(buffer[:l]) 33 | finally: 34 | free(buffer) 35 | 36 | return result 37 | 38 | def write_packet(obj,bytes data): 39 | uhid_write_packet(obj['fd'], data, len(data)) 40 | 41 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | // ++++++++++++++++++++++++++++++++++++++++++++ 8 | // Dependencies 9 | // ++++++++++++++++++++++++++++++++++++++++++++ 10 | 11 | const zbor_dep = b.dependency("zbor", .{ 12 | .target = target, 13 | .optimize = optimize, 14 | }); 15 | const zbor_module = zbor_dep.module("zbor"); 16 | 17 | const hidapi_dep = b.dependency("hidapi", .{ 18 | .target = target, 19 | .optimize = optimize, 20 | }); 21 | 22 | const uuid_dep = b.dependency("uuid", .{ 23 | .target = target, 24 | .optimize = optimize, 25 | }); 26 | const uuid_module = uuid_dep.module("uuid"); 27 | 28 | // ++++++++++++++++++++++++++++++++++++++++++++ 29 | // Module 30 | // ++++++++++++++++++++++++++++++++++++++++++++ 31 | 32 | // Authenticator Module 33 | // ------------------------------------------------ 34 | 35 | const keylib_module = b.addModule("keylib", .{ 36 | .root_source_file = b.path("lib/main.zig"), 37 | .imports = &.{ 38 | .{ .name = "zbor", .module = zbor_module }, 39 | .{ .name = "uuid", .module = uuid_module }, 40 | }, 41 | .target = target, 42 | .optimize = optimize, 43 | }); 44 | try b.modules.put(b.dupe("keylib"), keylib_module); 45 | 46 | const uhid_module = b.addModule("uhid", .{ 47 | .root_source_file = b.path("bindings/linux/src/uhid.zig"), 48 | .imports = &.{}, 49 | .target = target, 50 | .optimize = optimize, 51 | }); 52 | try b.modules.put(b.dupe("uhid"), uhid_module); 53 | 54 | // Re-export zbor module 55 | try b.modules.put(b.dupe("zbor"), zbor_module); 56 | 57 | // Client Module 58 | // ------------------------------------------------ 59 | 60 | const client_module = b.addModule("clientlib", .{ 61 | .root_source_file = b.path("lib/client.zig"), 62 | .imports = &.{ 63 | .{ .name = "zbor", .module = zbor_module }, 64 | .{ .name = "keylib", .module = keylib_module }, 65 | }, 66 | .target = target, 67 | .optimize = optimize, 68 | }); 69 | try b.modules.put(b.dupe("clientlib"), client_module); 70 | client_module.linkLibrary(hidapi_dep.artifact("hidapi")); 71 | 72 | // Examples 73 | // ------------------------------------------------ 74 | 75 | const client_example_mod = b.createModule(.{ 76 | .root_source_file = b.path("example/client.zig"), 77 | .target = target, 78 | .optimize = optimize, 79 | }); 80 | 81 | var client_example = b.addExecutable(.{ 82 | .name = "client", 83 | .root_module = client_example_mod, 84 | }); 85 | client_example.root_module.addImport("client", client_module); 86 | 87 | const client_example_step = b.step("client-example", "Build the client application example"); 88 | client_example_step.dependOn(&b.addInstallArtifact(client_example, .{}).step); 89 | 90 | const authenticator_example_mod = b.createModule(.{ 91 | .root_source_file = b.path("example/authenticator.zig"), 92 | .target = target, 93 | .optimize = optimize, 94 | }); 95 | 96 | var authenticator_example = b.addExecutable(.{ 97 | .name = "authenticator", 98 | .root_module = authenticator_example_mod, 99 | }); 100 | authenticator_example.root_module.addImport("keylib", keylib_module); 101 | authenticator_example.root_module.addImport("uhid", uhid_module); 102 | authenticator_example.root_module.addImport("zbor", zbor_dep.module("zbor")); 103 | authenticator_example.linkLibC(); 104 | 105 | const authenticator_example_step = b.step("auth-example", "Build the authenticator example"); 106 | authenticator_example_step.dependOn(&b.addInstallArtifact(authenticator_example, .{}).step); 107 | 108 | // C bindings 109 | // ------------------------------------------------ 110 | 111 | //const c_bindings = b.addStaticLibrary(.{ 112 | // .name = "keylib", 113 | // .root_source_file = .{ .path = "bindings/c/src/keylib.zig" }, 114 | // .target = target, 115 | // .optimize = optimize, 116 | //}); 117 | //c_bindings.root_module.addImport("keylib", keylib_module); 118 | //c_bindings.linkLibC(); 119 | //c_bindings.installHeadersDirectory( 120 | // b.path("bindings/c/include"), 121 | // "keylib", 122 | // .{ 123 | // .exclude_extensions = &.{}, 124 | // .include_extensions = &.{".h"}, 125 | // }, 126 | //); 127 | //b.installArtifact(c_bindings); 128 | 129 | const uhid_mod = b.createModule(.{ 130 | .root_source_file = b.path("bindings/linux/src/uhid-c.zig"), 131 | .target = target, 132 | .optimize = optimize, 133 | }); 134 | 135 | const uhid = b.addLibrary(.{ 136 | .linkage = .static, 137 | .name = "uhid", 138 | .root_module = uhid_mod, 139 | }); 140 | uhid.linkLibC(); 141 | uhid.installHeadersDirectory( 142 | b.path("bindings/linux/include"), 143 | "keylib", 144 | .{ 145 | .exclude_extensions = &.{}, 146 | .include_extensions = &.{".h"}, 147 | }, 148 | ); 149 | b.installArtifact(uhid); 150 | 151 | // Python bindings 152 | // ------------------------------------------------ 153 | 154 | // TODO: Figure out how to compile the cython code myself 155 | 156 | //const generate_uhid_bindings = b.addSystemCommand( 157 | // &[_][]const u8{ "cython3", "bindings/python/uhidmodule.pyx", "-I", "bindings/linux/include", "-o", "bindings/python/uhidmodule.c", "-3" }, 158 | //); 159 | 160 | //const uhid_py = b.addSharedLibrary(.{ 161 | // .name = "uhid.linux", 162 | // .root_source_file = .{ .path = "bindings/python/uhidmodule.c" }, 163 | // .target = target, 164 | // .optimize = optimize, 165 | //}); 166 | //uhid_py.step.dependOn(&generate_uhid_bindings.step); 167 | //uhid_py.linkLibrary(uhid); 168 | //uhid_py.linkSystemLibrary("python3"); 169 | //uhid_py.linkLibC(); 170 | //b.installArtifact(uhid_py); 171 | 172 | //const build_python_bindings_step = b.step("uhid-py", "Build uhid python bindings"); 173 | //build_python_bindings_step.dependOn(&uhid_py.step); 174 | 175 | // ++++++++++++++++++++++++++++++++++++++++++++ 176 | // Tests 177 | // ++++++++++++++++++++++++++++++++++++++++++++ 178 | 179 | // Creates a step for unit testing. 180 | const lib_tests = b.addTest(.{ 181 | .root_module = keylib_module, 182 | }); 183 | 184 | const test_step = b.step("test", "Run library tests"); 185 | test_step.dependOn(&b.addRunArtifact(lib_tests).step); 186 | } 187 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .keylib, 3 | .version = "0.6.1", 4 | .fingerprint = 0xc25aabcf9323b699, 5 | .dependencies = .{ 6 | .zbor = .{ 7 | .url = "https://github.com/r4gus/zbor/archive/refs/tags/0.17.2.tar.gz", 8 | .hash = "zbor-0.17.0-kr-CoHIkAwCy2WhoS6MgwSvyQsLWzzNy6a7UTHqMPMmO", 9 | }, 10 | .hidapi = .{ 11 | .url = "https://github.com/r4gus/hidapi/archive/refs/tags/0.15.0.tar.gz", 12 | .hash = "1220fcaee19f1bf69764c808ea7292062ebf4ec55f585ef298667c993e54bfbc8cb0", 13 | }, 14 | .uuid = .{ 15 | .url = "https://github.com/r4gus/uuid-zig/archive/refs/tags/0.3.1.tar.gz", 16 | .hash = "uuid-0.3.0-oOieIYF1AAA_BtE7FvVqqTn5uEYTvvz7ycuVnalCOf8C", 17 | }, 18 | }, 19 | .paths = .{ 20 | "bindings", 21 | "example", 22 | "lib", 23 | "static", 24 | "CITATION.cff", 25 | "LICENSE", 26 | "README.md", 27 | "build.zig", 28 | "build.zig.zon", 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /example/client.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const client = @import("client"); 3 | const authenticatorGetInfo = client.cbor_commands.authenticatorGetInfo; 4 | const client_pin = client.cbor_commands.client_pin; 5 | const cred_management = client.cbor_commands.cred_management; 6 | const Info = client.cbor_commands.Info; 7 | 8 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 9 | var allocator = gpa.allocator(); 10 | 11 | pub fn main() !void { 12 | const pw = if (std.os.argv.len >= 2) blk: { 13 | var i: usize = 0; 14 | while (std.os.argv[1][i] != 0) : (i += 1) {} 15 | break :blk std.os.argv[1][0..i]; 16 | } else { 17 | std.log.err("please provide a password", .{}); 18 | return; 19 | }; 20 | 21 | { 22 | // 1 The platform examines various option IDs in the authenti- 23 | // catorGetInfo response to determine its course of action 24 | 25 | // Get all devices connect to the platform 26 | var transports = try client.Transports.enumerate(allocator, .{}); 27 | defer transports.deinit(); 28 | 29 | // Choose a device 30 | if (transports.devices.len == 0) { 31 | std.log.err("No device found, exiting...", .{}); 32 | return; 33 | } 34 | 35 | var device = if (transports.devices.len == 1) blk: { 36 | break :blk &transports.devices[0]; 37 | } else blk: { 38 | std.log.info("Please choose a device from the following list:", .{}); 39 | for (transports.devices, 0..) |*device, i| { 40 | const x = try device.allocPrint(allocator); 41 | defer allocator.free(x); 42 | std.log.info(" {d} {s}", .{ i, x }); 43 | } 44 | // Here we would actually ask for some user input but 45 | // let's keep it simple... 46 | break :blk &transports.devices[0]; 47 | }; 48 | 49 | // Open a connection to the device 50 | try device.open(); 51 | defer device.close(); 52 | 53 | // Get information about the device and its capabilities 54 | const infos = try (try authenticatorGetInfo(device)).@"await"(allocator); 55 | defer infos.deinit(allocator); 56 | const info = try infos.deserializeCbor(Info, allocator); 57 | defer info.deinit(allocator); 58 | //std.log.info("info: {any}", .{info}); 59 | 60 | // 1.a If the credMgmt option is not present or false, exit and 61 | // fall back to manual selection. 62 | if ((info.options.credMgmt == null or !info.options.credMgmt.?) and 63 | (info.options.credentialMgmtPreview == null or !info.options.credentialMgmtPreview.?)) 64 | { 65 | std.log.err("The selected device doesn't support credMgmt", .{}); 66 | return; 67 | } 68 | 69 | // 1.b if both clientPin and uv option are either absent or false 70 | 71 | if (info.options.clientPin == null and info.options.uv == null) { 72 | std.log.err("The selected device doesn't support user verification", .{}); 73 | return; 74 | } 75 | 76 | if (!info.options.clientPin.? and !info.options.uv.?) { 77 | std.log.err("No user verification set up for device", .{}); 78 | return; 79 | } 80 | 81 | // 1.c if the uv option ID is present and set to true: 82 | var op: ?[]const u8 = null; 83 | 84 | // We prefer internal uv over pin 85 | if (info.options.uv != null and info.options.uv.?) { 86 | if (info.options.pinUvAuthToken != null and info.options.pinUvAuthToken.?) { 87 | op = "getPinUvAuthTokenUsingUvWithPermissions"; 88 | } 89 | } 90 | 91 | if (op == null) { 92 | if (info.options.pinUvAuthToken != null and info.options.pinUvAuthToken.?) { 93 | if (info.options.clientPin != null and info.options.clientPin.?) { 94 | op = "getPinUvAuthTokenUsingPinWithPermissions"; 95 | } 96 | } else { 97 | if (info.options.clientPin != null and info.options.clientPin.?) { 98 | op = "getPinToken"; 99 | } 100 | } 101 | } 102 | 103 | if (op == null) { 104 | std.log.err("Selected authenticator doesn't support pinUvAuthToken", .{}); 105 | return; 106 | } 107 | 108 | // 2 In preparation for obtaining pinUvAuthToken, the platform: 109 | 110 | // 2.a Obtains a shared secret 111 | 112 | if (info.pinUvAuthProtocols == null) { 113 | std.log.err("Device supports user verification but no pinUvAuthProtocols were returned as a result of calling getInfo", .{}); 114 | return; 115 | } 116 | 117 | const pinUvAuthProtocol = info.pinUvAuthProtocols.?[0]; 118 | 119 | var enc = try client_pin.getKeyAgreement(device, pinUvAuthProtocol, allocator); 120 | std.log.info("shared secret: {any}", .{enc}); 121 | 122 | // 3 Optain a pinUvAuthToken from the authenticator 123 | 124 | const token = if (std.mem.eql(u8, op.?, "getPinToken")) blk: { 125 | break :blk try client_pin.getPinToken(device, &enc, pw[0..], allocator); 126 | } else if (std.mem.eql(u8, op.?, "getPinUvAuthTokenUsingUvWithPermissions")) blk: { 127 | break :blk ""; // here we would return a token generated via getPinUvAuthTokenUsingUvWithPermissions 128 | } else blk: { 129 | break :blk ""; // here we would return a token generated via getPinUvAuthTokenUsingPinWithPermissions 130 | }; 131 | defer allocator.free(token); 132 | std.log.info("token: {s}", .{std.fmt.fmtSliceHexLower(token)}); 133 | 134 | // 4 the platform collects all RPs present on the given authenticator, removes RPs that 135 | // are not supported IdPs, and then selects the IdP for authentication 136 | 137 | // 4.a Create a empty set of idps 138 | var idps = std.ArrayList([]const u8).init(allocator); 139 | defer { 140 | for (idps.items) |idp| { 141 | allocator.free(idp); 142 | } 143 | } 144 | 145 | // 4.b Fill the set with RPs present on the authenticator 146 | const rp = try cred_management.enumerateRPsBegin(device, pinUvAuthProtocol, token, allocator, true); 147 | if (rp) |_rp| { 148 | try idps.append(try allocator.dupe(u8, _rp.rp.id.get())); 149 | 150 | var i: usize = 0; 151 | while (i < _rp.total.? - 1) : (i += 1) { 152 | if (try cred_management.enumerateRPsGetNextRP(device, allocator, true)) |rp2| { 153 | try idps.append(try allocator.dupe(u8, rp2.rp.id.get())); 154 | } 155 | } 156 | } else { 157 | std.log.info("no valid RPs found", .{}); 158 | return; 159 | } 160 | 161 | // 4.c Remove all RPs that are not valid IdPs 162 | var i: usize = 0; 163 | while (true) { 164 | if (i >= idps.items.len) break; 165 | if (!std.mem.eql(u8, "github.com", idps.items[i])) { 166 | // The only valid IdP in our case is github.com 167 | const s = idps.swapRemove(i); 168 | allocator.free(s); 169 | } else { 170 | i += 1; 171 | } 172 | } 173 | 174 | if (idps.items.len == 0) { 175 | std.log.info("no valid RPs found", .{}); 176 | return; 177 | } 178 | 179 | std.log.info("IdPs found:", .{}); 180 | for (idps.items) |idp| { 181 | std.log.info(" {s}", .{idp}); 182 | } 183 | 184 | const uri = try std.fmt.allocPrint(allocator, "https://{s}", .{idps.items[0]}); 185 | defer allocator.free(uri); 186 | 187 | // 5 if there has been a identity provider selected, authenticate 188 | // through the selected IdP 189 | 190 | var promise = try client.cbor_commands.credentials.get( 191 | device, 192 | uri, 193 | false, 194 | .{ 195 | .rpId = idps.items[0], 196 | .challenge = "\x01\x23\x45\x67\x89\xab", 197 | }, 198 | .{ 199 | .param = token, 200 | .protocol = pinUvAuthProtocol, 201 | }, 202 | allocator, 203 | ); 204 | 205 | while (true) { 206 | const S = promise.get(allocator); 207 | defer S.deinit(allocator); 208 | 209 | switch (S) { 210 | .pending => |p| { 211 | switch (p) { 212 | .processing => std.log.info("processing", .{}), 213 | .user_presence => std.log.info("user presence", .{}), 214 | .waiting => std.log.info("waiting", .{}), 215 | } 216 | }, 217 | .fulfilled => |d| { 218 | std.log.info("{s}", .{std.fmt.fmtSliceHexLower(d)}); 219 | break; 220 | }, 221 | .rejected => |e| { 222 | return e; 223 | }, 224 | } 225 | } 226 | } 227 | 228 | //if (gpa.detectLeaks()) { 229 | // std.log.info("leak", .{}); 230 | //} 231 | } 232 | -------------------------------------------------------------------------------- /lib/client.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Transports = @import("client/Transports.zig"); 4 | pub const cbor_commands = @import("client/cbor_commands.zig"); 5 | pub const err = @import("client/error.zig"); 6 | -------------------------------------------------------------------------------- /lib/client/Transport.zig: -------------------------------------------------------------------------------- 1 | //! Abstract representation of a USB, NFC, or Bluetooth device 2 | 3 | const std = @import("std"); 4 | const Self = @This(); 5 | 6 | pub const Error = error{ 7 | /// Unable to initialize the transport 8 | Init, 9 | OutOfMemory, 10 | Open, 11 | Read, 12 | Write, 13 | Timeout, 14 | NoChannel, 15 | InvalidPacketLength, 16 | InvalidPacket, 17 | InvalidCid, 18 | InvalidCmd, 19 | InvalidPar, 20 | InvalidLen, 21 | InvalidSeq, 22 | MsgTimeout, 23 | ChannelBusy, 24 | LockRequired, 25 | InvalidChannel, 26 | UnexpectedCommand, 27 | InvalidSequenceNumber, 28 | NonceMismatch, 29 | InvalidSize, 30 | Processing, 31 | UpNeeded, 32 | Other, 33 | }; 34 | 35 | /// Type erased pointer to the underlying object (e.g. Usb) 36 | obj: *anyopaque, 37 | 38 | _read: *const fn (self: *anyopaque, a: std.mem.Allocator) Error!?[]u8, 39 | _write: *const fn (self: *anyopaque, out: []const u8) Error!void, 40 | _close: *const fn (self: *anyopaque) void, 41 | _open: *const fn (self: *anyopaque) Error!void, 42 | _allocPrint: *const fn (self: *anyopaque, a: std.mem.Allocator) Error![]const u8, 43 | _deinit: *const fn (self: *anyopaque) void, 44 | 45 | pub fn read(self: *const Self, a: std.mem.Allocator) Error!?[]u8 { 46 | return self._read(self.obj, a); 47 | } 48 | 49 | pub fn write(self: *Self, out: []const u8) Error!void { 50 | return self._write(self.obj, out); 51 | } 52 | 53 | pub fn close(self: *Self) void { 54 | return self._close(self.obj); 55 | } 56 | 57 | pub fn open(self: *Self) Error!void { 58 | return self._open(self.obj); 59 | } 60 | 61 | pub fn allocPrint(self: *Self, a: std.mem.Allocator) Error![]const u8 { 62 | return self._allocPrint(self.obj, a); 63 | } 64 | 65 | pub fn deinit(self: *Self) void { 66 | return self._deinit(self.obj); 67 | } 68 | -------------------------------------------------------------------------------- /lib/client/Transports.zig: -------------------------------------------------------------------------------- 1 | //! A collection of Transport's 2 | //! 3 | //! Each Transport is the abstract representation of a (authenticator) device 4 | //! you can communicate with. 5 | 6 | const std = @import("std"); 7 | 8 | pub const Transport = @import("Transport.zig"); 9 | pub const usb = @import("transports/usb.zig"); 10 | 11 | pub const Error = error{ 12 | OutOfMemory, 13 | }; 14 | 15 | pub const EnumerateOptions = struct { 16 | funs: []const *const fn (a: std.mem.Allocator) Error!?[]Transport = &.{ 17 | usb.enumerate, 18 | }, 19 | }; 20 | 21 | const Self = @This(); 22 | 23 | devices: []Transport, 24 | allocator: std.mem.Allocator, 25 | 26 | pub fn deinit(self: *const Self) void { 27 | for (self.devices) |*dev| { 28 | dev.deinit(); 29 | } 30 | self.allocator.free(self.devices); 31 | } 32 | 33 | /// Find all connected devices 34 | pub fn enumerate(a: std.mem.Allocator, options: EnumerateOptions) Error!Self { 35 | var arr = std.ArrayList(Transport).init(a); 36 | defer arr.deinit(); 37 | 38 | for (options.funs) |fun| { 39 | if (try fun(a)) |v| { 40 | try arr.appendSlice(v); 41 | } 42 | } 43 | 44 | return Self{ 45 | .devices = try arr.toOwnedSlice(), 46 | .allocator = a, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /lib/client/error.zig: -------------------------------------------------------------------------------- 1 | pub const StatusCodes = error{ 2 | /// The command is not a valid CTAP command. 3 | ctap1_err_invalid_command, 4 | /// The command included an invalid parameter. 5 | ctap1_err_invalid_parameter, 6 | /// Invalid message or item length. 7 | ctap1_err_invalid_length, 8 | /// Invalid message sequencing. 9 | ctap1_err_invalid_seq, 10 | /// Message timed out. 11 | ctap1_err_timeout, 12 | /// Channel busy. 13 | ctap1_err_channel_busy, 14 | /// Command requires channel lock. 15 | ctap1_err_lock_required, 16 | /// Command not allowed on this cid. 17 | ctap1_err_invalid_channel, 18 | /// Invalid/ unexpected CBOR error. 19 | ctap2_err_cbor_unexpected_type, 20 | /// Error when parsing CBOR. 21 | ctap2_err_invalid_cbor, 22 | /// Missing non-optional parameter. 23 | ctap2_err_missing_parameter, 24 | /// Limit for number of items exceeded. 25 | ctap2_err_limit_exceeded, 26 | /// Unsupported extension. 27 | ctap2_err_unsupported_extension, 28 | /// Valid credential found in the excluded list. 29 | ctap2_err_credential_excluded, 30 | /// Processing (Lenghty operation is in progress). 31 | ctap2_err_processing, 32 | /// Credential not valid for the authenticator. 33 | ctap2_err_invalid_credential, 34 | /// Authenticator is waiting for user interaction. 35 | ctap2_err_user_action_pending, 36 | /// Processing, lengthy operation is in progress. 37 | ctap2_err_operation_pending, 38 | /// No request is pending. 39 | ctap2_err_no_operations, 40 | /// Authenticator does not support requested algorithm. 41 | ctap2_err_unsupported_algorithm, 42 | /// Not authorized for requested operation. 43 | ctap2_err_operation_denied, 44 | /// Internal key storage is full. 45 | ctap2_err_key_store_full, 46 | /// Authenticator cannot cancel as it is not busy. 47 | ctap2_err_not_busy, 48 | /// No outstanding operations. 49 | ctap2_err_no_operation_pending, 50 | /// Unsupported option. 51 | ctap2_err_unsupported_option, 52 | /// Not a valid option for current operation. 53 | ctap2_err_invalid_option, 54 | /// Pending keep alive was canceled. 55 | ctap2_err_keepalive_cancel, 56 | /// No valid credentials provided. 57 | ctap2_err_no_credentials, 58 | /// Timeout waiting for user interaction. 59 | ctap2_err_user_action_timeout, 60 | /// Continuation command, such as, `authenticatorGetNexAssertion` not allowed. 61 | ctap2_err_not_allowed, 62 | /// PIN invalid. 63 | ctap2_err_pin_invalid, 64 | /// PIN blocked. 65 | ctap2_err_pin_blocked, 66 | /// PIN authentication (`pinAuth`) verification failed. 67 | ctap2_err_pin_auth_invalid, 68 | /// PIN authentication (`pinAuth`) blocked. Requires power recycle to reset. 69 | ctap2_err_pin_auth_blocked, 70 | /// No PIN has been set. 71 | ctap2_err_pin_not_set, 72 | /// PIN is required for the selected operation. 73 | ctap2_err_pin_required, 74 | /// PIN policy violation. Currently only enforces minimum length. 75 | ctap2_err_pin_policy_violation, 76 | /// `pinToken` expired on authenticator. 77 | ctap2_err_pin_token_expired, 78 | /// Authenticator cannot handle this request due to memory constraints. 79 | ctap2_err_request_too_large, 80 | /// The current operation has timed out. 81 | ctap2_err_action_timeout, 82 | /// User presence is required for the requested operation. 83 | ctap2_err_up_required, 84 | /// built-in user verification is disabled. 85 | ctap2_err_uv_blocked, 86 | /// A checksum did not match. 87 | ctap2_err_integrity_failure, 88 | /// The requested subcommand is either invalid or not implemented. 89 | ctap2_err_invalid_subcommand, 90 | /// built-in user verification unsuccessful. The platform SHOULD retry. 91 | ctap2_err_uv_invalid, 92 | /// The permissions parameter contains an unauthorized permission. 93 | ctap2_err_unauthorized_permission, 94 | /// Other unspecified error. 95 | ctap1_err_other, 96 | /// CTAP 2 spac last error. 97 | ctap2_err_spec_last, 98 | /// Extension specific error. 99 | ctap2_err_extension_first, 100 | ctap2_err_extension_2, 101 | ctap2_err_extension_3, 102 | ctap2_err_extension_4, 103 | ctap2_err_extension_5, 104 | ctap2_err_extension_6, 105 | ctap2_err_extension_7, 106 | ctap2_err_extension_8, 107 | ctap2_err_extension_9, 108 | ctap2_err_extension_10, 109 | ctap2_err_extension_11, 110 | ctap2_err_extension_12, 111 | ctap2_err_extension_13, 112 | ctap2_err_extension_14, 113 | ctap2_err_extension_15, 114 | /// Extension specific error. 115 | ctap2_err_extension_last, 116 | /// Vendor specific error. 117 | ctap2_err_vendor_first, 118 | ctap2_err_vendor_2, 119 | ctap2_err_vendor_3, 120 | ctap2_err_vendor_4, 121 | ctap2_err_vendor_5, 122 | ctap2_err_vendor_6, 123 | ctap2_err_vendor_7, 124 | ctap2_err_vendor_8, 125 | ctap2_err_vendor_9, 126 | ctap2_err_vendor_10, 127 | ctap2_err_vendor_11, 128 | ctap2_err_vendor_12, 129 | ctap2_err_vendor_13, 130 | ctap2_err_vendor_14, 131 | ctap2_err_vendor_15, 132 | /// Vendor specific error. 133 | ctap2_err_vendor_last, 134 | // user defined -------------- 135 | client_timeout, 136 | }; 137 | 138 | pub fn errorFromInt(i: u8) StatusCodes { 139 | return switch (i) { 140 | 0x01 => StatusCodes.ctap1_err_invalid_command, 141 | 0x02 => StatusCodes.ctap1_err_invalid_parameter, 142 | 0x03 => StatusCodes.ctap1_err_invalid_length, 143 | 0x04 => StatusCodes.ctap1_err_invalid_seq, 144 | 0x05 => StatusCodes.ctap1_err_timeout, 145 | 0x06 => StatusCodes.ctap1_err_channel_busy, 146 | 0x0a => StatusCodes.ctap1_err_lock_required, 147 | 0x0b => StatusCodes.ctap1_err_invalid_channel, 148 | 0x11 => StatusCodes.ctap2_err_cbor_unexpected_type, 149 | 0x12 => StatusCodes.ctap2_err_invalid_cbor, 150 | 0x14 => StatusCodes.ctap2_err_missing_parameter, 151 | 0x15 => StatusCodes.ctap2_err_limit_exceeded, 152 | 0x16 => StatusCodes.ctap2_err_unsupported_extension, 153 | 0x19 => StatusCodes.ctap2_err_credential_excluded, 154 | 0x21 => StatusCodes.ctap2_err_processing, 155 | 0x22 => StatusCodes.ctap2_err_invalid_credential, 156 | 0x23 => StatusCodes.ctap2_err_user_action_pending, 157 | 0x24 => StatusCodes.ctap2_err_operation_pending, 158 | 0x25 => StatusCodes.ctap2_err_no_operations, 159 | 0x26 => StatusCodes.ctap2_err_unsupported_algorithm, 160 | 0x27 => StatusCodes.ctap2_err_operation_denied, 161 | 0x28 => StatusCodes.ctap2_err_key_store_full, 162 | 0x29 => StatusCodes.ctap2_err_not_busy, 163 | 0x2a => StatusCodes.ctap2_err_no_operation_pending, 164 | 0x2b => StatusCodes.ctap2_err_unsupported_option, 165 | 0x2c => StatusCodes.ctap2_err_invalid_option, 166 | 0x2d => StatusCodes.ctap2_err_keepalive_cancel, 167 | 0x2e => StatusCodes.ctap2_err_no_credentials, 168 | 0x2f => StatusCodes.ctap2_err_user_action_timeout, 169 | 0x30 => StatusCodes.ctap2_err_not_allowed, 170 | 0x31 => StatusCodes.ctap2_err_pin_invalid, 171 | 0x32 => StatusCodes.ctap2_err_pin_blocked, 172 | 0x33 => StatusCodes.ctap2_err_pin_auth_invalid, 173 | 0x34 => StatusCodes.ctap2_err_pin_auth_blocked, 174 | 0x35 => StatusCodes.ctap2_err_pin_not_set, 175 | 0x36 => StatusCodes.ctap2_err_pin_required, 176 | 0x37 => StatusCodes.ctap2_err_pin_policy_violation, 177 | 0x38 => StatusCodes.ctap2_err_pin_token_expired, 178 | 0x39 => StatusCodes.ctap2_err_request_too_large, 179 | 0x3a => StatusCodes.ctap2_err_action_timeout, 180 | 0x3b => StatusCodes.ctap2_err_up_required, 181 | 0x3c => StatusCodes.ctap2_err_uv_blocked, 182 | 0x3d => StatusCodes.ctap2_err_integrity_failure, 183 | 0x3e => StatusCodes.ctap2_err_invalid_subcommand, 184 | 0x3f => StatusCodes.ctap2_err_uv_invalid, 185 | 0x40 => StatusCodes.ctap2_err_unauthorized_permission, 186 | 0xe0 => StatusCodes.ctap2_err_extension_first, 187 | 0xe1 => StatusCodes.ctap2_err_extension_2, 188 | 0xe2 => StatusCodes.ctap2_err_extension_3, 189 | 0xe3 => StatusCodes.ctap2_err_extension_4, 190 | 0xe4 => StatusCodes.ctap2_err_extension_5, 191 | 0xe5 => StatusCodes.ctap2_err_extension_6, 192 | 0xe6 => StatusCodes.ctap2_err_extension_7, 193 | 0xe7 => StatusCodes.ctap2_err_extension_8, 194 | 0xe8 => StatusCodes.ctap2_err_extension_9, 195 | 0xe9 => StatusCodes.ctap2_err_extension_10, 196 | 0xea => StatusCodes.ctap2_err_extension_11, 197 | 0xeb => StatusCodes.ctap2_err_extension_12, 198 | 0xec => StatusCodes.ctap2_err_extension_13, 199 | 0xed => StatusCodes.ctap2_err_extension_14, 200 | 0xee => StatusCodes.ctap2_err_extension_15, 201 | 0xef => StatusCodes.ctap2_err_extension_last, 202 | 0xf0 => StatusCodes.ctap2_err_vendor_first, 203 | 0xf1 => StatusCodes.ctap2_err_vendor_2, 204 | 0xf2 => StatusCodes.ctap2_err_vendor_3, 205 | 0xf3 => StatusCodes.ctap2_err_vendor_4, 206 | 0xf4 => StatusCodes.ctap2_err_vendor_5, 207 | 0xf5 => StatusCodes.ctap2_err_vendor_6, 208 | 0xf6 => StatusCodes.ctap2_err_vendor_7, 209 | 0xf7 => StatusCodes.ctap2_err_vendor_8, 210 | 0xf8 => StatusCodes.ctap2_err_vendor_9, 211 | 0xf9 => StatusCodes.ctap2_err_vendor_10, 212 | 0xfa => StatusCodes.ctap2_err_vendor_11, 213 | 0xfb => StatusCodes.ctap2_err_vendor_12, 214 | 0xfc => StatusCodes.ctap2_err_vendor_13, 215 | 0xfd => StatusCodes.ctap2_err_vendor_14, 216 | 0xfe => StatusCodes.ctap2_err_vendor_15, 217 | 0xff => StatusCodes.ctap2_err_vendor_last, 218 | else => StatusCodes.ctap1_err_other, 219 | }; 220 | } 221 | -------------------------------------------------------------------------------- /lib/client/transports/ctaphid/ctaphid.zig: -------------------------------------------------------------------------------- 1 | //! Client side CTAPHID protocol implementation 2 | 3 | const std = @import("std"); 4 | 5 | const keylib = @import("keylib"); 6 | const ctaphid = keylib.ctap.transports.ctaphid; 7 | const Cmd = ctaphid.Cmd; 8 | const ErrorCodes = ctaphid.authenticator.ErrorCodes; 9 | pub const InitResponse = ctaphid.authenticator.InitResponse; 10 | const CtapHidMessageIterator = ctaphid.message.CtapHidMessageIterator; 11 | const sliceToInt = keylib.ctap.transports.ctaphid.authenticator.misc.sliceToInt; 12 | 13 | const DEFAULT_TIMEOUT_MS = 500; 14 | 15 | const Usb = @import("../usb.zig").Usb; 16 | 17 | pub fn init(usb: *Usb) !void { 18 | var nonce: [8]u8 = undefined; 19 | std.crypto.random.bytes(nonce[0..]); 20 | 21 | var request = CtapHidMessageIterator.new(0xffffffff, Cmd.init); 22 | request.data = nonce[0..]; 23 | 24 | try usb.write(request.next().?[0..64].*); 25 | 26 | const response = try ctaphid_read(usb, Cmd.init, 0xffffffff, DEFAULT_TIMEOUT_MS, usb.allocator); 27 | defer usb.allocator.free(response); 28 | 29 | if (!std.mem.eql(u8, nonce[0..], response[0..8])) return error.NonceMismatch; 30 | 31 | //std.log.info("init: {s}", .{std.fmt.fmtSliceHexLower(response)}); 32 | 33 | usb.channel = try InitResponse.deserialize(response); 34 | 35 | //std.log.info("init: {any}", .{usb.channel.?}); 36 | } 37 | 38 | pub fn cbor_write(usb: *Usb, cbor_data: []const u8) !void { 39 | if (usb.channel == null) { 40 | try init(usb); 41 | } 42 | 43 | var request = CtapHidMessageIterator.new(usb.channel.?.cid, Cmd.cbor); 44 | request.data = cbor_data; 45 | 46 | while (request.next()) |d| { 47 | try usb.write(d[0..64].*); 48 | } 49 | } 50 | 51 | pub fn cbor_read(usb: *Usb, a: std.mem.Allocator) ![]u8 { 52 | if (usb.channel == null) return error.NoChannel; 53 | 54 | const response = try ctaphid_read(usb, Cmd.cbor, usb.channel.?.cid, DEFAULT_TIMEOUT_MS, a); 55 | 56 | //std.log.info("cbor: {s}", .{std.fmt.fmtSliceHexLower(response)}); 57 | 58 | return response; 59 | } 60 | 61 | pub fn ctaphid_read(usb: *Usb, cmd: Cmd, cid: u32, tout_ms: i64, a: std.mem.Allocator) ![]u8 { 62 | var expected: ?usize = null; 63 | var total: usize = 0; 64 | var seq: i16 = -1; 65 | var data: [7609]u8 = undefined; 66 | const start = std.time.milliTimestamp(); 67 | 68 | while (true) { 69 | if (std.time.milliTimestamp() - start > tout_ms) return error.Timeout; 70 | 71 | var buffer: [64]u8 = undefined; 72 | const l = try usb.read(&buffer, cid); 73 | 74 | if (l > 0) { 75 | if (seq == -1) { 76 | if (l < 7) return error.InvalidPacketLength; 77 | if (buffer[4] & 0x80 == 0) return error.InvalidPacket; 78 | const _cid = sliceToInt(u32, buffer[0..4]); 79 | if (_cid != cid) return error.InvalidCid; 80 | const _cmd = @as(Cmd, @enumFromInt(buffer[4] & 0x7f)); 81 | if (_cmd != cmd) { 82 | if (_cmd == Cmd.err) { 83 | return switch (buffer[7]) { 84 | 0x01 => error.InvalidCmd, 85 | 0x02 => error.InvalidPar, 86 | 0x03 => error.InvalidLen, 87 | 0x04 => error.InvalidSeq, 88 | 0x05 => error.MsgTimeout, 89 | 0x06 => error.ChannelBusy, 90 | 0x0a => error.LockRequired, 91 | 0x0b => error.InvalidChannel, 92 | else => error.Other, 93 | }; 94 | } else if (_cmd == Cmd.keepalive) { 95 | // A Keepalive is not considered a response and doesnt end a transaction! 96 | // The calling function has to check for this error and handle it 97 | // by calling ctaphid_read again! 98 | return switch (buffer[7]) { 99 | 0x01 => error.Processing, 100 | 0x02 => error.UpNeeded, 101 | else => error.Other, 102 | }; 103 | } else { 104 | return error.UnexpectedCommand; 105 | } 106 | } 107 | expected = (@as(usize, @intCast(buffer[5])) << 8) + @as(usize, @intCast(buffer[6])); 108 | 109 | const size = l - 7; 110 | @memcpy(data[0..size], buffer[7..l]); 111 | total += @intCast(size); 112 | seq += 1; 113 | } else { 114 | if (l < 5) return error.InvalidPacketLength; 115 | if (buffer[4] & 0x80 != 0) return error.InvalidPacket; 116 | if (buffer[4] != seq) return error.InvalidSequenceNumber; 117 | 118 | const size = l - 5; 119 | @memcpy(data[total .. total + size], buffer[5..l]); 120 | total += @intCast(size); 121 | seq += 1; 122 | } 123 | } 124 | 125 | if (expected != null and total >= expected.?) { 126 | const o = try a.alloc(u8, expected.?); 127 | @memcpy(o, data[0..expected.?]); 128 | return o; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/client/transports/usb.zig: -------------------------------------------------------------------------------- 1 | //! Usb transport implementation for FIDO2/ PassKey authenticator 2 | //! 3 | //! This implementation allows you to interact with a authenticator over USB, 4 | //! using the CTAPHID protocol. 5 | const std = @import("std"); 6 | 7 | const hidapi = @cImport({ 8 | @cInclude("hidapi.h"); 9 | }); 10 | 11 | const Transport = @import("../Transport.zig"); 12 | const Transports = @import("../Transports.zig"); 13 | const ctaphid = @import("ctaphid/ctaphid.zig"); 14 | 15 | var initialized: bool = false; 16 | 17 | /// Abstract representation of a USB connection 18 | pub const Usb = struct { 19 | path: [:0]const u8, 20 | manufacturer: []const u8, 21 | product: []const u8, 22 | device: ?*hidapi.hid_device = null, 23 | channel: ?ctaphid.InitResponse = null, 24 | allocator: std.mem.Allocator, 25 | 26 | /// Deinitialize the given USB connection 27 | /// 28 | /// Calling this function will free all allocated memory, including 29 | /// the pointer to self! It will also close any open USB connection. 30 | pub fn deinit(self: *@This()) void { 31 | self.allocator.free(self.path); 32 | self.allocator.free(self.manufacturer); 33 | self.allocator.free(self.product); 34 | self.close(); 35 | self.allocator.destroy(self); 36 | } 37 | 38 | /// Establish a USB connection with the device pointed to by `path` 39 | pub fn open(self: *@This()) Transport.Error!void { 40 | const dev = hidapi.hid_open_path(self.path.ptr); 41 | if (dev == null) return error.Open; 42 | self.device = dev; 43 | _ = hidapi.hid_set_nonblocking(self.device.?, 1); 44 | ctaphid.init(self) catch return error.Init; 45 | } 46 | 47 | /// Close the USB connection 48 | pub fn close(self: *@This()) void { 49 | if (self.device != null) { 50 | hidapi.hid_close(self.device.?); 51 | self.device = null; 52 | } 53 | } 54 | 55 | /// Write a single USB packet to the device 56 | pub fn write(self: *@This(), out: [64]u8) Transport.Error!void { 57 | // first byte is the report number 58 | var o: [65]u8 = .{0} ** 65; 59 | @memcpy(o[1..], out[0..]); 60 | 61 | // Open the device if not already done 62 | if (self.device == null) { 63 | try self.open(); 64 | } 65 | 66 | if (hidapi.hid_write(self.device.?, o[0..].ptr, 65) < 0) { 67 | return error.Write; 68 | } 69 | } 70 | 71 | /// Read a single USB packet from the device 72 | pub fn read(self: *@This(), out: *[64]u8, cid: u32) Transport.Error!usize { 73 | var x: [65]u8 = .{0} ** 65; 74 | 75 | // Open the device if not already done 76 | if (self.device == null) { 77 | try self.open(); 78 | } 79 | 80 | const res = hidapi.hid_read(self.device.?, x[0..64].ptr, 64); 81 | if (res < 0) { 82 | return error.Read; 83 | } else if (res == 0) { 84 | return 0; 85 | } 86 | 87 | const r: usize = @intCast(res); 88 | // TODO: fix this 89 | // We determine if the first byte is the endpoint number by comparing 90 | // the first byte of the expected cid with the first byte of the packet. 91 | var offset: usize = if (x[0] != @as(u8, @intCast(cid & 0xff))) 1 else 0; 92 | offset = 0; 93 | 94 | //std.log.info("{s}", .{std.fmt.fmtSliceHexLower(x[0..r])}); 95 | @memcpy(out[0 .. r - offset], x[offset..r]); 96 | return r; 97 | } 98 | }; 99 | 100 | inline fn init() Transport.Error!void { 101 | if (!initialized) { 102 | if (hidapi.hid_init() < 0) { 103 | return error.Init; 104 | } 105 | initialized = true; 106 | } 107 | } 108 | 109 | /// Make sure to handle error.Processing and error.UpNeeded as those MUST NOT end the transaction! 110 | pub fn read(self: *anyopaque, a: std.mem.Allocator) Transport.Error!?[]u8 { 111 | try init(); 112 | const usb: *Usb = @ptrCast(@alignCast(self)); 113 | return ctaphid.cbor_read(usb, a) catch |e| { 114 | if (e == error.Timeout) return null else return e; 115 | }; 116 | } 117 | 118 | pub fn write(self: *anyopaque, data: []const u8) Transport.Error!void { 119 | try init(); 120 | const usb: *Usb = @ptrCast(@alignCast(self)); 121 | try ctaphid.cbor_write(usb, data); 122 | } 123 | 124 | pub fn close(self: *anyopaque) void { 125 | const usb: *Usb = @ptrCast(@alignCast(self)); 126 | usb.close(); 127 | } 128 | 129 | pub fn open(self: *anyopaque) Transport.Error!void { 130 | try init(); 131 | const usb: *Usb = @ptrCast(@alignCast(self)); 132 | try usb.open(); 133 | } 134 | 135 | pub fn deinit(self: *anyopaque) void { 136 | const usb: *Usb = @ptrCast(@alignCast(self)); 137 | usb.deinit(); 138 | } 139 | 140 | pub fn allocPrint(self: *anyopaque, a: std.mem.Allocator) Transport.Error![]const u8 { 141 | const usb: *Usb = @ptrCast(@alignCast(self)); 142 | return try std.fmt.allocPrint(a, "{s}: {s} {s}", .{ usb.path, usb.manufacturer, usb.product }); 143 | } 144 | 145 | /// Enumerate all connected USB devices and return those as Transport's that might be a authenticator 146 | pub fn enumerate(a: std.mem.Allocator) Transports.Error!?[]Transport { 147 | var devices = hidapi.hid_enumerate(0, 0); 148 | defer hidapi.hid_free_enumeration(devices); 149 | 150 | if (devices == null) return null; 151 | 152 | var arr = std.ArrayList(Transport).init(a); 153 | defer arr.deinit(); 154 | 155 | while (true) { 156 | if (devices.*.usage_page == 0xf1d0 and devices.*.usage == 0x01) { 157 | const u = try a.create(Usb); 158 | u.* = Usb{ 159 | .path = try a.dupeZ(u8, to_str(devices.*.path)), 160 | .manufacturer = try a.dupe(u8, wchar_t_to_str(devices.*.manufacturer_string)), 161 | .product = try a.dupe(u8, wchar_t_to_str(devices.*.product_string)), 162 | .allocator = a, 163 | }; 164 | 165 | const t = Transport{ 166 | .obj = @ptrCast(u), 167 | ._read = read, 168 | ._write = write, 169 | ._open = open, 170 | ._close = close, 171 | ._allocPrint = allocPrint, 172 | ._deinit = deinit, 173 | }; 174 | 175 | arr.append(t) catch { 176 | arr.deinit(); 177 | return null; 178 | }; 179 | } 180 | if (devices.*.next == null) break; 181 | devices = devices.*.next; 182 | } 183 | 184 | return arr.toOwnedSlice() catch { 185 | arr.deinit(); 186 | return null; 187 | }; 188 | } 189 | 190 | fn wchar_t_to_str(in: [*c]hidapi.wchar_t) []const u8 { 191 | var i: usize = 0; 192 | while (in[i] != 0) : (i += 1) {} 193 | return std.mem.sliceAsBytes(in[0..i]); 194 | } 195 | 196 | fn to_str(in: [*c]u8) []const u8 { 197 | var i: usize = 0; 198 | while (in[i] != 0) : (i += 1) {} 199 | return in[0..i]; 200 | } 201 | -------------------------------------------------------------------------------- /lib/common/AttestationStatement.zig: -------------------------------------------------------------------------------- 1 | //! Attestation statement is a specific type of signed data object, containing statements 2 | //! about a public key credential itself and the authenticator that created it. It 3 | //! contains an attestation signature created using the key of the attesting authority 4 | //! (except for the case of self attestation, when it is created using the credential 5 | //! private key) 6 | 7 | const std = @import("std"); 8 | const cbor = @import("zbor"); 9 | const fido = @import("../main.zig"); 10 | const dt = @import("data_types.zig"); 11 | 12 | pub const AttestationStatement = union(fido.common.AttestationStatementFormatIdentifiers) { 13 | /// This is a WebAuthn optimized attestation statement format. It uses a very compact 14 | /// but still extensible encoding method. It is implementable by authenticators with 15 | /// limited resources (e.g., secure elements). 16 | @"packed": struct { // basic, self, AttCA 17 | /// A COSEAlgorithmIdentifier containing the identifier of the algorithm used 18 | /// to generate the attestation signature. 19 | alg: cbor.cose.Algorithm, 20 | /// A byte string containing the attestation signature. 21 | /// 22 | /// TODO: A ABS256B can hold signatures up to 2048 bytes, e.g., RSA-2048. 23 | /// This has to be modified to accomodate larger signatures. 24 | sig: dt.ABS256B, 25 | // The elements of this array contain attestnCert and its certificate chain (if any), 26 | // each encoded in X.509 format. The attestation certificate attestnCert MUST be 27 | // the first element in the array. 28 | //TODO: x5c: ?[]const Cert = null, 29 | }, 30 | tpm: struct {}, // TODO: implement 31 | @"android-key": struct {}, // TODO: implement 32 | @"android-safetynet": struct {}, // TODO: implement 33 | @"fido-u2f": struct {}, // TODO: implement 34 | apple: struct {}, // TODO: implement 35 | /// The none attestation statement format is used to replace any authenticator-provided 36 | /// attestation statement when a WebAuthn Relying Party indicates it does not wish to 37 | /// receive attestation information, see § 5.4.7 Attestation Conveyance Preference 38 | /// Enumeration (enum AttestationConveyancePreference). 39 | none: struct {}, // no attestation 40 | 41 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 42 | return cbor.stringify(self, .{ 43 | .allocator = options.allocator, 44 | .ignore_override = true, 45 | .field_settings = &.{ 46 | .{ .name = "alg", .value_options = .{ .enum_serialization_type = .Integer } }, 47 | }, 48 | }, out); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /lib/common/AttestationStatementFormatIdentifiers.zig: -------------------------------------------------------------------------------- 1 | /// WebAuthn Attestation Statement Format Identifiers 2 | /// 3 | /// https://www.w3.org/TR/webauthn/#sctn-defined-attestation-formats 4 | pub const AttestationStatementFormatIdentifiers = enum { 5 | /// The "packed" attestation statement format is a WebAuthn-optimized format for attestation. It uses a very compact but still extensible encoding method. This format is implementable by authenticators with limited resources (e.g., secure elements). 6 | @"packed", 7 | /// The TPM attestation statement format returns an attestation statement in the same format as the packed attestation statement format, although the rawData and signature fields are computed differently. 8 | tpm, 9 | /// Platform authenticators on versions "N", and later, may provide this proprietary "hardware attestation" statement. 10 | @"android-key", 11 | /// Android-based platform authenticators MAY produce an attestation statement based on the Android SafetyNet API. 12 | @"android-safetynet", 13 | /// Used with FIDO U2F authenticators 14 | @"fido-u2f", 15 | /// Used with Apple devices' platform authenticators 16 | apple, 17 | /// Used to replace any authenticator-provided attestation statement when a WebAuthn Relying Party indicates it does not wish to receive attestation information. 18 | none, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/common/AttestationType.zig: -------------------------------------------------------------------------------- 1 | /// Type of attestation issued 2 | pub const AttestationType = enum { 3 | /// In this case, no attestation information is available. 4 | None, 5 | /// In the case of self attestation, also known as surrogate basic attestation [UAFProtocol], 6 | /// the Authenticator does not have any specific attestation key pair. Instead it uses the 7 | /// credential private key to create the attestation signature. Authenticators without 8 | /// meaningful protection measures for an attestation private key typically use this 9 | /// attestation type. 10 | Self, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/common/AttestedCredentialData.zig: -------------------------------------------------------------------------------- 1 | //! Attested credential data is a variable-length byte array added to the 2 | //! authenticator data when generating an attestation object for a given credential 3 | 4 | const std = @import("std"); 5 | const cbor = @import("zbor"); 6 | const dt = @import("data_types.zig"); 7 | 8 | const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256; 9 | 10 | /// The AAGUID of the authenticator 11 | aaguid: [16]u8, 12 | /// Byte length L of Credential ID, 16-bit unsigned big-endian integer 13 | credential_length: u16, 14 | /// Credential ID 15 | credential_id: dt.ABS64B, 16 | /// The credential public key encoded in COSE_Key format 17 | credential_public_key: dt.ABS256B, 18 | 19 | /// Encode the given AttestedCredentialData for usage with the AuthenticatorData struct 20 | pub fn encode(self: *const @This(), out: anytype) !void { 21 | try out.writeAll(self.aaguid[0..]); 22 | // length is encoded in big-endian format 23 | try out.writeByte(@as(u8, @intCast(self.credential_length >> 8))); 24 | try out.writeByte(@as(u8, @intCast(self.credential_length & 0xff))); 25 | try out.writeAll(self.credential_id.get()); 26 | try out.writeAll(self.credential_public_key.get()); 27 | } 28 | 29 | pub fn new( 30 | aaguid: [16]u8, 31 | credential_id: []const u8, 32 | credential_public_key: []const u8, 33 | ) !@This() { 34 | return .{ 35 | .aaguid = aaguid, 36 | .credential_length = @as(u16, @intCast(credential_id.len)), 37 | .credential_id = (try dt.ABS64B.fromSlice(credential_id)).?, 38 | .credential_public_key = (try dt.ABS256B.fromSlice(credential_public_key)).?, 39 | }; 40 | } 41 | 42 | test "attestation credential data" { 43 | const allocator = std.testing.allocator; 44 | var a = std.ArrayList(u8).init(allocator); 45 | defer a.deinit(); 46 | 47 | const k = cbor.cose.Key.fromP256Pub(.Es256, try EcdsaP256Sha256.PublicKey.fromSec1("\x04\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52")); 48 | var serialized_cred = std.ArrayList(u8).init(allocator); 49 | defer serialized_cred.deinit(); 50 | try cbor.stringify(&k, .{ .enum_serialization_type = .Integer }, serialized_cred.writer()); 51 | 52 | const acd = try new( 53 | .{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 54 | &.{ 0xb3, 0xf8, 0xcd, 0xb1, 0x80, 0x20, 0x91, 0x76, 0xfa, 0x20, 0x1a, 0x51, 0x6d, 0x1b, 0x42, 0xf8, 0x02, 0xa8, 0x0d, 0xaf, 0x48, 0xd0, 0x37, 0x88, 0x21, 0xa6, 0xfb, 0xdd, 0x52, 0xde, 0x16, 0xb7, 0xef, 0xf6, 0x22, 0x25, 0x72, 0x43, 0x8d, 0xe5, 0x85, 0x7e, 0x70, 0xf9, 0xef, 0x05, 0x80, 0xe9, 0x37, 0xe3, 0x00, 0xae, 0xd0, 0xdf, 0xf1, 0x3f, 0xb6, 0xa3, 0x3e, 0xc3, 0x8b, 0x81, 0xca, 0xd0 }, 55 | serialized_cred.items, 56 | ); 57 | 58 | const w = a.writer(); 59 | try acd.encode(w); 60 | 61 | try std.testing.expectEqualSlices(u8, a.items, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\xb3\xf8\xcd\xb1\x80\x20\x91\x76\xfa\x20\x1a\x51\x6d\x1b\x42\xf8\x02\xa8\x0d\xaf\x48\xd0\x37\x88\x21\xa6\xfb\xdd\x52\xde\x16\xb7\xef\xf6\x22\x25\x72\x43\x8d\xe5\x85\x7e\x70\xf9\xef\x05\x80\xe9\x37\xe3\x00\xae\xd0\xdf\xf1\x3f\xb6\xa3\x3e\xc3\x8b\x81\xca\xd0\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\x22\x58\x20\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52"); 62 | } 63 | -------------------------------------------------------------------------------- /lib/common/AuthenticatorData.zig: -------------------------------------------------------------------------------- 1 | //! The authenticator data structure encodes contextual bindings made by the 2 | //! authenticator. These bindings are controlled by the authenticator itself, 3 | //! and derive their trust from the WebAuthn Relying Party's assessment of 4 | //! the security properties of the authenticator. 5 | //! 6 | //! The authenticator data structure is a byte array of 37 bytes or more 7 | 8 | const std = @import("std"); 9 | const cbor = @import("zbor"); 10 | const fido = @import("../main.zig"); 11 | const dt = fido.common.dt; 12 | 13 | /// SHA-256 hash of the RP ID the credential is scoped to 14 | rpIdHash: [32]u8, 15 | /// Flags providing additional context to the given data 16 | flags: packed struct(u8) { 17 | /// User Present (UP) result. 18 | /// - 1 means the user is present. 19 | /// - 0 means the user is not present. 20 | up: u1, 21 | /// Reserved for future use. 22 | rfu1: u1, 23 | /// User Verified (UV) result. 24 | /// - 1 means the user is verified. 25 | /// - 0 means the user is not verified. 26 | uv: u1, 27 | /// Reserved for future use. 28 | rfu2: u3, 29 | /// Attested credential data includet (AT). 30 | /// Indicates whether the authenticator added attested 31 | /// credential data. 32 | at: u1, 33 | /// Extension data included (ED). 34 | /// Indicates if the authenticator data has extensions. 35 | ed: u1, 36 | }, 37 | /// Signature counter, 32-bit unsigned big-endian integer 38 | signCount: u32, 39 | /// Attested credential data 40 | /// 41 | /// One could say this is the most important chunk of data because it contains 42 | /// the credential (public key + cred_id) to be stored by the RP 43 | attestedCredentialData: ?fido.common.AttestedCredentialData = null, 44 | extensions: ?fido.ctap.extensions.Extensions = null, 45 | 46 | /// Encode the given AuthenticatorData as byte array 47 | pub fn encode(self: *const @This()) !dt.ABSAuthenticatorData { 48 | var ad = dt.ABSAuthenticatorData{}; 49 | var out = try ad.byteWriter(); 50 | 51 | try out.writeAll(self.rpIdHash[0..]); 52 | try out.writeByte(@as(u8, @bitCast(self.flags))); 53 | 54 | // counter is encoded in big-endian format 55 | try out.writeByte(@as(u8, @intCast((self.signCount >> 24) & 0xff))); 56 | try out.writeByte(@as(u8, @intCast((self.signCount >> 16) & 0xff))); 57 | try out.writeByte(@as(u8, @intCast((self.signCount >> 8) & 0xff))); 58 | try out.writeByte(@as(u8, @intCast(self.signCount & 0xff))); 59 | 60 | if (self.attestedCredentialData) |acd| { 61 | try acd.encode(out); 62 | } 63 | 64 | if (self.extensions) |extensions| { 65 | try cbor.stringify(extensions, .{}, out); 66 | } 67 | 68 | return ad; 69 | } 70 | 71 | test "authData encoding" { 72 | const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256; 73 | 74 | const allocator = std.testing.allocator; 75 | 76 | const k = cbor.cose.Key.fromP256Pub(.Es256, try EcdsaP256Sha256.PublicKey.fromSec1("\x04\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52")); 77 | var serialized_cred = std.ArrayList(u8).init(allocator); 78 | defer serialized_cred.deinit(); 79 | try cbor.stringify(&k, .{ .enum_serialization_type = .Integer }, serialized_cred.writer()); 80 | 81 | const acd = try fido.common.AttestedCredentialData.new( 82 | .{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 83 | &.{ 0xb3, 0xf8, 0xcd, 0xb1, 0x80, 0x20, 0x91, 0x76, 0xfa, 0x20, 0x1a, 0x51, 0x6d, 0x1b, 0x42, 0xf8, 0x02, 0xa8, 0x0d, 0xaf, 0x48, 0xd0, 0x37, 0x88, 0x21, 0xa6, 0xfb, 0xdd, 0x52, 0xde, 0x16, 0xb7, 0xef, 0xf6, 0x22, 0x25, 0x72, 0x43, 0x8d, 0xe5, 0x85, 0x7e, 0x70, 0xf9, 0xef, 0x05, 0x80, 0xe9, 0x37, 0xe3, 0x00, 0xae, 0xd0, 0xdf, 0xf1, 0x3f, 0xb6, 0xa3, 0x3e, 0xc3, 0x8b, 0x81, 0xca, 0xd0 }, 84 | serialized_cred.items, 85 | ); 86 | 87 | const ad = @This(){ 88 | .rpIdHash = .{ 0x21, 0x09, 0x18, 0x5f, 0x69, 0x3a, 0x01, 0xea, 0x1a, 0x26, 0x41, 0xf8, 0x2d, 0x52, 0xfb, 0xae, 0xee, 0x0a, 0x4f, 0x47, 0xe3, 0x37, 0x4d, 0xfe, 0xf8, 0x70, 0x83, 0x8d, 0xe4, 0x9b, 0x0e, 0x97 }, 89 | .flags = .{ 90 | .up = 1, 91 | .rfu1 = 0, 92 | .uv = 0, 93 | .rfu2 = 0, 94 | .at = 1, 95 | .ed = 0, 96 | }, 97 | .signCount = 0, 98 | .attestedCredentialData = acd, 99 | }; 100 | 101 | const raw_ad = try ad.encode(); 102 | 103 | try std.testing.expectEqualSlices(u8, raw_ad.get(), "\x21\x09\x18\x5f\x69\x3a\x01\xea\x1a\x26\x41\xf8\x2d\x52\xfb\xae\xee\x0a\x4f\x47\xe3\x37\x4d\xfe\xf8\x70\x83\x8d\xe4\x9b\x0e\x97\x41\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\xb3\xf8\xcd\xb1\x80\x20\x91\x76\xfa\x20\x1a\x51\x6d\x1b\x42\xf8\x02\xa8\x0d\xaf\x48\xd0\x37\x88\x21\xa6\xfb\xdd\x52\xde\x16\xb7\xef\xf6\x22\x25\x72\x43\x8d\xe5\x85\x7e\x70\xf9\xef\x05\x80\xe9\x37\xe3\x00\xae\xd0\xdf\xf1\x3f\xb6\xa3\x3e\xc3\x8b\x81\xca\xd0\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\x22\x58\x20\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52"); 104 | } 105 | -------------------------------------------------------------------------------- /lib/common/AuthenticatorOptions.zig: -------------------------------------------------------------------------------- 1 | //! Parameters to influence authenticator operation 2 | 3 | /// user presence: Instructs the authenticator to require user consent to 4 | /// complete the operation. 5 | up: ?bool = true, 6 | /// resident key: Instructs the authenticator to store the key material on the device. 7 | rk: ?bool = false, 8 | /// user verification: Instructs the authenticator to require a gesture that 9 | /// verifies the user to complete the request. Examples of such gestures 10 | /// are fingerprint scan or a PIN. 11 | /// 12 | /// Platforms MUST NOT include the "uv" option parameter if the authenticator does 13 | /// not support built-in user verification. 14 | uv: ?bool = false, 15 | -------------------------------------------------------------------------------- /lib/common/AuthenticatorTransports.zig: -------------------------------------------------------------------------------- 1 | /// This enumeration defines hints as to how clients might communicate with a 2 | /// particular authenticator in order to obtain an assertion for a specific credential 3 | pub const AuthenticatorTransports = enum { 4 | /// Indicates the respective authenticator can be contacted over removable USB 5 | usb, 6 | /// Indicates the respective authenticator can be contacted over Near Field 7 | /// Communication (NFC) 8 | nfc, 9 | /// Indicates the respective authenticator can be contacted over Bluetooth 10 | /// Smart (Bluetooth Low Energy / BLE) 11 | ble, 12 | /// Indicates the respective authenticator can be contacted over ISO/IEC 7816 13 | /// smart card with contacts 14 | @"smart-card", 15 | /// Indicates the respective authenticator can be contacted using a 16 | /// combination of (often separate) data-transport and proximity mechanisms. 17 | /// This supports, for example, authentication on a desktop computer using a 18 | /// smartphone. 19 | hybrid, 20 | /// Indicates the respective authenticator is contacted using a client 21 | /// device-specific transport, i.e., it is a platform authenticator. 22 | /// These authenticators are not removable from the client device. 23 | internal, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/common/AuthenticatorVersions.zig: -------------------------------------------------------------------------------- 1 | /// Supported version of the authenticator. 2 | pub const AuthenticatorVersions = enum { 3 | FIDO_2_1, 4 | FIDO_2_1_PRE, 5 | /// For CTAP2/FIDO2/Web Authentication authenticators. 6 | FIDO_2_0, 7 | /// For CTAP1/U2F authenticators. 8 | U2F_V2, 9 | 10 | pub fn to_string(self: @This()) []const u8 { 11 | return switch (self) { 12 | .FIDO_2_1 => "FIDO_2_1", 13 | .FIDO_2_1_PRE => "FIDO_2_1_PRE", 14 | .FIDO_2_0 => "FIDO_2_0", 15 | .U2F_V2 => "U2F_V2", 16 | }; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/common/Certifications.zig: -------------------------------------------------------------------------------- 1 | //! An authenticator’s supported certifications MAY be returned in the certifications 2 | //! member of an authenticatorGetInfo response. 3 | 4 | /// The [FIPS140-2] Cryptographic-Module-Validation-Program overall certification level. 5 | /// This is a integer from 1 to 4. 6 | @"FIPS-CMVP-2": ?u8 = null, 7 | 8 | /// The [FIPS140-3] [CMVP] or ISO/IEC 19790:2012(E) and ISO/IEC 24759:2017(E) overall 9 | /// certification level. This is a integer from 1 to 4. 10 | @"FIPS-CMVP-3": ?u8 = null, 11 | 12 | /// The [FIPS140-2] Cryptographic-Module-Validation-Program physical certification level. 13 | /// This is a integer from 1 to 4. 14 | @"FIPS-CMVP-2-PHY": ?u8 = null, 15 | 16 | /// The [FIPS140-3] [CMVP] or ISO/IEC 19790:2012(E) and ISO/IEC 24759:2017(E) physical 17 | /// certification level. This is a integer from 1 to 4. 18 | @"FIPS-CMVP-3-PHY": ?u8 = null, 19 | 20 | /// Common Criteria Evaluation Assurance Level [CC1V3-1R5]. This is a integer 21 | /// from 1 to 7. The intermediate-plus levels are not represented. 22 | @"CC-EAL": ?u8 = null, 23 | 24 | /// FIDO Alliance certification level. This is an integer from 1 to 6. The 25 | /// numbered levels are mapped to the odd numbers, with the plus levels 26 | /// mapped to the even numbers e.g., level 3+ is mapped to 6. 27 | FIDO: ?u8 = null, 28 | -------------------------------------------------------------------------------- /lib/common/PublicKeyCredentialDescriptor.zig: -------------------------------------------------------------------------------- 1 | //! Contains the attributes that are specified by a caller when referring to a 2 | //! public key credential. See WebAuthn §5.8.3. 3 | 4 | const std = @import("std"); 5 | const cbor = @import("zbor"); 6 | const fido = @import("../main.zig"); 7 | const dt = @import("data_types.zig"); 8 | 9 | const AuthenticatorTransports = fido.common.AuthenticatorTransports; 10 | const PublicKeyCredentialType = fido.common.PublicKeyCredentialType; 11 | 12 | /// The credential id 13 | id: dt.ABS64B, 14 | /// Type of the credential 15 | type: PublicKeyCredentialType, 16 | /// Transport methods 17 | transports: ?dt.ABSAuthenticatorTransports = null, 18 | 19 | pub fn new( 20 | id: []const u8, 21 | t: PublicKeyCredentialType, 22 | transports: ?[]const AuthenticatorTransports, 23 | ) !@This() { 24 | return .{ 25 | .id = (try dt.ABS64B.fromSlice(id)).?, 26 | .type = t, 27 | .transports = try dt.ABSAuthenticatorTransports.fromSlice(transports), 28 | }; 29 | } 30 | 31 | test "PublicKeyCredentialDescriptor test #1" { 32 | const allocator = std.testing.allocator; 33 | var str = std.ArrayList(u8).init(allocator); 34 | defer str.deinit(); 35 | 36 | const d = try new("\x5c\x7b\xc6\x57\x09\xed\xcd\xbc\x8a\x61\x2f\x1f\x5e\x97\xd0\x15\xbd\x0e\xc7\x33\x28\x0b\x5c\xb5\x78\x62\x6d\xba\x37\xa1\xe5\x10\xc3\x9e\x79\xf8\x20\x0e\x95\xf7\x9d\x50\x5c\x44\x35\x61\xac\x07\x1e\xa7\x14\x3a\xd0\x6e\xf4\x8b\x56\xdd\x5d\x71\x22\x79\x77\x51", .@"public-key", null); 37 | 38 | try @import("zbor").stringify(d, .{}, str.writer()); 39 | try std.testing.expectEqualSlices(u8, "\xa2\x62\x69\x64\x58\x40\x5c\x7b\xc6\x57\x09\xed\xcd\xbc\x8a\x61\x2f\x1f\x5e\x97\xd0\x15\xbd\x0e\xc7\x33\x28\x0b\x5c\xb5\x78\x62\x6d\xba\x37\xa1\xe5\x10\xc3\x9e\x79\xf8\x20\x0e\x95\xf7\x9d\x50\x5c\x44\x35\x61\xac\x07\x1e\xa7\x14\x3a\xd0\x6e\xf4\x8b\x56\xdd\x5d\x71\x22\x79\x77\x51\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79", str.items); 40 | } 41 | 42 | test "PublicKeyCredentialDescriptor test #2" { 43 | const descriptor = try new("\x5c\x7b\xc6\x57\x09\xed\xcd\xbc\x8a\x61\x2f\x1f\x5e\x97\xd0\x15\xbd\x0e\xc7\x33\x28\x0b\x5c\xb5\x78\x62\x6d\xba\x37\xa1\xe5\x10\xc3\x9e\x79\xf8\x20\x0e\x95\xf7\x9d\x50\x5c\x44\x35\x61\xac\x07\x1e\xa7\x14\x3a\xd0\x6e\xf4\x8b\x56\xdd\x5d\x71\x22\x79\x77\x51", .@"public-key", &.{ .usb, .internal }); 44 | const expected = "\xa3\x62\x69\x64\x58\x40\x5c\x7b\xc6\x57\x09\xed\xcd\xbc\x8a\x61\x2f\x1f\x5e\x97\xd0\x15\xbd\x0e\xc7\x33\x28\x0b\x5c\xb5\x78\x62\x6d\xba\x37\xa1\xe5\x10\xc3\x9e\x79\xf8\x20\x0e\x95\xf7\x9d\x50\x5c\x44\x35\x61\xac\x07\x1e\xa7\x14\x3a\xd0\x6e\xf4\x8b\x56\xdd\x5d\x71\x22\x79\x77\x51\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x6a\x74\x72\x61\x6e\x73\x70\x6f\x72\x74\x73\x82\x63\x75\x73\x62\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c"; 45 | 46 | var arr = std.ArrayList(u8).init(std.testing.allocator); 47 | defer arr.deinit(); 48 | 49 | try cbor.stringify(descriptor, .{}, arr.writer()); 50 | 51 | try std.testing.expectEqualSlices(u8, expected, arr.items); 52 | 53 | const di = try cbor.DataItem.new(expected); 54 | const descriptor2 = try cbor.parse(@This(), di, .{}); 55 | 56 | try std.testing.expectEqualStrings(descriptor.id.get(), descriptor2.id.get()); 57 | try std.testing.expectEqual(descriptor.type, descriptor2.type); 58 | try std.testing.expectEqualSlices(AuthenticatorTransports, descriptor.transports.?.get(), descriptor2.transports.?.get()); 59 | } 60 | -------------------------------------------------------------------------------- /lib/common/PublicKeyCredentialParameters.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const PublicKeyCredentialType = @import("PublicKeyCredentialType.zig").PublicKeyCredentialType; 4 | 5 | /// This member specifies the cryptographic signature algorithm with which 6 | /// the newly generated credential will be used, and thus also the type of 7 | /// asymmetric key pair to be generated, e.g., RSA or Elliptic Curve. 8 | alg: cbor.cose.Algorithm, 9 | /// The type of credential to be created. The value SHOULD be a member of 10 | /// PublicKeyCredentialType but client platforms MUST ignore unknown values, 11 | /// ignoring any PublicKeyCredentialParameters with an unknown type. 12 | type: PublicKeyCredentialType = .@"public-key", 13 | 14 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 15 | _ = options; 16 | return cbor.stringify(self, .{ 17 | .ignore_override = true, 18 | .field_settings = &.{ 19 | .{ .name = "alg", .value_options = .{ .enum_serialization_type = .Integer } }, 20 | }, 21 | }, out); 22 | } 23 | 24 | test "PublicKeyCredentialDescriptor test 1" { 25 | const di = try cbor.DataItem.new("\xa2\x63\x61\x6c\x67\x26\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79"); 26 | 27 | const c = try cbor.parse(@This(), di, .{}); 28 | 29 | try std.testing.expectEqual(cbor.cose.Algorithm.Es256, c.alg); 30 | try std.testing.expectEqual(PublicKeyCredentialType.@"public-key", c.type); 31 | } 32 | 33 | test "PublicKeyCredentialDescriptor test 2" { 34 | const allocator = std.testing.allocator; 35 | var str = std.ArrayList(u8).init(allocator); 36 | defer str.deinit(); 37 | 38 | const desc = @This(){ 39 | .alg = .Es256, 40 | }; 41 | 42 | try cbor.stringify(desc, .{}, str.writer()); 43 | 44 | try std.testing.expectEqualSlices(u8, "\xa2\x63\x61\x6c\x67\x26\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79", str.items); 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/PublicKeyCredentialType.zig: -------------------------------------------------------------------------------- 1 | /// This enumeration defines the valid credential types 2 | pub const PublicKeyCredentialType = enum { 3 | @"public-key", 4 | }; 5 | -------------------------------------------------------------------------------- /lib/common/RelyingParty.zig: -------------------------------------------------------------------------------- 1 | //! Representation of a relying party 2 | 3 | const std = @import("std"); 4 | const cbor = @import("zbor"); 5 | const dt = @import("data_types.zig"); 6 | 7 | /// Relying party identifier 8 | /// 9 | /// A relying party identifier is a valid domain string identifying the WebAuthn 10 | /// Relying Party on whose behalf a given registration or authentication ceremony 11 | /// is being performed. 12 | /// 13 | /// TODO: 128 bytes should be enough but maybe we can also truncate the id as 14 | /// described by the CTAP2 spec. 15 | id: dt.ABS128T, 16 | /// Name of the relying party 17 | name: ?dt.ABS64T = null, 18 | 19 | pub fn new( 20 | id: []const u8, 21 | name: ?[]const u8, 22 | ) !@This() { 23 | return .{ 24 | .id = (try dt.ABS128T.fromSlice(id)).?, 25 | .name = try dt.ABS64T.fromSlice(name), 26 | }; 27 | } 28 | 29 | test "RelyingParty test #1" { 30 | const pkorg = try new("passkey.org", "Yubico Demo"); 31 | const expected = "\xa2\x62\x69\x64\x6b\x70\x61\x73\x73\x6b\x65\x79\x2e\x6f\x72\x67\x64\x6e\x61\x6d\x65\x6b\x59\x75\x62\x69\x63\x6f\x20\x44\x65\x6d\x6f"; 32 | 33 | var arr = std.ArrayList(u8).init(std.testing.allocator); 34 | defer arr.deinit(); 35 | 36 | try cbor.stringify(pkorg, .{}, arr.writer()); 37 | 38 | try std.testing.expectEqualSlices(u8, expected, arr.items); 39 | 40 | const di = try cbor.DataItem.new(expected); 41 | const pkorg2 = try cbor.parse(@This(), di, .{}); 42 | 43 | try std.testing.expectEqualStrings(pkorg.id.get(), pkorg2.id.get()); 44 | try std.testing.expectEqualStrings(pkorg.name.?.get(), pkorg2.name.?.get()); 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/User.zig: -------------------------------------------------------------------------------- 1 | //! Representation of a user 2 | 3 | const std = @import("std"); 4 | const cbor = @import("zbor"); 5 | const dt = @import("data_types.zig"); 6 | 7 | /// The user handle of the user account. A user handle is an opaque byte 8 | /// sequence with a maximum size of 64 bytes, and is not meant to be 9 | /// displayed to the user. 10 | id: dt.ABS64B = .{}, 11 | /// A human-palatable identifier for a user account. It is intended only for 12 | /// display, i.e., aiding the user in determining the difference between user 13 | /// accounts with similar displayNames. For example, "alexm", 14 | /// "alex.mueller@example.com" or "+14255551234". 15 | name: ?dt.ABS64T = null, 16 | /// A human-palatable name for the user account, intended only for display. 17 | /// For example, "Alex Müller" or "田中倫". The Relying Party SHOULD let 18 | /// the user choose this, and SHOULD NOT restrict the choice more than necessary. 19 | displayName: ?dt.ABS64T = null, 20 | 21 | pub fn new( 22 | id: []const u8, 23 | name: ?[]const u8, 24 | displayName: ?[]const u8, 25 | ) !@This() { 26 | return .{ 27 | .id = (try dt.ABS64B.fromSlice(id)).?, 28 | .name = try dt.ABS64T.fromSlice(name), 29 | .displayName = try dt.ABS64T.fromSlice(displayName), 30 | }; 31 | } 32 | 33 | pub fn getName(self: *const @This()) []const u8 { 34 | if (self.displayName) |dn| return dn.get(); 35 | if (self.name) |n| return n.get(); 36 | return ""; 37 | } 38 | 39 | test "User test #1" { 40 | const klaus = try new("\x0c\x43\x0e\xff\xff\x5f\x5f\x5f\x44\x45\x4d\x4f", "klaus", "klaus"); 41 | const expected = "\xa3\x62\x69\x64\x4c\x0c\x43\x0e\xff\xff\x5f\x5f\x5f\x44\x45\x4d\x4f\x64\x6e\x61\x6d\x65\x65\x6b\x6c\x61\x75\x73\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x65\x6b\x6c\x61\x75\x73"; 42 | 43 | var arr = std.ArrayList(u8).init(std.testing.allocator); 44 | defer arr.deinit(); 45 | 46 | try cbor.stringify(klaus, .{}, arr.writer()); 47 | 48 | try std.testing.expectEqualSlices(u8, expected, arr.items); 49 | 50 | const di = try cbor.DataItem.new(expected); 51 | const klaus2 = try cbor.parse(@This(), di, .{}); 52 | 53 | try std.testing.expectEqualStrings(klaus.id.get(), klaus2.id.get()); 54 | try std.testing.expectEqualStrings(klaus.name.?.get(), klaus2.name.?.get()); 55 | try std.testing.expectEqualStrings(klaus.displayName.?.get(), klaus2.displayName.?.get()); 56 | } 57 | -------------------------------------------------------------------------------- /lib/common/data_types.zig: -------------------------------------------------------------------------------- 1 | const cbor = @import("zbor"); 2 | const fido = @import("../main.zig"); 3 | 4 | const AuthenticatorTransports = fido.common.AuthenticatorTransports; 5 | const PublicKeyCredentialDescriptor = fido.common.PublicKeyCredentialDescriptor; 6 | const AttestationStatementFormatIdentifiers = fido.common.AttestationStatementFormatIdentifiers; 7 | const PublicKeyCredentialParameters = fido.common.PublicKeyCredentialParameters; 8 | 9 | pub const ABS32B = cbor.ArrayBackedSlice(32, u8, .Byte); 10 | pub const ABS48B = cbor.ArrayBackedSlice(48, u8, .Byte); 11 | pub const ABS64B = cbor.ArrayBackedSlice(64, u8, .Byte); 12 | pub const ABS256B = cbor.ArrayBackedSlice(256, u8, .Byte); 13 | pub const ABS512B = cbor.ArrayBackedSlice(512, u8, .Byte); 14 | 15 | pub const ABS32T = cbor.ArrayBackedSlice(32, u8, .Text); 16 | pub const ABS64T = cbor.ArrayBackedSlice(64, u8, .Text); 17 | pub const ABS128T = cbor.ArrayBackedSlice(128, u8, .Text); 18 | 19 | // Currently there are 6 different transports defined (usb, nfc, ble, smart-card, hybrid, and internal) 20 | pub const ABSAuthenticatorTransports = cbor.ArrayBackedSlice(6, AuthenticatorTransports, .Other); 21 | // TODO: 6 could be not enough if there are many credentials registered for a site 22 | pub const ABSPublicKeyCredentialDescriptor = cbor.ArrayBackedSlice(6, PublicKeyCredentialDescriptor, .Other); 23 | pub const ABSAttestationStatementFormatIdentifiers = cbor.ArrayBackedSlice(6, AttestationStatementFormatIdentifiers, .Other); 24 | pub const ABSPublicKeyCredentialParameters = cbor.ArrayBackedSlice(6, PublicKeyCredentialParameters, .Other); 25 | pub const ABSAuthenticatorData = cbor.ArrayBackedSlice(256, u8, .Byte); 26 | -------------------------------------------------------------------------------- /lib/ctap/StatusCodes.zig: -------------------------------------------------------------------------------- 1 | /// CTAP status codes. 2 | pub const StatusCodes = enum(u8) { 3 | /// Indicates successful response. 4 | ctap1_err_success = 0x00, 5 | /// The command is not a valid CTAP command. 6 | ctap1_err_invalid_command = 0x01, 7 | /// The command included an invalid parameter. 8 | ctap1_err_invalid_parameter = 0x02, 9 | /// Invalid message or item length. 10 | ctap1_err_invalid_length = 0x03, 11 | /// Invalid message sequencing. 12 | ctap1_err_invalid_seq = 0x04, 13 | /// Message timed out. 14 | ctap1_err_tiemout = 0x05, 15 | /// Channel busy. 16 | ctap1_err_channel_busy = 0x06, 17 | /// Command requires channel lock. 18 | ctap1_err_lock_required = 0x0a, 19 | /// Command not allowed on this cid. 20 | ctap1_err_invalid_channel = 0x0b, 21 | /// Invalid/ unexpected CBOR error. 22 | ctap2_err_cbor_unexpected_type = 0x11, 23 | /// Error when parsing CBOR. 24 | ctap2_err_invalid_cbor = 0x12, 25 | /// Missing non-optional parameter. 26 | ctap2_err_missing_parameter = 0x14, 27 | /// Limit for number of items exceeded. 28 | ctap2_err_limit_exceeded = 0x15, 29 | /// Unsupported extension. 30 | ctap2_err_unsupported_extension = 0x16, 31 | /// Valid credential found in the excluded list. 32 | ctap2_err_credential_excluded = 0x19, 33 | /// Processing (Lenghty operation is in progress). 34 | ctap2_err_processing = 0x21, 35 | /// Credential not valid for the authenticator. 36 | ctap2_err_invalid_credential = 0x22, 37 | /// Authenticator is waiting for user interaction. 38 | ctap2_err_user_action_pending = 0x23, 39 | /// Processing, lengthy operation is in progress. 40 | ctap2_err_operation_pending = 0x24, 41 | /// No request is pending. 42 | ctap2_err_no_operations = 0x25, 43 | /// Authenticator does not support requested algorithm. 44 | ctap2_err_unsupported_algorithm = 0x26, 45 | /// Not authorized for requested operation. 46 | ctap2_err_operation_denied = 0x27, 47 | /// Internal key storage is full. 48 | ctap2_err_key_store_full = 0x28, 49 | /// Authenticator cannot cancel as it is not busy. 50 | ctap2_err_not_busy = 0x29, 51 | /// No outstanding operations. 52 | ctap2_err_no_operation_pending = 0x2a, 53 | /// Unsupported option. 54 | ctap2_err_unsupported_option = 0x2b, 55 | /// Not a valid option for current operation. 56 | ctap2_err_invalid_option = 0x2c, 57 | /// Pending keep alive was canceled. 58 | ctap2_err_keepalive_cancel = 0x2d, 59 | /// No valid credentials provided. 60 | ctap2_err_no_credentials = 0x2e, 61 | /// Timeout waiting for user interaction. 62 | ctap2_err_user_action_timeout = 0x2f, 63 | /// Continuation command, such as, `authenticatorGetNexAssertion` not allowed. 64 | ctap2_err_not_allowed = 0x30, 65 | /// PIN invalid. 66 | ctap2_err_pin_invalid = 0x31, 67 | /// PIN blocked. 68 | ctap2_err_pin_blocked = 0x32, 69 | /// PIN authentication (`pinAuth`) verification failed. 70 | ctap2_err_pin_auth_invalid = 0x33, 71 | /// PIN authentication (`pinAuth`) blocked. Requires power recycle to reset. 72 | ctap2_err_pin_auth_blocked = 0x34, 73 | /// No PIN has been set. 74 | ctap2_err_pin_not_set = 0x35, 75 | /// PIN is required for the selected operation. 76 | ctap2_err_pin_required = 0x36, 77 | /// PIN policy violation. Currently only enforces minimum length. 78 | ctap2_err_pin_policy_violation = 0x37, 79 | /// `pinToken` expired on authenticator. 80 | ctap2_err_pin_token_expired = 0x38, 81 | /// Authenticator cannot handle this request due to memory constraints. 82 | ctap2_err_request_too_large = 0x39, 83 | /// The current operation has timed out. 84 | ctap2_err_action_timeout = 0x3a, 85 | /// User presence is required for the requested operation. 86 | ctap2_err_up_required = 0x3b, 87 | /// built-in user verification is disabled. 88 | ctap2_err_uv_blocked = 0x3c, 89 | /// A checksum did not match. 90 | ctap2_err_integrity_failure = 0x3d, 91 | /// The requested subcommand is either invalid or not implemented. 92 | ctap2_err_invalid_subcommand = 0x3e, 93 | /// built-in user verification unsuccessful. The platform SHOULD retry. 94 | ctap2_err_uv_invalid = 0x3f, 95 | /// The permissions parameter contains an unauthorized permission. 96 | ctap2_err_unauthorized_permission = 0x40, 97 | /// Other unspecified error. 98 | ctap1_err_other = 0x7f, 99 | /// CTAP 2 spac last error. 100 | ctap2_err_spec_last = 0xdf, 101 | /// Extension specific error. 102 | ctap2_err_extension_first = 0xe0, 103 | ctap2_err_extension_2 = 0xe1, 104 | ctap2_err_extension_3 = 0xe2, 105 | ctap2_err_extension_4 = 0xe3, 106 | ctap2_err_extension_5 = 0xe4, 107 | ctap2_err_extension_6 = 0xe5, 108 | ctap2_err_extension_7 = 0xe6, 109 | ctap2_err_extension_8 = 0xe7, 110 | ctap2_err_extension_9 = 0xe8, 111 | ctap2_err_extension_10 = 0xe9, 112 | ctap2_err_extension_11 = 0xea, 113 | ctap2_err_extension_12 = 0xeb, 114 | ctap2_err_extension_13 = 0xec, 115 | ctap2_err_extension_14 = 0xed, 116 | ctap2_err_extension_15 = 0xee, 117 | /// Extension specific error. 118 | ctap2_err_extension_last = 0xef, 119 | /// Vendor specific error. 120 | ctap2_err_vendor_first = 0xf0, 121 | ctap2_err_vendor_2 = 0xf1, 122 | ctap2_err_vendor_3 = 0xf2, 123 | ctap2_err_vendor_4 = 0xf3, 124 | ctap2_err_vendor_5 = 0xf4, 125 | ctap2_err_vendor_6 = 0xf5, 126 | ctap2_err_vendor_7 = 0xf6, 127 | ctap2_err_vendor_8 = 0xf7, 128 | ctap2_err_vendor_9 = 0xf8, 129 | ctap2_err_vendor_10 = 0xf9, 130 | ctap2_err_vendor_11 = 0xfa, 131 | ctap2_err_vendor_12 = 0xfb, 132 | ctap2_err_vendor_13 = 0xfc, 133 | ctap2_err_vendor_14 = 0xfd, 134 | ctap2_err_vendor_15 = 0xfe, 135 | /// Vendor specific error. 136 | ctap2_err_vendor_last = 0xff, 137 | }; 138 | -------------------------------------------------------------------------------- /lib/ctap/auth/Authenticator.zig: -------------------------------------------------------------------------------- 1 | //! Representation of a FIDO2 authenticator 2 | 3 | const std = @import("std"); 4 | const cbor = @import("zbor"); 5 | const fido = @import("../../main.zig"); 6 | 7 | const Allocator = std.mem.Allocator; 8 | 9 | const Callbacks = fido.ctap.authenticator.callbacks.Callbacks; 10 | const Ctap2CommandMapping = fido.ctap.authenticator.callbacks.Ctap2CommandMapping; 11 | const Data = fido.ctap.authenticator.callbacks.Data; 12 | const DataIterator = fido.ctap.authenticator.callbacks.DataIterator; 13 | const Error = fido.ctap.authenticator.callbacks.Error; 14 | const Settings = fido.ctap.authenticator.Settings; 15 | const PinUvAuth = fido.ctap.pinuv.PinUvAuth; 16 | const SigAlg = fido.ctap.crypto.SigAlg; 17 | const AttestationType = fido.common.AttestationType; 18 | const PublicKeyCredentialParameters = fido.common.PublicKeyCredentialParameters; 19 | 20 | const Response = fido.ctap.authenticator.Response; 21 | const StatusCodes = fido.ctap.StatusCodes; 22 | const Commands = fido.ctap.commands.Commands; 23 | const dt = fido.common.dt; 24 | const ClientDataHash = fido.ctap.crypto.ClientDataHash; 25 | 26 | pub const Auth = struct { 27 | const Self = @This(); 28 | 29 | /// Callbacks provided by the underlying platform 30 | callbacks: Callbacks, 31 | 32 | /// Offer users the option to "override" certain functions related 33 | /// to CTAP2 commands. This has (at least) two advantages: 34 | /// 1. Users can use their own functions, e.g. they only need a specific 35 | /// want to experiment, want a updated version of a callback. 36 | /// 2. We dont need to provide the full spec but only the basics. 37 | /// Users can then add what they need. 38 | commands: []const Ctap2CommandMapping = &.{ 39 | .{ .cmd = 0x01, .cb = fido.ctap.commands.authenticator.authenticatorMakeCredential }, 40 | .{ .cmd = 0x02, .cb = fido.ctap.commands.authenticator.authenticatorGetAssertion }, 41 | .{ .cmd = 0x04, .cb = fido.ctap.commands.authenticator.authenticatorGetInfo }, 42 | .{ .cmd = 0x06, .cb = fido.ctap.commands.authenticator.authenticatorClientPin }, 43 | .{ .cmd = 0x08, .cb = fido.ctap.commands.authenticator.authenticatorGetNextAssertion }, 44 | .{ .cmd = 0x0b, .cb = fido.ctap.commands.authenticator.authenticatorSelection }, 45 | }, 46 | 47 | /// Authenticator settings that represent the authenticators capabilities 48 | settings: Settings, 49 | 50 | /// Pin uv auth protocol 51 | token: PinUvAuth, 52 | 53 | /// A list of signature algorithms 54 | /// 55 | /// This list should match the algorithms defined within the settings. 56 | algorithms: []const fido.ctap.crypto.SigAlg, 57 | 58 | attestation: AttestationType = .Self, 59 | 60 | /// Determines if the authenticator should use a constant signature counter. 61 | /// 62 | /// A Relying Party stores the signature counter of the most recent authenticatorGetAssertion operation. 63 | /// In subsequent authenticatorGetAssertion operations, the Relying Party compares the stored signature 64 | /// counter value with the new signCount value returned in the assertion’s authenticator data. 65 | /// 66 | /// * `false` - The signature counter is never incremented (stays always 0). This can be used for shared 67 | /// resident keys (passkeys) where "clone detection" is not required. 68 | /// * `true` - Increment the signature counter for each successful signature creation. 69 | constSignCount: bool = false, 70 | 71 | getAssertion: ?struct { 72 | ts: i64, 73 | count: usize, 74 | total: usize, 75 | up: bool, 76 | uv: bool, 77 | allowList: ?dt.ABSPublicKeyCredentialDescriptor = null, 78 | rpId: dt.ABS128T, 79 | cdh: ClientDataHash, 80 | } = null, 81 | 82 | /// Cryptographic secure (P)RNG 83 | random: std.Random, 84 | 85 | milliTimestamp: *const fn () i64, 86 | 87 | pub fn default(callbacks: Callbacks) @This() { 88 | return .{ 89 | .callbacks = callbacks, 90 | .settings = .{ 91 | .versions = &.{ .FIDO_2_0, .FIDO_2_1 }, 92 | .extensions = &.{.credProtect}, 93 | .aaguid = "\x6f\x15\x82\x74\xaa\xb6\x44\x3d\x9b\xcf\x8a\x3f\x69\x29\x7c\x88".*, 94 | .options = .{ 95 | .credMgmt = false, 96 | .rk = true, 97 | .uv = if (callbacks.uv) |_| true else null, 98 | // This is a platform authenticator even if we use usb for ipc 99 | .plat = true, 100 | // We don't support client pin 101 | .clientPin = null, 102 | .pinUvAuthToken = true, 103 | .alwaysUv = false, 104 | }, 105 | .pinUvAuthProtocols = &.{.V2}, 106 | .transports = &.{.usb}, 107 | .algorithms = &.{.{ .alg = .Es256 }}, 108 | .firmwareVersion = 0xcafe, 109 | .remainingDiscoverableCredentials = 100, 110 | }, 111 | .token = PinUvAuth.v2(std.crypto.random), 112 | .algorithms = &.{ 113 | fido.ctap.crypto.algorithms.Es256, 114 | }, 115 | .milliTimestamp = std.time.milliTimestamp, 116 | .random = std.crypto.random, 117 | }; 118 | } 119 | 120 | pub fn init(self: *@This()) !void { 121 | // Initialize piNUv 122 | self.token.initialize(); 123 | } 124 | 125 | pub fn handle( 126 | self: *@This(), 127 | out: *[fido.ctap.transports.ctaphid.authenticator.MAX_DATA_SIZE]u8, 128 | request: []const u8, 129 | ) []const u8 { 130 | var buffer: [fido.ctap.transports.ctaphid.authenticator.MAX_DATA_SIZE]u8 = undefined; 131 | var fba = std.heap.FixedBufferAllocator.init(&buffer); 132 | const allocator = fba.allocator(); 133 | // Buffer for the response message 134 | var res = std.ArrayList(u8).init(allocator); 135 | var response = res.writer(); 136 | response.writeByte(0x00) catch { 137 | std.log.err("Auth.handle: unable to initialize response", .{}); 138 | out[0] = @intFromEnum(StatusCodes.ctap1_err_other); 139 | return out[0..1]; 140 | }; 141 | 142 | // Decode the command of the given message 143 | if (request.len < 1) { 144 | out[0] = @intFromEnum(StatusCodes.ctap1_err_invalid_length); 145 | return out[0..1]; 146 | } 147 | const cmd = request[0]; 148 | 149 | // Updates (and possibly invalidates) an existing pinUvAuth token. This has to 150 | // be done before handling any request. 151 | self.token.pinUvAuthTokenUsageTimerObserver(self.milliTimestamp()); 152 | 153 | if (request.len > 1) { 154 | std.log.info("request({d}): {s}", .{ cmd, std.fmt.fmtSliceHexLower(request[1..]) }); 155 | } 156 | 157 | for (self.commands) |command| { 158 | if (command.cmd == cmd) { 159 | const status = command.cb( 160 | self, 161 | request[1..], 162 | &res, 163 | ); 164 | 165 | out[0] = @intFromEnum(status); 166 | if (status != .ctap1_err_success) { 167 | return out[0..1]; 168 | } 169 | 170 | break; 171 | } 172 | } else { 173 | std.log.err("invalid command: {d}", .{cmd}); 174 | out[0] = @intFromEnum(StatusCodes.ctap2_err_not_allowed); 175 | return out[0..1]; 176 | } 177 | 178 | std.log.info("response({d}): {s}", .{ cmd, std.fmt.fmtSliceHexLower(res.items) }); 179 | @memcpy(out[0..res.items.len], res.items); 180 | return out[0..res.items.len]; 181 | } 182 | 183 | /// Given a set of credential parameters, select the first algorithm that is also supported by the authenticator. 184 | pub fn selectSignatureAlgorithm(self: *@This(), params: []const PublicKeyCredentialParameters) ?SigAlg { 185 | for (params) |param| { 186 | for (self.algorithms) |alg| { 187 | if (param.alg == alg.alg) { 188 | return alg; 189 | } 190 | } 191 | } 192 | return null; 193 | } 194 | 195 | /// Returns true if the authenticator supports (built in) user verification 196 | pub fn uvSupported(self: *@This()) bool { 197 | return self.settings.options.uv != null and 198 | self.settings.options.uv.? and 199 | self.callbacks.uv != null; 200 | } 201 | 202 | pub fn clientPinSupported(self: *@This()) ?bool { 203 | _ = self; 204 | // We dont support clientPin (for now). The rational for this is 205 | // that the focus of this library shifted towards platform authenticators 206 | // which can implement builtin user verification (even passwords if 207 | // they like). 208 | return null; 209 | } 210 | 211 | /// Returns true if the authenticator is protected by some form of user verification 212 | pub fn isProtected(self: *@This()) bool { 213 | return self.uvSupported() or if (self.clientPinSupported()) |cp| cp else false; 214 | } 215 | 216 | /// Returns true if the authenticator supports resident keys/ discoverable credentials/ passkey 217 | pub fn rkSupported(self: *@This()) bool { 218 | return self.settings.options.rk; 219 | } 220 | 221 | /// Returns true if always uv is enables, false otherwise 222 | pub fn alwaysUv(self: *@This()) !bool { 223 | const settings = self.callbacks.read_settings(); 224 | return settings.always_uv; 225 | } 226 | 227 | /// Returns true if the authenticator doesn't require some form of user verification 228 | pub fn makeCredUvNotRqd(self: *@This()) bool { 229 | return self.settings.options.makeCredUvNotRqd; 230 | } 231 | 232 | pub fn noMcGaPermissionsWithClientPin(self: *@This()) bool { 233 | return self.settings.options.noMcGaPermissionsWithClientPin; 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /lib/ctap/auth/Callbacks.zig: -------------------------------------------------------------------------------- 1 | //! Callbacks provided by the underlying platform/ the user 2 | //! 3 | //! The callbacks are required to keep the library platform 4 | //! agnostic. 5 | 6 | const std = @import("std"); 7 | const fido = @import("../../main.zig"); 8 | const cks = @import("cks"); 9 | const dt = fido.common.dt; 10 | 11 | pub const CallbackError = error{ 12 | Success, 13 | DoesAlreadyExist, 14 | DoesNotExist, 15 | KeyStoreFull, 16 | OutOfMemory, 17 | Timeout, 18 | Other, 19 | }; 20 | 21 | pub const Error = enum(i32) { 22 | SUCCESS = 0, 23 | DoesAlreadyExist = -1, 24 | DoesNotExist = -2, 25 | KeyStoreFull = -3, 26 | OutOfMemory = -4, 27 | Timeout = -5, 28 | Other = -6, 29 | }; 30 | 31 | /// Result value of the `up` callback 32 | pub const UpResult = enum(i32) { 33 | /// The user has denied the action 34 | Denied = 0, 35 | /// The user has accepted the action 36 | Accepted = 1, 37 | /// The user presence check has timed out 38 | Timeout = 2, 39 | }; 40 | 41 | /// Result value of the `uv` callback 42 | pub const UvResult = enum(i32) { 43 | /// The user has denied the action 44 | Denied = 0, 45 | /// The user has accepted the action 46 | Accepted = 1, 47 | /// The user has accepted the action 48 | AcceptedWithUp = 2, 49 | /// The user presence check has timed out 50 | Timeout = 3, 51 | }; 52 | 53 | pub const DataIterator = struct { 54 | d: [*c][*c]u8 = 0, 55 | i: usize = 0, 56 | allocator: std.mem.Allocator, 57 | 58 | pub fn next(self: *@This()) ?[]const u8 { 59 | if (self.d == null) return null; 60 | 61 | if (self.d[self.i] == null) { 62 | return null; 63 | } else { 64 | defer self.i += 1; 65 | return self.d[self.i][0..strlen(self.d[self.i])]; 66 | } 67 | } 68 | 69 | pub fn deinit(self: *@This()) void { 70 | if (self.d == null) return; 71 | 72 | var i: usize = 0; 73 | while (self.d[i] != null) { 74 | var x = self.d[i]; 75 | const l = strlen(x); 76 | // First overwrite the region with 0. This prevents sensitive information 77 | // from lingering in memory for longer than neccessary. 78 | @memset(x[0..l], 0); 79 | // Then free the memory 80 | self.allocator.free(x[0 .. l + 1]); 81 | i += 1; 82 | x = self.d[i]; 83 | } 84 | self.allocator.free(self.d[0 .. i + 1]); 85 | } 86 | }; 87 | 88 | inline fn strlen(s: [*c]const u8) usize { 89 | var i: usize = 0; 90 | while (s[i] != 0) : (i += 1) {} 91 | return i; 92 | } 93 | 94 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 95 | // Callback Types 96 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 97 | 98 | pub const UpCallback = *const fn ( 99 | /// Information about the context (e.g., make credential) 100 | info: []const u8, 101 | /// Information about the user (e.g., `David Sugar (david@example.com)`) 102 | user: ?fido.common.User, 103 | /// Information about the relying party (e.g., `Github (github.com)`) 104 | rp: ?fido.common.RelyingParty, 105 | ) UpResult; 106 | 107 | /// Type of the User Verification (UV) callback 108 | /// 109 | /// This callback can be backed by ANY form of 110 | /// user verification like a password, finger 111 | /// print, ... 112 | pub const UvCallback = ?*const fn ( 113 | /// Information about the context (e.g., make credential) 114 | info: []const u8, 115 | /// Information about the user (e.g., `David Sugar (david@example.com)`) 116 | user: ?fido.common.User, 117 | /// Information about the relying party (e.g., `Github (github.com)`) 118 | rp: ?fido.common.RelyingParty, 119 | ) UvResult; 120 | 121 | /// Read the first credential associated with `id` and `rp` into out 122 | /// 123 | /// ## Argument options 124 | /// 125 | /// * `id = null` and `rp = null` - Return all credential entries. 126 | /// * `id != null` and `rp = null` - Return the entry with the specified `id`. This can be a credential or settings. 127 | /// * `id = null` and `rp != null` - Return all credentials associated with the given `rp` ID. 128 | pub const ReadFirstCallback = *const fn ( 129 | id: ?dt.ABS64B, 130 | rp: ?dt.ABS128T, 131 | hash: ?[32]u8, 132 | ) CallbackError!fido.ctap.authenticator.Credential; 133 | 134 | /// This function can be called multiple times after calling the ReadFirstCallback to obtain the remaining credentials. 135 | pub const ReadNextCallback = *const fn () CallbackError!fido.ctap.authenticator.Credential; 136 | 137 | pub const ReadSettingsCallback = *const fn () fido.ctap.authenticator.Meta; 138 | pub const WriteSettingsCallback = *const fn (data: fido.ctap.authenticator.Meta) void; 139 | 140 | /// Write `data` to permanent storage (e.g., database, filesystem, ...) 141 | pub const CreateCallback = *const fn ( 142 | data: fido.ctap.authenticator.Credential, 143 | ) CallbackError!void; 144 | 145 | /// Delete the entry with the given `id` 146 | /// 147 | /// Returns either Error.SUCCESS on success or an error. 148 | pub const DeleteCallback = *const fn ( 149 | id: [*c]const u8, 150 | ) callconv(.C) Error; 151 | 152 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 153 | // Data Structures for CTAP2 (CBOR) commands 154 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 155 | 156 | /// This callback signature is used for CTAP2 command-functions like: 157 | /// * `authenticatorGetAssertion` 158 | /// * `authenticatorMakeCredential` 159 | pub const Ctap2CommandCallback = *const fn ( 160 | /// Pointer to the authenticator struct 161 | auth: *fido.ctap.authenticator.Auth, 162 | /// CBOR encoded params 163 | params: []const u8, 164 | /// ArrayList for the respones 165 | *std.ArrayList(u8), 166 | ) fido.ctap.StatusCodes; 167 | 168 | pub const Ctap2CommandMapping = struct { 169 | cmd: u8, 170 | cb: Ctap2CommandCallback, 171 | }; 172 | 173 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 174 | // Some other (optional) callbacks 175 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 176 | 177 | /// A callback that gets the decrypted PIN hash passed to it. 178 | /// This allows things like deriving a secret from it for 179 | /// en-/decrypting secrets based on the PIN. 180 | /// 181 | /// For this to work you need to use the default authenticatorClientPin 182 | /// function or incorporate this call into your own function. 183 | pub const ProcessPinHash = *const fn (ph: []const u8) void; 184 | 185 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 186 | // Callbacks 187 | // +++++++++++++++++++++++++++++++++++++++++++++++++++ 188 | 189 | pub const Callbacks = struct { 190 | /// Request user presence 191 | up: UpCallback, 192 | 193 | /// User verification callback 194 | /// 195 | /// This callback should execute a built-in user verification method. 196 | /// 197 | /// This callback is optional. Client request with the uv flag set will 198 | /// fail if this callback isn't provided. 199 | /// 200 | /// Possible methods are: 201 | /// - Password 202 | /// - Finger print sensor 203 | /// - Pattern 204 | uv: UvCallback, 205 | 206 | read_first: ReadFirstCallback, 207 | read_next: ReadNextCallback, 208 | write: CreateCallback, 209 | delete: DeleteCallback, 210 | read_settings: ReadSettingsCallback, 211 | write_settings: WriteSettingsCallback, 212 | processPinHash: ?ProcessPinHash = null, 213 | }; 214 | -------------------------------------------------------------------------------- /lib/ctap/auth/Credential.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fido = @import("../../main.zig"); 3 | const cbor = @import("zbor"); 4 | const dt = fido.common.dt; 5 | 6 | /// Credential ID 7 | id: dt.ABS64B, 8 | 9 | /// User information 10 | user: fido.common.User, 11 | 12 | /// Information about the relying party 13 | rp: fido.common.RelyingParty, 14 | 15 | /// Number of signatures issued using the given credential 16 | sign_count: u64, 17 | 18 | key: cbor.cose.Key, 19 | 20 | /// Epoch time stamp this credential was created 21 | created: i64, 22 | 23 | /// Is this credential discoverable or not 24 | /// 25 | /// This is kind of stupid but authenticatorMakeCredential 26 | /// docs state, that you're not allowed to create a discoverable 27 | /// credential if not explicitely requested. The docs also state 28 | /// that you're allowed to keep (some) state, e.g., store the key. 29 | discoverable: bool = false, 30 | 31 | policy: fido.ctap.extensions.CredentialCreationPolicy = .userVerificationOptional, 32 | // 33 | ///// Belongs to hmac secret 34 | //cred_random_with_uv: [32]u8 = undefined, 35 | // 36 | ///// Belongs to hmac secret 37 | //cred_random_without_uv: [32]u8 = undefined, 38 | 39 | pub fn desc(_: void, lhs: @This(), rhs: @This()) bool { 40 | return lhs.created > rhs.created; 41 | } 42 | -------------------------------------------------------------------------------- /lib/ctap/auth/Meta.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fido = @import("../../main.zig"); 3 | 4 | /// Number of pin retries left 5 | pinRetries: u8 = 8, 6 | /// Number of uv retries left 7 | uvRetries: u8 = 8, 8 | /// Pin has to be changed 9 | force_pin_change: bool = false, 10 | /// The minimum pin length 11 | min_pin_length: u8 = 4, 12 | /// Enforce user verification 13 | always_uv: bool = false, 14 | /// Pin with a max length of 63 bytes 15 | pin: ?[63]u8 = null, 16 | /// Gloabl credential usage counter 17 | usage_count: u64 = 0, 18 | -------------------------------------------------------------------------------- /lib/ctap/auth/Options.zig: -------------------------------------------------------------------------------- 1 | //! Authenticator options. 2 | //! 3 | //! When an option is not present, the default is applied. 4 | 5 | /// present + true: enterprise attestation supported and enabled 6 | /// present + false: enterprise attestation supported but disabled 7 | /// absent: enterprise attestation not supported 8 | ep: ?bool = null, 9 | /// Resident key: Indicates that the device is capable of storing keys on 10 | /// the device itself and therefore can satisfy the `authenticatorGetAssertion` 11 | /// request with `allowList` parameter not specified or empty. 12 | rk: bool = false, 13 | /// User presence. 14 | /// true: indicates that the device is capable of testing user presence. 15 | up: bool = true, 16 | /// User verification: Device is capable of verifiying the user within itself. 17 | /// present + true: device is capable of user verification within itself and 18 | /// has been configured. 19 | /// present + false: device is capable of user verification within itself and 20 | /// has not been yet configured. 21 | /// absent: device is not capable of user verification within itself. 22 | /// 23 | /// A device that can only do Client PIN will not return the "uv" parameter. 24 | uv: ?bool = null, 25 | /// Platform device: Indicates that the device is attached to the client 26 | /// and therefore can't be removed and used on another client. 27 | plat: bool = false, 28 | /// present + true: requesting the acfg permission when invoking 29 | /// getPinUvAuthTokenUsingUvWithPermissions is supported. 30 | /// present + flase or absent: acfg permission not supported 31 | uvAcfg: ?bool = null, 32 | /// present + true: authenticator supports always require user verification 33 | /// present + false: authenticator supports always require user verification but its disabled 34 | /// absent: doesent support always require user verification 35 | /// if present + true: the authenticator MUST set the value of makeCredUvNotRqd to false 36 | alwaysUv: ?bool = null, 37 | /// present + true: requesting credMgmt is supported 38 | /// present + false or absent: not supported 39 | credMgmt: ?bool = null, 40 | /// authenticatorConfig command is supported Y/n 41 | authnrCfg: ?bool = null, 42 | bioEnroll: ?bool = null, 43 | /// present + true: device is capable of accepting a PIN from the client and 44 | /// PIN has been set 45 | /// present + false: device is capable of accepting a PIN from the client and 46 | /// PIN has not been set yet. 47 | /// absent: indicates that the device is not capable of accepting a PIN from the client. 48 | clientPin: ?bool = null, 49 | /// Authenticator supports largeBlobs Y/n 50 | largeBlobs: ?bool = null, 51 | /// present + true: the authenticator supports authenticatorClientPIN's 52 | /// getPinUvAuthTokenUsingPinWithPermissions subcommand. 53 | /// If the uv option id is present and set to true, then 54 | /// the authenticator supports authenticatorClientPIN's 55 | /// getPinUvAuthTokenUsingUvWithPermissions subcommand. 56 | /// present + false or absent: the authenticator does not support authenticatorClientPIN's 57 | /// getPinUvAuthTokenUsingPinWithPermissions and 58 | /// getPinUvAuthTokenUsingUvWithPermissions subcommands. 59 | pinUvAuthToken: ?bool = null, 60 | /// Support for making non-discoverable credentials without requiring User Verification. 61 | makeCredUvNotRqd: bool = false, 62 | /// present + true: pinUvAuthToken not allowd for credential creation and assertion 63 | /// present + false or absetn: can be used for credential creation and assertion 64 | noMcGaPermissionsWithClientPin: bool = false, 65 | /// This is used by authenticators that implemented the credMgmt feature before 66 | /// if got added to the CTAP2 spec. 67 | credentialMgmtPreview: ?bool = null, 68 | 69 | // TODO: support remaining options 70 | -------------------------------------------------------------------------------- /lib/ctap/auth/Response.zig: -------------------------------------------------------------------------------- 1 | pub const ResponseTag = enum { ok, err }; 2 | 3 | /// Authenticator response 4 | pub const Response = union(ResponseTag) { 5 | /// Slice containing the response message 6 | ok: []const u8, 7 | /// A CTAP status code 8 | err: u8, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/ctap/commands/Commands.zig: -------------------------------------------------------------------------------- 1 | /// Commands supported by the CTAP protocol. 2 | pub const Commands = enum(u8) { 3 | /// Request generation of a new credential in the authenticator. 4 | authenticatorMakeCredential = 0x01, 5 | /// Request cryptographic proof of user authentication as well as user consent to a given 6 | /// transaction, using a previously generated credential that is bound to the authenticator 7 | /// and relying party identifier. 8 | authenticatorGetAssertion = 0x02, 9 | /// Request a list of all supported protocol versions, supported extensions, AAGUID of the 10 | /// device, and its capabilities 11 | authenticatorGetInfo = 0x04, 12 | /// Key agreement, setting a new PIN, changing a existing PIN, getting a `pinToken`. 13 | authenticatorClientPin = 0x06, 14 | /// Reset an authenticator back to factory default state, invalidating all generated credentials. 15 | authenticatorReset = 0x07, 16 | /// The client calls this method when the authenticatorGetAssertion response contains the 17 | /// `numberOfCredentials` member and the number of credentials exceeds 1. 18 | authenticatorGetNextAssertion = 0x08, 19 | authenticatorBioEnrollment = 0x09, 20 | authenticatorCredentialManagement = 0x0a, 21 | /// This command allows the platform to let a user select a certain authenticator by asking for user presence. 22 | authenticatorSelection = 0x0b, 23 | /// This command allows a platform to store a larger amount of information associated with a credential. 24 | authenticatorLargeBlobs = 0x0c, 25 | /// This command is used to configure various authenticator features through the use of its subcommands. 26 | authenticatorConfig = 0x0d, 27 | /// Vendor specific implementation. 28 | /// Command codes in the range between authenticatorVendorFirst and authenticatorVendorLast 29 | /// may be used for vendor-specific implementations. For example, the vendor may choose to 30 | /// put in some testing commands. Note that the FIDO client will never generate these commands. 31 | /// All other command codes are reserved for future use and may not be used. 32 | authenticatorVendorFirst = 0x40, 33 | authenticatorCredentialManagementYubico = 0x41, 34 | /// Vendor specific implementation. 35 | authenticatorVendorLast = 0xbf, 36 | 37 | pub fn fromRaw(byte: u8) !Commands { 38 | switch (byte) { 39 | 0x01 => return .authenticatorMakeCredential, 40 | 0x02 => return .authenticatorGetAssertion, 41 | 0x04 => return .authenticatorGetInfo, 42 | 0x06 => return .authenticatorClientPin, 43 | 0x07 => return .authenticatorReset, 44 | 0x08 => return .authenticatorGetNextAssertion, 45 | 0x09 => return .authenticatorBioEnrollment, 46 | 0x0a => return .authenticatorCredentialManagement, 47 | 0x0b => return .authenticatorSelection, 48 | 0x0c => return .authenticatorLargeBlobs, 49 | 0x0d => return .authenticatorConfig, 50 | 0x40 => return .authenticatorVendorFirst, 51 | 0x41 => return .authenticatorCredentialManagementYubico, 52 | 0xbf => return .authenticatorVendorLast, 53 | else => return error.InvalidCommand, 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /lib/ctap/commands/authenticator/authenticatorClientPin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Hkdf = std.crypto.kdf.hkdf.HkdfSha256; 3 | const cbor = @import("zbor"); 4 | const fido = @import("../../../main.zig"); 5 | const dt = fido.common.dt; 6 | 7 | pub fn authenticatorClientPin( 8 | auth: *fido.ctap.authenticator.Auth, 9 | request: []const u8, 10 | out: *std.ArrayList(u8), 11 | ) fido.ctap.StatusCodes { 12 | const retry_state = struct { 13 | threadlocal var ctr: u8 = 3; 14 | threadlocal var powerCycleState: bool = false; 15 | }; 16 | 17 | const client_pin_param = cbor.parse( 18 | fido.ctap.request.ClientPin, 19 | cbor.DataItem.new(request) catch { 20 | return .ctap2_err_invalid_cbor; 21 | }, 22 | .{}, 23 | ) catch { 24 | return .ctap2_err_invalid_cbor; 25 | }; 26 | 27 | var client_pin_response: ?fido.ctap.response.ClientPin = null; 28 | 29 | // Handle one of the sub-commands 30 | switch (client_pin_param.subCommand) { 31 | .getPinRetries => { 32 | const settings = auth.callbacks.read_settings(); 33 | 34 | client_pin_response = .{ 35 | .pinRetries = settings.pinRetries, 36 | .powerCycleState = retry_state.powerCycleState, 37 | }; 38 | }, 39 | .getUVRetries => { 40 | const settings = auth.callbacks.read_settings(); 41 | 42 | client_pin_response = .{ 43 | .uvRetries = settings.uvRetries, 44 | }; 45 | }, 46 | .getKeyAgreement => { 47 | const protocol = if (client_pin_param.pinUvAuthProtocol) |prot| prot else { 48 | return fido.ctap.StatusCodes.ctap2_err_missing_parameter; 49 | }; 50 | 51 | // return error if authenticator doesn't support the selected protocol. 52 | if (protocol != auth.token.version) { 53 | return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; 54 | } 55 | 56 | client_pin_response = .{ 57 | .keyAgreement = auth.token.getPublicKey(), 58 | }; 59 | }, 60 | .getPinUvAuthTokenUsingUvWithPermissions => { 61 | if (retry_state.ctr == 0) { 62 | return fido.ctap.StatusCodes.ctap2_err_pin_auth_blocked; 63 | } 64 | 65 | if (client_pin_param.pinUvAuthProtocol == null or 66 | client_pin_param.permissions == null or 67 | client_pin_param.permissions == null or 68 | client_pin_param.keyAgreement == null) 69 | { 70 | return fido.ctap.StatusCodes.ctap2_err_missing_parameter; 71 | } 72 | 73 | if (client_pin_param.pinUvAuthProtocol.? != auth.token.version) { 74 | return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; 75 | } 76 | 77 | if (client_pin_param.permissions.? == 0) { 78 | return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; 79 | } 80 | 81 | // Check if all requested premissions are valid 82 | const options = auth.settings.options; 83 | const cm = client_pin_param.cmPermissionSet() and (options.credMgmt == null or options.credMgmt.? == false); 84 | const be = client_pin_param.bePermissionSet() and (options.bioEnroll == null); 85 | const lbw = client_pin_param.lbwPermissionSet() and (options.largeBlobs == null or options.largeBlobs.? == false); 86 | const acfg = client_pin_param.acfgPermissionSet() and (options.authnrCfg == null or options.authnrCfg.? == false); 87 | // The mc and ga permissions are always considered authorized, thus they are not listed below. 88 | if (cm or be or lbw or acfg) { 89 | return fido.ctap.StatusCodes.ctap2_err_unauthorized_permission; 90 | } 91 | 92 | if (!auth.uvSupported()) { 93 | return fido.ctap.StatusCodes.ctap2_err_not_allowed; 94 | } 95 | 96 | const settings = auth.callbacks.read_settings(); 97 | 98 | if (settings.uvRetries == 0) { 99 | return fido.ctap.StatusCodes.ctap2_err_uv_blocked; 100 | } 101 | 102 | var user_present = false; 103 | switch (auth.token.performBuiltInUv( 104 | true, 105 | auth, 106 | "User Verification", 107 | null, 108 | null, 109 | )) { 110 | .Blocked => return fido.ctap.StatusCodes.ctap2_err_uv_blocked, 111 | .Timeout => return fido.ctap.StatusCodes.ctap2_err_user_action_timeout, 112 | .Denied => { 113 | return fido.ctap.StatusCodes.ctap2_err_uv_invalid; 114 | }, 115 | .Accepted => {}, 116 | .AcceptedWithUp => user_present = true, 117 | } 118 | 119 | auth.token.resetPinUvAuthToken(); // invalidates existing tokens 120 | auth.token.beginUsingPinUvAuthToken(user_present, auth.milliTimestamp()); 121 | 122 | auth.token.permissions = client_pin_param.permissions.?; 123 | 124 | // If the rpId parameter is present, associate the permissions RP ID 125 | // with the pinUvAuthToken. 126 | if (client_pin_param.rpId) |rpId| { 127 | auth.token.setRpId(rpId.get()) catch { 128 | // rpId is unexpectedly long 129 | return fido.ctap.StatusCodes.ctap1_err_other; 130 | }; 131 | } 132 | 133 | // Obtain the shared secret 134 | const shared_secret = auth.token.ecdh( 135 | client_pin_param.keyAgreement.?, 136 | ) catch { 137 | return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; 138 | }; 139 | 140 | // The authenticator returns the encrypted pinUvAuthToken for the 141 | // specified pinUvAuthProtocol, i.e. encrypt(shared secret, pinUvAuthToken). 142 | var enc_shared_secret: [48]u8 = undefined; 143 | auth.token.encrypt( 144 | &auth.token, 145 | shared_secret.get(), 146 | enc_shared_secret[0..], 147 | auth.token.pin_token[0..], 148 | ); 149 | 150 | // Response 151 | client_pin_response = .{ 152 | .pinUvAuthToken = (dt.ABS48B.fromSlice(&enc_shared_secret) catch unreachable).?, 153 | }; 154 | }, 155 | else => { 156 | return fido.ctap.StatusCodes.ctap2_err_invalid_subcommand; 157 | }, 158 | } 159 | 160 | // Serialize response and return 161 | if (client_pin_response) |resp| { 162 | cbor.stringify(resp, .{}, out.writer()) catch { 163 | return fido.ctap.StatusCodes.ctap1_err_other; 164 | }; 165 | } 166 | 167 | return fido.ctap.StatusCodes.ctap1_err_success; 168 | } 169 | -------------------------------------------------------------------------------- /lib/ctap/commands/authenticator/authenticatorGetNextAssertion.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const cks = @import("cks"); 4 | const fido = @import("../../../main.zig"); 5 | const uuid = @import("uuid"); 6 | const helper = @import("helper.zig"); 7 | 8 | pub fn authenticatorGetNextAssertion( 9 | auth: *fido.ctap.authenticator.Auth, 10 | request: []const u8, 11 | out: *std.ArrayList(u8), 12 | ) fido.ctap.StatusCodes { 13 | _ = request; 14 | 15 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 16 | // Validate 17 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 18 | 19 | const seconds_30 = 30000; 20 | 21 | if (auth.getAssertion == null) { 22 | return .ctap2_err_not_allowed; 23 | } 24 | 25 | if (auth.getAssertion.?.count >= auth.getAssertion.?.total) { 26 | auth.getAssertion = null; 27 | return .ctap2_err_not_allowed; 28 | } 29 | 30 | if (auth.milliTimestamp() - auth.getAssertion.?.ts > seconds_30) { 31 | auth.getAssertion = null; 32 | return .ctap2_err_not_allowed; 33 | } 34 | 35 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 36 | // Get Credential 37 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 38 | var selected_credential: ?fido.ctap.authenticator.Credential = null; 39 | var credential = auth.callbacks.read_next() catch { 40 | return fido.ctap.StatusCodes.ctap2_err_no_credentials; 41 | }; 42 | 43 | while (true) { 44 | var skip = false; 45 | const policy = credential.policy; 46 | 47 | // if credential protection for a credential is marked as 48 | // userVerificationRequired, and the "uv" bit is false in 49 | // the response, remove that credential from the applicable 50 | // credentials list 51 | if (policy == .userVerificationRequired and !auth.getAssertion.?.uv) { 52 | skip = true; 53 | } 54 | 55 | // if credential protection for a credential is marked as 56 | // userVerificationOptionalWithCredentialIDList and there 57 | // is no allowList passed by the client and the "uv" bit is 58 | // false in the response, remove that credential from the 59 | // applicable credentials list 60 | if (policy == .userVerificationOptionalWithCredentialIDList and auth.getAssertion.?.allowList == null and !auth.getAssertion.?.uv) { 61 | skip = true; 62 | } 63 | 64 | // TODO: check allow list 65 | 66 | if (!skip) { 67 | selected_credential = credential; 68 | auth.getAssertion.?.count += 1; 69 | break; 70 | } 71 | 72 | credential = auth.callbacks.read_next() catch { 73 | break; 74 | }; 75 | } 76 | 77 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | // Generate Assertion 79 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 80 | if (selected_credential == null) { 81 | std.log.err("getNextAssertion: no credential", .{}); 82 | return fido.ctap.StatusCodes.ctap2_err_no_credentials; 83 | } 84 | 85 | std.log.info("getNextAssertion: found credential\n {s}", .{selected_credential.?.rp.id.get()}); 86 | var write_back: bool = false; 87 | if (!auth.constSignCount) { 88 | selected_credential.?.sign_count += 1; 89 | write_back = true; 90 | } 91 | const usageCnt = selected_credential.?.sign_count; 92 | 93 | const user = if (auth.getAssertion.?.uv) blk: { 94 | // User identifiable information (name, DisplayName, icon) 95 | // inside the publicKeyCredentialUserEntity MUST NOT be returned 96 | // if user verification is not done by the authenticator 97 | break :blk selected_credential.?.user; 98 | } else blk: { 99 | break :blk fido.common.User{ .id = selected_credential.?.user.id }; 100 | }; 101 | 102 | var auth_data = fido.common.AuthenticatorData{ 103 | .rpIdHash = undefined, 104 | .flags = .{ 105 | .up = if (auth.getAssertion.?.up) 1 else 0, 106 | .rfu1 = 0, 107 | .uv = if (auth.getAssertion.?.uv) 1 else 0, 108 | .rfu2 = 0, 109 | .at = 0, 110 | .ed = 0, 111 | }, 112 | .signCount = @intCast(usageCnt), 113 | }; 114 | std.crypto.hash.sha2.Sha256.hash( // calculate rpId hash 115 | auth.getAssertion.?.rpId.get(), 116 | &auth_data.rpIdHash, 117 | .{}, 118 | ); 119 | 120 | const ad = auth_data.encode() catch { 121 | std.log.err("getNextAssertion: authData encode error", .{}); 122 | return fido.ctap.StatusCodes.ctap1_err_other; 123 | }; 124 | 125 | // -------------------- ---------------- 126 | // | authenticatorData | | clientDataHash | 127 | // -------------------- ---------------- 128 | // | | 129 | // ------------------------- | | 130 | // | 131 | // PRIVATE KEY -----------> SIGN 132 | // | 133 | // v 134 | // ASSERTION SIGNATURE 135 | var sig_buffer: [256]u8 = undefined; 136 | var fba = std.heap.FixedBufferAllocator.init(&sig_buffer); 137 | const allocator = fba.allocator(); 138 | 139 | const sig = selected_credential.?.key.sign( 140 | &.{ ad.get(), &auth.getAssertion.?.cdh }, 141 | allocator, 142 | ) catch { 143 | std.log.err( 144 | "getAssertion: signature creation failed for credential with id: {s}", 145 | .{std.fmt.fmtSliceHexLower(selected_credential.?.id.get())}, 146 | ); 147 | return fido.ctap.StatusCodes.ctap1_err_other; 148 | }; 149 | 150 | const gar = fido.ctap.response.GetAssertion{ 151 | .credential = fido.common.PublicKeyCredentialDescriptor.new( 152 | selected_credential.?.id.get(), 153 | .@"public-key", 154 | null, 155 | ) catch { 156 | return fido.ctap.StatusCodes.ctap1_err_other; 157 | }, 158 | .authData = ad.get(), 159 | .signature = sig, 160 | .user = user, 161 | }; 162 | 163 | cbor.stringify(gar, .{}, out.writer()) catch { 164 | std.log.err("getNextAssertion: cbor encoding error", .{}); 165 | return fido.ctap.StatusCodes.ctap1_err_other; 166 | }; 167 | return .ctap1_err_success; 168 | } 169 | -------------------------------------------------------------------------------- /lib/ctap/commands/authenticator/authenticatorSelection.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fido = @import("../../../main.zig"); 3 | 4 | pub fn authenticatorSelection( 5 | auth: *fido.ctap.authenticator.Auth, 6 | request: []const u8, 7 | out: *std.ArrayList(u8), 8 | ) fido.ctap.StatusCodes { 9 | _ = request; 10 | _ = out; 11 | 12 | const up = auth.callbacks.up( 13 | "Use this authenticator?", 14 | null, 15 | null, 16 | ); 17 | 18 | return switch (up) { 19 | .Denied => fido.ctap.StatusCodes.ctap2_err_operation_denied, 20 | .Accepted => fido.ctap.StatusCodes.ctap1_err_success, 21 | .Timeout => fido.ctap.StatusCodes.ctap2_err_user_action_timeout, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /lib/ctap/commands/authenticator/get_info.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../../main.zig"); 4 | 5 | /// Report a list of the authenticators supported protocol versions and 6 | /// extensions, its AAGUID, and other aspects of its overall capabilities. 7 | pub fn authenticatorGetInfo( 8 | auth: *fido.ctap.authenticator.Auth, 9 | request: []const u8, 10 | out: *std.ArrayList(u8), 11 | ) fido.ctap.StatusCodes { 12 | _ = request; 13 | 14 | const settings = auth.callbacks.read_settings(); 15 | 16 | auth.settings.minPINLength = settings.min_pin_length; 17 | auth.settings.forcePINChange = settings.force_pin_change; 18 | auth.settings.options.alwaysUv = settings.always_uv; 19 | 20 | cbor.stringify(auth.settings, .{}, out.writer()) catch { 21 | return fido.ctap.StatusCodes.ctap1_err_other; 22 | }; 23 | return fido.ctap.StatusCodes.ctap1_err_success; 24 | } 25 | -------------------------------------------------------------------------------- /lib/ctap/commands/authenticator/helper.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fido = @import("../../../main.zig"); 3 | 4 | /// Verify that the pinUvAuthToken support matches the given parameter 5 | /// 6 | /// This covers 1. and 2. of GetAssertion and MakeCredential 7 | /// 8 | /// Returns CTAP_ERR_SUCCESS if everything is ok 9 | pub fn verifyPinUvAuthParam( 10 | auth: *const fido.ctap.authenticator.Auth, 11 | param: anytype, 12 | ) fido.ctap.StatusCodes { 13 | if (param.pinUvAuthParam != null) { 14 | if (param.pinUvAuthProtocol == null) { 15 | return fido.ctap.StatusCodes.ctap2_err_missing_parameter; 16 | } else if (param.pinUvAuthProtocol.? != auth.token.version) { 17 | return fido.ctap.StatusCodes.ctap1_err_invalid_parameter; 18 | } 19 | } 20 | 21 | return fido.ctap.StatusCodes.ctap1_err_success; 22 | } 23 | -------------------------------------------------------------------------------- /lib/ctap/crypto/Id.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../main.zig"); 4 | const Hmac = std.crypto.auth.hmac.sha2.HmacSha256; 5 | const Hkdf = std.crypto.kdf.hkdf.HkdfSha256; 6 | const MasterSecret = fido.ctap.crypto.master_secret.MasterSecret; 7 | const CredentialCreationPolicy = fido.ctap.extensions.CredentialCreationPolicy; 8 | 9 | pub const CTX_LEN = 32; 10 | pub const ID_LEN = CTX_LEN + Hmac.mac_length; 11 | 12 | raw: [ID_LEN]u8, 13 | 14 | pub fn new( 15 | alg: cbor.cose.Algorithm, 16 | policy: CredentialCreationPolicy, 17 | ms: MasterSecret, 18 | rpid: []const u8, 19 | rand: std.rand.Random, 20 | ) @This() { 21 | var id = @This(){ .raw = undefined }; 22 | 23 | // Encode signature algorithm bound to this id 24 | @memcpy(id.raw[0..4], alg.to_raw()[0..4]); 25 | 26 | // Encode credential creation policy 27 | id.raw[4] = @intFromEnum(policy); 28 | 29 | // Create a 28 byte random context 30 | rand.bytes(id.raw[5..32]); 31 | 32 | // Bind rpid to the credential using a MAC 33 | const mk = deriveMacKey(ms); 34 | var m = Hmac.init(&mk); 35 | m.update(id.raw[0..32]); // The context 36 | m.update(rpid); 37 | m.final(id.raw[32..]); 38 | 39 | // Return `ALG || POL || CTX || MAC(ALG || POL || CTX || rpId)` 40 | return id; 41 | } 42 | 43 | pub fn from_raw( 44 | raw: []const u8, 45 | ms: MasterSecret, 46 | rpid: []const u8, 47 | ) !@This() { 48 | // Verify length 49 | if (raw.len != ID_LEN) { 50 | return error.InvalidIdLength; 51 | } 52 | 53 | // Verify MAC 54 | var mac: [Hmac.mac_length]u8 = undefined; 55 | const mk = deriveMacKey(ms); 56 | var m = Hmac.init(&mk); 57 | m.update(raw[0..32]); 58 | m.update(rpid); 59 | m.final(mac[0..]); 60 | 61 | if (!std.mem.eql(u8, raw[CTX_LEN..], mac[0..])) { 62 | return error.InvalidMac; 63 | } 64 | 65 | return @This(){ .raw = raw[0..ID_LEN].* }; 66 | } 67 | 68 | pub fn getAlg(self: *const @This()) cbor.cose.Algorithm { 69 | return cbor.cose.Algorithm.from_raw(self.raw[0..4].*); 70 | } 71 | 72 | pub fn getPolicy(self: *const @This()) CredentialCreationPolicy { 73 | const pol: CredentialCreationPolicy = @enumFromInt(self.raw[4]); 74 | return pol; 75 | } 76 | 77 | pub fn deriveSeed(self: *const @This(), ms: MasterSecret) [32]u8 { 78 | var seed: [32]u8 = undefined; 79 | Hkdf.expand(seed[0..], self.raw[0..CTX_LEN], ms); 80 | return seed; 81 | } 82 | 83 | /// Derive a deterministic sub-key for message authentication codes. 84 | fn deriveMacKey(ms: MasterSecret) [Hmac.mac_length]u8 { 85 | var mac_key: [Hmac.mac_length]u8 = undefined; 86 | Hkdf.expand(mac_key[0..], "MACKEY", ms); 87 | return mac_key; 88 | } 89 | -------------------------------------------------------------------------------- /lib/ctap/crypto/SigAlg.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const Allocator = std.mem.Allocator; 4 | const fido = @import("../../main.zig"); 5 | const dt = fido.common.dt; 6 | 7 | /// The algorithm used 8 | alg: cbor.cose.Algorithm, 9 | /// Create a new random key-pair 10 | create: *const fn (rand: std.Random) ?cbor.cose.Key, 11 | /// Deterministically creates a new key-pair using the given seed 12 | create_det: *const fn (seed: []const u8) ?cbor.cose.Key, 13 | /// Sign the given data 14 | sign: *const fn ( 15 | raw_private_key: []const u8, 16 | data_seq: []const []const u8, 17 | out: []u8, 18 | ) ?[]const u8, 19 | from_priv: *const fn (priv: []const u8) ?cbor.cose.Key, 20 | -------------------------------------------------------------------------------- /lib/ctap/crypto/ecdh.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const crypto = std.crypto; 3 | const mem = std.mem; 4 | const fmt = std.fmt; 5 | 6 | const EncodingError = crypto.errors.EncodingError; 7 | const IdentityElementError = crypto.errors.IdentityElementError; 8 | const WeakPublicKeyError = crypto.errors.WeakPublicKeyError; 9 | 10 | pub const EcdhP256 = Ecdh(crypto.ecc.P256); 11 | 12 | pub fn Ecdh(comptime Curve: type) type { 13 | return struct { 14 | pub const secret_length = Curve.scalar.encoded_length; 15 | pub const public_length = Curve.Fe.encoded_length; 16 | 17 | pub const KeyPair = struct { 18 | /// Length (in bytes) of a compressed sec1-encoded key. 19 | pub const compressed_sec1_encoded_length = 1 + Curve.Fe.encoded_length; 20 | /// Length (in bytes) of a uncompressed sec1-encoded key. 21 | pub const uncompressed_sec1_encoded_length = 1 + 2 * Curve.Fe.encoded_length; 22 | 23 | /// The public key is the product `eG` of a generator `G` and a private key `e`. 24 | public_key: Curve, 25 | /// A secret scalar e. 26 | secret_key: [secret_length]u8, 27 | 28 | /// Create a new key pair using a RANDOMLY generated seed. 29 | pub fn create(seed: [secret_length]u8) !KeyPair { 30 | var kp: KeyPair = undefined; 31 | @memcpy(&kp.secret_key, &seed); 32 | kp.public_key = try recoverPublicKey(kp.secret_key); 33 | return kp; 34 | } 35 | 36 | /// Encode the public key using the compressed SEC-1 format. 37 | pub fn toCompressedSec1(kp: KeyPair) [compressed_sec1_encoded_length]u8 { 38 | return kp.public_key.toCompressedSec1(); 39 | } 40 | 41 | /// Encoding the public key using the uncompressed SEC-1 format. 42 | pub fn toUncompressedSec1(kp: KeyPair) [uncompressed_sec1_encoded_length]u8 { 43 | return kp.public_key.toUncompressedSec1(); 44 | } 45 | }; 46 | 47 | /// Compute the public key for a given private key. 48 | pub fn recoverPublicKey(secret_key: [secret_length]u8) IdentityElementError!Curve { 49 | return try Curve.basePoint.mul(secret_key, .little); 50 | } 51 | 52 | pub fn scalarmultXY(secret_key: [secret_length]u8, pub_x: [public_length]u8, pub_y: [public_length]u8) !Curve { 53 | const x = try Curve.Fe.fromBytes(pub_x[0..].*, .big); 54 | const y = try Curve.Fe.fromBytes(pub_y[0..].*, .big); 55 | const c = try Curve.fromAffineCoordinates(.{ .x = x, .y = y }); 56 | const secret = try c.mul(secret_key, .little); 57 | return secret; 58 | } 59 | }; 60 | } 61 | 62 | const TestVector = struct { 63 | seed_1: []const u8, // little endian 64 | sec1_pub_1: []const u8, // big endian 65 | seed_2: []const u8, // little endian 66 | sec1_pub_2: []const u8, // big endian 67 | shared: []const u8, 68 | }; 69 | 70 | // 1 ------------------------------------------------------------------------------ 71 | // k: 2950161782655570540676804833206013362761296170713583524683291525408100401277 72 | // x: 72012666485689920686349371229719520079129025318776017657777002731415147050435 73 | // y: 82774346030720574195738104733363612151633888318189006270161920746901497752063 74 | // 2 ------------------------------------------------------------------------------ 75 | // k: 40798969142379699069411319556357690403406277790329503364538485924349264581035 76 | // x: 35880122959583058435597142902132466821046587199372209813642092314971621865447 77 | // y: 97019214247574472146350611690487571862063450007374962849005135920536613627823 78 | // 1*2 ---------------------------------------------------------------------------- 79 | // x: 74480588571804104165548909343629343101087858447149553902224553189006333009730 80 | // y: 17514364307054875620496156261313566698070339587494778530888946462521182668340 81 | 82 | test "ecdh: generate key" { 83 | const vectors = [_]TestVector{ 84 | .{ .seed_1 = "\x7d\x40\x1f\xff\x9d\x6e\x6a\x6d\xcf\xb6\xa4\xbe\x8d\x2a\xc7\x6b\xe6\xb4\x52\xaf\xb5\x0e\xdb\x50\xa5\x4f\x28\x4c\x7e\xbb\x85\x06", .sec1_pub_1 = "\x04\x9f\x35\xb9\x8e\x8f\xac\xc2\xe3\x0f\x81\xab\xe2\x89\x46\x97\x4f\x6d\xe6\xb6\x3f\x3d\x53\xed\xd7\xa6\x61\x4e\x38\xfd\xb6\xf5\xc3\xb7\x00\x9e\x9e\x29\xab\xc0\x9e\x62\x96\xa8\x14\x3a\x75\x00\x54\x69\x57\xbb\x75\x85\xb8\xfd\xdb\xc1\x27\x66\x5f\x58\x74\xa9\xff", .seed_2 = "\xab\xd1\x48\x54\x46\xa3\x23\xd3\xd0\xa1\x24\x3d\xc3\x44\xd1\xf6\x36\xd3\xb4\x56\xb4\xd0\xac\x24\xf0\x46\xa1\xd8\xf0\x65\x33\x5a", .sec1_pub_2 = "\x04\x4f\x53\x6e\x0f\xb0\xe9\x99\xa7\x3f\x0e\x3d\x02\xae\x59\x2b\xd2\xe5\x7f\x77\xfd\xbd\x41\xa2\x8b\x12\x65\xe0\xcf\x5e\xe0\xb7\xe7\xd6\x7e\xed\xe6\x38\x3a\x49\x36\xe8\xb4\x3c\x38\xd8\xbe\x96\xdb\x4a\x1f\x9a\x15\xbd\x81\x22\xb0\x3e\x0b\x0c\x99\x4e\x1d\x27\xaf", .shared = "\x04\xa4\xaa\x84\xec\x5f\xa1\x01\x49\x5a\x37\x1a\x3b\x1a\xf7\x75\xe2\xb1\x5b\x36\xb2\xda\xff\x25\x73\x07\xc2\x1e\x16\xd8\x61\x5f\x42\x26\xb8\xc7\x66\x21\x8e\x93\xf1\xbb\x0a\x90\xa5\x31\x48\x41\xab\x31\x8f\xb2\x03\x35\x6c\xca\x1a\x85\xd5\x6a\xeb\x44\x70\x46\x34" }, 85 | }; 86 | 87 | for (vectors) |vector| { 88 | const kp = try EcdhP256.KeyPair.create(vector.seed_1[0..32].*); 89 | try std.testing.expectEqualSlices(u8, vector.sec1_pub_1, &kp.toUncompressedSec1()); 90 | 91 | const kp2 = try EcdhP256.KeyPair.create(vector.seed_2[0..32].*); 92 | try std.testing.expectEqualSlices(u8, vector.sec1_pub_2, &kp2.toUncompressedSec1()); 93 | 94 | const s1 = try EcdhP256.scalarmultXY(kp.secret_key, vector.sec1_pub_2[1..33].*, vector.sec1_pub_2[33..65].*); 95 | try std.testing.expectEqualSlices(u8, vector.shared, &s1.toUncompressedSec1()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/ctap/crypto/master_secret.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Hmac = std.crypto.auth.hmac.sha2.HmacSha256; 3 | const Hkdf = std.crypto.kdf.hkdf.HkdfSha256; 4 | const Aes256Ocb = std.crypto.aead.aes_ocb.Aes256Ocb; 5 | 6 | pub const MS_LEN = Hkdf.prk_length; 7 | /// Stored by the authenticator and used to derive all other secrets 8 | pub const MasterSecret = [MS_LEN]u8; 9 | 10 | /// Create a new, random master secret using a hash based key derivation function 11 | pub fn createMasterSecret(rand: std.rand.Random) MasterSecret { 12 | var ikm: [32]u8 = undefined; 13 | var salt: [16]u8 = undefined; 14 | rand.bytes(ikm[0..]); 15 | rand.bytes(salt[0..]); 16 | return Hkdf.extract(&salt, &ikm); 17 | } 18 | 19 | /// Derive a deterministic sub-key for message authentication codes. 20 | pub fn deriveMacKey(ms: MasterSecret) [Hmac.mac_length]u8 { 21 | var mac_key: [Hmac.mac_length]u8 = undefined; 22 | Hkdf.expand(mac_key[0..], "MAC", ms); 23 | return mac_key; 24 | } 25 | 26 | pub fn deriveEncKey(ms: MasterSecret) [Aes256Ocb.key_length]u8 { 27 | var enc_key: [Aes256Ocb.key_length]u8 = undefined; 28 | Hkdf.expand(enc_key[0..], "ENC", ms); 29 | return enc_key; 30 | } 31 | -------------------------------------------------------------------------------- /lib/ctap/crypto/sigalgs/Es256.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fido = @import("../../../main.zig"); 3 | const cbor = @import("zbor"); 4 | const SigAlg = fido.ctap.crypto.SigAlg; 5 | const EcdsaP256Sha256 = @import("../ecdsa.zig").EcdsaP256Sha256; 6 | const dt = fido.common.dt; 7 | 8 | pub const Es256 = SigAlg{ 9 | .alg = .Es256, 10 | .create = create, 11 | .create_det = create_det, 12 | .sign = sign, 13 | .from_priv = from_priv, 14 | }; 15 | 16 | pub fn create(rand: std.Random) ?cbor.cose.Key { 17 | // Create key pair 18 | var seed: [32]u8 = undefined; 19 | rand.bytes(&seed); 20 | return create_det(&seed); 21 | } 22 | 23 | pub fn create_det(seed: []const u8) ?cbor.cose.Key { 24 | const kp = EcdsaP256Sha256.KeyPair.create(seed[0..32].*) catch return null; 25 | return cbor.cose.Key.fromP256PrivPub(.Es256, kp.secret_key, kp.public_key); 26 | } 27 | 28 | pub fn sign( 29 | raw_private_key: []const u8, 30 | data_seq: []const []const u8, 31 | out: []u8, 32 | ) ?[]const u8 { 33 | if (raw_private_key.len != 32) return null; 34 | 35 | var kp = EcdsaP256Sha256.KeyPair.fromSecretKey( 36 | try EcdsaP256Sha256.SecretKey.fromBytes(raw_private_key[0..32].*), 37 | ) catch return null; 38 | var signer = try kp.signer(null); 39 | 40 | // Append data that should be signed together 41 | for (data_seq) |data| { 42 | signer.update(data); 43 | } 44 | 45 | // Sign the data 46 | const sig = signer.finalize() catch return null; 47 | var buffer: [EcdsaP256Sha256.Signature.der_encoded_max_length]u8 = undefined; 48 | const der = sig.toDer(&buffer); 49 | 50 | if (out.len < der.len) return null; 51 | @memcpy(out[0..der.len], der); 52 | return out[0..der.len]; 53 | } 54 | 55 | pub fn from_priv(priv: []const u8) ?cbor.cose.Key { 56 | if (priv.len != 32) return null; 57 | 58 | var kp = EcdsaP256Sha256.KeyPair.fromSecretKey( 59 | try EcdsaP256Sha256.SecretKey.fromBytes(priv[0..32].*), 60 | ) catch return null; 61 | 62 | const sec1 = kp.public_key.toUncompressedSec1(); 63 | const pubk = cbor.cose.Key{ .P256 = .{ 64 | .alg = .Es256, 65 | .x = sec1[1..33].*, 66 | .y = sec1[33..65].*, 67 | } }; 68 | 69 | return pubk; 70 | } 71 | -------------------------------------------------------------------------------- /lib/ctap/extensions/CredentialCreationPolicy.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | /// Protection level for credentials 3 | /// 4 | /// Authenticators supporting some form of user verification MUST process this extension 5 | /// and persist the credProtect value with the credential, even if the authenticator is 6 | /// not protected by some form of user verification at the time. 7 | /// 8 | /// Authenticators may choose a higher policy than requested. 9 | pub const CredentialCreationPolicy = enum(u8) { 10 | /// This reflects "FIDO_2_0" semantics. In this configuration, performing some 11 | /// form of user verification is OPTIONAL with or without credentialID list. 12 | /// This is the default state of the credential if the extension is not specified. 13 | userVerificationOptional = 0x01, 14 | /// In this configuration, credential is discovered only when its credentialID 15 | /// is provided by the platform or when some form of user verification is performed. 16 | userVerificationOptionalWithCredentialIDList = 0x02, 17 | /// This reflects that discovery and usage of the credential MUST be preceded 18 | /// by some form of user verification. 19 | userVerificationRequired = 0x03, 20 | 21 | pub fn toString(self: @This()) []const u8 { 22 | return switch (self) { 23 | .userVerificationOptional => "userVerificationOptional", 24 | .userVerificationOptionalWithCredentialIDList => "userVerificationOptionalWithCredentialIDList", 25 | .userVerificationRequired => "userVerificationRequired", 26 | }; 27 | } 28 | 29 | pub fn fromString(s_: ?[]const u8) ?@This() { 30 | if (s_ == null) return null; 31 | const s = s_.?; 32 | if (std.mem.eql(u8, s, "userVerificationOptional")) { 33 | return @This().userVerificationOptional; 34 | } else if (std.mem.eql(u8, s, "userVerificationOptionalWithCredentialIDList")) { 35 | return @This().userVerificationOptionalWithCredentialIDList; 36 | } else if (std.mem.eql(u8, s, "userVerificationRequired")) { 37 | return @This().userVerificationRequired; 38 | } else { 39 | return null; 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/ctap/extensions/Extensions.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fido = @import("../../main.zig"); 3 | const cbor = @import("zbor"); 4 | const dt = fido.common.dt; 5 | 6 | /// This registration extension allows relying parties to specify a credential 7 | /// protection policy when creating a credential. 8 | credProtect: ?fido.ctap.extensions.CredentialCreationPolicy = null, 9 | 10 | /// This extension is used by the platform to retrieve a symmetric secret from 11 | /// the authenticator when it needs to encrypt or decrypt data using that symmetric 12 | /// secret. This symmetric secret is scoped to a credential. The authenticator 13 | /// and the platform each only have the part of the complete secret to prevent 14 | /// offline attacks. 15 | @"hmac-secret": ?fido.ctap.extensions.HmacSecret = null, 16 | 17 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 18 | return cbor.stringify(self, .{ 19 | .allocator = options.allocator, 20 | .ignore_override = true, 21 | .field_settings = &.{ 22 | .{ .name = "credProtect", .value_options = .{ .enum_serialization_type = .Integer } }, 23 | }, 24 | }, out); 25 | } 26 | 27 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 28 | return try cbor.parse(@This(), item, .{ 29 | .allocator = options.allocator, 30 | .ignore_override = true, // prevent infinite loops 31 | .field_settings = &.{ 32 | .{ .name = "credProtect", .value_options = .{ .enum_serialization_type = .Integer } }, 33 | }, 34 | }); 35 | } 36 | 37 | test "hmac secret extension #1" { 38 | const allocator = std.testing.allocator; 39 | 40 | const x = @This(){ .@"hmac-secret" = .{ .create = true } }; 41 | 42 | var a = std.ArrayList(u8).init(allocator); 43 | defer a.deinit(); 44 | 45 | try cbor.stringify(&x, .{}, a.writer()); 46 | 47 | try std.testing.expectEqualSlices(u8, "\xa1\x6b\x68\x6d\x61\x63\x2d\x73\x65\x63\x72\x65\x74\xf5", a.items); 48 | } 49 | 50 | test "hmac secret extension #2" { 51 | const allocator = std.testing.allocator; 52 | 53 | const x = @This(){ .@"hmac-secret" = .{ .create = false } }; 54 | 55 | var a = std.ArrayList(u8).init(allocator); 56 | defer a.deinit(); 57 | 58 | try cbor.stringify(&x, .{}, a.writer()); 59 | 60 | try std.testing.expectEqualSlices(u8, "\xa1\x6b\x68\x6d\x61\x63\x2d\x73\x65\x63\x72\x65\x74\xf4", a.items); 61 | } 62 | 63 | test "hmac secret extension #3" { 64 | const allocator = std.testing.allocator; 65 | 66 | const x = @This(){ .@"hmac-secret" = .{ .get = .{ 67 | .keyAgreement = cbor.cose.Key{ 68 | .P256 = .{ 69 | .kty = .Ec2, 70 | .alg = .EcdhEsHkdf256, 71 | .crv = .P256, 72 | .x = "\x0d\xe6\x47\x97\x75\xc5\xb7\x04\xbf\x78\x00\x73\x80\x9d\xe1\xb3\x6a\x29\x13\x2e\x18\x77\x09\xc1\xe3\x64\xf2\x99\xf8\x84\x77\x69".*, 73 | .y = "\x3b\xbe\x9b\xed\xcc\x1a\xc8\x32\x8b\xa6\x39\x7a\x5f\x46\xaf\x85\xfc\x7c\x51\xb3\x5b\xed\xfd\x9e\x3e\x47\xac\x6f\x34\x24\x8b\x35".*, 74 | }, 75 | }, 76 | .saltEnc = (try dt.ABS64B.fromSlice("\x59\xe1\x95\xfc\x58\xc6\x14\xc0\x7c\x99\xf5\x87\x49\x5f\x37\x48\x71\xe9\x87\x3a\xd3\x7d\x5b\xca\x1e\xed\x20\x09\x26\xc3\xc6\xba\x52\x8d\x77\xa4\x8a\xf9\x59\x2b\xd7\xe7\xa8\x80\x51\x88\x7f\x21\x4e\x13\xcf\xdf\x40\x6c\x3a\x1c\x57\xd5\x29\xba\xbf\x98\x7d\x4a")).?, 77 | .saltAuth = (try dt.ABS32B.fromSlice("\x17\xb9\x3f\x3b\xdb\x95\x38\x0e\xd5\x12\xec\x6f\x54\x2c\xe1\x40")).?, 78 | } } }; 79 | 80 | var a = std.ArrayList(u8).init(allocator); 81 | defer a.deinit(); 82 | 83 | try cbor.stringify(&x, .{}, a.writer()); 84 | 85 | try std.testing.expectEqualSlices(u8, "\xa1\x6b\x68\x6d\x61\x63\x2d\x73\x65\x63\x72\x65\x74\xa3\x01\xa5\x01\x02\x03\x38\x18\x20\x01\x21\x58\x20\x0d\xe6\x47\x97\x75\xc5\xb7\x04\xbf\x78\x00\x73\x80\x9d\xe1\xb3\x6a\x29\x13\x2e\x18\x77\x09\xc1\xe3\x64\xf2\x99\xf8\x84\x77\x69\x22\x58\x20\x3b\xbe\x9b\xed\xcc\x1a\xc8\x32\x8b\xa6\x39\x7a\x5f\x46\xaf\x85\xfc\x7c\x51\xb3\x5b\xed\xfd\x9e\x3e\x47\xac\x6f\x34\x24\x8b\x35\x02\x58\x40\x59\xe1\x95\xfc\x58\xc6\x14\xc0\x7c\x99\xf5\x87\x49\x5f\x37\x48\x71\xe9\x87\x3a\xd3\x7d\x5b\xca\x1e\xed\x20\x09\x26\xc3\xc6\xba\x52\x8d\x77\xa4\x8a\xf9\x59\x2b\xd7\xe7\xa8\x80\x51\x88\x7f\x21\x4e\x13\xcf\xdf\x40\x6c\x3a\x1c\x57\xd5\x29\xba\xbf\x98\x7d\x4a\x03\x50\x17\xb9\x3f\x3b\xdb\x95\x38\x0e\xd5\x12\xec\x6f\x54\x2c\xe1\x40", a.items); 86 | } 87 | -------------------------------------------------------------------------------- /lib/ctap/extensions/HmacSecret.zig: -------------------------------------------------------------------------------- 1 | const cbor = @import("zbor"); 2 | const fido = @import("../../main.zig"); 3 | const dt = fido.common.dt; 4 | 5 | pub const HmacSecretTag = enum { create, get, output }; 6 | 7 | pub const HmacSecret = union(HmacSecretTag) { 8 | create: bool, 9 | get: struct { 10 | /// Public key of platform key-agreement key (also used by pinUv protocol) 11 | keyAgreement: cbor.cose.Key, 12 | /// Encryption of the one or two salts (called salt1 (32 bytes) and salt2 (32 bytes)) 13 | /// using the shared secret as follows: 14 | /// One salt case: encrypt(shared secret, salt1) 15 | /// Two salt case: encrypt(shared secret, salt1 || salt2) 16 | saltEnc: dt.ABS64B, 17 | /// authenticate(shared secret, saltEnc) 18 | saltAuth: dt.ABS32B, 19 | /// As selected when getting the shared secret. CTAP2.1 platforms MUST include this 20 | /// parameter if the value of pinUvAuthProtocol is not 1. 21 | pinUvAuthProtocol: ?fido.ctap.pinuv.common.PinProtocol = null, 22 | }, 23 | output: dt.ABS64B, 24 | 25 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 26 | _ = options; 27 | 28 | try cbor.stringify(self.*, .{ 29 | .field_settings = &.{ 30 | .{ .name = "keyAgreement", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 31 | .{ .name = "saltEnc", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 32 | .{ .name = "saltAuth", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 33 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 34 | }, 35 | .ignore_override = true, 36 | }, out); 37 | } 38 | 39 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 40 | return try cbor.parse(@This(), item, .{ 41 | .allocator = options.allocator, 42 | .field_settings = &.{ 43 | .{ .name = "keyAgreement", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 44 | .{ .name = "saltEnc", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 45 | .{ .name = "saltAuth", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 46 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 47 | }, 48 | .ignore_override = true, 49 | }); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /lib/ctap/request/ClientPin.zig: -------------------------------------------------------------------------------- 1 | //! Parameters of the client pin command 2 | 3 | const std = @import("std"); 4 | const Hmac = std.crypto.auth.hmac.sha2.HmacSha256; 5 | 6 | const cbor = @import("zbor"); 7 | const fido = @import("../../main.zig"); 8 | const dt = fido.common.dt; 9 | 10 | /// pinUvAuthProtocol: PIN protocol version chosen by the client. 11 | pinUvAuthProtocol: ?fido.ctap.pinuv.common.PinProtocol = null, 12 | /// subCommand: The authenticator Client PIN sub command currently 13 | /// being requested. 14 | subCommand: fido.ctap.pinuv.common.SubCommand, 15 | /// keyAgreement: Public key of platformKeyAgreementKey. The 16 | /// COSE_Key-encoded public key MUST contain the optional "alg" 17 | /// parameter and MUST NOT contain any other optional parameters. 18 | /// The "alg" parameter MUST contain a COSEAlgorithmIdentifier value. 19 | keyAgreement: ?cbor.cose.Key = null, 20 | /// pinUvAuth: HMAC-SHA-256 of encrypted contents 21 | /// using sharedSecret. See Setting a new PIN, Changing existing 22 | /// PIN and Getting pinToken from the authenticator for more details. 23 | pinUvAuthParam: ?dt.ABS32B = null, 24 | /// newPinEnc: Encrypted new PIN using sharedSecret. Encryption is 25 | /// done over UTF-8 representation of new PIN. 26 | newPinEnc: ?dt.ABS64B = null, 27 | /// pinHashEnc: Encrypted SHA-256 of PIN using sharedSecret. 28 | pinHashEnc: ?dt.ABS32B = null, 29 | /// permissions: Bitfield of permissions. If present, MUST NOT be 0. 30 | permissions: ?u32 = null, 31 | /// rpId: The RP ID to assign as the permissions RP ID. 32 | rpId: ?dt.ABS128T = null, 33 | 34 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 35 | _ = options; 36 | 37 | try cbor.stringify(self.*, .{ 38 | .field_settings = &.{ 39 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 40 | .{ .name = "subCommand", .field_options = .{ .alias = "2", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 41 | .{ .name = "keyAgreement", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 42 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 43 | .{ .name = "newPinEnc", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 44 | .{ .name = "pinHashEnc", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 45 | .{ .name = "permissions", .field_options = .{ .alias = "9", .serialization_type = .Integer } }, 46 | .{ .name = "rpId", .field_options = .{ .alias = "10", .serialization_type = .Integer }, .value_options = .{ .slice_serialization_type = .TextString } }, 47 | }, 48 | .ignore_override = true, 49 | }, out); 50 | } 51 | 52 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 53 | return try cbor.parse(@This(), item, .{ 54 | .allocator = options.allocator, 55 | .field_settings = &.{ 56 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 57 | .{ .name = "subCommand", .field_options = .{ .alias = "2", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 58 | .{ .name = "keyAgreement", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 59 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 60 | .{ .name = "newPinEnc", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 61 | .{ .name = "pinHashEnc", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 62 | .{ .name = "permissions", .field_options = .{ .alias = "9", .serialization_type = .Integer } }, 63 | .{ .name = "rpId", .field_options = .{ .alias = "10", .serialization_type = .Integer }, .value_options = .{ .slice_serialization_type = .TextString } }, 64 | }, 65 | .ignore_override = true, 66 | }); 67 | } 68 | 69 | pub fn mcPermissionSet(self: *const @This()) bool { 70 | return if (self.permissions) |p| p & 0x01 != 0 else false; 71 | } 72 | 73 | pub fn gaPermissionSet(self: *const @This()) bool { 74 | return if (self.permissions) |p| p & 0x02 != 0 else false; 75 | } 76 | 77 | pub fn cmPermissionSet(self: *const @This()) bool { 78 | return if (self.permissions) |p| p & 0x04 != 0 else false; 79 | } 80 | 81 | pub fn bePermissionSet(self: *const @This()) bool { 82 | return if (self.permissions) |p| p & 0x08 != 0 else false; 83 | } 84 | 85 | pub fn lbwPermissionSet(self: *const @This()) bool { 86 | return if (self.permissions) |p| p & 0x10 != 0 else false; 87 | } 88 | 89 | pub fn acfgPermissionSet(self: *const @This()) bool { 90 | return if (self.permissions) |p| p & 0x20 != 0 else false; 91 | } 92 | -------------------------------------------------------------------------------- /lib/ctap/request/CredentialManagement.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../main.zig"); 4 | 5 | /// The sub command currently being requested 6 | subCommand: SubCommand, 7 | /// Map of sub command parameters 8 | subCommandParams: ?SubCommandParams = null, 9 | /// PIN/UV protocol version chosen by the platform 10 | pinUvAuthProtocol: ?fido.ctap.pinuv.common.PinProtocol = null, 11 | /// First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken 12 | pinUvAuthParam: ?[]const u8 = null, 13 | 14 | pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void { 15 | if (self.subCommandParams) |subCommandParams| { 16 | subCommandParams.deinit(allocator); 17 | } 18 | 19 | if (self.pinUvAuthParam) |puap| { 20 | allocator.free(puap); 21 | } 22 | } 23 | 24 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 25 | _ = options; 26 | 27 | try cbor.stringify(self, .{ 28 | .field_settings = &.{ 29 | .{ .name = "subCommand", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 30 | .{ .name = "subCommandParams", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 31 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "3", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 32 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 33 | }, 34 | .ignore_override = true, 35 | }, out); 36 | } 37 | 38 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 39 | return try cbor.parse(@This(), item, .{ 40 | .allocator = options.allocator, 41 | .ignore_override = true, // prevent infinite loops 42 | .field_settings = &.{ 43 | .{ .name = "subCommand", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 44 | .{ .name = "subCommandParams", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 45 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "3", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 46 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 47 | }, 48 | }); 49 | } 50 | 51 | // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 52 | 53 | /// List of sub commands for credential management 54 | pub const SubCommand = enum(u8) { 55 | /// get credentials metadata information 56 | getCredsMetadata = 0x01, 57 | enumerateRPsBegin = 0x02, 58 | enumerateRPsGetNextRP = 0x03, 59 | enumerateCredentialsBegin = 0x04, 60 | enumerateCredentialsGetNextCredential = 0x05, 61 | deleteCredential = 0x06, 62 | updateUserInformation = 0x07, 63 | }; 64 | 65 | pub const SubCommandParams = struct { 66 | /// RP ID SHA-256 hash 67 | rpIDHash: ?[32]u8 = null, 68 | /// Credential identifier 69 | credentialID: ?fido.common.PublicKeyCredentialDescriptor = null, 70 | /// User Entity 71 | user: ?fido.common.User = null, 72 | 73 | pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void { 74 | if (self.credentialID) |credId| { 75 | credId.deinit(allocator); 76 | } 77 | 78 | if (self.user) |user| { 79 | user.deinit(allocator); 80 | } 81 | } 82 | 83 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 84 | _ = options; 85 | 86 | try cbor.stringify(self, .{ 87 | .field_settings = &.{ 88 | .{ .name = "rpIDHash", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 89 | .{ .name = "credentialID", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 90 | .{ .name = "user", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 91 | }, 92 | .ignore_override = true, 93 | }, out); 94 | } 95 | 96 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 97 | return try cbor.parse(@This(), item, .{ 98 | .allocator = options.allocator, 99 | .ignore_override = true, // prevent infinite loops 100 | .field_settings = &.{ 101 | .{ .name = "rpIDHash", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 102 | .{ .name = "credentialID", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 103 | .{ .name = "user", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 104 | }, 105 | }); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /lib/ctap/request/GetAssertion.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../main.zig"); 4 | const dt = fido.common.dt; 5 | 6 | const PinUvAuthParam = fido.ctap.pinuv.common.PinUvAuthParam; 7 | const PinProtocol = fido.ctap.pinuv.common.PinProtocol; 8 | 9 | const ClientDataHash = fido.ctap.crypto.ClientDataHash; 10 | 11 | /// Relying party identifier. 12 | rpId: dt.ABS128T, // 1 13 | /// Hash of the serialized client data collected by the host. 14 | clientDataHash: ClientDataHash, // 2 15 | /// A sequence of PublicKeyCredentialDescriptor structures, each 16 | /// denoting a credential, as specified in [WebAuthN]. If this parameter is 17 | /// present and has 1 or more entries, the authenticator MUST only generate 18 | /// an assertion using one of the denoted credentials. 19 | allowList: ?dt.ABSPublicKeyCredentialDescriptor = null, // 3 20 | extensions: ?fido.ctap.extensions.Extensions = null, 21 | /// Parameters to influence authenticator operation. 22 | options: ?fido.common.AuthenticatorOptions = null, // 5 23 | /// Result of calling authenticate(pinUvAuthToken, clientDataHash) 24 | pinUvAuthParam: ?PinUvAuthParam = null, // 6 25 | /// PIN protocol version selected by client. 26 | pinUvAuthProtocol: ?PinProtocol = null, 27 | enterpriseAttestation: ?u64 = null, 28 | attestationFormatsPreference: ?dt.ABSAttestationStatementFormatIdentifiers = null, 29 | 30 | pub fn requestsUv(self: *const @This()) bool { 31 | return self.options != null and self.options.?.uv != null and self.options.?.uv.?; 32 | } 33 | 34 | pub fn requestsRk(self: *const @This()) bool { 35 | return self.options != null and self.options.?.rk != null and self.options.?.rk.?; 36 | } 37 | 38 | pub fn requestsUp(self: *const @This()) bool { 39 | // if up missing treat it as being present with the value true 40 | return if (self.options != null and self.options.?.up != null) self.options.?.up.? else true; 41 | } 42 | 43 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 44 | return cbor.stringify(self, .{ 45 | .allocator = options.allocator, 46 | .ignore_override = true, 47 | .field_settings = &.{ 48 | .{ .name = "rpId", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .slice_serialization_type = .TextString } }, 49 | .{ .name = "clientDataHash", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 50 | .{ .name = "allowList", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 51 | .{ .name = "options", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 52 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 53 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "7", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 54 | .{ .name = "enterpriseAttestation", .field_options = .{ .alias = "8", .serialization_type = .Integer } }, 55 | .{ .name = "attestationFormatsPreference", .field_options = .{ .alias = "9", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 56 | }, 57 | }, out); 58 | } 59 | 60 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 61 | return try cbor.parse(@This(), item, .{ 62 | .allocator = options.allocator, 63 | .ignore_override = true, 64 | .field_settings = &.{ 65 | .{ .name = "rpId", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .slice_serialization_type = .TextString } }, 66 | .{ .name = "clientDataHash", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 67 | .{ .name = "allowList", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 68 | .{ .name = "options", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 69 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 70 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "7", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 71 | .{ .name = "enterpriseAttestation", .field_options = .{ .alias = "8", .serialization_type = .Integer } }, 72 | .{ .name = "attestationFormatsPreference", .field_options = .{ .alias = "9", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 73 | }, 74 | }); 75 | } 76 | 77 | test "get assertion parse 1" { 78 | const payload = "\xa6\x01\x6b\x77\x65\x62\x61\x75\x74\x68\x6e\x2e\x69\x6f\x02\x58\x20\x6e\x0c\xb5\xf9\x7c\xae\xb8\xbf\x79\x7a\x62\x14\xc7\x19\x1c\x80\x8f\xe5\xa5\x50\x21\xf9\xfb\x76\x6e\x81\x83\xcd\x8a\x0d\x55\x0b\x03\x81\xa2\x62\x69\x64\x58\x40\xf9\xff\xff\xff\x95\xea\x72\x74\x2f\xa6\x03\xc3\x51\x9f\x9c\x17\xc0\xff\x81\xc4\x5d\xbb\x46\xe2\x3c\xff\x6f\xc1\xd0\xd5\xb3\x64\x6d\x49\x5c\xb1\x1b\x80\xe5\x78\x88\xbf\xba\xe3\x89\x8d\x69\x85\xfc\x19\x6c\x43\xfd\xfc\x2e\x80\x18\xac\x2d\x5b\xb3\x79\xa1\xf0\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x05\xa1\x62\x75\x70\xf4\x06\x58\x20\x30\x5b\x38\x2d\x1c\xd9\xb9\x71\x4d\x51\x98\x30\xe5\xb0\x02\xcb\x6c\x38\x25\xbc\x05\xf8\x7e\xf1\xbc\xda\x36\x4d\x2d\x4d\xb9\x10\x07\x02"; 79 | 80 | const di = try cbor.DataItem.new(payload); 81 | 82 | const get_assertion_param = try cbor.parse(@This(), di, .{}); 83 | 84 | try std.testing.expectEqualStrings("webauthn.io", get_assertion_param.rpId.get()); 85 | try std.testing.expectEqualSlices(u8, "\x6e\x0c\xb5\xf9\x7c\xae\xb8\xbf\x79\x7a\x62\x14\xc7\x19\x1c\x80\x8f\xe5\xa5\x50\x21\xf9\xfb\x76\x6e\x81\x83\xcd\x8a\x0d\x55\x0b", &get_assertion_param.clientDataHash); 86 | try std.testing.expectEqual(@as(usize, @intCast(1)), get_assertion_param.allowList.?.len); 87 | try std.testing.expectEqualSlices(u8, "\xf9\xff\xff\xff\x95\xea\x72\x74\x2f\xa6\x03\xc3\x51\x9f\x9c\x17\xc0\xff\x81\xc4\x5d\xbb\x46\xe2\x3c\xff\x6f\xc1\xd0\xd5\xb3\x64\x6d\x49\x5c\xb1\x1b\x80\xe5\x78\x88\xbf\xba\xe3\x89\x8d\x69\x85\xfc\x19\x6c\x43\xfd\xfc\x2e\x80\x18\xac\x2d\x5b\xb3\x79\xa1\xf0", get_assertion_param.allowList.?.get()[0].id.get()); 88 | try std.testing.expectEqual(fido.common.PublicKeyCredentialType.@"public-key", get_assertion_param.allowList.?.get()[0].type); 89 | try std.testing.expectEqual(false, get_assertion_param.options.?.up.?); 90 | try std.testing.expectEqualSlices(u8, "\x30\x5b\x38\x2d\x1c\xd9\xb9\x71\x4d\x51\x98\x30\xe5\xb0\x02\xcb\x6c\x38\x25\xbc\x05\xf8\x7e\xf1\xbc\xda\x36\x4d\x2d\x4d\xb9\x10", get_assertion_param.pinUvAuthParam.?.get()); 91 | try std.testing.expectEqual(PinProtocol.V2, get_assertion_param.pinUvAuthProtocol.?); 92 | } 93 | 94 | test "get assertion stringify 1" { 95 | const allocator = std.testing.allocator; 96 | var x = std.ArrayList(u8).init(allocator); 97 | defer x.deinit(); 98 | 99 | const payload = "\xa6\x01\x6b\x77\x65\x62\x61\x75\x74\x68\x6e\x2e\x69\x6f\x02\x58\x20\x6e\x0c\xb5\xf9\x7c\xae\xb8\xbf\x79\x7a\x62\x14\xc7\x19\x1c\x80\x8f\xe5\xa5\x50\x21\xf9\xfb\x76\x6e\x81\x83\xcd\x8a\x0d\x55\x0b\x03\x81\xa2\x62\x69\x64\x58\x40\xf9\xff\xff\xff\x95\xea\x72\x74\x2f\xa6\x03\xc3\x51\x9f\x9c\x17\xc0\xff\x81\xc4\x5d\xbb\x46\xe2\x3c\xff\x6f\xc1\xd0\xd5\xb3\x64\x6d\x49\x5c\xb1\x1b\x80\xe5\x78\x88\xbf\xba\xe3\x89\x8d\x69\x85\xfc\x19\x6c\x43\xfd\xfc\x2e\x80\x18\xac\x2d\x5b\xb3\x79\xa1\xf0\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x05\xa1\x62\x75\x70\xf4\x06\x58\x20\x30\x5b\x38\x2d\x1c\xd9\xb9\x71\x4d\x51\x98\x30\xe5\xb0\x02\xcb\x6c\x38\x25\xbc\x05\xf8\x7e\xf1\xbc\xda\x36\x4d\x2d\x4d\xb9\x10\x07\x02"; 100 | 101 | const gap = @This(){ 102 | .rpId = (try dt.ABS128T.fromSlice("webauthn.io")).?, 103 | .clientDataHash = "\x6e\x0c\xb5\xf9\x7c\xae\xb8\xbf\x79\x7a\x62\x14\xc7\x19\x1c\x80\x8f\xe5\xa5\x50\x21\xf9\xfb\x76\x6e\x81\x83\xcd\x8a\x0d\x55\x0b".*, 104 | .allowList = (try dt.ABSPublicKeyCredentialDescriptor.fromSlice(&.{try fido.common.PublicKeyCredentialDescriptor.new("\xf9\xff\xff\xff\x95\xea\x72\x74\x2f\xa6\x03\xc3\x51\x9f\x9c\x17\xc0\xff\x81\xc4\x5d\xbb\x46\xe2\x3c\xff\x6f\xc1\xd0\xd5\xb3\x64\x6d\x49\x5c\xb1\x1b\x80\xe5\x78\x88\xbf\xba\xe3\x89\x8d\x69\x85\xfc\x19\x6c\x43\xfd\xfc\x2e\x80\x18\xac\x2d\x5b\xb3\x79\xa1\xf0", .@"public-key", null)})).?, 105 | .options = .{ 106 | .up = false, 107 | .rk = null, 108 | .uv = null, 109 | }, 110 | .pinUvAuthParam = (try dt.ABS32B.fromSlice("\x30\x5b\x38\x2d\x1c\xd9\xb9\x71\x4d\x51\x98\x30\xe5\xb0\x02\xcb\x6c\x38\x25\xbc\x05\xf8\x7e\xf1\xbc\xda\x36\x4d\x2d\x4d\xb9\x10")).?, 111 | .pinUvAuthProtocol = .V2, 112 | }; 113 | 114 | try cbor.stringify(gap, .{}, x.writer()); 115 | 116 | try std.testing.expectEqualSlices(u8, payload, x.items); 117 | } 118 | -------------------------------------------------------------------------------- /lib/ctap/request/MakeCredential.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../main.zig"); 4 | const dt = fido.common.dt; 5 | 6 | const RelyingParty = fido.common.RelyingParty; 7 | const User = fido.common.User; 8 | const PublicKeyCredentialParameters = fido.common.PublicKeyCredentialParameters; 9 | const PublicKeyCredentialDescriptor = fido.common.PublicKeyCredentialDescriptor; 10 | const AuthenticatorOptions = fido.common.AuthenticatorOptions; 11 | 12 | const PinUvAuthParam = fido.ctap.pinuv.common.PinUvAuthParam; 13 | const PinProtocol = fido.ctap.pinuv.common.PinProtocol; 14 | 15 | const ClientDataHash = fido.ctap.crypto.ClientDataHash; 16 | 17 | /// Hash of the ClientData contextual binding 18 | clientDataHash: ClientDataHash, 19 | /// PublicKeyCredentialRpEntity 20 | rp: RelyingParty, 21 | /// PublicKeyCredentialUserEntity 22 | user: User, 23 | /// A sequence of CBOR maps 24 | pubKeyCredParams: dt.ABSPublicKeyCredentialParameters, 25 | /// excludeList: A sequence of PublicKeyCredentialDescriptor structures. 26 | /// The authenticator returns an error if the authenticator already contains 27 | /// one of the credentials enumerated in this sequence. 28 | excludeList: ?dt.ABSPublicKeyCredentialDescriptor = null, 29 | extensions: ?fido.ctap.extensions.Extensions = null, 30 | /// authenticator options: Parameters to influence authenticator operation. 31 | options: ?AuthenticatorOptions = null, 32 | /// Result of calling authenticate(pinUvAuthToken, clientDataHash) 33 | pinUvAuthParam: ?PinUvAuthParam = null, 34 | /// PIN protocol version chosen by the client. 35 | pinUvAuthProtocol: ?PinProtocol = null, 36 | enterpriseAttestation: ?u64 = null, 37 | 38 | pub fn requestsUv(self: *const @This()) bool { 39 | return self.options != null and self.options.?.uv != null and self.options.?.uv.?; 40 | } 41 | 42 | pub fn requestsRk(self: *const @This()) bool { 43 | return self.options != null and self.options.?.rk != null and self.options.?.rk.?; 44 | } 45 | 46 | pub fn requestsUp(self: *const @This()) bool { 47 | // if up missing treat it as being present with the value true 48 | return if (self.options != null and self.options.?.up != null) self.options.?.up.? else true; 49 | } 50 | 51 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 52 | return cbor.stringify(self, .{ 53 | .allocator = options.allocator, 54 | .ignore_override = true, 55 | .field_settings = &.{ 56 | .{ .name = "clientDataHash", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 57 | .{ .name = "rp", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 58 | .{ .name = "user", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 59 | .{ .name = "pubKeyCredParams", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 60 | .{ .name = "excludeList", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 61 | .{ .name = "extensions", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 62 | .{ .name = "options", .field_options = .{ .alias = "7", .serialization_type = .Integer } }, 63 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "8", .serialization_type = .Integer } }, 64 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "9", .serialization_type = .Integer } }, 65 | .{ .name = "enterpriseAttestation", .field_options = .{ .alias = "10", .serialization_type = .Integer } }, 66 | }, 67 | }, out); 68 | } 69 | 70 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 71 | return try cbor.parse(@This(), item, .{ 72 | .allocator = options.allocator, 73 | .ignore_override = true, // prevent infinite loops 74 | .field_settings = &.{ 75 | .{ .name = "clientDataHash", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 76 | .{ .name = "rp", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 77 | .{ .name = "user", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 78 | .{ .name = "pubKeyCredParams", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 79 | .{ .name = "excludeList", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 80 | .{ .name = "extensions", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 81 | .{ .name = "options", .field_options = .{ .alias = "7", .serialization_type = .Integer } }, 82 | .{ .name = "pinUvAuthParam", .field_options = .{ .alias = "8", .serialization_type = .Integer } }, 83 | .{ .name = "pinUvAuthProtocol", .field_options = .{ .alias = "9", .serialization_type = .Integer } }, 84 | .{ .name = "enterpriseAttestation", .field_options = .{ .alias = "10", .serialization_type = .Integer } }, 85 | }, 86 | }); 87 | } 88 | 89 | test "make credential parse 1" { 90 | const payload = "\xa4\x01\x58\x20\xc0\x39\x91\xac\x3d\xff\x02\xba\x1e\x52\x0f\xc5\x9b\x2d\x34\x77\x4a\x64\x1a\x4c\x42\x5a\xbd\x31\x3d\x93\x10\x61\xff\xbd\x1a\x5c\x02\xa2\x62\x69\x64\x69\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x64\x6e\x61\x6d\x65\x74\x73\x77\x65\x65\x74\x20\x68\x6f\x6d\x65\x20\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x03\xa3\x62\x69\x64\x58\x20\x78\x1c\x78\x60\xad\x88\xd2\x63\x32\x62\x2a\xf1\x74\x5d\xed\xb2\xe7\xa4\x2b\x44\x89\x29\x39\xc5\x56\x64\x01\x27\x0d\xbb\xc4\x49\x64\x6e\x61\x6d\x65\x6a\x6a\x6f\x68\x6e\x20\x73\x6d\x69\x74\x68\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x66\x6a\x73\x6d\x69\x74\x68\x04\x81\xa2\x63\x61\x6c\x67\x26\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79"; 91 | 92 | const di = try cbor.DataItem.new(payload); 93 | 94 | const mcp = try cbor.parse(@This(), di, .{}); 95 | 96 | try std.testing.expectEqualSlices(u8, "\xc0\x39\x91\xac\x3d\xff\x02\xba\x1e\x52\x0f\xc5\x9b\x2d\x34\x77\x4a\x64\x1a\x4c\x42\x5a\xbd\x31\x3d\x93\x10\x61\xff\xbd\x1a\x5c", &mcp.clientDataHash); 97 | try std.testing.expectEqualSlices(u8, "localhost", mcp.rp.id.get()); 98 | try std.testing.expectEqualSlices(u8, "sweet home localhost", mcp.rp.name.?.get()); 99 | try std.testing.expectEqualSlices(u8, "\x78\x1c\x78\x60\xad\x88\xd2\x63\x32\x62\x2a\xf1\x74\x5d\xed\xb2\xe7\xa4\x2b\x44\x89\x29\x39\xc5\x56\x64\x01\x27\x0d\xbb\xc4\x49", mcp.user.id.get()); 100 | try std.testing.expectEqualSlices(u8, "john smith", mcp.user.name.?.get()); 101 | try std.testing.expectEqualSlices(u8, "jsmith", mcp.user.displayName.?.get()); 102 | try std.testing.expectEqual(mcp.pubKeyCredParams.get()[0].alg, cbor.cose.Algorithm.Es256); 103 | try std.testing.expectEqual(mcp.pubKeyCredParams.get()[0].type, .@"public-key"); 104 | } 105 | 106 | test "make credential stringify 1" { 107 | const allocator = std.testing.allocator; 108 | var x = std.ArrayList(u8).init(allocator); 109 | defer x.deinit(); 110 | 111 | const payload = "\xa4\x01\x58\x20\xc0\x39\x91\xac\x3d\xff\x02\xba\x1e\x52\x0f\xc5\x9b\x2d\x34\x77\x4a\x64\x1a\x4c\x42\x5a\xbd\x31\x3d\x93\x10\x61\xff\xbd\x1a\x5c\x02\xa2\x62\x69\x64\x69\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x64\x6e\x61\x6d\x65\x74\x73\x77\x65\x65\x74\x20\x68\x6f\x6d\x65\x20\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x03\xa3\x62\x69\x64\x58\x20\x78\x1c\x78\x60\xad\x88\xd2\x63\x32\x62\x2a\xf1\x74\x5d\xed\xb2\xe7\xa4\x2b\x44\x89\x29\x39\xc5\x56\x64\x01\x27\x0d\xbb\xc4\x49\x64\x6e\x61\x6d\x65\x6a\x6a\x6f\x68\x6e\x20\x73\x6d\x69\x74\x68\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x66\x6a\x73\x6d\x69\x74\x68\x04\x81\xa2\x63\x61\x6c\x67\x26\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79"; 112 | 113 | const mcp = @This(){ 114 | .clientDataHash = "\xc0\x39\x91\xac\x3d\xff\x02\xba\x1e\x52\x0f\xc5\x9b\x2d\x34\x77\x4a\x64\x1a\x4c\x42\x5a\xbd\x31\x3d\x93\x10\x61\xff\xbd\x1a\x5c".*, 115 | .rp = try RelyingParty.new( 116 | "localhost", 117 | "sweet home localhost", 118 | ), 119 | .user = try User.new( 120 | "\x78\x1c\x78\x60\xad\x88\xd2\x63\x32\x62\x2a\xf1\x74\x5d\xed\xb2\xe7\xa4\x2b\x44\x89\x29\x39\xc5\x56\x64\x01\x27\x0d\xbb\xc4\x49", 121 | "john smith", 122 | "jsmith", 123 | ), 124 | .pubKeyCredParams = (try dt.ABSPublicKeyCredentialParameters.fromSlice(&.{ 125 | .{ .alg = .Es256, .type = .@"public-key" }, 126 | })).?, 127 | }; 128 | 129 | try cbor.stringify(mcp, .{}, x.writer()); 130 | 131 | try std.testing.expectEqualSlices(u8, payload, x.items); 132 | } 133 | -------------------------------------------------------------------------------- /lib/ctap/response/ClientPin.zig: -------------------------------------------------------------------------------- 1 | //! Response of a client pin command 2 | 3 | const std = @import("std"); 4 | 5 | const cbor = @import("zbor"); 6 | const fido = @import("../../main.zig"); 7 | const dt = fido.common.dt; 8 | 9 | /// Authenticator key agreement public key in COSE_Key format. This will 10 | /// be used to establish a sharedSecret between platform and the authenticator. 11 | keyAgreement: ?cbor.cose.Key = null, 12 | /// Encrypted pinToken using sharedSecret to be used in 13 | /// subsequent authenticatorMakeCredential and 14 | /// authenticatorGetAssertion operations. 15 | pinUvAuthToken: ?dt.ABS48B = null, 16 | /// Number of PIN attempts remaining before lockout. This 17 | /// is optionally used to show in UI when collecting the PIN in 18 | /// Setting a new PIN, Changing existing PIN and Getting pinToken 19 | /// from the authenticator flows. 20 | pinRetries: ?u8 = null, 21 | /// Present and true if the authenticator requires a power 22 | /// cycle before any future PIN operation, false if no power cycle needed. 23 | powerCycleState: ?bool = null, 24 | /// Number of uv attempts remaining before lockout. 25 | uvRetries: ?u8 = null, 26 | 27 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 28 | _ = options; 29 | 30 | try cbor.stringify(self.*, .{ 31 | .field_settings = &.{ 32 | .{ .name = "keyAgreement", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 33 | .{ .name = "pinUvAuthToken", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 34 | .{ .name = "pinRetries", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 35 | .{ .name = "powerCycleState", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 36 | .{ .name = "uvRetries", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 37 | }, 38 | .ignore_override = true, 39 | }, out); 40 | } 41 | 42 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 43 | return try cbor.parse(@This(), item, .{ 44 | .allocator = options.allocator, 45 | .ignore_override = true, // prevent infinite loops 46 | .field_settings = &.{ 47 | .{ .name = "keyAgreement", .field_options = .{ .alias = "1", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 48 | .{ .name = "pinUvAuthToken", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 49 | .{ .name = "pinRetries", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 50 | .{ .name = "powerCycleState", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 51 | .{ .name = "uvRetries", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 52 | }, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /lib/ctap/response/CredentialManagement.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../main.zig"); 4 | 5 | /// Number of existing discoverable credentials present on the authenticator 6 | existingResidentCredentialsCount: ?u32 = null, 7 | /// Number of maximum possible remaining discoverable credentials which can be created on the authenticator 8 | maxPossibleRemainingResidentCredentialsCount: ?u32 = null, 9 | /// RP Information 10 | rp: ?fido.common.RelyingParty = null, 11 | /// RP ID SHA-256 hash 12 | rpIDHash: ?[32]u8 = null, 13 | /// total number of RPs present on the authenticator 14 | totalRPs: ?u32 = null, 15 | /// User Information 16 | user: ?fido.common.User = null, 17 | /// PublicKeyCredentialDescriptor 18 | credentialID: ?fido.common.PublicKeyCredentialDescriptor = null, 19 | /// Public key of the credential 20 | publicKey: ?cbor.cose.Key = null, 21 | /// Total number of credentials present on the authenticator for the RP in question 22 | totalCredentials: ?u32 = null, 23 | /// Credential protection policy 24 | credProtect: ?fido.ctap.extensions.CredentialCreationPolicy = null, 25 | /// Large blob encryption key 26 | largeBlobKey: ?[]const u8 = null, 27 | 28 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 29 | _ = options; 30 | 31 | try cbor.stringify(self, .{ 32 | .field_settings = &.{ 33 | .{ .name = "existingResidentCredentialsCount", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 34 | .{ .name = "maxPossibleRemainingResidentCredentialsCount", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 35 | .{ .name = "rp", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 36 | .{ .name = "rpIDHash", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 37 | .{ .name = "totalRPs", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 38 | .{ .name = "user", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 39 | .{ .name = "credentialID", .field_options = .{ .alias = "7", .serialization_type = .Integer } }, 40 | .{ .name = "publicKey", .field_options = .{ .alias = "8", .serialization_type = .Integer } }, 41 | .{ .name = "totalCredentials", .field_options = .{ .alias = "9", .serialization_type = .Integer } }, 42 | .{ .name = "credProtect", .field_options = .{ .alias = "10", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 43 | .{ .name = "largeBlobKey", .field_options = .{ .alias = "11", .serialization_type = .Integer } }, 44 | }, 45 | .ignore_override = true, 46 | }, out); 47 | } 48 | 49 | pub fn cborParse(item: cbor.DataItem, options: cbor.Options) !@This() { 50 | return try cbor.parse(@This(), item, .{ 51 | .allocator = options.allocator, 52 | .ignore_override = true, // prevent infinite loops 53 | .field_settings = &.{ 54 | .{ .name = "existingResidentCredentialsCount", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 55 | .{ .name = "maxPossibleRemainingResidentCredentialsCount", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 56 | .{ .name = "rp", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 57 | .{ .name = "rpIDHash", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 58 | .{ .name = "totalRPs", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 59 | .{ .name = "user", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 60 | .{ .name = "credentialID", .field_options = .{ .alias = "7", .serialization_type = .Integer } }, 61 | .{ .name = "publicKey", .field_options = .{ .alias = "8", .serialization_type = .Integer } }, 62 | .{ .name = "totalCredentials", .field_options = .{ .alias = "9", .serialization_type = .Integer } }, 63 | .{ .name = "credProtect", .field_options = .{ .alias = "10", .serialization_type = .Integer }, .value_options = .{ .enum_serialization_type = .Integer } }, 64 | .{ .name = "largeBlobKey", .field_options = .{ .alias = "11", .serialization_type = .Integer } }, 65 | }, 66 | }); 67 | } 68 | 69 | pub fn deinit(self: *const @This(), allocator: std.mem.Allocator) void { 70 | if (self.largeBlobKey) |lbk| { 71 | allocator.free(lbk); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/ctap/response/GetAssertion.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | const fido = @import("../../main.zig"); 4 | 5 | const PublicKeyCredentialDescriptor = fido.common.PublicKeyCredentialDescriptor; 6 | const User = fido.common.User; 7 | 8 | /// PublicKeyCredentialDescriptor structure containing the 9 | /// credential identifier whose private key was used to generate the 10 | /// assertion. May be omitted if the allowList has exactly one Credential. 11 | credential: PublicKeyCredentialDescriptor, // 1 12 | /// authData: The signed-over contextual bindings made by the authenticator, 13 | /// as specified in [WebAuthN]. 14 | authData: []const u8, // 2 15 | /// signature: The assertion signature produced by the authenticator, as 16 | /// specified in [WebAuthN]. 17 | signature: []const u8, // 3 18 | /// PublicKeyCredentialUserEntity structure containing the user account 19 | /// information. 20 | /// 21 | /// FIDO Devices: For discoverable credentials on FIDO devices, at least 22 | /// user "id" is mandatory. 23 | /// 24 | /// For single account per RP case, authenticator returns "id" field to 25 | /// the platform which will be returned to the WebAuthn layer. 26 | /// 27 | /// For multiple accounts per RP case, where the authenticator does not 28 | /// have a display, authenticator returns "id" as well as other fields 29 | /// to the platform. 30 | user: ?User = null, // 4 31 | /// numberOfCredentials: Total number of account credentials for the RP. 32 | /// This member is required when more than one account for the RP and the 33 | /// authenticator does not have a display. Omitted when returned for the 34 | /// authenticatorGetNextAssertion method. 35 | numberOfCredentials: ?u64 = null, // 5 36 | /// Indicates that a credential was selected by the user via interaction 37 | /// directly with the authenticator, and thus the platform does not need 38 | /// to confirm the credential. 39 | userSelected: ?bool = null, // 6 40 | /// The contents of the associated largeBlobKey if present for the asserted 41 | /// credential, and if largeBlobKey was true in the extensions input. 42 | largeBlobKey: ?[]const u8 = null, // 7 43 | 44 | pub fn cborStringify(self: *const @This(), options: cbor.Options, out: anytype) !void { 45 | _ = options; 46 | 47 | try cbor.stringify( 48 | self, 49 | .{ 50 | .field_settings = &.{ 51 | .{ .name = "credential", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 52 | .{ .name = "authData", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 53 | .{ .name = "signature", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 54 | .{ .name = "user", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 55 | .{ .name = "numberOfCredentials", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 56 | .{ .name = "userSelected", .field_options = .{ .alias = "6", .serialization_type = .Integer } }, 57 | .{ .name = "largeBlobKey", .field_options = .{ .alias = "7", .serialization_type = .Integer } }, 58 | }, 59 | .ignore_override = true, 60 | }, 61 | out, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /lib/ctap/response/MakeCredential.zig: -------------------------------------------------------------------------------- 1 | //! The authenticatorMakeCredential response structure contains an attestation object 2 | //! plus additional information 3 | //! 4 | //! ``` 5 | //! // ATTESTATION OBJECT 6 | /// ________________________________________________________ 7 | /// | "fmt": "fido-u2f" | "attStmt": ... | "authData": ... | 8 | /// -------------------------------------------------------- 9 | /// | | 10 | /// ---------------------------- V 11 | /// | 12 | /// | 32 bytes 1 4 var var 13 | /// | ____________________________________________________________ 14 | /// | | RP ID hash | FLAGS | COUNTER | ATTESTED CRED. DATA | EXT | 15 | /// | ------------------------------------------------------------ 16 | /// | | | 17 | /// | V | 18 | /// | _____________________ | 19 | /// | |ED|AT|0|0|0|UV|0|UP| | 20 | /// | --------------------- | 21 | /// | V 22 | /// | _______________________________________________ 23 | /// | | AAGUID | L | CREDENTIAL ID | CRED. PUB. KEY | 24 | /// | ----------------------------------------------- 25 | /// | 16 bytes 2 L var len (COSE key) 26 | /// | 27 | /// V __________________________________ 28 | /// if Basic or Privacy CA: |"alg": ...|"sig": ...|"x5c": ...| 29 | /// ---------------------------------- 30 | /// _______________________________________ 31 | /// if ECDAA: |"alg": ...|"sig": ...|"ecdaaKeyId": ..| 32 | /// ``` 33 | const std = @import("std"); 34 | const cbor = @import("zbor"); 35 | const fido = @import("../../main.zig"); 36 | 37 | const AttestationStatementFormatIdentifiers = fido.common.AttestationStatementFormatIdentifiers; 38 | const AuthenticatorData = fido.common.AuthenticatorData; 39 | const AttestationStatement = fido.common.AttestationStatement; 40 | 41 | const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256; 42 | 43 | /// The attestation statement format identifier 44 | fmt: AttestationStatementFormatIdentifiers, 45 | /// Authenticator data 46 | authData: AuthenticatorData, 47 | /// Attestation statement 48 | attStmt: AttestationStatement, 49 | /// Indicates whether an enterprise attestation was returned for this credential. 50 | /// If epAtt is absent or present and set to false, then an enterprise attestation 51 | /// was not returned. If epAtt is present and set to true, then an enterprise 52 | /// attestation was returned 53 | epAtt: ?bool = null, 54 | /// Contains the largeBlobKey for the credential, if requested with the largeBlobKey 55 | /// extension 56 | largeBlobKey: ?[]const u8 = null, 57 | 58 | pub fn cborStringify(self: *const @This(), _: cbor.Options, out: anytype) !void { 59 | const AO = struct { 60 | fmt: AttestationStatementFormatIdentifiers, 61 | authData: []const u8, 62 | attStmt: AttestationStatement, 63 | }; 64 | 65 | // Encode authData which is not CBOR 66 | const ad = try self.authData.encode(); 67 | 68 | try cbor.stringify( 69 | AO{ .fmt = self.fmt, .authData = ad.get(), .attStmt = self.attStmt }, 70 | .{ 71 | .field_settings = &.{ 72 | .{ .name = "fmt", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 73 | .{ .name = "authData", .field_options = .{ .alias = "2", .serialization_type = .Integer } }, 74 | .{ .name = "attStmt", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 75 | .{ .name = "eppAtt", .field_options = .{ .alias = "4", .serialization_type = .Integer } }, 76 | .{ .name = "largeBlobKey", .field_options = .{ .alias = "5", .serialization_type = .Integer } }, 77 | }, 78 | .ignore_override = true, 79 | }, 80 | out, 81 | ); 82 | } 83 | 84 | test "attestationObject encoding - no attestation" { 85 | const allocator = std.testing.allocator; 86 | //var authData = std.ArrayList(u8).init(allocator); 87 | //defer authData.deinit(); 88 | var attObj = std.ArrayList(u8).init(allocator); 89 | defer attObj.deinit(); 90 | 91 | const k = cbor.cose.Key.fromP256Pub(.Es256, try EcdsaP256Sha256.PublicKey.fromSec1("\x04\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52")); 92 | var serialized_cred = std.ArrayList(u8).init(allocator); 93 | defer serialized_cred.deinit(); 94 | try cbor.stringify(&k, .{ .enum_serialization_type = .Integer }, serialized_cred.writer()); 95 | 96 | const acd = try fido.common.AttestedCredentialData.new( 97 | .{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 98 | &.{ 0xb3, 0xf8, 0xcd, 0xb1, 0x80, 0x20, 0x91, 0x76, 0xfa, 0x20, 0x1a, 0x51, 0x6d, 0x1b, 0x42, 0xf8, 0x02, 0xa8, 0x0d, 0xaf, 0x48, 0xd0, 0x37, 0x88, 0x21, 0xa6, 0xfb, 0xdd, 0x52, 0xde, 0x16, 0xb7, 0xef, 0xf6, 0x22, 0x25, 0x72, 0x43, 0x8d, 0xe5, 0x85, 0x7e, 0x70, 0xf9, 0xef, 0x05, 0x80, 0xe9, 0x37, 0xe3, 0x00, 0xae, 0xd0, 0xdf, 0xf1, 0x3f, 0xb6, 0xa3, 0x3e, 0xc3, 0x8b, 0x81, 0xca, 0xd0 }, 99 | serialized_cred.items, 100 | ); 101 | 102 | const ad = AuthenticatorData{ 103 | .rpIdHash = .{ 0x21, 0x09, 0x18, 0x5f, 0x69, 0x3a, 0x01, 0xea, 0x1a, 0x26, 0x41, 0xf8, 0x2d, 0x52, 0xfb, 0xae, 0xee, 0x0a, 0x4f, 0x47, 0xe3, 0x37, 0x4d, 0xfe, 0xf8, 0x70, 0x83, 0x8d, 0xe4, 0x9b, 0x0e, 0x97 }, 104 | .flags = .{ 105 | .up = 1, 106 | .rfu1 = 0, 107 | .uv = 0, 108 | .rfu2 = 0, 109 | .at = 1, 110 | .ed = 0, 111 | }, 112 | .signCount = 0, 113 | .attestedCredentialData = acd, 114 | }; 115 | 116 | //try ad.encode(authData.writer()); 117 | 118 | const ao = @This(){ 119 | .fmt = .@"packed", 120 | .authData = ad, 121 | .attStmt = AttestationStatement{ .none = .{} }, 122 | }; 123 | 124 | try cbor.stringify(ao, .{ .allocator = allocator }, attObj.writer()); 125 | 126 | // {1: "packed", 2: h'2109185f693a01ea1a2641f82d52fbaeee0a4f47e3374dfef870838de49b0e974100000000000000000000000000000000000000000040b3f8cdb180209176fa201a516d1b42f802a80daf48d0378821a6fbdd52de16b7eff6222572438de5857e70f9ef0580e937e300aed0dff13fb6a33ec38b81cad0a5010203262001215820d9f4c2a352136f19c9a95da8824ab5cdc4d5631ebcfd5bdbb0bfff253609129e225820ef404b880765576007888a3ed6abffb4257b7123553325d450613cb5bc9a3a52', 3: {}} 127 | try std.testing.expectEqualSlices(u8, "\xa3\x01\x66\x70\x61\x63\x6b\x65\x64\x02\x58\xc4\x21\x09\x18\x5f\x69\x3a\x01\xea\x1a\x26\x41\xf8\x2d\x52\xfb\xae\xee\x0a\x4f\x47\xe3\x37\x4d\xfe\xf8\x70\x83\x8d\xe4\x9b\x0e\x97\x41\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\xb3\xf8\xcd\xb1\x80\x20\x91\x76\xfa\x20\x1a\x51\x6d\x1b\x42\xf8\x02\xa8\x0d\xaf\x48\xd0\x37\x88\x21\xa6\xfb\xdd\x52\xde\x16\xb7\xef\xf6\x22\x25\x72\x43\x8d\xe5\x85\x7e\x70\xf9\xef\x05\x80\xe9\x37\xe3\x00\xae\xd0\xdf\xf1\x3f\xb6\xa3\x3e\xc3\x8b\x81\xca\xd0\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\x22\x58\x20\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52\x03\xa0", attObj.items); 128 | } 129 | -------------------------------------------------------------------------------- /lib/ctap/transports/ctaphid/Cmd.zig: -------------------------------------------------------------------------------- 1 | /// CTAPHID commands 2 | pub const Cmd = enum(u8) { 3 | /// Transaction that echoes the data back. 4 | ping = 0x01, 5 | /// Encapsulated CTAP1/U2F message. 6 | msg = 0x03, 7 | /// Place an exclusive lock for one channel 8 | lock = 0x04, 9 | /// Allocate a new CID or synchronize channel. 10 | init = 0x06, 11 | /// Request authenticator to provide some visual or audible identification 12 | wink = 0x08, 13 | /// Encapsulated CTAP CBOR encoded message. 14 | cbor = 0x10, 15 | /// Cancel any outstanding requests on the given CID. 16 | cancel = 0x11, 17 | /// The request is still being processed 18 | keepalive = 0x3b, 19 | /// Error response message (see `ErrorCodes`). 20 | err = 0x3f, 21 | }; 22 | 23 | pub const CMD_LENGTH = @sizeOf(Cmd); 24 | -------------------------------------------------------------------------------- /lib/ctap/transports/ctaphid/message.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const misc = @import("misc.zig"); 3 | const command = @import("Cmd.zig"); 4 | 5 | /// Size of a USB full speed packet 6 | pub const PACKET_SIZE = 64; 7 | /// Size of the initialization packet header 8 | pub const IP_HEADER_SIZE = 7; 9 | /// Size of the continuation packet header 10 | pub const CP_HEADER_SIZE = 5; 11 | /// Data size of a initialization packet 12 | pub const IP_DATA_SIZE = PACKET_SIZE - IP_HEADER_SIZE; 13 | /// Data size of a continuation packet 14 | pub const CP_DATA_SIZE = PACKET_SIZE - CP_HEADER_SIZE; 15 | 16 | // Offset of the CMD header field 17 | const CMD_OFFSET = misc.CID_LENGTH; 18 | // Offset of the BCNT header field 19 | const BCNT_OFFSET = CMD_OFFSET + command.CMD_LENGTH; 20 | // Offset of the data section (initialization packet) 21 | const IP_DATA_OFFSET = BCNT_OFFSET + misc.BCNT_LENGTH; 22 | // Offset of the SEQ header field 23 | const SEQ_OFFSET = misc.CID_LENGTH; 24 | // Offset of the data section (continuation packet) 25 | const CP_DATA_OFFSET = SEQ_OFFSET + misc.SEQ_LENGTH; 26 | 27 | // Command identifier; Bit 7 of the CMD header field must 28 | // be set to mark a initialization packet. 29 | const COMMAND_ID = 0x80; 30 | 31 | /// Iterator for a CTAPHID message. 32 | /// 33 | /// The iterator acts as a view into a data slice (the bytes to be sent between client and authenticator). 34 | /// 35 | /// The first time `next()` is called, the iterator will return a `u8` slice of size `PACKET_SIZE` (64 Bytes, i.e. USB full speed) which contains a initialization packet, i.e. header plus the first data bytes (`PACKET_SIZE`-7). Every continuous call to `next()` will return a continuation packet until all data bytes have been encoded. 36 | pub const CtapHidMessageIterator = struct { 37 | cntr: usize = 0, 38 | seq: misc.Seq = 0, 39 | buffer: [PACKET_SIZE]u8 = undefined, 40 | data: []const u8 = &.{}, 41 | allocator: ?std.mem.Allocator = null, 42 | cid: misc.Cid, 43 | cmd: command.Cmd, 44 | 45 | pub fn new( 46 | cid: misc.Cid, 47 | cmd: command.Cmd, 48 | ) CtapHidMessageIterator { 49 | return .{ 50 | .cid = cid, 51 | .cmd = cmd, 52 | }; 53 | } 54 | 55 | pub fn deinit(self: *const @This()) void { 56 | if (self.allocator) |a| { 57 | a.free(self.data); 58 | } 59 | } 60 | 61 | /// Get the next data packet. 62 | /// 63 | /// Returns `null` if all data bytes have been processed. 64 | pub fn next(self: *@This()) ?[]const u8 { 65 | if (self.cntr < self.data.len or (self.data.len == 0 and self.cntr == 0)) { 66 | // Zero the whole buffer 67 | @memset(self.buffer[0..], 0); 68 | 69 | var len: usize = undefined; 70 | var off: usize = undefined; 71 | if (self.cntr == 0) { // initialization packet 72 | len = if (self.data.len <= IP_DATA_SIZE) self.data.len else IP_DATA_SIZE; 73 | off = IP_DATA_OFFSET; 74 | 75 | misc.intToSlice(self.buffer[0..misc.CID_LENGTH], self.cid); 76 | self.buffer[CMD_OFFSET] = @intFromEnum(self.cmd) | COMMAND_ID; 77 | misc.intToSlice(self.buffer[BCNT_OFFSET .. BCNT_OFFSET + misc.BCNT_LENGTH], @as(misc.Bcnt, @intCast(self.data.len))); 78 | } else { 79 | len = if (self.data.len - self.cntr <= CP_DATA_SIZE) self.data.len - self.cntr else CP_DATA_SIZE; 80 | off = CP_DATA_OFFSET; 81 | 82 | misc.intToSlice(self.buffer[0..misc.CID_LENGTH], self.cid); 83 | self.buffer[SEQ_OFFSET] = self.seq; 84 | 85 | self.seq += 1; 86 | } 87 | 88 | @memcpy(self.buffer[off .. off + len], self.data[self.cntr .. self.cntr + len]); 89 | self.cntr += len; 90 | 91 | if (self.data.len == 0) { 92 | // special case: data slice is empty. 93 | // prevents this block from executing twice. 94 | self.cntr = 1; 95 | } 96 | 97 | return self.buffer[0..]; 98 | } else { 99 | return null; 100 | } 101 | } 102 | }; 103 | 104 | /// Create a new `CtapHidMessageIterator`. 105 | pub fn iterator( 106 | cid: misc.Cid, 107 | cmd: command.Cmd, 108 | data: []const u8, 109 | ) CtapHidMessageIterator { 110 | return CtapHidMessageIterator{ 111 | .cntr = 0, 112 | .seq = 0, 113 | .buffer = undefined, 114 | .data = data, 115 | .cid = cid, 116 | .cmd = cmd, 117 | }; 118 | } 119 | 120 | test "Response Iterator 1" { 121 | const allocator = std.testing.allocator; 122 | var mem = try allocator.alloc(u8, 57); 123 | defer allocator.free(mem); 124 | 125 | @memset(mem[0..], 'a'); 126 | 127 | var iter = iterator(0x11223344, command.Cmd.init, mem); 128 | 129 | const r1 = iter.next(); 130 | try std.testing.expectEqualSlices(u8, "\x11\x22\x33\x44\x86\x00\x39aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", r1.?); 131 | 132 | try std.testing.expectEqual(iter.next(), null); 133 | try std.testing.expectEqual(iter.next(), null); 134 | } 135 | 136 | test "Response Iterator 2" { 137 | const allocator = std.testing.allocator; 138 | var mem = try allocator.alloc(u8, 17); 139 | defer allocator.free(mem); 140 | 141 | @memset(mem[0..], 'a'); 142 | 143 | var iter = iterator(0x11223344, command.Cmd.init, mem); 144 | 145 | const r1 = iter.next(); 146 | try std.testing.expectEqualSlices(u8, "\x11\x22\x33\x44\x86\x00\x11aaaaaaaaaaaaaaaaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", r1.?); 147 | 148 | try std.testing.expectEqual(iter.next(), null); 149 | } 150 | 151 | test "Response Iterator 3" { 152 | const allocator = std.testing.allocator; 153 | var mem = try allocator.alloc(u8, 74); 154 | defer allocator.free(mem); 155 | 156 | @memset(mem[0..57], 'a'); 157 | @memset(mem[57..74], 'b'); 158 | 159 | var iter = iterator(0xcafebabe, command.Cmd.cbor, mem); 160 | 161 | const r1 = iter.next(); 162 | try std.testing.expectEqualSlices(u8, "\xca\xfe\xba\xbe\x90\x00\x4aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", r1.?); 163 | 164 | const r2 = iter.next(); 165 | try std.testing.expectEqualSlices(u8, "\xca\xfe\xba\xbe\x00bbbbbbbbbbbbbbbbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", r2.?); 166 | 167 | try std.testing.expectEqual(iter.next(), null); 168 | } 169 | 170 | test "Response Iterator 4" { 171 | const allocator = std.testing.allocator; 172 | var mem = try allocator.alloc(u8, 128); 173 | defer allocator.free(mem); 174 | 175 | @memset(mem[0..57], 'a'); 176 | @memset(mem[57..116], 'b'); 177 | @memset(mem[116..128], 'c'); 178 | 179 | var iter = iterator(0xcafebabe, command.Cmd.cbor, mem); 180 | 181 | const r1 = iter.next(); 182 | try std.testing.expectEqualSlices(u8, "\xca\xfe\xba\xbe\x90\x00\x80aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", r1.?); 183 | 184 | const r2 = iter.next(); 185 | try std.testing.expectEqualSlices(u8, "\xca\xfe\xba\xbe\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", r2.?); 186 | 187 | const r3 = iter.next(); 188 | try std.testing.expectEqualSlices(u8, "\xca\xfe\xba\xbe\x01cccccccccccc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", r3.?); 189 | 190 | try std.testing.expectEqual(iter.next(), null); 191 | } 192 | 193 | test "Response Iterator 5" { 194 | var iter = iterator(0xcafebabe, command.Cmd.cbor, &.{}); 195 | 196 | const r1 = iter.next(); 197 | try std.testing.expectEqualSlices(u8, "\xca\xfe\xba\xbe\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", r1.?); 198 | 199 | try std.testing.expectEqual(iter.next(), null); 200 | try std.testing.expectEqual(iter.next(), null); 201 | try std.testing.expectEqual(iter.next(), null); 202 | } 203 | -------------------------------------------------------------------------------- /lib/ctap/transports/ctaphid/misc.zig: -------------------------------------------------------------------------------- 1 | pub const Cid = u32; 2 | pub const Nonce = u64; 3 | pub const Bcnt = u16; 4 | pub const Seq = u8; 5 | pub const CID_LENGTH = @sizeOf(Cid); 6 | pub const NONCE_LENGTH = @sizeOf(Nonce); 7 | pub const BCNT_LENGTH = @sizeOf(Bcnt); 8 | pub const SEQ_LENGTH = @sizeOf(Seq); 9 | 10 | /// Convert a slice into an integer of the given type T 11 | /// (big endian, i.e. network byte order). 12 | /// 13 | /// IMPORTANT!: There are no checks that slice fits in T. 14 | pub fn sliceToInt(comptime T: type, slice: []const u8) T { 15 | var res: T = 0; 16 | var i: usize = 0; 17 | while (i < @sizeOf(T)) : (i += 1) { 18 | res += @as(T, @intCast(slice[i])); 19 | 20 | if (i < @sizeOf(T) - 1) { 21 | res <<= 8; 22 | } 23 | } 24 | return res; 25 | } 26 | 27 | /// Convert an integer into a slice 28 | /// (big endian, i.e. network byte order). 29 | /// 30 | /// IMPORTANT!: There are no checks that T fits into the slice. 31 | pub fn intToSlice(slice: []u8, i: anytype) void { 32 | // big endian 33 | var x = i; 34 | const SIZE: usize = @sizeOf(@TypeOf(i)); 35 | 36 | var j: usize = 0; 37 | while (j < SIZE) : (j += 1) { 38 | slice[SIZE - 1 - j] = @as(u8, @intCast(x & 0xff)); 39 | x >>= 8; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /static/design.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zig-Sec/keylib/29e5dc4c546439dbd30425849613fa058325b0d8/static/design.odg -------------------------------------------------------------------------------- /static/design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zig-Sec/keylib/29e5dc4c546439dbd30425849613fa058325b0d8/static/design.pdf -------------------------------------------------------------------------------- /static/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zig-Sec/keylib/29e5dc4c546439dbd30425849613fa058325b0d8/static/design.png -------------------------------------------------------------------------------- /static/pinUvAuthToken.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zig-Sec/keylib/29e5dc4c546439dbd30425849613fa058325b0d8/static/pinUvAuthToken.odg --------------------------------------------------------------------------------