Skip to content

Getting Started

David Sugar edited this page Oct 17, 2023 · 8 revisions

Welcome, this guide should help you get started with keylib. If you have any questions or encounter bugs, please open an issue.

Keylib is a library designed to assist you in implementing PassKeys, which are platform authenticators compatible with FIDO2. This library handles the task of generating new credentials and assertions when required, and your responsibility is to furnish it with the necessary callbacks. Keylib currently offers two interfaces, one for Zig and another one for C.

Build system

GCC + Make Coming soon..
Zig build system

If you don't have Zig available on your system, download it from here, unpack it, and then add it to your path.

Create a new project, e.g.:

mkdir my-project
cd my-project
zig init-exe

Then setup your project by creating a build.zig.zon file (make sure you update the hash):

.{                                                                                                                                                                                                          
    .name = "my-project",
    .version = "0.1.0",
    .dependencies = .{
        .keylib = .{
            .url = "https://codeberg.org/r4gus/keylib/archive/master.tar.gz",
            .hash = "1220696b391343ed11d1c2972779ceca6759f815031df2fca4a84a6dd90de050e0b1",
        },
    },
}

You can get the correct hash by using a wrong hash, and then running zig build. The correct hash will be displayed in a error message.

Update your build.zig file:

C Create the file `src/main.c` with a main function.

Then update you build.zig file:

const std = @import("std");                                                                                                                                                                                 

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const keylib_dep = b.dependency("keylib", .{
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "my-project",
        .root_source_file = .{ .path = "src/main.c" },
        .target = target,
        .optimize = optimize,
    });
    exe.linkLibrary(keylib_dep.artifact("keylib"));
    exe.linkLibrary(keylib_dep.artifact("uhid"));
    exe.linkLibC();
    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

You can include the required header using #include "keylib/keylib.h" in your main.c.

Zig Coming soon...

Finally run zig build to build the project. The binary can be found in zig-out/bin/my-project. You can also run you project by executing zig build run.

1) Callbacks

The first step when using this library is to implement the required callbacks (C, Zig).

C

The first thing we must do is include the keylib.h, header file into our code. This file includes enum definitions, the callback signatures, and functions for interacting with the library.

#include "keylib/keylib.h" // Error_Denied, Error_DoesNotExist, ...

Next we implement the uv (user verification) and up (user presence) callbacks. The uv callback is used to authenticate a user, e.g. by asking for a password. What authentication method you want to use for your application is up to you. The up callback is used to verify the (physical) presence of a user. You can for example do this by displaying a button the user can click on. Both functions return UpResult_Accepted on success, UpResult_Denied if rejected (e.g. user entered wrong password or clicked cancel), and UpResult_Timeout if a timeout has occurred.

// Make a user presence check, e.g. display a button and ask the user if she wants to confirm the action.
// The arguments info, user, and rp are null terminated strings that provide additional context that
// you can display.
//
// This function should return Accepted, Denied, or Timeout
int my_up(const char* info, const char* user, const char* rp) {
    printf("up\n");
    return UpResult_Denied;
}

// This callback should implement some form of user verification, e.g. when called, ask a user for a password.
//
// This function should return Accepted, Denied, or Timeout
int my_uv() {
    printf("uv\n");                                                                                  
    return UpResult_Denied;
}

Next we implement the select_cred callback. This callback is used if more than one credential is bound to the same relying party (the service the credential was created for). The callback will provide a null terminated array of user strings. Those strings should be displayed on the screen and the user must then select a user. The function returns either a user index (starting from 0) or a negative error value.

// Let the user select one of multiple credentials associated with a relying party.
//
// You should either return the users index or -1.
int my_select_cred(const char* rpId, char** users) {
    printf("select\n");
    return -1;
}

The authenticator also needs some way to read, write and delete data. The read callback takes a id string (the unique credential id), a rp string (the relying party id/ base url), and a pointer to a string array.

  • If id is not null:
    • Create a string array with two elements, where the first element contains the requested data and the second element is a null terminator. Assign the array to out. Then return SUCCESS.
  • If id is null and rp is not null:
    • Create a null terminated string array that contains all data associated with the given rp. Assign the array to out. Then return SUCCESS.
  • Else:
    • Create a null terminated string array that contains all stored data. Assign the array to out. Then return SUCCESS.

Return DoesNotExist if no data could be found.

// Read data from permanent storage.
int my_read(const char* id, const char* rp, char*** out) {
    printf("read");
    return DoesNotExist;
}

NOTE: It is important that the array itself and all (data) strings are null terminated!

The following is an example how the creation of a array with one element could look like:

char** x = malloc(sizeof(char*) * 2);
x[0] = data; // think of data as a hex encodex, null terminated string
x[1] = NULL;
*out = x;
return Error_SUCCESS;

All memory assigned to out is owned by the authenticator, i.e. it's NOT you responsibility to free it.


The write callback is used to persist the given data. It is both used to create new entries and to update existing ones. You can assume that the credential id is unique, i.e. if the id already exists update (overwrite) the existing entry, otherwise create a new one. Make sure you associate the data with the given relying party id (rp) so you can find it later.

// Persist the given data and make sure that it can be found using its id and associated rp (relying party).
int my_write(const char* id, const char* rp, const char* data) {
    printf("write");
    return -1;
}

The delete callback is quite simple, find and delete the data associated with the given id.

// Delete the data associated with the given id.
int my_delete(const char* id, const char* rp) {
    printf("delete\n");
    return -1;
}
Zig Coming soon...

2) Instantiation of Auth

After we have implemented the required callbacks, the next step is to instantiate the Auth struct. This is usually done at the beginning of your main function.

C
int main() {
    // Init Start

    Callbacks c = {
        my_up, my_uv, my_select_cred, my_read, my_write, my_delete
    };

    void* auth = auth_init(c);

   // Init End

    // Deinit Start

    uhid_close(fd);

    // Deinit End

    return 0;
}

As you can see, we first create a Callbacks struct from all the callbacks we created and pass this to auth_init. The auth_init function will create a new (default) authenticator, initialize it and then return a void* that points to the struct. You should make sure that the returned pointer is not NULL before using it.

Zig Coming soon...

3) Setup transport

On its own, the authenticator struct is not very useful. You need some way to interact with a client. Currently the CTAP2 spec lists three way: USB, NFC, and Bluetooth.

This library offers a CTAPHID interface that helps you implement a authenticator that communicates over USB. We also provide a wrapper for /dev/uhid on Linux, if you want to create a virtual USB device. While the CTAPHID interface comes with keylib.h, you must include keylib/uhid.h (and also link the uhid library that is part of this project) if you want to use the uhid functions.

C The following is meant to illustrate how a platform authenticator implementation on Linux could look like. If you want to support another platform or transport, you have to implement the interface yourself. If you do so, think about contributing back to this project.
int main() {
    // Init Start
    // ...

    void* ctaphid = ctaphid_init();
    
    int fd = uhid_open();

    // Init End

    // Deinit Start

    uhid_close(fd);

    ctaphid_deinit(ctaphid);
    
    // ...
    // Deinit End

   return 0;
}

The ctaphid_init function returns a pointer to a CTAPHID handler. The handler expects USB packets and gradually builds a request message from them. After it has assembled a message, the handler will pass the message to the authenticator using the Auth.handle function. The authenticator acts upon the request and returns a response message. The response from the authenticator is converted by the handler into a CtapHidMessageIterator, which is returned to the calling function (main in our case). The iterator will provide you with USB packets ready for transmission.

int main() {
    // Init Start
    // Init End
    
    while (1) {
        char buffer[64];                         
        
        int packet_length = uhid_read_packet(fd, &buffer[0]); // read a packet (if available)
        if (packet_length) {
            // The handler will either return NULL or a pointer to
            // a ctaphid packet iterator.                   
            void* iter = ctaphid_handle(ctaphid, &buffer[0], packet_length, auth);
    
            // Every call to next will return a 64 byte packet ready
            // to be sent to the host.                   
            if (iter) {
                char out[64];     
    
                while(ctaphid_iterator_next(iter, &out[0])) {
                    uhid_write_packet(fd, &out[0], 64);
                }
    
                // Don't forget to free the iterator           
                ctaphid_iterator_deinit(iter);
            }
        }                             
    }

    // Deinit Start
    // Deinit End

   return 0;
}
Zig Coming soon...

Conclusion

As you can see it's quite easy to implement a working authenticator using this project. The difficulty lies in choosing the right surrounding infrastructure (e.g. a database for storing credentials, a user interface, ...). You should also keep in mind that credentials should not be stored in plain text.

If you have any questions or encounter problems feel free to open a issue.

Clone this wiki locally