Home SST: Secure Serial Tunnel - Signing and verifying - Part 1
Post
Cancel

SST: Secure Serial Tunnel - Signing and verifying - Part 1

Yes, I know. Air-Gap means that a network should be physically isolated from any unsecure networks. But, what if I need to reach a database via internet or VPN Tunnel? Some kind of semi Air-Gap perhaps?

A friend of mine asked me, if there is any possibility to minimize the risk of exposing a sensitive database to the Internet. My first thoughts were: “yes by not exposing it to the Internet”.

He told me, that he is searching for a solution to notify his clients with the latest information about their account balance without exposing the whole database to the outside world.

He was talking about things he picked up from other “experts” suggesting him: Software or Hardware Firewall, VLANs, MAC-Filters, etc. I told him:

“Yes, that would be an idea, if you trust in the Hardware (and Software) that is most likely not even open source.”

As you can guess from the title, my suggestion was to use the old fashion serial communication.

Again, there is no point of calling it air-gapped if it is connected to an unsecure network such as the Internet. This is more a Proof-of-Concept or a specific solution for a specific request. Do not use this in production, specially if you don’t have a proper plan.

Back to the retro tech

USB RS-485 Adapters Serial communication like RS-232 are underrated. They can still be very useful in this modern age. That is why we still found them in many UPS, server hardware and other industrial devices mostly in form of a DE-9 Connector also known as COM PORT.

But just using the serial port from server to server would be not that secure. Everyone with a signal analyzer or oscilloscope would be able to decode and read the data between the servers.

So we need to encrypt or at least sign the data between the server and the client hardware. Sure, we could use two Arduino Uno’s and write some sketch for it to listen to the serial input and forward it to the computer as a transparent bridge, but there is even a much easier solution.

We can just use USB-to-RS485 Adapters. Why RS-485? Because it has some advantages compared to plain old RS-232. Multiple devices can communicate on the same bus. Cable length can reach almost 1000 meters by using twisted-pair cables. It is highly immune to industrial noise.

For our test, we use the twisted-pair of an Ethernet cable. Connect the twisted-pair cable A+ with A+ and B- with B-.

Do not cross the cables like by UART connetions where TX goes to RX and RXto TX.

If you don’t have RS485 hardware available or trouble with it, try virtual serial ports with socat. Install and run sudo socat -v PTY,link=/dev/tx,mode=666 PTY,link=/dev/rx,mode=666 in the terminal. Replace /dev/ttyUSB0 with /dev/tx and /dev/ttyUSB1 with /dev/rx in the examples.

To test the USB-to-RS485 Adapters, plug both to your computer and open two terminals. (Assuming the devices are named /dev/ttyUSB0 and /dev/ttyUSB1)

1
screen /dev/ttyUSB0 115200

In another terminal we type:

1
screen /dev/ttyUSB1 115200

If we start typing something in the frist terminal, we should see it on the second terminal and vice versa. Let’s try another example:

1
cat /dev/urandom | tr -dc "[0-9a-zA-Z]" > /dev/ttyUSB0

Type in the second terminal:

1
cat /dev/ttyUSB1

If everything works fine, you should see a bunch of random ASCII characters appearing on the second terminal. Now we can write some simple Python script to send messages between the servers via serial communication.

But before we do that, we should think about a way to verify the messages between the servers.

Verifying Messages with EdDSA (ED25519)

To verify messages between the servers, we use elliptic curve cryptography. The messages will be digitally signed with the Ed25519 algorithm.1

The server side that is not connected to the Internet, generates a key-pair and signes the messages before they get transmitted via serial port.

The client side that is connected to the Internet, receives the message and digital signature, verifies the message and pushes it to the customers.

Server side test script

Here is an example server side code in Python3. The script checks if the file server_privatekey.pem exists in the keys folder. If it can’t find the private key, it generates a new pair of private and public keys.

The script converts the dict notification_message into a JSON string, encodes to bytes and hashs it with SHA512. The prehashed message gets signed and printed out to the screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python3

# check out https://www.technopolis.tv/SST-Secure-Serial-Tunnel-Part-1

from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from Crypto.Hash import SHA512
from os.path import exists
from binascii import hexlify

import json

def sign_message(data):
    message = json.dumps(data).encode()
    prehashed_message = SHA512.new(message)

    key = ECC.import_key(open("keys/server_privatekey.pem").read())
    signer = eddsa.new(key, "rfc8032")
    signature = signer.sign(prehashed_message)
    
    print(f"Message:   {message}")
    print(f"Message:   {hexlify(message)}")
    print(f"Signature: {hexlify(signature)}")


if __name__ == "__main__":
    print("Checking if private key exists... ", end="")

    if exists("keys/server_privatekey.pem"):
        print("found\nLoading private key...")

        file = open("keys/server_privatekey.pem","rt")
        server_key = ECC.import_key(file.read())
        server_publickey = server_key.public_key().export_key(format="PEM")

    else:
        print("\nCan't find private key!\nGenerating new private key...")

        server_key = ECC.generate(curve="Ed25519")
        server_publickey = server_key.public_key().export_key(format="PEM")

        file = open("keys/server_privatekey.pem","wt")
        file.write(server_key.export_key(format="PEM"))
        file.close()
        
    file = open("keys/server_publickey.pem","wt")
    file.write(server_publickey)
    file.close()

    print(f"Use this public key at the client side:\n\n{server_publickey}\n")

    notification_message = {"id": 54321, "notification": "Account #54321 signed in"}
    sign_message(notification_message)

The console output of server_test.py should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
$ python3 server_test.py 
Checking if private key exists... found
Loading private key
Use this public key at the client side:

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEASOymo0TW8O6omMgd06zETObEp+z1ATNdfFv53vnm9pE=
-----END PUBLIC KEY-----

Message:   b'{"id": 54321, "notification": "Account #54321 signed in"}'
Message:   b'7b226964223a2035343332312c20226e6f74696669636174696f6e223a20224163636f756e7420233534333231207369676e656420696e227d'
Signature: b'456a19334f9c6abb7651589fb866ed1410add1b043ec3ebd04582a93184e294a804cdae10010666404359604376ccdfec8e6193fbb23d125e05d5c0e3e4fcd03'

Client side test script

If the server_public.pem file is not available in the keys folder, we can just copy the public key and paste it manually into the file.

We copy now the hexlifyied message 7b2269...6e227d and signature 456a19...4fcd03 into our client side Python script client_test.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python3

# check out https://www.technopolis.tv/SST-Secure-Serial-Tunnel-Part-1

from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from Crypto.Hash import SHA512
from binascii import unhexlify

import json

def verify_message(message, signature):
    print(f"Message:   {unhexlify(message)}")
    print(f"Message:   {message}")
    print(f"Signature: {signature}")

    server_publickey = ECC.import_key(open("keys/server_publickey.pem").read())
    verifier = eddsa.new(server_publickey, "rfc8032")
    prehashed_message = SHA512.new(unhexlify(message))

    try:
        verifier.verify(prehashed_message, unhexlify(signature))
        print("\nThe message is authentic")
        data = json.loads(unhexlify(message))
        print(f"\nid: {data['id']}\nnotification: {data['notification']}")

    except ValueError:
        print("\nThe message is not authentic")


if __name__ == "__main__":
    notification_message = b"7b226964223a2035343332312c20226e6f74696669636174696f6e223a20224163636f756e7420233534333231207369676e656420696e227d"
    notification_signature = b"ed3b144dfadcbb90f65928789c8a493cc1ab9c214be9de7b8e6db1589ad33c5b461e1f4f6168190cb2ec483a9c90e7cfc3921ba58e75ce1317ed955c4e18200d"
    
    verify_message(notification_message, notification_signature)

The console output of client_test.py should look something like this:

1
2
3
4
5
6
7
8
9
$ python3 client_test.py 
Message:  b'{"id": 54321, "notification": "Account #54321 signed in"}'
Message:  b'7b226964223a2035343332312c20226e6f74696669636174696f6e223a20224163636f756e7420233534333231207369676e656420696e227d'
Signature: b'456a19334f9c6abb7651589fb866ed1410add1b043ec3ebd04582a93184e294a804cdae10010666404359604376ccdfec8e6193fbb23d125e05d5c0e3e4fcd03'

The message is authentic

id: 54321
notification: Account #54321 signed in

Great! Everything works fine. Now with the help of pyserial we can start to build the transfer and receive part via serial port.

Final Script

Finally we can merge our signing and verifying part with the transmitting and receiving script. The messaging protocol is a mix of binary header and signature concat together with a JSON ASCII message at the end. As magic bytes we use the big endian integer 485 (0x1e5).

After passing parameters like device path and baud the script queries the database for new notifications. In our test case the script just randomly generated notifications, signes and returns it back to the main loop for sending it over the serial port.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python3

# run this on the server that is NOT connected to the internet
# check out https://www.technopolis.tv/SST-Secure-Serial-Tunnel-Part-1

from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from Crypto.Hash import SHA512
from os.path import exists
from binascii import hexlify

import sys
import serial
import json
import time
import random

header = b"\x01\xe5" # big endian 16 bit integer for "485"

def sign_message(data, server_key):
    message = json.dumps(data).encode()
    prehashed_message = SHA512.new(message)

    signer = eddsa.new(server_key, "rfc8032")
    signature_prehashed = signer.sign(prehashed_message)
    
    print(f"Message:   {message}")
    print(f"Hexlify:   {hexlify(message)}")
    print(f"Signature: {hexlify(signature_prehashed)}\n")

    return [message, signature_prehashed]

def get_updates():
    # query information from database and return it as dict

    # for test purposes, we generate random notifications
    id = random.randint(50000, 90000)
    action = random.choice(["signed in", "signed out", "insufficient credit", "transaction completed", "transaction failed"])
    notification = f"Account #{id} {action}"
    
    return {"id": id, "notification": notification}

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"usage: {sys.argv[0]} /dev/ttyUSB0 9600")
        exit(1)

    else:
        port = sys.argv[1]
        baud = sys.argv[2]

        if exists("keys/server_privatekey.pem"):
            file = open("keys/server_privatekey.pem","rt")
            server_key = ECC.import_key(file.read())
            server_publickey = server_key.public_key().export_key(format="PEM")

        else:
            server_key = ECC.generate(curve="Ed25519")
            server_publickey = server_key.public_key().export_key(format="PEM")

            file = open("keys/server_privatekey.pem","wt")
            file.write(server_key.export_key(format="PEM"))
            file.close()
            
        file = open("keys/server_publickey.pem","wt")
        file.write(server_publickey)
        file.close()

        try:
            ser = serial.Serial(port, baud, timeout = 0.1)
        except Exception as e:
            print(e)
            exit(1)

        while True:
            message, signature = sign_message(get_updates(), server_key)
            ser.write(header + signature + message)
            time.sleep(.2)

The script can be run on the server with the database that is not connected to the Internet.

1
$ python3 main_server.py /dev/ttyUSB0 9600      # or use /dev/tx instead

On the client side, the script reads incoming serial data to check if the magic bytes are available. If the header 0x1e5 is found, additionally 64 bytes for signature and 80 bytes for the message get passed into variables signature and message. A successfully verified message can trigger a function call to push the notification to the customer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python3

# run this on the server that is connected to the internet
# check out https://www.technopolis.tv/SST-Secure-Serial-Tunnel-Part-1

from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from Crypto.Hash import SHA512
from os.path import exists
from binascii import hexlify

import sys
import serial
import json

header = b"\x01\xe5" # big endian 16 bit integer for "485"

def verify_message(message, signature, server_publickey):
    verifier = eddsa.new(server_publickey, 'rfc8032')
    prehashed_message = SHA512.new((message))

    try:
        verifier.verify(prehashed_message, (signature))      
        data = json.loads((message))
        print(f"Account: #{data['id']}\nNotification: {data['notification']}")
        print("Status: The message is authentic\n")

        # call a function to push the notification to the customer
    except ValueError:
        print(f"Data: {hexlify(message)}")
        print(f"Signature: {hexlify(signature)}")
        print("Status: The message is not authentic\n")


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"usage: {sys.argv[0]} /dev/ttyUSB0 9600")
        exit(1)

    else:
        port = sys.argv[1]
        baud = sys.argv[2]

        if not exists("keys/server_publickey.pem"):
            print("Can't find server_publickey.pem file.")
            exit(1)

        server_publickey = ECC.import_key(open("keys/server_publickey.pem").read())
        
        try:
            ser = serial.Serial(port, baud, timeout = 0.1)
        except Exception as e:
            print(e)
            exit(1)

        while True:
            header_rx = ser.read(2)

            if len(header_rx) == 2 and bytes(header_rx) == bytes(header):
                signature = ser.read(64)
                message = ser.read(80)
                verify_message(message, signature, server_publickey)

The script can be run on the server that is connected to the Internet.

1
$ python3 main_client.py /dev/ttyUSB1 9600      # or use /dev/rx instead

Secure Serial Tunnel - Signing and verifying

Nice, screenshot above shows the two final scripts in action.

In the upcoming blog post, we will try to reduce the protocol size by implementing a binary only protocol and use encryption instead of digital signatures.

Stay tuned!

Sources

Play Minecraft without purchasing it (100% legal)

Reverse Engineering Arduino RFID device and dumping firmware