Skip to content

Instantly share code, notes, and snippets.

@nginx-gists
Last active March 21, 2023 12:53
Show Gist options
  • Save nginx-gists/85be3fdc8c9a55c446988bbbfdcf0a1d to your computer and use it in GitHub Desktop.
Save nginx-gists/85be3fdc8c9a55c446988bbbfdcf0a1d to your computer and use it in GitHub Desktop.
NGINX Plus for the IoT: Encrypting and Authenticating MQTT Traffic
function parseCSKVpairs(cskvpairs, key) {
if ( cskvpairs.length ) {
var kvpairs = cskvpairs.split(',');
for ( var i = 0; i < kvpairs.length; i++ ) {
var kvpair = kvpairs[i].split('=');
if ( kvpair[0].toUpperCase() == key ) {
return kvpair[1];
}
}
}
return ""; // Default condition
}
var client_messages = 1;
var client_id_str = "-";
function getClientId(s) {
s.on('upload', function (data, flags) {
if ( data.length == 0 ) { // Initial calls may contain no data, so
s.log("No buffer yet"); // ask that we get called again
//s.done(1); // (supposing that code=1 means that)
return;
} else if ( client_messages == 1 ) { // Connect is first packet from the client
// Connect packet is 1, using upper 4 bits (00010000 to 00011111)
var packet_type_flags_byte = data.charCodeAt(0);
s.log("MQTT packet type+flags = " + packet_type_flags_byte.toString());
if ( packet_type_flags_byte >= 16 && packet_type_flags_byte < 32 ) {
// Calculate remaining length with variable encoding scheme
var multiplier = 1;
var remaining_len_val = 0;
var remaining_len_byte;
for (var remaining_len_pos = 1; remaining_len_pos < 5; remaining_len_pos++ ) {
remaining_len_byte = data.charCodeAt(remaining_len_pos);
if ( remaining_len_byte == 0 ) break; // Stop decoding on 0
remaining_len_val += (remaining_len_byte & 127) * multiplier;
multiplier *= 128;
}
// Extract ClientId based on length defined by 2-byte encoding
var payload_offset = remaining_len_pos + 12; // Skip fixed header
var client_id_len_msb = data.charCodeAt(payload_offset).toString(16);
var client_id_len_lsb = data.charCodeAt(payload_offset + 1).toString(16);
if ( client_id_len_lsb.length < 2 ) client_id_len_lsb = "0" + client_id_len_lsb;
var client_id_len_int = parseInt(client_id_len_msb + client_id_len_lsb, 16);
client_id_str = data.substr(payload_offset + 2, client_id_len_int);
s.log("ClientId value = " + client_id_str);
// If client authentication then check certificate CN matches ClientId
var client_cert_cn = parseCSKVpairs(s.variables.ssl_client_s_dn, "CN");
if ( client_cert_cn.length && client_cert_cn != client_id_str ) {
s.log("Client certificate common name (" + client_cert_cn + ") does not match client ID");
s.deny(); // Close the TCP connection (logged as 500)
}
} else {
s.log("Received unexpected MQTT packet type+flags: " + packet_type_flags_byte.toString());
}
}
client_messages++;
s.allow();
});
}
function setClientId(s) {
return client_id_str;
}
ssl_verify_client on; # Clients must supply certificate
ssl_verify_depth 2; # In case of intermediate CA
ssl_client_certificate /etc/nginx/certs/cafile.pem; # Issuer of client certificates
# vim: syntax=nginx
server {
listen 8883 ssl; # MQTT secure port
preread_buffer_size 1k;
js_preread getClientId;
ssl_certificate /etc/nginx/certs/my_cert.crt;
ssl_certificate_key /etc/nginx/certs/my_cert.key;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:128m; # 128MB ~= 500k sessions
ssl_session_tickets on;
ssl_session_timeout 8h;
proxy_pass hive_mq;
proxy_connect_timeout 1s;
access_log /var/log/nginx/mqtt_access.log mqtt;
error_log /var/log/nginx/mqtt_error.log info; # nginScript debug logging
}
# vim: syntax=nginx
@nginx-gists
Copy link
Author

Regarding the comment made 27-Mar-2019 by MRobertEvers: the getClientId function has now been updated to use the s object as refactored in NGINX JavaScript 0.2.4. Our apologies for any confusion or inconvenience caused by the delayed update.

@rayray221
Copy link

I had to make two changes to get this to work.

First, line 45:
client_id_str = data.substr(payload_offset + 2, client_id_len_int);

This no longer works because it strips the first two characters off of the client_id.

I modified it to:
client_id_str = data.substr(payload_offset, client_id_len_int);

Also, the parseCSKVpairs function is no longer working when a client certificate isn't passed. The cskvpairs.length fails because it is undefined. I modified line 2 to include the following:

if ( undefined !== cskvpairs && cskvpairs.length ) {

Hopefully this helps others. Note, I'm not a javascript developer so there may be better ways to handle this :)

@oliver-rew
Copy link

Maybe I am doing something wrong, but this does not appear to work. When I connect with an MQTT client ID that does not match the certificate common name, I see this error message confirming the mismatched was detected:

Client certificate common name xxx does not match client ID

...which also means s.deny() is called as well. However, return is not called, so flow continues to eventually also call s.allow() before returning (and after s.deny()) is called, and this means the connection succeeds with the client ID / CN mismatch. If I am a return after s.deny(), then it works because s.allow() is not called.

Am I doing something wrong? Seems like this has worked for others, but it clearly does not for me.

@iRevive
Copy link

iRevive commented Mar 21, 2023

To anyone wondering why it does not work in some scenarios: MQTT 5 has a different structure of the Connect packet and some fields are dynamically populated.

This one works with MQTT 5: https://gist.github.com/iRevive/a441c7f880bba10ca9a3bb7ee686d222

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment