Skip to content

Commit

Permalink
more flexible crypto handshake without the need for external keys, wip
Browse files Browse the repository at this point in the history
  • Loading branch information
tbocek committed Feb 18, 2025
1 parent 341831e commit 2fae877
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 63 deletions.
145 changes: 99 additions & 46 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ for each connection, thus allowing many short-lived connections.
* User decides on perfect forward secrecy. 2 options: a) no perfect forward secrecy for 1st message
if payload is sent in first message (request and reply). b) perfect forward secrecy with empty first message
* FIN/ACK teardown with timeout (no 3-way teardown as in TCP)
* Goal: less than 2k LoC
* No FEC at the moment
* Goal: less than 3k LoC

## Assumptions

Expand All @@ -53,50 +53,101 @@ for each connection, thus allowing many short-lived connections.

## Messages Format (encryption layer)

Current version: 0
The magic byte is 0xa9 to better identify the protocol and the current version is 0. The available types are:

Available types:
* 00b: INIT_S0
* 01b: INIT_R0
* 10b: DATA_0 for rollover at crypto sequence number 0
* 11b: DATA (everything else)
* 000b: INIT_HANDSHAKE_S0
* 001b: INIT_HANDSHAKE_R0
* 010b: INIT_WITH_CRYPTO_S0
* 011b: INIT_WITH_CRYPTO_R0
* 100b: DATA_0 for rollover at crypto sequence number 0
* 101b: DATA (everything else)
* 110b: not used
* 111b: not used

The available types are encoded. We need to encode, as packets may arrive twice, and we need to know
The available types are not encrypted as packets may arrive twice, and we need to know
how to decode them.

### Type INIT_S0, min: 137 bytes (113 bytes until payload + min payload 8 bytes + 16 bytes MAC)
### Type INIT_HANDSHAKE_S0, min: 136 bytes (can be larger due to filler, no data, since no encryption)

Minimum is 100 bytes, but since the reply (INIT_HANDSHAKE_R0) is minimum 136, we start the filler at size 36,
so minimum is here 136 as well (to prevent amplification attacks).

```mermaid
---
title: "TomTP INIT_HANDSHAKE_S0 Packet"
---
packet-beta
0-7: "Magic Byte 0xa9"
8-12: "Version"
13-15: "Type"
16-271: "Public Key Sender Id (X25519)"
272-527: "Public Key Sender Ephemeral (X25519)"
528-783: "Public Key Sender Ephemeral Rollover (X25519)"
784-799: "Filler length (16bit), example 1 byte"
800-807: "Fill, example 1 byte"
```

### Type INIT_HANDSHAKE_R0, min: 136 bytes (112 bytes until payload + min payload 8 bytes + 16 bytes MAC)

The reply can contain data as it can be encrypted with perfect forward secrecy. In order to get data, INIT_HANDSHAKE_S0
needs to fill up so that we can get data here.

```mermaid
---
title: "TomTP INIT_HANDSHAKE_R0 Packet"
---
packet-beta
0-7: "Magic Byte 0xa9"
8-12: "Version"
13-15: "Type"
16-79: "Connection Id (64bit)"
80-335: "Public Key Receiver Id (X25519)"
336-591: "Public Key Receiver Ephemeral (X25519)"
592-847: "Public Key Receiver Ephemeral Rollover (X25519)"
848-895: "Double Encrypted Crypto Sequence Number (48bit)"
896-959: "Data (variable, but min 8 bytes)"
960-1087: "MAC (HMAC-SHA256) (128bit)"
```

### Type INIT_WITH_CRYPTO_S0, min: 138 bytes (114 bytes until payload + min payload 8 bytes + 16 bytes MAC)

If we have a crpyto key, we can already encrypet with the first message, but it will no non-perfect forward secrecy. The
user can decide if he wants to send data

```mermaid
---
title: "TomTP INIT_S0 Packet"
title: "TomTP INIT_WITH_CRYPTO_S0 Packet"
---
packet-beta
0-5: "Version"
6-7: "Type"
8-71: "Connection Id (64bit)"
72-327: "Public Key Sender Id (X25519)"
328-583: "Public Key Sender Ephemeral (X25519)"
584-839: "Public Key Sender Ephemeral Rollover (X25519)"
840-887: "Double Encrypted Crypto Sequence Number (48bit)"
888-903: "Filler length (16bit), example 1 byte"
904-911: "Fill, example 1 byte "
912-975: "Data (variable, but min 8 bytes)"
976-1103: "MAC (HMAC-SHA256)"
0-7: "Magic Byte 0xa9"
8-12: "Version"
13-15: "Type"
16-79: "Connection Id (64bit)"
80-335: "Public Key Sender Id (X25519)"
336-591: "Public Key Sender Ephemeral (X25519)"
592-847: "Public Key Sender Ephemeral Rollover (X25519)"
848-895: "Double Encrypted Crypto Sequence Number (48bit)"
896-911: "Filler length (16bit), example 1 byte"
912-919: "Fill, example 1 byte"
920-983: "Data (variable, but min 8 bytes)"
984-1111: "MAC (HMAC-SHA256)"
```

### Type INIT_R0, min: 103 bytes (79 bytes until payload + min payload 8 bytes + 16 bytes MAC)
### Type INIT_WITH_CRYPTO_R0, min: 104 bytes (80 bytes until payload + min payload 8 bytes + 16 bytes MAC)
```mermaid
---
title: "TomTP INIT_R0 Packet"
title: "TomTP INIT_WITH_CRYPTO_R0 Packet"
---
packet-beta
0-5: "Version"
6-7: "Type"
8-71: "Connection Id (64bit)"
72-327: "Public Key Receiver Ephemeral (X25519)"
328-583: "Public Key Receiver Ephemeral Rollover (X25519)"
584-631: "Double Encrypted Crypto Sequence Number (48bit)"
632-695: "Data (variable, but min 8 bytes)"
696-823: "MAC (HMAC-SHA256) (128bit)"
0-7: "Magic Byte 0xa9"
8-12: "Version"
13-15: "Type"
16-79: "Connection Id (64bit)"
72-335: "Public Key Receiver Ephemeral (X25519)"
336-591: "Public Key Receiver Ephemeral Rollover (X25519)"
592-639: "Double Encrypted Crypto Sequence Number (48bit)"
640-703: "Data (variable, but min 8 bytes)"
704-831: "MAC (HMAC-SHA256) (128bit)"
```

### Type DATA_0, min: 71 bytes (47 bytes until payload + min payload 8 bytes + 16 bytes MAC)
Expand All @@ -105,27 +156,29 @@ packet-beta
title: "TomTP DATA_0 Packet"
---
packet-beta
0-5: "Version"
6-7: "Type"
8-71: "Connection Id (64bit)"
72-327: "Public Key Sender/Receiver Ephemeral Rollover (X25519)"
328-375: "Double Encrypted Crypto Sequence Number (48bit)"
376-439: "Data (variable, but min 8 bytes)"
440-567: "MAC (HMAC-SHA256) (128bit)"
0-7: "Magic Byte 0xa9"
8-12: "Version"
13-15: "Type"
16-79: "Connection Id (64bit)"
80-335: "Public Key Sender/Receiver Ephemeral Rollover (X25519)"
336-383: "Double Encrypted Crypto Sequence Number (48bit)"
384-447: "Data (variable, but min 8 bytes)"
448-575: "MAC (HMAC-SHA256) (128bit)"
```

### Type DATA, min: 39 bytes (15 bytes until payload + min payload 8 bytes + 16 bytes MAC)
### Type DATA, min: 40 bytes (16 bytes until payload + min payload 8 bytes + 16 bytes MAC)
```mermaid
---
title: "TomTP DATA Packet"
---
packet-beta
0-5: "Version"
6-7: "Type"
8-71: "Connection Id (64bit)"
72-119: "Double Encrypted Crypto Sequence Number (48bit)"
120-183: "Data (variable, min. 8 bytes)"
184-311: "MAC (HMAC-SHA256) (128bit)"
0-7: "Magic Byte 0xa9"
8-12: "Version"
13-15: "Type"
16-79: "Connection Id (64bit)"
80-127: "Double Encrypted Crypto Sequence Number (48bit)"
128-191: "Data (variable, min. 8 bytes)"
192-319: "MAC (HMAC-SHA256) (128bit)"
```

The length of the complete INIT_R0 needs to be same or smaller INIT_S0, thus we need to fill up the INIT message.
Expand Down Expand Up @@ -252,7 +305,7 @@ Only if data length is greater than zero:

### Overhead
- **Total Overhead for Data Packets:**
52 bytes (crypto header 39 bytes + payload header 13 bytes) with 0 data (for a 1400-byte packet, this results in an overhead of ~3.7%).
53 bytes (crypto header 40 bytes + payload header 13 bytes) with 0 data (for a 1400-byte packet, this results in an overhead of ~3.8%).

### Communication States

Expand Down
4 changes: 2 additions & 2 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (l *Listener) decode(buffer []byte, remoteAddr netip.AddrPort) (conn *Conne
connId, msgType, err := decodeConnId(buffer)
conn = l.connMap[connId]

if conn == nil && msgType == InitS0MsgType {
if conn == nil && msgType == InitWithCryptoS0MsgType {
m, conn, err = l.decodeCryptoNew(buffer, remoteAddr)
} else if conn != nil {
m, err = l.decodeCryptoExisting(buffer, remoteAddr, conn, msgType)
Expand Down Expand Up @@ -157,7 +157,7 @@ func (l *Listener) decodeCryptoExisting(buffer []byte, remoteAddr netip.AddrPort
var err error

switch msgType {
case InitR0MsgType:
case InitWithCryptoR0MsgType:
slog.Debug("DecodeNew Rcv", debugGoroutineID(), l.debug(remoteAddr))
var pubKeyEpRcv *ecdh.PublicKey
var pubKeyEpRcvRollover *ecdh.PublicKey
Expand Down
48 changes: 34 additions & 14 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import (
type MsgType uint8

const (
InitS0MsgType MsgType = iota
InitR0MsgType
InitHandshakeS0MsgType MsgType = iota
InitHandshakeR0MsgType
InitWithCryptoS0MsgType
InitWithCryptoR0MsgType
Data0MsgType
DataMsgType

VersionMagic uint8 = 33
Magic uint8 = 0xa9
Version = 0
)

const (
Expand All @@ -28,7 +31,7 @@ const (
MinPayloadSize = 8
PubKeySize = 32

HeaderSize = 1
HeaderSize = 2
ConnIdSize = 8
MsgHeaderSize = HeaderSize + ConnIdSize

Expand Down Expand Up @@ -68,8 +71,11 @@ func EncodeWriteInitS0(
// Write the public key
headerAndCryptoBuffer := make([]byte, MsgHeaderSize+InitS0CryptoSize)

// Write magic
headerAndCryptoBuffer[0] = Magic

// Write version
headerAndCryptoBuffer[0] = (VersionMagic << 2) | uint8(InitS0MsgType)
headerAndCryptoBuffer[1] = (Version << 3) | uint8(InitWithCryptoS0MsgType)

// Write connection ID (pubKeyIdShortRcv XOR pubKeyIdShortSnd)
connId := Uint64(pubKeyIdRcv.Bytes()) ^ Uint64(pubKeyIdSnd.Bytes())
Expand Down Expand Up @@ -119,8 +125,11 @@ func EncodeWriteInitR0(
// Write the public key
headerAndCryptoBuffer := make([]byte, MsgHeaderSize+InitR0CryptoSize)

// Write magic
headerAndCryptoBuffer[0] = Magic

// Write version
headerAndCryptoBuffer[0] = (VersionMagic << 2) | uint8(InitR0MsgType)
headerAndCryptoBuffer[1] = (Version << 3) | uint8(InitWithCryptoR0MsgType)

// Write connection ID (pubKeyIdShortRcv XOR pubKeyIdShortSnd)
connId := Uint64(pubKeyIdRcv.Bytes()) ^ Uint64(pubKeyIdSnd.Bytes())
Expand Down Expand Up @@ -158,8 +167,11 @@ func EncodeWriteData0(
// Preallocate buffer with capacity for header and crypto dataToSend
headerAndCryptoBuffer := make([]byte, MsgHeaderSize+Data0CryptoSize)

// Write magic
headerAndCryptoBuffer[0] = Magic

// Write version
headerAndCryptoBuffer[0] = (VersionMagic << 2) | uint8(Data0MsgType)
headerAndCryptoBuffer[1] = (Version << 3) | uint8(Data0MsgType)

// Write connection ID
connId := Uint64(pubKeyIdRcv.Bytes()) ^ Uint64(pubKeyIdSnd.Bytes())
Expand Down Expand Up @@ -193,8 +205,11 @@ func EncodeWriteData(
// Preallocate buffer with capacity for header and connection ID
headerBuffer := make([]byte, MsgHeaderSize)

// Write magic
headerBuffer[0] = Magic

// Write version
headerBuffer[0] = (VersionMagic << 2) | uint8(DataMsgType)
headerBuffer[1] = (Version << 3) | uint8(DataMsgType)

// Write connection ID
connId := Uint64(pubKeyIdRcv.Bytes()) ^ Uint64(pubKeyIdSnd.Bytes())
Expand Down Expand Up @@ -256,13 +271,18 @@ func decodeConnId(encData []byte) (connId uint64, msgType MsgType, err error) {
return 0, Data0MsgType, errors.New("header needs to be at least 9 bytes")
}

header := encData[0]
versionMagic := header >> 2
magic := encData[0]
header := encData[1]
version := header >> 3
// Extract message type using mask
msgType = MsgType(header & 0x03)

if versionMagic != VersionMagic {
return 0, Data0MsgType, errors.New("unsupported magic version")
if magic != Magic {
return 0, Data0MsgType, errors.New("unsupported magic number")
}

if version != Version {
return 0, Data0MsgType, errors.New("unsupported version version")
}

connId = Uint64(encData[HeaderSize:MsgHeaderSize])
Expand Down Expand Up @@ -328,7 +348,7 @@ func DecodeInitS0(
actualData := decryptedData[2+int(fillerLen):]

return pubKeyIdSnd, pubKeyEpSnd, pubKeyEpSndRollover, &Message{
MsgType: InitS0MsgType,
MsgType: InitWithCryptoS0MsgType,
PayloadRaw: actualData,
SharedSecret: sharedSecret,
SnConn: snConn,
Expand Down Expand Up @@ -375,7 +395,7 @@ func DecodeInitR0(
}

return pubKeyEpRcv, pubKeyEpRcvRollover, &Message{
MsgType: InitR0MsgType,
MsgType: InitWithCryptoR0MsgType,
PayloadRaw: decryptedData,
SharedSecret: sharedSecret,
SnConn: snConn,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module tomtp

go 1.23
go 1.24

require (
github.com/MatusOllah/slogcolor v1.5.0
Expand Down

0 comments on commit 2fae877

Please sign in to comment.