---
Proposed: 2021-01-26
Implemented: ~2022
Standardized: 2026-03-09
Protocol: simplex-messaging v1, evolved through v7
---

> **Implementation note:** All cryptographic primitives changed from this proposal. Transport: TLS 1.2/1.3 replaced the custom RSA handshake. E2E: Double ratchet with AES-GCM replaced per-message RSA-OAEP encryption. Auth: Ed25519/X25519 DH-based authenticated encryption (SMP v7) replaced RSA-PSS signatures. The transmission format (signature CRLF signed) was implemented as proposed.

# SMP agent: cryptography

3 main directions of work to enable basic level of security for communication via SMP agents and servers at the current stage of the project:

- Transport encryption - enable encryption for TCP.

- Initial handshake using asymmetric key pairs, starting with out-of-band message.

- E2E encryption of messages between SMP agents relayed over SMP servers.

- Authentication of agent commands with SMP servers.

For initial implementation I propose approach to be as simple as possible as long as it meets our security requirements. So no pluggable encryption mechanisms, no configuration, no integration with [Noise Protocol Framework](https://noiseprotocol.org), only the most necessary Crypto schemes pre-decided per area of application.

## Transport encryption

One of the consideration is to use [noise protocol framework](https://noiseprotocol.org/noise.html), this section describes ad hoc protocol though.

During TCP session both client and server should use symmetric AES 256 bit encryption using two session keys and two base IVs that will be agreed during the handshake. Both client and the server should maintain two 32-bit word counters, one for sent and one for the received messages. The IV for each message should be computed by xor-ing the sequential message counter, starting from 0, with the first 32 bits of agreed base IV. TODO - explain it in a more formal way, also document how 32-bit word is encoded - with the most or least significant byte first (currently encodeWord32 from Network.Transport.Internal is used)

To establish the session keys and base IVs, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) in advance in order to be able to establish connection.

The handshake sequence is the following:

1. Once the connection is established, the server sends server_header and its public RSA key encoded in X509 binary format to the client.
2. The client compares the hash of the received key with the hash it already has (e.g. received as part of connection invitation or server in NEW command). If the hash does not match, the client must terminate the connection. TODO as the hash is optional in server syntax at the moment, hash comparison will be optional as well. Probably it should become required.
3. If the hash is the same, the client should generate random symmetric AES keys and base IVs that will be used as session keys/IVs by the client and the server.
4. The client then should construct client_handshake block and send it to the server encrypted with the server public key: `rsa-encrypt(client_handshake)`. `snd_aes_key` and `snd_base_iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv_aes_key` and `rcv_base_iv` will be used by the client to decrypt **received** messages and by the server to encrypt them.
5. The server should decrypt the received keys and base IVs with its private key.
6. In case of successful decryption, the server should send encrypted welcome block (encrypted_welcome_block) that contains SMP protocol version.

All the subsequent data both from the client and from the server should be sent encrypted using symmetric AES keys and base IVs (incremented by counters on both sides) sent by the client during the handshake.

Each transport block sent by the client and the server has this syntax:

```abnf
server_header = block_size protocol key_size
block_size = 4*4(OCTET) ; 4-byte block size sent by the server, currently the client rejects if > 65536 bytes
protocol = 2*2(OCTET) ; currently it is 0, that means binary RSA key
key_size = 2*2(OCTET) ; the encoded key size in bytes (binary encoded in X509 standard)

client_handshake = client_block_size client_protocol snd_aes_key snd_base_iv rcv_aes_key rcv_base_iv
client_block_size = 4*4(OCTET) ; 4-byte block size sent by the client, currently it is ignored by the server - reserved
client_protocol = 2*2(OCTET) ; currently it is 0 - reserved
snd_aes_key = 32*32(OCTET)
snd_base_iv = 16*16(OCTET)
rcv_aes_key = 32*32(OCTET)
rcv_base_iv = 16*16(OCTET)

transport_block = aes_body_auth_tag aes_encrypted_body
; size is sent by server during handshake, usually 8192 bytes
aes_encrypted_body = 1*OCTET
aes_body_auth_tag = 16*16(OCTET)

encrypted_welcome_block = transport_block
welcome_block = smp_version SP pad ; decrypt(encrypted_welcome_block)
smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format
  ; for example: v123.456.789-alpha.7
pad = 1*OCTET
```

## Possible future improvements/changes

- server id (256 bits), so that only the users that have it can connect to the server. This ID will have to be passed to the server during the handshake
- block size agreed during handshake
- transport encryption protocol agreed during handshake
- welcome block containing SMP mode (smp_mode)

```abnf
smp_mode = smp_public / smp_authenticated
smp_public = %s"pub" ; public (default) - no auth to create and manage queues
smp_authenticated = %s"auth" ; server authentication with AUTH command (TBD) is required to create and manage queues
```

## Initial handshake
### Why handshake has to be with asymmetric keys

The following only makes sense under the assumption that public keys are not really used as "public", but are created as random and treated as secret instead.

    E controls servers & listens out-of-band.
    Keys are generated on the clients (A & B), queues are generated on servers.
    1. A generates Apub, Aprv, BAq
    2. A --oob-> B: Apub, BAq; E listens this, however she can't substitute this (passive attack on out-of-band, active on servers)
    3. E generates EpubA, EprvA, AEq
    4. E --BAq-> A: EpubA, AEq - encrypted with Apub;
    Alice thinks this message is from Bob
    5. B generates Bpub, Bprv, ABq
    6. B --BAq-> ~~A~~E: Bpub, ABq - encrypted with Apub;
    Eve controls servers so this doesn't get sent to Alice, instead it is received by Eve
    7. Eve has to send to Bob via ABq encrypting with his Bpub. By controlling servers E could know ABq so she wouldn't have to decrypt it - she knows where to send. Even so she can't decrypt Bpub w/t Aprv. The problem [for Eve] is that Bpub and Aprv are generated on the clients, which she doesn't control.

If keys were symmetric Eve could simply decrypt Bob's key with the key Alice sent out-of-band.

### Where MITM attempt fails

In asymmetric setup the following happens:

- In Bob to Alice direction Eve intercepts messages from Bob and re-encrypts them using Apub.

- In Alice to Bob direction Alice encrypts messages with EpubA and Eve can decrypt them with EprvA. Eve can't, however, re-encrypt them for Bob because she doesn't know Bpub. She also couldn't substitute it because it is out of her control. Alice wouldn't [technically] notice the MITM attempt, but Bob will not be receiving decryptable messages, and he would have to tell Alice out-of-band.

> **To be discussed:** Implementation-wise Bob's connection probably should be shut down if he receives a message he fails to decrypt, or after some timeout if he doesn't receive messages.

### Handshake implementation

TODO

## E2E encryption

For E2E encryption of messages between SMP agents we should go with some robust [Authenticated Encyption](https://en.wikipedia.org/wiki/Authenticated_encryption) scheme following [Encrypt-then-MAC](https://en.wikipedia.org/wiki/Authenticated_encryption#Encrypt-then-MAC_(EtM)) approach.

Since we have a shared secret Apub, Bpub (if Apub is compromised connection should be shut down on Bob's side, see [above](#-Where-MITM-attempt-fails)) there is no point in using digital signatures over MACs for message authentication other than non-repudiation. Besides [digital signatures generally being less performant than MACs](https://crypto.stackexchange.com/a/37657), the non-repudiation quality I believe may in fact be more undesirable than not for many possible applications. If some applications require non-repudiation it can be implemented later on with digital signatures on application level. See a good answer on differences of MAC and digital signature qualities [here](https://crypto.stackexchange.com/a/5647).

Symmetric keys are generated per message and encrypted with receiver's public key (encryption key).

The syntax of each encrypted message body is the following:

```abnf
encrypted_message_body = rsa_signature encrypted_body
encrypted_body = rsa_encrypted_header aes_encrypted_body
rsa_signature = 256*256(OCTET) ; sign(encrypted_body) - assuming 2048 bit key size
rsa_encrypted_header = 256*256(OCTET) ; encrypt(header) - assuming 2048 bit key size
aes_encrypted_body = 1*OCTET ; encrypt(body)

header = aes_key aes_iv auth_tag payload_size
aes_key = 32*32(OCTET)
aes_iv = 16*16(OCTET)
auth_tag = 16*16(OCTET)
payload_size = 4*4(OCTET)

body = payload pad
```

Future considerations:
- Generation of symmetric keys per session and session rotation;

## E2E implementation

### Message encryption

For each message, the agent performs the following actions to encrypt it:

1. Generates random AES256 key with [getRandomBytes]() - see [tutorial](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Tutorial.html)
2. Pad the message using some padding scheme TBC - we could consider [PADMÉ](https://petsymposium.org/2019/files/papers/issue4/popets-2019-0056.pdf), but there is no existing implementation. This particular scheme is protected against both understanding the nature of the traffic and has low overhead (< 12%). There are various other padding schemes described [here](https://en.wikipedia.org/wiki/Padding_(cryptography)) - the proposal is to postpone this decision and not to include any padding in the chat prototype to be released.
3. Encrypt the message using this AES256 key with [AES-GCM-SIV](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Cipher-AESGCMSIV.html) (AEAD scheme) using the function `encrypt` with a random nonce.
4. Encrypt the AES256 key with public encryption key for the queue using RSA-OAEP [encrypt](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-OAEP.html#v:encrypt) function parameterized with [SHA256](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Hash-Algorithms.html#t:SHA256) algorithm.
5. The message to send is concatenation of:
    1. encrypted AES256 key,
    2. AuthTag from encryption in step 3, 
    3. encrypted message (in this order).

As there is no additional data that is sent e2e in clear text, we potentially need some simpler algorithm, possibly the one implemented in the tutorial, that implements authenticated encryption without additional (unencrypted) data.

Or we could use this algorithm to allow some data that is sent e2e in clear text later.

### Message decryption

Agent decrypts the message following the same steps in the opposite order:

1. Split encrypted message to AES256 key, AuthTag and encrypted message - tag and key have fixed size (?).
2. Decrypt AES256 key using the decryption key (the private key that recipient has).
3. Decrypt the message using AES256 key.
4. Remove the padding - scheme TBC, not in the initial implementation.

## Authentication with SMP server

Each command sent to the server should include digital signature in the first part of the SMP transmission.

To compute the signature, the agent will use [signSafer](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-PSS.html#v:signSafer) function parameterized with [SHA256](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Hash-Algorithms.html#t:SHA256) algorithm with the "signed" part of the transmission.

To verify the signature, the server will use [verify](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-PSS.html#v:verify) function with the same hash algorithm.

The above implies that the way the transmissions are processed should change.

It is simpler during the sending, because it is possible to serialise signed part, prepend the signature and then send it.

It is more difficult during the receiving because to read the whole transmission it has to be parsed first (in order to have the size of the message in SEND command and to read the remaining binary), then serialized back, and only after it the signature can be verified. Possible solution is to have both unparsed and parsed transmission in some data structure (e.g. tuple) to avoid unnecessary serialisation.

The proposed change is also to change SMP protocol in this way:
1. to have components of a signed transmission part on a single line, space-separated, rather than on multiple lines (to avoid byte-string differences because of different presentation of the end of line)
2. the separation between SEND and binary part does not matter as binary is read separately and can be consistently appended with CRLF (arguably, it also applies to the rest of transmission part, but it is just simpler to read and parse one line and then append binary, if necessary, for signature verification).

```abnf
transmission = [signature] CRLF signed CRLF
signed = [corrId] SP [queueId] SP msg
```

Off topic: SMP agent protocol can also be similarly changed to align with SMP protocol.

## Concerns

- MITM between SMP agent and server is still possible w/t transport encryption.
