Pairing and Access Control: Part 2 – Client Implementation

This is the second blog post of the two posts about access control in Nabto. The first post introduced access control and described how to use the uNabto framework and the access control list (ACL) module to enforce access control and pair users. Recall, the overall flow is as follows:

  1. user creates an RSA keypair on a client device and associates it with a name, e.g. “Joe’s iPhone 8”
  2. in a trusted setting (similar to WPS for wifi configuration), the target IoT device is put into “open for local pairing” mode, e.g. at the first boot
  3. the user connects to the device on the local network while the device is in pairing mode
  4. the user’s public key is registered as the owner on the device through the device’s access control list

In this post we will see how to implement things in the client, ie the following remaining steps:

  1. in the client app, using the Nabto Client SDK:
    1. create RSA keypair and open session
    2. discover local devices
    3. invoke pairing functions on the desired local device through Nabto RPC

Step 2.1: Create Keypair and Open Session

Creating the keypair is done through the Client SDK function nabtoCreateSelfSignedProfile. Details are provided in section 5.5.2 in TEN025 Nabto Client SDK. It takes two arguments, an id and a password – the id is used for later referencing this keypair. It is also denoted “name” in some examples and documents.

When later starting a communication session using nabtoOpenSession where you want to use this specific keypair, you reference it through the id/name used when creating the keypair. Details about nabtoOpenSession can be found in section 5.1 in TEN025 Nabto Client SDK.

It looks as follows using the most common client platform SDKs:

Plain C (all platforms)

Using the Plain C SDK (the Nabto Libs bundle available from Nabto Downloads):

const char* name = "My macbook";
const char* password = "dummy";

nabto_status_t status = nabtoCreateSelfSignedProfile(name, password);
if (status != NABTO_OK) {
  die("nabtoCreateSelfSignedProfile failed with status %d", status)
}

nabto_handle_t session;
status = nabtoOpenSession(&session, name, password)
if (status != NABTO_OK) {
  die("nabtoOpenSession failed with status %d", status)
}

// use session

iOS

Using the Nabto iOS Client:

let client = { return NabtoClient.instance() as! NabtoClient }()

let name = "My iPhone"
let password = "dummy"

var status = client.nabtoCreateSelfSignedProfile(name, withPassword: password)
if (status != .NCS_OK) {
   die("nabtoCreateSelfSignedProfile failed with status %d", status)
}

status = client.nabtoOpenSession(name, withPassword: password)
if (status != .NCS_OK) {
   die("nabtoOpenSession failed with status %d", status)
}

// use session

Android

Using the Nabto Android Client:

NabtoApi api = new NabtoApi(new NabtoAndroidAssetManager(this));

String name = "My Smartsung";
String password = "dummy";

NabtoStatus status = api.createSelfSignedProfile(name, password);
if (status != NabtoStatus.OK) {
   die("createSelfSignedProfile failed with status " + status);
}

Session session = api.openSession(name, password);
if (session.getStatus() != NabtoStatus.OK) {
   die("openSessionFailed with status " + session.getStatus());
}                      

// use session

Step 2.2: Discover Local Devices

After creating the keypair, local devices to attempt pairing with can be discovered with the Client SDK functon nabtoGetLocalDevices.

Plain C (all platforms)

Using the Plain C SDK (the Nabto Libs bundle available from Nabto Downloads):

char** devices;
int devicesLength;
nabto_status_t status = nabtoGetLocalDevices(&devices, &devicesLength);

if (status == NABTO_OK) {
   // use list of device for e.g. presenting a list to the user for picking a device to pair with
} else {
   die("nabtoGetLocalDevices failed with status %d", status);
}

iOS

Using the Nabto iOS Client:

let client = { return NabtoClient.instance() as! NabtoClient }()

if let list = client.nabtoGetLocalDevices() as? [String] {
  // use list of device for e.g. presenting a list to the user for picking a device to pair with
} else {
  die("nabtoGetLocalDevices failed with status %d", status)
}

Android

Using the Nabto Android Client:

Collection<String> deviceNames = api.getLocalDevices();
// use list of device for e.g. presenting a list to the user for picking a device to pair with

Step 2.3: Invoke Pairing Functions

Once the device has been discovered, you can transfer the client’s fingerprint to the device for pairing through Nabto RPC: When establishing a local Nabto connection, the Nabto framework automatically transfers the client’s fingerprint to the device. When you invoke a Nabto RPC function on this connection, you can then invoke functionality to add this fingerprint to the device’s ACL.

This manipulation of the ACL happens at the application level, ie something you control completely. This is also the case for looking up in the ACL in response to callbacks from the framework to authorize requests. Nabto just provides an example ACL database and common RPC functions for manipulating the ACL that you can include in your application for simplicity.

As local Nabto RPC invocation happens in cleartext per default, the communication must take place in a “trusted setting”. For instance you can allow it to happen only a few minutes after a “pairing button” has been pressed, very similar to WiFi WPS based provisioning.

The RPC interface definition for standard functions for manipulating the ACL database are available in unabto_queries-fp-acl-snippet.xml. The standard RPC function for the pairing step is pair_with_device.json (opcode 11010):

 <!-- pair client (user) with this device -->
  <query name="pair_with_device.json" id="11010">
    <request>
      <parameter name="name" type="raw"/>
    </request>
    <response format="json">
      <parameter name="status" type="uint8"/>
      <parameter name="fingerprint" type="raw" representation="hex"/>
      <parameter name="name" type="raw"/>
      <parameter name="permissions" type="uint32"/>
    </response>
  </query>

The following sections show how to invoke this function from the Nabto Client. See step 1.1 in the previous post for the uNabto device implementation of this function. Note that the name parameter must be URL encoded for all platforms. The parameter is used on the device to associate each public key fingerprint in the ACL with a descriptive name.

Additional notes on security of this step: Optionally, the local communication can be encrypted, but it complicates matters. It may the subject of a separate blog post – but if curious, look at the nabtoSetLocalConnectionPsk functions in the Nabto Client SDK. For further information about the communication protocol and why the fingerprint is not just extracted on the device as part of a regular RSA key exchange, see the “How it Works” intro section in the first post

Plain C (all platforms)

Using the Plain C SDK (the Nabto Libs bundle available from Nabto Downloads):

// somehow read the unabto_queries-fp-acl-snippet.xml file into a string
const char* rpcInterface = "<unabto_queries xmlns:xsi="http://www..."; 

char* json;
// session opened in step 2.1
nabto_status_t status = nabtoRpcSetDefaultInterface(session, rpcInterface, &json);
if (status == NABTO_FAILED_WITH_JSON_MESSAGE) {
  die(json); // json must be freed
} else if (status != NABTO_OK) {
  die("nabtoRpcInvoke failed with status %d");
}

// TODO: replace simplified URL with constructed string that uses the discovered device id from step 1.2
nabtoRpcInvoke(session, "nabto://device-id.domain.net/pair_with_device.json?name=My%20Macbook", &json)
if (status == NABTO_FAILED_WITH_JSON_MESSAGE) {
  die(json); // json must be freed
} else if (status != NABTO_OK) {
  die("nabtoRpcInvoke failed with status %d");
}

// show confirmation to user, json must be freed

iOS

Using the Nabto iOS Client:

let client = { return NabtoClient.instance() as! NabtoClient }()

// somehow read the unabto_queries-fp-acl-snippet.xml file into a string
let rpcInterface: String = "<unabto_queries xmlns:xsi="http://www..."; 

var buffer: UnsafeMutablePointer<Int8>? = nil
var status = client.nabtoRpcSetDefaultInterface(rpcInterface, withErrorMessage: &buffer)
if (status == .NCS_FAILED_WITH_JSON_MESSAGE) {
  die(buffer); 
} else if (status != .NCS_OK) {
  die("nabtoRpcSetDefaultInterface failed with status %d", status);
}

// TODO: replace simplified URL with constructed string that uses the discovered device id from step 2.1
status = client.nabtoRpcInvoke("nabto://device-id.domain.net/pair_with_device.json?name=My%20iPhone", withResultBuffer: &buffer);
if (status == .NCS_FAILED_WITH_JSON_MESSAGE) {
  die(buffer); 
} else if (status != .NCS_OK) {
  die("nabtoRpcInvoke failed with status %d", status);
}

// show confirmation to user

Android

Using the Nabto Android Client:

NabtoApi api = new NabtoApi(new NabtoAndroidAssetManager(this));

// somehow read the unabto_queries-fp-acl-snippet.xml file into a string
String rpcInterface = "<unabto_queries xmlns:xsi="http://www..."; 

// session opened in step 2.1
api.rpcSetDefaultInterface(rpcInterface, session)
RpcResult result = api.rpcSetDefaultInterface(INTERFACE_XML, session);
if (result.getStatus() != NabtoStatus.OK) {
  die("rpcSetDefaultInterface failed with status " + result.getStatus());
}

// TODO: replace simplified URL with constructed string that uses the discovered device id from step 2.2
result = api.rpcInvoke("nabto://device-id.domain.net/pair_with_device.json?name=My%20Smartsung", session);
if (result.getStatus() == NabtoStatus.OK) {
  // show confirmation to user
} else {
  die("rpcInvoke failed with status + result.getStatus());
}

Leave a Reply

Your email address will not be published.