Distributed Publish & Subscribe for IoT
Securing the communications


Enabling network layer security

Network layer security can be used to secure unicast communications between two nodes (a single hop). This includes the subscription, acknowledgement, and subscription acknowledgement messages. When the two nodes are linked , it also includes publications. Multicast publications are not secured. One of the end-to-end mechanisms described below must be used to secure multicast publications.

The first step to enabling network layer security is to build with a transport that supports it.

$ scons transport=dtls

The DTLS transport supports two mechanisms for securing the connection: pre-shared keys, and certificates.

DTLS with pre-shared keys

#define BYTE_STR(s) { (const uint8_t*)s, sizeof(s) - 1 }
static const DPS_Key PSK = { DPS_KEY_SYMMETRIC, { .symmetric = BYTE_STR("1234") } };
static const DPS_KeyId PSK_ID = BYTE_STR("Tutorial Network PSK");

To use pre-shared keys (PSKs), we'll need to agree on a PSK and its identifier. BYTE_STR is a convenience macro used in the tutorials for using strings anywhere a buffer and its length is expected in the APIs.

const char *separators = "/.";
DPS_KeyStore* keyStore = DPS_CreateKeyStore(PskAndIdHandler, PskHandler, NULL, NULL);
const DPS_KeyId* keyId = NULL;
DPS_Node* node = DPS_CreateNode(separators, keyStore, keyId);
if (!node) {
goto Exit;
}

All of the security mechanisms require that the node be created with a key store. The DTLS PSK mechanism requires that a DPS_KeyAndIdHandler and DPS_KeyHandler be provided.

static DPS_Status PskAndIdHandler(DPS_KeyStoreRequest* request)
{
return DPS_SetKeyAndId(request, &PSK, &PSK_ID);
}

The key and identifier handler simply uses DPS_SetKeyAndId() to return the PSK and identifier we agreed on earlier.

static int IsSameKeyId(const DPS_KeyId* a, const DPS_KeyId* b)
{
return (a->len == b->len) && !memcmp(a->id, b->id, a->len);
}
static DPS_Status PskHandler(DPS_KeyStoreRequest* request, const DPS_KeyId* keyId)
{
if (IsSameKeyId(keyId, &PSK_ID)) {
return DPS_SetKey(request, &PSK);
}
}

The key handler is used for PSKs as well as other keys, so it must compare the incoming keyId against the PSK identifier and either return the PSK using DPS_SetKey() or return DPS_ERR_MISSING.

See also
DPS_CreateMemoryKeyStore(), DPS_SetNetworkKey()

DTLS with certificates

extern const char* CA_CERTIFICATE;
typedef struct {
DPS_KeyId keyId;
DPS_Key key;
} Certificate;
extern const Certificate CERTIFICATES[];
#include "tutorial_certs.c"

To use certificates, we'll need a certificate for each node and the certificates of the authorities that issued them. DPS supports Elliptic Curve Cryptography (ECC) certificates. The certificates above are omitted due to their size.

const char *separators = "/.";
DPS_KeyStore* keyStore = DPS_CreateKeyStore(NULL, CertificateHandler,
NULL, CertificateAuthoritiesHandler);
const DPS_KeyId* keyId = nodeId;
DPS_Node* node = DPS_CreateNode(separators, keyStore, keyId);
if (!node) {
goto Exit;
}

Again, all of the security mechanisms require that the node be created with a key store. The DTLS certificate mechanism requires that a DPS_KeyHandler and DPS_CAHandler be provided.

The certificate mechanism also requires that the keyId parameter be provided. This is the key identifier of the public key (_DPS_KeyCert::cert), private key (_DPS_KeyCert::privateKey), and optional password (_DPS_KeyCert::password) of the created node. This keyId value will be provided to the DPS_KeyHandler when the node's own certificate is requested.

static DPS_Status CertificateAuthoritiesHandler(DPS_KeyStoreRequest* request)
{
return DPS_SetCA(request, CA_CERTIFICATE);
}

The certificate authorities handler simply uses DPS_SetCA() to return the certificate authorities we trust. To include more than one certificate as shown in this example, concatenate the certificates together.

static DPS_Status CertificateHandler(DPS_KeyStoreRequest* request, const DPS_KeyId* keyId)
{
const Certificate* certificate;
for (certificate = CERTIFICATES; certificate->keyId.id; ++certificate) {
if (IsSameKeyId(keyId, &certificate->keyId)) {
return DPS_SetKey(request, &certificate->key);
}
}
}

The key handler is used for certificates as well as other keys, so it must compare the incoming keyId against the certificate identifier and either return the certificate using DPS_SetKey() or return DPS_ERR_MISSING.

Only the certificate with the key identifier provided to DPS_CreateNode() needs to include the private key (_DPS_KeyCert::privateKey) and optional password (_DPS_KeyCert::password). For all other certificates, the public key (_DPS_KeyCert::cert) is sufficient.

See also
DPS_SetTrustedCA(), DPS_SetCertificate()

Protecting the payload

While network layer security allows us to secure the communications of a single hop, encrypting the payload allows us to secure the payload across multiple hops. The payload can only be decrypted by the receiving node.

Encrypting a payload uses two keys: the content encryption key that encrypts the payload and the key encryption key that encrypts the content encryption key. This indirection allows a single instance of the payload to be encrypted for multiple recipients.

The content encryption key is always an ephemeral AES key. The key encryption key may be a symmetric or asymmetric key.

Encrypting with a symmetric key

static const uint8_t AES_256_KEY[DPS_AES_256_KEY_LEN] = {
0x37, 0xf9, 0x6f, 0x85, 0x72, 0x0c, 0xa0, 0x1b, 0x85, 0x51, 0x50, 0x45, 0x22, 0xa7, 0x30, 0x55,
0x4f, 0x05, 0x9c, 0xc4, 0xf4, 0xbb, 0xa6, 0x37, 0xfc, 0x0a, 0x90, 0x53, 0x64, 0xe1, 0xb7, 0x9c
};
static const DPS_Key SYMMETRIC_KEY = { DPS_KEY_SYMMETRIC, { .symmetric = { AES_256_KEY, DPS_AES_256_KEY_LEN } } };
static const DPS_KeyId SYMMETRIC_KEY_ID = BYTE_STR("Tutorial Symmetric Key");

To use a symmetric key encryption key, we'll need to agree on its value and identifier.

const char *separators = "/.";
DPS_KeyStore* keyStore = DPS_CreateKeyStore(NULL, SymmetricKeyHandler, EphemeralSymmetricKeyHandler, NULL);
const DPS_KeyId* keyId = NULL;
DPS_Node* node = DPS_CreateNode(separators, keyStore, keyId);
if (!node) {
goto Exit;
}

Again, all of the security mechanisms require that the node be created with a key store. The payload encryption mechanism requires that a DPS_KeyHandler and DPS_EphemeralKeyHandler be provided.

static DPS_Status SymmetricKeyHandler(DPS_KeyStoreRequest* request, const DPS_KeyId* keyId)
{
if (IsSameKeyId(keyId, &SYMMETRIC_KEY_ID)) {
return DPS_SetKey(request, &SYMMETRIC_KEY);
}
}

The key handler is used for symmetric keys as well as other keys, so it must compare the incoming keyId against the symmetric key encryption key identifier and either return the key using DPS_SetKey() or return DPS_ERR_MISSING.

static DPS_Status EphemeralSymmetricKeyHandler(DPS_KeyStoreRequest* request, const DPS_Key* key)
{
if (key->type == DPS_KEY_SYMMETRIC) {
uint8_t key[DPS_AES_256_KEY_LEN];
GenerateRandomKey(key);
DPS_Key ephemeralKey = { DPS_KEY_SYMMETRIC, { .symmetric = { key, DPS_AES_256_KEY_LEN } } };
return DPS_SetKey(request, &ephemeralKey);
}
}

The ephemeral key handler is used for symmetric keys as well as other keys, so it must examine the incoming key type to determine what type of ephemeral key to return. It creates a random key of the requested type and returns it using DPS_SetKey(). If it cannot do that, it must return DPS_ERR_MISSING.

ret = DPS_PublicationAddSubId(pub, &SYMMETRIC_KEY_ID);
if (ret != DPS_OK) {
goto Exit;
}

Lastly, we add the key encryption key identifier to the publication.

This may be called multiple times with different key identifiers for a single publication. This allows an application to, for example, associate topics with key identifiers and initialize and encrypt a publication with multiple topics.

Acknowledgement payloads will be protected using the same key encryption key identifier the recipient used to decrypt the publication.

See also
DPS_SetContentKey()

Encrypting with an asymmetric key

The steps necessary to use an asymmetric key encryption key are very similar to the steps needed to use a symmetric key encryption key, described above. Only the differences are highlighted below.

Note
Acknowledgement payloads will be encrypted using the same asymmetric key identifier the recipient used to decrypt the publication. As this is an ephemeral key, the encryption will fail. For this reason it is recommended to use this mechanism only with authenticated senders which prevents this failure.
extern const DPS_Key ASYMMETRIC_KEY;
static const DPS_KeyId ASYMMETRIC_KEY_ID = BYTE_STR("Tutorial Asymmetric Key");

To use asymmetric key encryption keys, the publisher will need the public key (_DPS_KeyCert::cert) of each recipient and each recipient will also need the private key (_DPS_KeyCert::privateKey) and optional password (_DPS_KeyCert::password). The term recipient is used here instead of subscriber since a single recipient key may be used by multiple subscribers.

For this example we are using only one recipient. DPS supports Elliptic Curve Cryptography (ECC) certificates. The certificate above is elided due to its size.

static DPS_Status EphemeralAsymmetricKeyHandler(DPS_KeyStoreRequest* request, const DPS_Key* key)
{
if (key->type == DPS_KEY_SYMMETRIC) {
uint8_t key[DPS_AES_256_KEY_LEN];
GenerateRandomKey(key);
DPS_Key ephemeralKey = { DPS_KEY_SYMMETRIC, { .symmetric = { key, DPS_AES_256_KEY_LEN } } };
return DPS_SetKey(request, &ephemeralKey);
} else if (key->type == DPS_KEY_EC) {
uint8_t x[66], y[66], d[66];
GenerateEphemeralKey(key->ec.curve, x, y, d);
DPS_Key ephemeralKey = { DPS_KEY_EC, { .ec = { key->ec.curve, x, y, d } } };
return DPS_SetKey(request, &ephemeralKey);
}
}

The ephemeral key handler is used for both symmetric and asymmetric keys, so it must examine the incoming key type to determine what type of ephemeral key to return. For DPS_KEY_EC types, the ECC curve must also be examined.

The ephemeral ECC key requested here is the ephemeral sender key. The actual key encryption key is derived from the sender and recipient ECC keys.

In either case, the key handler creates a random key of the requested type (and curve) and returns it using DPS_SetKey(). If it cannot do that, it must return DPS_ERR_MISSING.

Authenticating the message sender

const char *separators = "/.";
DPS_KeyStore* keyStore = DPS_CreateKeyStore(NULL, KeyHandler, EphemeralKeyHandler, NULL);
const DPS_KeyId* keyId = nodeId;
DPS_Node* node = DPS_CreateNode(separators, keyStore, keyId);
if (!node) {
goto Exit;
}

Authenticating the sender requires that the keyId parameter be provided. This is the key identifier of the public key (_DPS_KeyCert::cert), private key (_DPS_KeyCert::privateKey), and optional password (_DPS_KeyCert::password) of the created node. This keyId value will be provided to the DPS_KeyHandler when the node's own certificate is requested.

static DPS_Status KeyHandler(DPS_KeyStoreRequest* request, const DPS_KeyId* keyId)
{
const Certificate* certificate;
for (certificate = CERTIFICATES; certificate->keyId.id; ++certificate) {
if (IsSameKeyId(keyId, &certificate->keyId)) {
return DPS_SetKey(request, &certificate->key);
}
}
if (IsSameKeyId(keyId, &SYMMETRIC_KEY_ID)) {
return DPS_SetKey(request, &SYMMETRIC_KEY);
}
if (IsSameKeyId(keyId, &ASYMMETRIC_KEY_ID)) {
return DPS_SetKey(request, &ASYMMETRIC_KEY);
}
}

The key handler implementation above supports all the mechanisms described so far.

static DPS_Status EphemeralKeyHandler(DPS_KeyStoreRequest* request, const DPS_Key* key)
{
if (key->type == DPS_KEY_SYMMETRIC) {
uint8_t key[DPS_AES_256_KEY_LEN];
GenerateRandomKey(key);
DPS_Key ephemeralKey = { DPS_KEY_SYMMETRIC, { .symmetric = { key, DPS_AES_256_KEY_LEN } } };
return DPS_SetKey(request, &ephemeralKey);
} else if (key->type == DPS_KEY_EC) {
uint8_t x[66], y[66], d[66];
GenerateEphemeralKey(key->ec.curve, x, y, d);
DPS_Key ephemeralKey = { DPS_KEY_EC, { .ec = { key->ec.curve, x, y, d } } };
return DPS_SetKey(request, &ephemeralKey);
}
}

The ephemeral key handler implementation above supports all the mechanisms described so far.

DPS_PRINT("sender=%.*s\n", keyId->len, keyId->id);
const DPS_KeyId* keyId = DPS_AckGetSenderKeyId(pub);
DPS_PRINT("sender=%.*s\n", keyId->len, keyId->id);

In the publication or acknowledgement handler, the authenticated key identifier can be retrieved with DPS_PublicationGetSenderKeyId() or DPS_AckGetSenderKeyId().

Adding access control

Adding access control is a matter of combining the existing mechanisms to enforce any policies the application needs. A simple example is shown below.

Defining a policy

typedef struct {
const char *topic;
const DPS_KeyId keyId;
enum {
PUB = (1<<0),
SUB = (1<<1),
ACK = (1<<2)
} bits;
} AccessControlEntry;
static const AccessControlEntry AccessControlList[] = {
{ "a/b/c/d", BYTE_STR("alice"), PUB },
{ "a/b/c/d", BYTE_STR("bob"), SUB | ACK },
{ "a/b/c/d", BYTE_STR("trudy"), SUB },
{ NULL, { NULL, 0 }, 0 }
};
static int IsAllowed(const DPS_KeyId* keyId, int bits, const DPS_Publication* pub)
{
const AccessControlEntry* ace;
for (ace = AccessControlList; ace->keyId.id; ++ace) {
if (IsSameKeyId(keyId, &ace->keyId) && (bits & ace->bits)) {
size_t i;
for (i = 0; i < DPS_PublicationGetNumTopics(pub); ++i) {
if (!strcmp(ace->topic, DPS_PublicationGetTopic(pub, i))) {
return DPS_TRUE;
}
}
}
}
return DPS_FALSE;
}

In the above we will allow alice to publish to topic a/b/c/d, bob to subscribe and acknowledge to topic a/b/c/d, and trudy to subscribe to topic a/b/c/d.

IsAllowed implements the policy by searching through the access control list for matching key identifiers and access bits and uses DPS_PublicationGetNumTopics() and DPS_PublicationGetTopic() to check the topic.

Implementing subscription control

ret = DPS_InitPublication(pub, topics, numTopics, noWildCard, NULL, AcknowledgementHandler);
if (ret != DPS_OK) {
goto Exit;
}
const AccessControlEntry* ace;
for (ace = AccessControlList; ace->keyId.id; ++ace) {
if (IsAllowed(&ace->keyId, SUB, pub)) {
ret = DPS_PublicationAddSubId(pub, &ace->keyId);
if (ret != DPS_OK) {
goto Exit;
}
}
}

After initializing the publication, we use DPS_PublicationAddSubId() to add only the key identifiers of allowed subscribers. Any other subscribers will be unable to decrypt the publication.

Implementing publication control

if (!IsAllowed(keyId, PUB, pub)) {
DPS_PRINT("Rejecting publication\n");
return;
}
/* Proceed with application handling of publication... */

In the publication handler before any application-specific handling is done, we use DPS_PublicationGetSenderKeyId() to first check if the sender is allowed to publish to the topic. If not, we reject the publication.

Implementing acknowledgement control

const DPS_KeyId* keyId = DPS_AckGetSenderKeyId(pub);
if (!IsAllowed(keyId, ACK, pub)) {
DPS_ERRPRINT("Rejecting acknowledgement\n");
return;
}
/* Proceed with application handling of acknowledgement... */

In the acknowledgement handler, before any application-specific handling is done, we use DPS_AckGetSenderKeyId() to first check if the sender is allowed to acknowledge the topic. If not, we reject the acknowledgement.