Skip to main content

Implement Extra Agent Security

How To Provide Additional Protection For Agents That Host APIs

In its simplest form, communication between an end-user and their imp-enabled device involves a tablet or smartphone app talking to the device’s agent by way of a REST API that you have designed and implemented in your agent code using the standard HTTP request handler callback and recommended methodology.

This approach provides a number of security benefits. Firstly, the agent’s URL is provided to the mobile app when the end-user first configures their device using BlinkUp™. It is unique, randomly generated and obsolete URLs are never re-used.

Secondly, the operations provided by your API are known only to you and those of your team members who have worked on the mobile app and the agent code. This is security by obscurity.

Finally, all communication between the app and device agents is passed via secure HTTPS with full encryption.

This level of security is sufficient for most applications. However, some developers and manufacturers may demand further levels of security. Although no part of the system explicitly makes the full agent URL public, the URL should not be regarded as secret.

A simple way to guard against this contingency is to add a further shared secret, which can be used to authorize or reject attempts to access your agent’s API even if the request is made to a valid agent URL. This also provides a rudimentary check that the system has been compromised in some way: any agent can notify you if it has received an access attempt it was unable to authorize. In such circumstances, the shared secret can be reset and end-users asked to re-configure their devices using BlinkUp to gain new agent URLs.

Listed below are five methods for achieving this basic shared-secret approach to further securing agent-hosted APIs.

Method One

The first of these is the recommended method, but is more complex than the later examples. When a device is configured in the field by an end-user, the mobile app use to perform the configuration generates a new UUID using native mobile OS APIs. This secret is then sent to the agent, which stores it for future use. From this point on, when a command is sent from app to agent, possibly via your server, it is accompanied by an HMAC (Hash-based Message Authentication Code) — the result of applying a standard hash function to the shared UUID.

Upon receipt of an incoming API call, the agent checks that, first, the request contains an HMAC and, second, that it matches the hash generated by hashing the agent’s own copy of the shared secret. If it does match, the request is considered authentic and the command contained in the request can be actioned. The comparison is made using the imp API method crypto.equals().

If the request contains no HMAC or its value does not match the hashed local shared key, the request is rejected.

// Assumes a random UUID has been generated as a hash key at first BlinkUp and shared
// between app, agent and your server as required. In the code below, the agent stores
// this value in the variable 'sharedKey'; the agent code that sets 'sharedKey' is not
// included here.
//
// iOS: NSString *sharedKey = [[NSUUID UUID] UUIDString];
// Android: String sharedKey = UUID.randomUUID().toString();
// Messages issued by app (or via server) as ?sig=<SIGNATURE>&command=<COMMAND STRING>
// sig value is transmitted in Base64 form
function hmac512Auth(request, response) {
// Check the request for the signature
if !("sig" in request.query) {
// No signature in query, so reject request and bail
response.send(401, "Access denied");
} else {
if ("command" in request.query) {
local hmac = http.base64decode(request.query.sig);
local hash = http.hash.hmacsha512(request.query.command, sharedKey);
if (crypto.equals(hmac, hash)) {
// The credentials are correct...
response.send(200, "Access granted");
// ...so allow the agent to process the request
processAuthorizedRequest(request.query.command, response);
} else {
// Signatures do not match, so reject request and bail
response.send(401, "Access denied");
}
}
}
}
// Assign a callback handler for incoming HTTP requests
http.onrequest(hmac512Auth);
view raw hmac.agent.nut hosted with ❤ by GitHub

Method Two

The second example, like those that follow it, is considered less secure because the secret is included with each message sent to the agent, and is baked into each and every agent based on your source code. The quid pro quo is that it is easier to implement: no code is required to persist the secret across agent restarts, for instance.

The first of these examples provides the least visible technique as the shared secret, an API key, is delivered ‘out of band’ in the HTTP request headers. This is the recommended approach

// Assumes a random UUID has been generated as string at first BlinkUp and shared
// between app, agent and your server as required. In the code below, the agent stores
// this value in the variable 'API_KEY'; the agent code that sets 'API_KEY' is not
// included here.
//
// iOS: NSString *API_KEY = [[NSUUID UUID] UUIDString];
// Android: String API_KEY = UUID.randomUUID().toString();
// This function checks the API key is in the header "X-API-Key"
// eg. { "X-API-Key" : "11834b91-9194-405b-b29f-7dc423bc043b" }
function simpleAuthOne(request, response) {
// Check the API key of every request
if (!("x-api-key" in request.headers) || request.headers["x-api-key"] != API_KEY) {
// Either there is no X-API-Key header, or the passed API key is wrong
response.send(401, "Access denied");
// Inform your server that this agent URL has been compromised
// Assumes you have an API in place to record compromised agent URLs
local warning = http.post("https://your.server.com/", {}, http.agenturl);
local resp = warning.sendsync();
// Take no further action with this request
return;
}
// The credentials are correct...
response.send(200, "Access granted");
// ...so allow the agent to process the request
processAuthorizedRequest(request, response);
}
// Assign a callback handler for incoming HTTP requests
http.onrequest(simpleAuthOne);

Method Three

It is not always possible for the mobile app to modify the HTTP headers included with the request made to the agent API, so the next two methods are intended to provide alternative options for this circumstance. The first of these requires that an extra query parameter be added to the end of every request containing the API key.

// Assumes a random UUID has been generated as string at first BlinkUp and shared
// between app, agent and your server as required. In the code below, the agent stores
// this value in the variable 'API_KEY'; the agent code that sets 'API_KEY' is not
// included here.
//
// iOS: NSString *API_KEY = [[NSUUID UUID] UUIDString];
// Android: String API_KEY = UUID.randomUUID().toString();
// This function checks the API key is in the query parameter "apikey"
// eg. https://agent.electricimp.com/<Agent ID>/?apikey=11834b91-9194-405b-b29f-7dc423bc043b
function simpleAuthTwo(request, response) {
// Check the API key of every request
if (!("apikey" in request.query) || request.query["apikey"] != API_KEY) {
// The query lacks the apiKey parameter or it contains an incorrect value
response.send(401, "Access denied");
// Inform your server that this agent URL has been compromised
// Assumes you have an API in place to record compromised agent URLs
local warning = http.post("https://your.server.com/", {}, http.agenturl);
local resp = warning.sendsync();
// Take no further action with this request
return;
}
// The credentials are correct...
response.send(200, "Access granted");
// ...so allow the agent to process the request
processAuthorizedRequest(request, response);
}
// Assign a callback handler for incoming HTTP requests
http.onrequest(simpleAuthTwo);

Method Four

The second method (see above) embeds the API key into the URL path itself.

// Assumes a random UUID has been generated as string at first BlinkUp and shared
// between app, agent and your server as required. In the code below, the agent stores
// this value in the variable 'API_KEY'; the agent code that sets 'API_KEY' is not
// included here.
//
// iOS: NSString *API_KEY = [[NSUUID UUID] UUIDString];
// Android: String API_KEY = UUID.randomUUID().toString();
// This function checks the API key is the first item in the URL path
// eg https://agent.electricimp.com/<Agent ID>/11834b91-9194-405b-b29f-7dc423bc043b/doSomething
function simpleAuthThree(request, response) {
// Check the API key of every request
local path = split(request.path, "/");
if (path.len() == 0 || path[0] != API_KEY) {
// The path doesn't contain the API key, or the value is incorrect
response.send(401, "Access denied");
// Inform your server that this agent URL has been compromised
// Assumes you have an API in place to record compromised agent URLs
local warning = http.post("https://your.server.com", {}, http.agenturl);
local resp = warning.sendsync();
// Take no further action with this request
return;
}
// The credentials are correct...
response.send(200, "Access granted");
// ...so allow the agent to process the request
processAuthorizedRequest(request, response);
}
// Assign a callback handler for incoming HTTP requests
http.onrequest(simpleAuthThree);

Method Five

The final example uses Electric Imp’s Rocky framework, implemented as a library that you can freely incorporate into your own agent code. Rocky helps you design powerful REST APIs in agents and includes built-in functions that simplify the process of responding to HTTP requests.

// Assumes a random UUID has been generated as string at first BlinkUp and shared
// between app, agent and your server as required. In the code below, the agent stores
// this value in the variable 'API_KEY'; the agent code that sets 'API_KEY' is not
// included here.
//
// iOS: NSString *API_KEY = [[NSUUID UUID] UUIDString];
// Android: String API_KEY = UUID.randomUUID().toString();
// This approach requires the Rocky library.
// Check https://developer.electricimp.com/libraries/utilities/rocky
// for the latest version
#require "rocky.class.nut:2.0.2"
// Instantiate a Rocky object
app <- Rocky();
// This expects a standard HTTP basic authorization header
// eg. Authorization: Basic YXBpa2V5OjExODM0YjkxLTkxOTQtNDA1Yi1iMjlmLTdkYzQyM2JjMDQzYg==
app.authorize(function(context) {
return context.auth.pass == API_KEY;
})
// Handle any verb used against the root (/) url path.
// If it gets here, it has already been authenticated by Rocky.
app.on("*", "/", function(context) {
context.send(200, "Access granted");
});
view raw rocky.agent.nut hosted with ❤ by GitHub

All of these methods apply equally to architectures in which the mobile app doesn’t communicate directly with each agent, but talks to them via your own server infrastructure. Such an approach allows you to integrate app-agent communications into your user account management system and, for instance, preserve product state data — handy if a given end-user is likely to run multiple instances of your app in order to control a one product or a set of products.

All of the following techniques are limited to API calls to agents from servers or mobile apps and are not effective security measures for use by a web browser. Different techniques, such as user account logins and session authentication, should be considered for web-based interfaces. Alternatives such as third-party OAuth authentication are particularly effective if your userbase overlaps with those of services such as Google, Facebook or Twitter.