4.6. DTLS connection using a trust store
DTLS connection using certificates uses the simplest server certificate verification mode. This tutorial uses a stricter mode that combines two checks:
the server certificate configured in the Security Object must match the leaf certificate presented by the server, as in the previous tutorial,
the certificate chain presented by the server must also be verifiable using a CA certificate provided in the trust store.
Note
Code related to this tutorial can be found under examples/tutorial/AT-TrustStore in the Anjay Lite source directory.
4.6.1. Prerequisites & build flags
The example uses certificate-based DTLS and external crypto storage. External crypto storage allows the Security Object credentials to be represented as external identities. The TLS integration layer then resolves those identities into the actual certificate or key material when it needs them. For details, see Crypto storage.
The required build-time configuration can be found in
examples/tutorial/AT-TrustStore/CMakeLists.txt:
set(ANJ_WITH_CERTIFICATES ON)
set(ANJ_SEC_OBJ_MAX_PUBLIC_KEY_OR_IDENTITY_SIZE 1500)
set(ANJ_SEC_OBJ_MAX_SECRET_KEY_SIZE 1500)
set(ANJ_SEC_OBJ_MAX_SERVER_PUBLIC_KEY_SIZE 1500)
set(ANJ_WITH_EXTERNAL_CRYPTO_STORAGE ON)
set(ANJ_WITH_CRYPTO_STORAGE_DEFAULT ON)
The flags shown above have the following meaning:
ANJ_WITH_CERTIFICATES: Enable support for certificates in Anjay Lite.ANJ_SEC_OBJ_MAX_PUBLIC_KEY_OR_IDENTITY_SIZE: Configures the size of the buffer that holds Public Key or Identity.ANJ_SEC_OBJ_MAX_SECRET_KEY_SIZE: Configures the size of the buffer that holds Secret Key.ANJ_SEC_OBJ_MAX_SERVER_PUBLIC_KEY_SIZE: Configures the size of the buffer that holds Server Public Key.ANJ_WITH_EXTERNAL_CRYPTO_STORAGE: Enables the external crypto storage API.ANJ_WITH_CRYPTO_STORAGE_DEFAULT: Enables the default POSIX/file-based implementation of that API.
4.6.2. External credential identities
The example stores the file names in anj_crypto_security_info_t structures
as external identities:
static void security_info_from_path(anj_crypto_security_info_t *out_info,
const char *path) {
memset(out_info, 0, sizeof(*out_info));
out_info->source = ANJ_CRYPTO_DATA_SOURCE_EXTERNAL;
out_info->info.external.identity = path;
}
With the default POSIX crypto storage backend, these identities are plain file paths resolved when the TLS backend needs the certificate or key material. For a more complete explanation of this mechanism, see Crypto storage.
4.6.3. Configuring the Security Object
The Security Object still contains the credentials specific to the LwM2M Server account. In this example these are the client certificate, the matching private key and the server leaf certificate:
static int install_security_obj(anj_t *anj,
anj_dm_security_obj_t *security_obj) {
anj_crypto_security_info_t client_cert;
anj_crypto_security_info_t client_key;
anj_crypto_security_info_t server_cert;
static anj_net_certificate_usage_t certificate_usage =
ANJ_NET_CERTIFICATE_SERVICE_CERTIFICATE_CONSTRAINT;
security_info_from_path(&client_cert, CLIENT_CERT_PATH);
security_info_from_path(&client_key, CLIENT_KEY_PATH);
security_info_from_path(&server_cert, SERVER_LEAF_CERT_PATH);
anj_dm_security_instance_init_t security_inst = {
.ssid = 1,
.server_uri = "coaps://eu.iot.avsystem.cloud:5684",
.security_mode = ANJ_DM_SECURITY_CERTIFICATE,
.public_key_or_identity = client_cert,
.secret_key = client_key,
.server_public_key = server_cert,
.certificate_usage = &certificate_usage
};
anj_dm_security_obj_init(security_obj);
if (anj_dm_security_obj_add_instance(security_obj, &security_inst)
|| anj_dm_security_obj_install(anj, security_obj)) {
return -1;
}
// ...
return 0;
}
The important difference from the basic certificate tutorial is
certificate_usage. ANJ_NET_CERTIFICATE_SERVICE_CERTIFICATE_CONSTRAINT
means that the server certificate configured as server_public_key identifies
the expected service certificate, while the chain is validated using the global
trust store.
In other words, server_public_key still constrains the exact server leaf
certificate, while the trust store supplies the CA certificates used for chain
validation. This is why the example uses both leaf_cert.pem and
ca_cert.pem.
Note
For more information on certificate usage types see Certificate Usage.
4.6.4. Configuring the trust store
The trust store is configured in anj_configuration_t before
anj_core_init(). It is global for the client instance and contains CA
certificates used during certificate-chain validation:
int main(int argc, char *argv[]) {
// ...
anj_t anj;
anj_dm_device_obj_t device_obj;
anj_dm_server_obj_t server_obj;
anj_dm_security_obj_t security_obj;
anj_crypto_security_info_t trust_store_ca[1];
security_info_from_path(&trust_store_ca[0], TRUST_STORE_CA_PATH);
anj_net_trust_store_t trust_store = {
.ca_certs = trust_store_ca,
.ca_certs_count = 1
};
anj_configuration_t config = {
.endpoint_name = argv[1],
.trust_store = &trust_store
};
if (anj_core_init(&anj, &config)) {
log(L_ERROR, "Failed to initialize Anjay Lite");
return -1;
}
// ...
}
ca_certs points to an array of trust anchors. This tutorial uses a single
CA certificate, but additional CA certificates can be added by extending the
array and updating ca_certs_count.
4.6.5. Running the example
The example expects the following files to be present in the current working directory (i.e., the directory from which the application is launched):
client_cert.der- client certificate in DER format,client_key.der- corresponding private key in DER format,leaf_cert.pem- server leaf certificate in PEM format,ca_cert.pem- CA certificate in PEM format, used to verify the certificate chain sent by the server.
If these files are not found, the application will fail to start due to errors when attempting to load the credentials.
You can generate an EC private key and a self-signed certificate for running this
example using openssl:
# Generate EC private key using the prime256v1 curve
openssl ecparam -name prime256v1 -genkey -noout -outform der -out client_key.der
# Generate a self-signed certificate using that key
openssl req -x509 -new -key client_key.der -inform der \
-out client_cert.der -outform der -days 365 \
-subj "/CN=my-endpoint-name"
Note
When connecting a device to Coiote DM, the endpoint name passed to the
application (e.g., my-endpoint-name) must match the Common Name (CN)
used when generating the certificate. The Coiote DM server validates that
these values match.
The server certificate can be obtained directly from the server using
openssl s_client. The command below extracts the first certificate from the
server-provided certificate chain, i.e. the leaf certificate, into
leaf_cert.pem.
openssl s_client -dtls -connect eu.iot.avsystem.cloud:5684 -showcerts </dev/null 2>/dev/null \
| awk '
/-----BEGIN CERTIFICATE-----/ { cert++ }
cert == 1 { print }
/-----END CERTIFICATE-----/ && cert == 1 { exit }
' > leaf_cert.pem
To populate the trust store for this example, the last certificate from the
chain sent by the server during the DTLS handshake can be extracted and saved as
ca_cert.pem using the command below:
openssl s_client -dtls -connect eu.iot.avsystem.cloud:5684 -showcerts </dev/null 2>/dev/null \
| awk '
/-----BEGIN CERTIFICATE-----/ { cert = ""; in_cert = 1 }
in_cert { cert = cert $0 "\n" }
/-----END CERTIFICATE-----/ { in_cert = 0; last = cert }
END { printf "%s", last }
' > ca_cert.pem
Warning
For this tutorial, the last certificate from the server-provided chain is used as the trust store certificate. In a production environment, the trust store should be provisioned with CA certificates that are explicitly trusted by the device operator and expected to remain valid throughout the device lifetime. The certificate chain sent by the server may change, for example when server certificates are renewed or the server configuration is updated.
Because self-signed certificates are used, the LwM2M server trust store must be updated to include the device’s certificate before attempting to connect. Once the certificate has been uploaded to the server, you can run the example by executing the application with the endpoint name as an argument, for example:
./anjay_lite_at_trust_store my-endpoint-name
Make sure to run the command from the directory that contains the
client_cert.der, client_key.der, leaf_cert.pem and ca_cert.pem
files, or adjust the file paths in the source code accordingly.