Pairing

Nabto Edge uses Public Key Authentication as the primary means of authenticating clients vs device and vice versa: Devices have lists of authorized client public keys and clients have lists of known devices.

Establishing the lists of authorized keys is denoted pairing: When starting from scratch, both lists are blank - somehow authorized keys must be added. Adding the first client public key to a device is conceptually the most complicated, subsequent clients can then be securely added using a secure connection established using the first public key if desired.

Exactly how pairing is implemented is up to the specific solution - Nabto provides various mechanisms to support pairing. But anything that makes sense in the given application scenario to put a public key in the access control list will work. For instance, a camera can be paired with a smartphone client by scanning a QR code with the public key on a users’ phone. But many deployments do not have such privilege and other means must be used.

Current support for pairing and roadmap features

In the current 5.1 release of Nabto Edge, the pairing process is supported in a way similar to Nabto Micro (Nabto 4 and below): The device can be put in pairing mode where it listens for local network requests. Public keys are then exchanged on the local network and each peer can display the opposite peer’s public key fingerprint to the user (if feasible) to “eyeball” confirm there is no man-in-the-middle trying to inject a malicious key.

For the device to be able to show its own and the user’s public key fingerprint, it of course requires some kind of display. If no such display is available, a few steps can be taken to increase the probabilty that it is indeed the actual user and not an attacker (or actual device towards the client):

  1. encourage user to make sure there are only trusted devices on the LAN (e.g., disconnect other ethernet peers or temporarily change wifi password)
  2. encourage user to disconnect WAN access
  3. make a temporal restriction as known from WPS: only allow pairing in a fixed time interval, e.g. in a brief window after boot or after pressing a button

This hassle to increase security during pairing is remedied by upcoming releases as outlined in the roadmap section.

Nabto Edge IAM

Management of access control lists on the device (public key white liste implementation as well as role and policy based access control) is covered by the Nabto Edge IAM API. Applications can optionally use this instead of bringing their own such implementation. The Nabto Edge IAM API included with the current version of the Nabto Edge Embedded SDK is a pre-release API. This means that the specific API is subject to change before the stable release.

Example applications for the Nabto Edge Embedded SDK use the Nabto IAM module in its current pre-released version. It in turn uses the lower level Nabto Authorization API which is stable in its current release - so you could roll your own whitelist management implementation based on the stable APIs and just use the current version of Nabto IAM as inspiration.

Roadmap features

More features will soon be added to support secure pairing (Q3 2020):

  • J-PAKE based exchange of public keys - this eliminates the need for the user to manually confirm the validity of the public key
  • Remote token- and password based pairing
  • Stable Nabto IAM API: The IAM API that is part of the current Nabto Edge Embedded SDK release is subject to change, see above

Pairing procedure in Nabto Edge 5.1

With the current version of the Nabto Edge platform, a successful pairing can take place in the following way:

  1. User prepares the environment as per the restrictions suggested above (prevent untrusted clients from accessing device)
  2. Device application is started in pairing mode for a limited time, ie it starts a listener on a specific socket for incoming pairing requests on LAN.
  3. Client application establishes connection to device
  4. Client application does not recognize the device public key and shows public key fingerprint to user
  5. If possible, user compares the device public key fingerprint shown in the client application with what is seen on the device display - or e.g. what is printed on a label from the factory
  6. If possible, user compares the client public key fingerprint shown on the device display with what is seen on the client display
  7. If applicable, user accepts the opposite fingerprints in both the client and device applications - e.g. by tapping a button in a mobile app in the client and pressing a physical button on a device or selecting an OSD option

Now pairing is complete and the client can connect to the device without any restrictions on the environment:

  1. Client connects to the regular non-pairing ports on the device
  2. Client verifies that the device’s public key matches the one registered for the device id during pairing
  3. Device verifies that the client’s public key matches the one registered for the device id during pairing
  4. Both peers allow the process to proceed and the user may use the device

An example pairing implementation is shown below.

Examples

In this example, a device is created with two COAP endpoints. The /pair endpoint only accepts requests from local connections, and adds the fingerprint of the connected client to its whitelist. Similarly, only whitelisted client fingerprints will be allowed access to the /hello endpoint.

The example client connects to a device ensuring it has a local connection. When connected, it will invoke the /pair endpoint to add its fingerprint to the whitelist in the device. Once whitelisted, the client will invoke the /hello endpoint and exit.

Client Side of Pairing (plain C)

#include <stdio.h>
#include <nabto/nabto_client.h>
// Options used for connection, example device will only allow local
// connections to pair, so we ensure the client does not allow Remote.
const char* options =
"{"
    "\"ProductId\": \"pr-f4nqpowq\","
    "\"DeviceId\": \"de-zqw7vehm\","
    "\"ServerKey\": \"sk-3fdf3786de79188e654ac167cea5ecc5\","
    "\"Remote\": false,"
    "\"PrivateKey\": \"-----BEGIN EC PRIVATE KEY-----\\nMHcCAQEEIKyb9kLRzZuzd/K/TikwowF6aI5tJ4rLjm7UMysP+HzcoAoGCCqGSM49\\nAwEHoUQDQgAEKyo54YuBSBMfR5E094DCsnj9YVbCNCoWm5SMoLrtaDdH4w6ERMFW\\niyn7/vHygP9SkbBHc1eWY7Gl8E9E+E9rsQ==\\n-----END EC PRIVATE KEY-----\\n\""
"}";

int main(void) {
    // initialize the client and connect
    char* f;
    NabtoClient* context = nabto_client_new();
    NabtoClientConnection* connection = nabto_client_connection_new(context);
    NabtoClientFuture* future = nabto_client_future_new(context);
    nabto_client_connection_set_options(connection, options);
    nabto_client_connection_connect(connection, future);
    nabto_client_future_wait(future);

    // Verify device fingerprint to ensure we are talking to the right
    // device.
    nabto_client_connection_get_device_fingerprint_full_hex(connection, &f);

    printf("Connected to device with fingerprint: %s\n", f);
    printf("Is the fingerprint correct?\n");
    printf("Assuming yes and start pairing\n");

    // If fingerprint was accepted we post to the /pair endpoint to
    // whitelist our fingerprint in the device.
    NabtoClientCoap* request = nabto_client_coap_new(connection, "POST", "/pair");
    nabto_client_coap_execute(request, future);
    nabto_client_future_wait(future);

    uint16_t statusCode;
    nabto_client_coap_get_response_status_code(request, &statusCode);
    if (statusCode != 205) {
        printf("Pairing failed: %d\n", statusCode);
        nabto_client_coap_free(request);
    } else {
        // Our fingerprint is now in the whitelist and we have access
        // to other coap endpoints.
        printf("Paired, we should now have access to /hello\n");
        nabto_client_coap_free(request);
        request = nabto_client_coap_new(connection, "GET", "/hello");
        nabto_client_coap_execute(request, future);
        nabto_client_future_wait(future);

        nabto_client_coap_get_response_status_code(request, &statusCode);
        if (statusCode != 205) {
            printf("hello request failed!\n");
        } else {
            void* payload = NULL;
            size_t payloadLength = 0;
            nabto_client_coap_get_response_payload(request, &payload, &payloadLength);
            printf("received hello response payload: %s\n", (char*)payload);
            // Now that we are paired we could also have created a new
            // connection where remote connections are allowed, and
            // still have access to the hello endpoint
        }
        nabto_client_coap_free(request);
    }

    nabto_client_connection_close(connection, future);
    nabto_client_future_wait(future);
    nabto_client_future_free(future);
    nabto_client_connection_free(connection);
    nabto_client_free(context);


}

Embedded Device Side of Pairing

#include <stdio.h>
#include <nabto/nabto_device.h>
#include <nabto/nabto_device_experimental.h>

const char* productId = "pr-f4nqpowq";
const char* deviceId = "de-zqw7vehm";
const char* coapPairPath[] = { "pair", NULL };
const char* coapHelloPath[] = { "hello", NULL };

char* privateKey =
    "-----BEGIN EC PARAMETERS-----\n"
    "BggqhkjOPQMBBw==\n"
    "-----END EC PARAMETERS-----\n"
    "-----BEGIN EC PRIVATE KEY-----\n"
    "MHcCAQEEINc9q3Ku6iIHUh44y1/8zUAOYL2+f1JEd96so+D336KQoAoGCCqGSM49\n"
    "AwEHoUQDQgAEx847zIaCSk8zvZ6XsQzBKyDiv5RrqtxLGQWvGl85lZjn6Y3gdU1a\n"
    "YcJ7P/1GQlbCuorDFqtiWGEPpGoIju07mg==\n"
    "-----END EC PRIVATE KEY-----\n";

char* fp;
NabtoDevice* device;

NabtoDeviceListener* helloListen;
NabtoDeviceFuture* helloFuture;
NabtoDeviceCoapRequest* helloRequest;

NabtoDeviceListener* pairListen;
NabtoDeviceFuture* pairFuture;
NabtoDeviceCoapRequest* pairRequest;

char* whiteList[10];
uint8_t whiteListLen = 0;

void start_pair_listen();
void handle_pair_request(NabtoDeviceFuture* future, NabtoDeviceError ec, void* userData);
bool checkClientFingerprint(char* clientFp);

void start_hello_listen();
void handle_hello_request(NabtoDeviceFuture* future, NabtoDeviceError ec, void* userData);

int main(void) {
    // Initialize the device and start
    device = nabto_device_new();
    nabto_device_set_private_key(device, privateKey);
    nabto_device_get_device_fingerprint_full_hex(device, &fp);
    nabto_device_set_product_id(device, productId);
    nabto_device_set_device_id(device, deviceId);
    nabto_device_enable_mdns(device);
    nabto_device_start(device);
    // We print the fingerprint to show what we should expect in the
    // client.
    printf("device started with fingerprint: %s\n", fp);

    // Create a COAP endpoint for pairing
    pairListen = nabto_device_listener_new(device);
    nabto_device_coap_init_listener(device, pairListen, NABTO_DEVICE_COAP_POST, coapPairPath);
    pairFuture = nabto_device_future_new(device);

    // Create a COAP endpoint for normal usage
    helloListen = nabto_device_listener_new(device);
    nabto_device_coap_init_listener(device, helloListen, NABTO_DEVICE_COAP_GET, coapHelloPath);
    helloFuture = nabto_device_future_new(device);

    // Start listeners for the COAP endpoints
    start_pair_listen();
    start_hello_listen();

    // Wait for termination and clean up
    getchar();
    for (int i = 0; i<whiteListLen; i++) {
        nabto_device_string_free(whiteList[i]);
    }
    nabto_device_free(device);
    nabto_device_listener_free(pairListen);
    nabto_device_listener_free(helloListen);
    nabto_device_future_free(pairFuture);
    nabto_device_future_free(helloFuture);
    nabto_device_string_free(fp);
}

void start_pair_listen()
{
    nabto_device_listener_new_coap_request(pairListen, pairFuture, &pairRequest);
    nabto_device_future_set_callback(pairFuture, &handle_pair_request, NULL);
}

void handle_pair_request(NabtoDeviceFuture* future, NabtoDeviceError ec, void* userData)
{
    if (ec == NABTO_DEVICE_EC_OK) {
        // A client wants to pair!

        // Get the connection reference and verify that it is a local
        // connection as we will not allow pairing on remote
        // connection.
        NabtoDeviceConnectionRef ref = nabto_device_coap_request_get_connection_ref(pairRequest);
        if (!nabto_device_connection_is_local(device, ref)) {
            nabto_device_coap_error_response(pairRequest, 403, "Access Denied");
        } else {
            // Get the client fingerprint and verify we are talking
            // with the right client.
            char* clientFp;
            nabto_device_connection_get_client_fingerprint_full_hex(device, ref, &clientFp);
            if(checkClientFingerprint(clientFp)) {
                // add the fingerprint of the client to our whitelist
                // to allow access to the /hello endpoint
                whiteList[whiteListLen] = clientFp;
                whiteListLen++;
                nabto_device_coap_response_set_code(pairRequest, 205);
                nabto_device_coap_response_ready(pairRequest);
                printf("Client fingerprint added to whitelist\n");
            } else {
                nabto_device_string_free(clientFp);
                nabto_device_coap_error_response(pairRequest, 403, "Access Denied");
            }
        }
        nabto_device_coap_request_free(pairRequest);
        start_pair_listen();
    } else {
        printf("An error occurred when handling pair CoAP request, ec=%d\n", ec);
    }
}

bool checkClientFingerprint(char* clientFp)
{
    printf("Got client fingerprint: %s, please verify it is correct\n", clientFp);
    printf("assuming it is\n");
    return true;
}

void start_hello_listen()
{
    nabto_device_listener_new_coap_request(helloListen, helloFuture, &helloRequest);
    nabto_device_future_set_callback(helloFuture, &handle_hello_request, NULL);
}

void handle_hello_request(NabtoDeviceFuture* future, NabtoDeviceError ec, void* userData)
{
    if (ec == NABTO_DEVICE_EC_OK) {
        // A client wants to access /hello. We get the fingerprint of
        // the client to ensure it has access.
        NabtoDeviceConnectionRef ref = nabto_device_coap_request_get_connection_ref(helloRequest);
        char* clientFp;
         nabto_device_connection_get_client_fingerprint_full_hex(device, ref, &clientFp);
         for (int i = 0; i<whiteListLen; i++) {
             // Go through the whiteList and look for a valid
             // fingerprint
             if (strcmp(clientFp, whiteList[i]) == 0) {
                 // The client was whitelisted! We should make a
                 // response.
                 nabto_device_coap_response_set_code(helloRequest, 205);
                 nabto_device_coap_response_set_content_format(helloRequest, NABTO_DEVICE_COAP_CONTENT_FORMAT_TEXT_PLAIN_UTF8);
                 nabto_device_coap_response_set_payload(helloRequest, "hello", strlen("hello")+1);
                 nabto_device_coap_response_ready(helloRequest);
                 printf("Responded to hello CoAP request\n");
                 nabto_device_coap_request_free(helloRequest);
                 start_hello_listen();
                 return;
             }
         }
         // We did not find the fingerprint in our whitelist. Reject
         // the COAP request.
         nabto_device_coap_error_response(helloRequest, 403, "Access Denied");
         printf("CoAP hello request from unpaired client\n");
         nabto_device_coap_request_free(helloRequest);
         start_hello_listen();
         return;
    } else {
        printf("An error occurred when handling hello CoAP request, ec=%d\n", ec);
    }
}