ZURÜCK

Encrypted ØMQ + node.js

ECC Nachrichten-Verschlüsselung mit ZeroMQ sockets und node.js

Datum: 2020-05, Tags: ECC, encryption, cryptography, zeromq, node.js

ZeroMQ oder ØMQ, 0MQ oder auch ZMQ ist eine Messaging Bibliothek, ähnlich wie Apache™ Kafka, RabbitMQ, oder andere. ZeroMQ stellt eine Message Queue zur Verfügung, sowie unter anderem erweiterte Unix Sockets, mit denen Nachrichten im Netzwerk verteilt werden können. Es ist eine Middleware-Komponente, mit der komplexe Softwarelandschaften entkoppelt werden können, jedoch arbeitet ZeroMQ im Gegensatz zu anderen Systemen nicht mit einem dedizierten Message Broker.

Als Bibliothek ist ZeroMQ für eine Vielzahl von Programmiersprachen verfügbar. Angegeben wir auch PHP über PECL. Auch in den offiziellen Docs von PHP wird auf ZMQ verwiesen, jedoch ist die letzte "Beta" Version 1.1.3 aus dem Jahr 2016. Offensichtlich gibt es keinen Maintainer mehr für die Erweiterung, daher würde ich persönlich eher davon abraten, die zu benutzen, und eher auf eine andere Sprache setzen, z.B. Python oder node.js.

Etwas ähnliches habe ich leider gerade mit der IMAP-Erweiterung für PHP erfahren, die wurde in meiner bevorzugten Distribution OpenSuSE ab der Version 15.1 nicht mehr unterstützt.

Aber zurück zum Thema: ZeroMQ basiert auf Socket-Kommunikation. Auf der Webseite wird beschrieben, dass die Bibliothek die klassischen Unix/BSD Sockets erweitert hat:

„Wir nahmen einen normalen TCP-Socket, injizierten ihm eine Mischung radioaktiver Isotope, die aus einem geheimen sowjetischen Atomforschungsprojekt gestohlen worden waren, bombardierten ihn mit kosmischen Strahlen aus der Zeit um 1950 und legten ihn in die Hände eines drogenverwirrten Comic-Autors mit einem schlecht getarnten Fetisch für pralle Muskeln, der in Spandex gekleidet war. Ja, ZeroMQ Sockets sind die weltrettenden Superhelden der vernetzten Welt.”

Klingt eigentlich ganz gut, nur dass der Netzwerk-Traffic, der in diesem Fall zwischen den beteiligten Systemen entsteht, nicht verschlüsselt ist. Lieder lässt sich auch kein TLS oder eine Verschlüsselung über den Transport-Layer darüber stülpen. Dafür müsste ZeroMQ selbst eine OpenSSL Bilbliothek o.ä. einsetzen. Das haben die Entwickler jedoch nicht getan, sondern statt dessen interne Verschlüsselung implementiert, siehe CurveZMQ und ZeroMQ Message Transport Protocol. Dabei wir die Verschlüsselung der "Elliptic Curve Cryptography" ECC verwendet, das will ich ausprobieren.

ØMQ ohne Verschlüsselung

Ich möchte ZeroMQ für ein Publish-Subscribe Pattern mit node.js einsetzen. Dafür benötige ich node.js ab Version 10, sowie die dazugehörige ZeroMQ Bibliothek. Ein "publisher" soll alle paar Sekunden eine Nachricht veröffentlichen, ein "subscriber" soll sich auf am publisher registrieren und die Nachrichten einfach nur ausgeben. Das ganze mache ich auf meiner lokalen Umgebung unter Linux mit User-Berechtigungen.

Ich erstelle mir ein Verzeichnis "zmq", lade erst einmal die node-Module.

~mkdir zmq
~cd zmq
~/zmqnode -v
v10.16.3
~/zmqnpm install zeromq

> zeromq@6.0.0-beta.6 install /home/jens/node_modules/zeromq
> node-gyp-build

npm WARN saveError ENOENT: no such file or directory, open '/home/jens/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/home/jens/package.json'
npm WARN jens No description
npm WARN jens No repository field.
npm WARN jens No README data
npm WARN jens No license field.

+ zeromq@6.0.0-beta.6
added 2 packages from 2 contributors and audited 2 packages in 6.384s
found 0 vulnerabilities

Hier ist es wichtig, dass wir die Version zeromq@6 laden, obwohl die als Beta Version gekennzeichnet ist. Dafür wird node.js Version 10 oder höher benötigt.

Dann lege ich die beiden Dateien publisher.js und subscriber.js an.

publisher.js
#!/usr/bin/env node

const zmq = require("zeromq")

async function run() {
    const sock = new zmq.Publisher

    await sock.bind("tcp://127.0.0.1:3000")
    console.log("Publisher bound to port 3000")

    while (true) {
        console.log("sending a multipart message envelope")
        await sock.send(["kitty cats", "meow!"])
        await new Promise(resolve => setTimeout(resolve, 2000))
    }
}

run()
subscriber.js
#!/usr/bin/env node

const zmq = require("zeromq")

async function run() {
    const sock = new zmq.Subscriber

    sock.connect("tcp://127.0.0.1:3000")
    sock.subscribe("kitty cats")
    console.log("Subscriber connected to port 3000")

    for await (const [topic, msg] of sock) {
        console.log("received a message related to '", topic.toString(), "' containing message: '", msg.toString(), "'")
    }
}

run()

Nun kann ich in meiner Shell den pulisher starten. Ich mache eine zweite Shell auf und starte auch den subscriber, der daraufhin die Nachrichten des publishers ausgibt (Beenden jeweils mit [CTRL-C]).

~/zmqnode publisher.js
Publisher bound to port 3000
sending a multipart message envelope
sending a multipart message envelope
        ...
^C
~/zmqnode subscriber.js
Subscriber connected to port 3000
received a message related to ' kitty cats ' containing message: ' meow! '
received a message related to ' kitty cats ' containing message: ' meow! '
        ...
^C

Hmm, offenbar kommen die Nachrichten mit führendem und nachfolgendem Freizeichen an. Was soll's, das ist vorerst kein Problem. Während des Nachrichtenaustauschs schaue ich mir den Netzwerk-Traffic genauer an, der über den Socket auf dem Host 'localhost' und Port 3000 läuft. tcpdump ist mein Freund, dafür benötige ich allerdings doch root-Rechte, die ich über sudo bekomme:

~/zmqsudo tcpdump -Xx -i any port 3000
        ...
16:09:23.865880 IP localhost.hbci > localhost.51476: Flags [P.], seq 111:130, ack 105, win 342, options [nop,nop,TS val 1616817062 ecr 1616815100], length 19
0x0000:  4500 0047 5386 4000 4006 e928 7f00 0001  E..GS.@.@..(....
0x0010:  7f00 0001 0bb8 c914 3466 5558 9659 64a9  ........4fUX.Yd.
0x0020:  8018 0156 fe3b 0000 0101 080a 605e aba6  ...V.;......`^..
0x0030:  605e a3fc 010a 6b69 7474 7920 6361 7473  `^....kitty.cats
0x0040:  0005 6d65 6f77 21                        ..meow!
        ...

Da haben wir den Salat, der Traffic wird in Klartext übermittelt. Jeder könnte den Traffic zwischen den beiden Systemen mitlesen. Wir sehen auch, dass der eigentliche Traffic, die payload, nur 19 Bytes lang ist. Schauen wir uns mal an, wie das mit Verschlüsselung aussieht.

ØMQ mit Verschlüsselung

Für die Verschlüsselung benötigen wir ein Schlüsselpaar mit öffentlichem und privatem Schlüssel. Für deren Erstellung hat ZeroMQ ein eigenes Linux-Paket zusammengestellt, in dem das tool curve_keygen enthalten ist. Das Paket heißt in meiner Distribution zeromq-tools und wird über zypper installiert. Nach der Installation kann ich mir in der Shell ein Schlüsselpaar erzeugen lassen:

~/zmqcurve_keygen
This tool generates a CurveZMQ keypair, as two printable strings you can
use in configuration files or source code. The encoding uses Z85, which
is a base-85 format that is described in 0MQ RFC 32, and which has an
implementation in the z85_codec.h source used by this tool. The keypair
always works with the secret key held by one party and the public key
distributed (securely!) to peers wishing to connect to it.

== CURVE PUBLIC KEY ==
Fx#6+NL@:k</F*TL:uG<bf9pQnkhk.!v$W-I&!@<

== CURVE SECRET KEY ==
ZouP&IFYyIpl6}-7ullwd04WB8)[pzUwU4%uxRg8

Diesen Schlüssel benutze ich für den publisher, wobei der nur seinen secret key benötigt. Der public key des publishers wird von dem subscriber benötigt. Zudem benötigt auch der subscriber einen Schlüsselsatz, den können wir jedoch in der API erzeugen lassen. Die beiden Dateien sehen dann wie folgt aus:

publisher.js
#!/usr/bin/env node

const zmq = require("zeromq")

async function run() {
    const sock = new zmq.Publisher

    sock.curveServer = true
    sock.curveSecretKey = "ZouP&IFYyIpl6}-7ullwd04WB8)[pzUwU4%uxRg8"

    await sock.bind("tcp://127.0.0.1:3000")
    console.log("Publisher bound to port 3000")

    while (true) {
        console.log("sending a multipart message envelope")
        await sock.send(["kitty cats", "meow!"])
        await new Promise(resolve => setTimeout(resolve, 2000))
    }
}

run()
subscriber.js
#!/usr/bin/env node

const zmq = require("zeromq")

async function run() {
    const sock = new zmq.Subscriber

    let keys = zmq.curveKeyPair()
    sock.curvePublicKey = keys.publicKey
    sock.curveSecretKey = keys.secretKey
    sock.curveServerKey = "Fx#6+NL@:k</F*TL:uG<bf9pQnkhk.!v$W-I&!@<"

    sock.connect("tcp://127.0.0.1:3000")
    sock.subscribe("kitty cats")
    console.log("Subscriber connected to port 3000")

    for await (const [topic, msg] of sock) {
        console.log("received a message related to '", topic.toString(), "' containing message: '", msg.toString(), "'")
    }
}

run()

Wenn ich nun wieder den publisher un den subscriber starte, bekomme ich wieder die Ausgabe wie vorher. Wenn ich die Keys manipuliere jedoch nicht, das scheint also schon einmal zu funktionieren. Mit tcpdump bekomme ich folgende Ausgabe:

~/zmqsudo tcpdump -Xx -i any port 3000
        ...
16:37:11.162960 IP localhost.hbci > localhost.51652: Flags [P.], seq 456:541, ack 598, win 359, options [nop,nop,TS val 1618484354 ecr 1618482352], length 85
0x0000:  4500 0089 6a0a 4000 4006 d262 7f00 0001  E...j.@.@..b....
0x0010:  7f00 0001 0bb8 c9c4 2976 1651 c67c 219f  ........)v.Q.|!.
0x0020:  8018 0167 fe7d 0000 0101 080a 6078 1c82  ...g.}......`x..
0x0030:  6078 14b0 002b 074d 4553 5341 4745 0000  `x...+.MESSAGE..
0x0040:  0000 0000 0006 3d7d 31cc 3ce4 ad74 e4bb  ......=}1.<..t..
0x0050:  a235 7598 f5a9 c19a 9d59 7e31 88f7 86f4  .5u......Y~1....
0x0060:  7b00 2607 4d45 5353 4147 4500 0000 0000  {.&.MESSAGE.....
0x0070:  0000 079e 15e4 5477 2d90 826d b167 38a4  ......Tw-..m.g8.
0x0080:  525f d438 9256 8118 b5                   R_.8.V...
        ...

Zusammenfassung

Das sieht schon einmal deutlich besser aus. Keine Nachrichten im Klartext, die payload hat nun 85 Bytes, das ist gut vier mal so viel, wie beim unverschlüsselten Traffic. Es ist übrigens auch UTF-8 kompatibel, z.B. mit der Nachricht. "meow 🐱 !". Siehe auch UTF-8 Zeichentabelle. Ob die Verschlüsselung hält, was sie verspricht und wie sich das auf die Performance des Systems auswirkt, kann ich an dieser Stelle nicht beurteilen. Insgesamt macht das für mich einen sehr guten Eindruck.