Secure decoupled messaging with DANE and the TLSA resource record
Decoupled and Secure
Decoupled application design gets in the way of secure communication, but a little known feature of DNS can provide message security.
Traditional security mechanisms like Transport Layer Security (TLS) provide the ability to authenticate both sides of a direct session between two parties, and to encrypt the traffic passing over the authenticated session. For applications that fit into the footprint of the client/server architecture, TLS is a fine solution for authentication and encryption.
However, as applications become more sophisticated, client/server applications are often challenged to maintain availability with a large number of clients. Middleware layers often serve as a means for providing more graceful scaling. The practice of adding layers to the application stack connecting communicating parties is called decoupling. Decoupled applications – applications that may contain components like message queues or brokers between the message sender and receiver – have been around for many years. Decoupled designs are now employed for building massive IoT applications, like smart cities and facilities automation.
Message brokers and other middleware components offer many advantages, but they also add some complications. One problem is that a message broker prevents the sender and receiver from establishing a direct session that can be secured with TLS. If you don't have a direct connection, how do you encrypt the data and also authenticate both sides of a session?
This article describes a standards-based solution to the message security problem in a decoupled application.
Message Security in Decoupled Apps
The challenge with authentication and encryption across decoupled applications is that there is no direct session to secure between the sender and receiver. You need to apply security to the messages themselves to ensure that they arrive unparsed and unchanged.
Digital identity, in its most simple form, is a method by which an entity can prove ownership of its name. The usefulness of a digital identity is directly tied to how widely recognized the identity is, and how effectively the identity resists abuse by impersonation. Digital certificates are used to bind a name to a public key, but different certificate authorities might create certificates for the same name, if the right controls are not in place. A name is only guaranteed to be unique within the scope of a single certificate authority (CA). In other words, the CA's namespace is the boundary of protection against naming collisions.
The Domain Name System (DNS) is most commonly used to associate a name (www.example.com) with an IP address. The list of DNS standards for representing other types of information is quite long. For this project, I will use a record format called TLSA, which is defined by the DNS-based Authentication of Named Entities (DANE) standard [1]. According to RFC 6698, DANE enables "administrators of domain names to specify the keys used in that domain's TLS servers." DANE places the control of which public keys can be associated with specific TLS-protected services in the hands of the administrator of the DNS zone. By binding the DNS name to a certificate, DANE mitigates naming collisions across CAs, greatly increasing the level of difficulty involved in website impersonation. Only the certificate found at the server's name in DNS may be used to authenticate the server side of the connection.
A key component of DANE is the TLSA DNS record format. (See the box entitled "DNS Resource Records.") The TLSA record is a multifunction tool, enabling the user to present information about public keys in a variety of different ways. The original purpose of the TLSA record was to publish constraints around how a public key can be associated with a name in DNS. For instance, a TLSA record can specify that only a PKI-validated certificate containing a specific public key may be used to authenticate a server operating on a specific port. This is known as the service certificate constraint mode of the TLSA record. Using the TLSA record in this way allows the application owner to specify a separate certificate for service authentication for each TLS-secured port on a server. I will be using the TLSA record for something a little different: certificate discovery, to enable message-based authentication and encryption.
DNS Resource Records
This article assumes that you already have some familiarity with DNS. But for a little refresher, DNS doles out bits of information in the form of resource records. The classic record type is the A record, which returns a 32-bit IPv4 address for the specified DNS name, but DNS supports dozens of other record types. For instance, the MX record maps the domain name to a mail transfer agent and the CNAME record provides an alias "canonical name" for the specified domain. This flexible system allows developers to extend DNS by simply adding new resource record types.
The dig
command-line tool is a quick and easy way to interact with DNS. To query DNS for the TLSA record defined by the DANE standard, use the following command:
dig -t TLSA ${IDENTITY_NAME}
replacing ${IDENTITY_NAME}
with the DNS name where you expect to find a certificate. This command will come in handy later, when you configure your device's identity in DNS.
Encrypted and Authenticated Messaging System
To demonstrate these concepts in a real-life scenario, I will show you how to configure DNS to let users locate certificates via the TLSA record. Using DNS for discovering certificates allows me to distribute the public key so that whoever receives the messages can validate the sender, and anyone with the public key can send encrypted messages to the device. For the message transport between devices, I will use the free MQTT message broker service provided by HiveMQ.com. (See the box entitled "MQTT.")
MQTT
Message Queuing Telemetry Transport, or MQTT [2], is a message brokering protocol maintained by OASIS, an open standards group. MQTT is lightweight and very widely used in IoT applications. MQTT is especially useful for protecting an application from surges of information that may occur in large IoT applications, which would otherwise require the central processing service to scale dramatically.
This example is provided as a proof of concept. Adapt and expand as needed for your own network, and, as always, be aware of the need to conform to security policies for you own organization.
For this test, you'll need:
- At least one Raspberry Pi 3 or 4
- Internet connectivity for the devices
- A 16GB microSD card
- A way to get the application image onto the Pi's microSD card (USB microSD adapter)
- An account with Balena.io, a cloud platform for supporting IoT projects (I will use the Balena.io account for managing the code on the devices)
- An account with a DNS hosting provider, or your own DNS server (make sure you choose a DNS project that can present TLSA records)
It is also possible to use docker-compose
for this experiment instead of Balena.io and a Raspberry Pi – see the instructions in the code repository's README.md
file.
The first step is to navigate to https://github.com/ValiMail/dane-message-security-mqtt and click on Deploy to Balena. This will allow you to create a new application in your Balena account, and it will start the build for the application code.
Download the Balena image for the device. Then install Balena Etcher and use it to copy the image to the microSD card. Next, put the microSD card in the Pi and plug in power and network cables. The Pi will automatically register with the Balena service and download the application.
Understanding the Message Application
Before I go on (and while you wait for Balena to build and ship the application to the device), I'll briefly describe the theory of operation for this messaging application. The application runs three services (as configured in docker-compose.yml
): messaging_sender
, messaging_receiver
, and maintenance
. As you might have guessed, the messaging_sender
service is responsible for signing, encrypting, and transmitting messages. The messaging_receiver
service is responsible for retrieving, decrypting, and verifying messages. The maintenance
service is used for managing your keys and certificates on the device.
This is a decoupled application, so the messaging_sender
service does not directly connect to the recipient device's message_receiver
service. Instead, the devices use HiveMQ's free message broker service to get messages from one device to the other. As an added benefit, the use of a public message broker allows you to avoid firewall port forwarding – the message broker acts as a server for the purpose of establishing a connection, and the message broker's publishers and subscribers are clients of the message broker.
When the messaging_receiver
service starts and detects a usable configuration, it connects to the message broker as a subscriber. On inspecting applications/messaging_receiver/src/application.py
, you may notice that there are queues in place to act as buffers between the distinct steps of message processing. Although this could have been written in a more synchronous manner, this approach was taken to improve the readability of the code and more closely represent how a subscriber might use buffering or queueing to handle a dynamic volume of events.
When a message arrives via MQTT, the message is placed in a queue containing encrypted messages, named ENCRYPTED_MESSAGES
. A thread monitors the ENCRYPTED_MESSAGES
queue for new messages. When a new encrypted message appears, the thread attempts to use the device's own private key to decrypt it. The mechanics of this process are handled in the dane_jwe_jws library [3]. If the message is successfully decrypted, it is then placed in the DECRYPTED_MESSAGES
queue, where the thread that performs authentication picks it up.
The process of authenticating the message is largely handled in the dane_jwe_jws library. The JWS [4] message format contains a signature-protected field named x5u. The x5u field is used to convey the URI where the message sender's x.509 certificate is located. When the receiver's message_is_authentic()
function is called, the underlying dane_jwe_jws library extracts the contents of the x5u
field and uses the DNS URI contained therein to retrieve the sender's certificate from DNS. If the message authenticates against the public key contained in the certificate, the decrypted message is placed in the AUTHENTICATED_MESSAGES
queue. The message printer picks up the authenticated messages and writes them to stdout, along with the authenticated sender's ID.
The messaging_sender
service is far simpler than the messaging_receiver
service. When a message is sent, the send_message.py
script (Listing 1) creates a JWS object. The JWS object contains the user's message and the enclosed x5u
field is populated with the DNS URI for the sender's identity, so that the recipient can retrieve the sender's certificate from the DNS record. The send_message.py
script then uses the device's private key to sign the JWS object. As with the messaging_receiver
service, the details of the JWS and JWE object construction, signing, and encrypting are handled in the dane_jwe_jws library.
Listing 1
send_message.py
01 #!/usr/bin/env python3 02 """Send encrypted messages over MQTT.""" 03 import argparse 04 import os 05 import sys 06 07 from dane_jwe_jws.authentication import Authentication 08 from dane_jwe_jws.encryption import Encryption 09 from dane_discovery.exceptions import TLSAError 10 import paho.mqtt.publish as publish 11 12 from idlib import Bootstrap 13 14 15 def main(): 16 """Wrap all.""" 17 parser = argparse.ArgumentParser() 18 parser.add_argument("recipient", help="Recipient DNS name") 19 parser.add_argument("message", help="Message for recipient") 20 args = parser.parse_args() 21 env_config = get_config() 22 try: 23 payload = sign_and_encrypt(env_config["identity_name"], env_config["crypto_path"], 24 env_config["app_uid"], args.message, args.recipient) 25 except TLSAError as err: 26 print("Trouble retrieving certificate from DNS: {}".format(err)) 27 sys.exit(2) 28 topic_name = args.recipient 29 publish.single(topic_name, payload, hostname=env_config["mqtt_host"], 30 port=int(env_config["mqtt_port"])) 31 32 33 def sign_and_encrypt(source_name, crypto_path, app_uid, message, recipient): 34 """Return a signed and encrypted JSON object.""" 35 crypto = Bootstrap(source_name, crypto_path, app_uid) 36 signed = Authentication.sign(message, crypto.get_path_for_pki_asset("key"), source_name) 37 return Encryption.encrypt(signed, recipient) 38 39 40 def get_config(): 41 """Get config from environment variables.""" 42 var_names = ["identity_name", "crypto_path", "mqtt_host", 43 "mqtt_port", "app_uid"] 44 config = {} 45 for x in var_names: 46 config[x] = os.getenv(x.upper()) 47 for k, v in config.items(): 48 if v is None: 49 print("Missing essential configuration env var: {}".format(k.upper())) 50 if None in config.values(): 51 sys.exit(1) 52 return config 53 54 if __name__ == "__main__": 55 main() 56 57 # copyright 2021 GitHub, Inc.
Once a signed object is generated, the send_message.py
script uses DNS to locate the intended recipient's certificate. The public key is extracted from the certificate and used to generate an encrypted JWE object. Finally, the send_message.py
script connects to the message broker as a publisher and publishes the JWE object (which contains the signed JWS object) using the recipient device's DNS name as the topic. Once the message is sent to the broker, the send_message.py
script disconnects from the broker and exits.
At this point, the device should be running the proof-of-concept code, which was built by Balena.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.