Protocol Specification

SCP - Secure Channel Protocol

A custom TLS-inspired secure channel protocol :3 implementing PSK auth, X25519 ephemeral key exchange and ChaCha20-Poly1305 encryption. Shout out to keeb for name. ^_^

1.

Overview

SCP creates a secure, bidirectional communication channel between two parties over TCP. It provides mutual authentication using a pre-shared key (PSK) and authenticated encryption using ChaCha20-Poly1305.

SCP is *NOT* a replacement of TLS, it is an independant protocol i built to learn and demonstrate the core things of a secure channel like handshake, key exchange, key derivation, encryption, etc. This is all implemented in Go.

The protocol completes in 2 round trips before the data phase begins. Both parties must have the same PSK before initialising the connection, there is NO certification infrastructure (yet).

2.

Packet Format

Every SCP message is framed as a packet with 5 byte header followed by a variable length payload.

+------------+------------------+----------------------+
| Type (1B)  | Length (4B, BE)  | Payload (N bytes)    |
+------------+------------------+----------------------+

Type - tells the message type (see Section 3)
Length - unsigned 32-bit big-endian integer, length of the payload in bytes
Payload - message specific content

The maximum payload size is 2^32 - 1 bytes. You should enforce limits appropriate to your context if you are using the package.

3.

Message Types

ValueNameDirectionDescription
0x01MsgClientHelloC -> SStarts the handshake. Contains client ephemeral public key, none and PSK Proof.
0x02MsgServerHelloS -> CServer response, contains the same stuff
0x03MsgDoneC <-> SHandshake verification, payload is an encrypted MAC proving session key derviation was success
0x04MsgDataC <-> SEncrypted application data, contains nonce and ChaCha20-Poly1305 encrypted ciphertext
0x05MsgErrorC <-> SProtocol error, contains error code and human readable message, terminates connection
4.

Handshake

The SCP handshake complestes in 2 round trips

Client                    Server
  |                         |
  |--- ClientHello -------->|
  |    pub[32] nonce[16]    |
  |    psk_proof[32]        |
  |                         |
  |<-- ServerHello ---------|
  |    pub[32] nonce[16]    |
  |    psk_proof[32]        |
  |                         |
  | [both derive ECDH +     |
  |  session key via HKDF]  |
  |                         |
  |--- Done --------------->|
  |    Enc(HMAC(sk,         |
  |    "client-done"))      |
  |                         |
  |<-- Done ----------------|
  |    Enc(HMAC(sk,         |
  |    "server-done"))      |
  |                         |
  |<=== channel open ======>|
4.1

PSK Proof - ClientHello

The client computes its PSK proof as:

client_psk_proof = HMAC-SHA256(PSK, client_nonce || client_public key)

The nonce binds the proof to this specific session, preventing replay attacks.

4.2

PSK Proof - ServerHello

The server computes its PSK proof as:

server_psk_proof = HMAC-SHA256(PSK, client_nonce || server_nonce || server_public_key || client_public_key)

The server proof includes both nonces and both public keys, binding it to the exact exchange in progress.

4.3

Done Messages

After deriving the session key each party sends a MsgDone packet to prove they derived the correct key. the client sends first:

client_done_mac  = HMAC-SHA256(sessionKey, "client-done")
client_done_nonce = nonceCounter.Next()
client_done_payload = ChaCha20-Poly1305_Encrypt(key=sessionKey, nonce=client_done_nonce, plaintext=client_done_mac)

The server verifies, then responds symmetrically with "server-done" as the "domain separator".

Domain separation ("client-done" vs "server-done") ensures neither party's Done can be replayed as the other's, even though both use the same session key

4.4

Error Handling

If any verification step fails, the detecing party sends MsgError with the appropriate error code and closes the connection.

CodeNameCondition
0x00ErrUnknownUnspecified error
0x01ErrInvalidPSKPSK Proof verification failed
0x02ErrHandshakeFailDone MAC verification failed
0x03ErrDecryptFailChaCha20-Poly1305 decryption failed
0x04ErrInvalidMessageUnexpected message type received
5.

Key Derivation

The session key is derived from ECDH shared secret using HKDF-SHA256:

shared_secret  = X25519(my_private_key, peer_public_key)

session_key    = HKDF-SHA256(
  ikm  = shared_secret,          // raw ECDH output
  salt = PSK,                     // authenticates key derivation
  info = "SCP-session"
          || client_nonce
          || server_nonce,        // binds key to this specific session
  len  = 32 bytes
)

ikm - the raw X25519 output is the primary entropy source. HKDF conditions it into a uniform key suitable for ChaCha20-Poly1305

salt = PSK - using the PSK as HKDF salt mixes authentication into key derivation. A session key derived without the correct PSK will differ from the one derived WITH it, even if the ECDH exchange produces same raw output.

info - the context string "SCP-session" + both nonces make the the derived key unique to to this handshake. Two sessions with the same PSK and same ECDH key but different nonce will produce different session key, so thats never gonna happens.

6.

Data Phase

After a sucecssful handshake, both parties may exchange MsgData packets in either direction, each packet is independently encrypted and authenticated.

to do: add ts

Nonce - a 12-byte big-endian encoded counter, it increments with every sent message and is included in the packet so the receiver can decrypt without maintaning synchronized state.

Each party maintains an independent send counter, client and server nonce sequence do not interfere with each other

7.

Cryptographic Primitives

PrimitiveAlgorithmPurpose
Key ExchangeX25519 (RFC 7748)Ephemeral ECDH
Symmetric cipherChaCha20-Poly1305 (RFC 8439)AEAD encryption
Key DerivationHKDF-SHA256 (RFC 5869)Session key from ECDH output
MACHMAC-SHA256 (RFC 2104)PSK Proofs + Done verification
Handshake nonceCSPRNG, 16 bytesSession binding
Data nonceCounter, 12 bytesEncrypting nonce
8.

Wire Format Reference

9.1

MsgClientHello (0x01) - 80 bytes payload

Offset  Size  Field
0       32    client_ephemeral_public_key   X25519 public key
32      16    client_nonce                  Random, 128-bit
48      32    psk_proof                     HMAC-SHA256(PSK, nonce||pub_key)
9.2

MsgServerHello (0x02) - 80 bytes payload

Offset  Size  Field
0       32    server_ephemeral_public_key   X25519 public key
32      16    server_nonce                  Random, 128-bit
48      32    psk_proof                     HMAC-SHA256(PSK, c_nonce||s_nonce||s_pub||c_pub)
9.3

MsgDone (0x03) - 60 bytes payload

Offset  Size  Field
0       12    nonce                         ChaCha20-Poly1305 nonce (counter)
12      48    ciphertext                    Encrypt(sessionKey, nonce, HMAC(sessionKey, role))

role is "client-done" or "server-done". HMAC output is 32 bytes + 16 byte Poly1305 tag = 48 bytes ciphertext.

9.4

MsgData (0x04) - variable payload

Offset  Size   Field
0       12     nonce        ChaCha20-Poly1305 nonce (send counter)
12      N+16   ciphertext   Encrypt(sessionKey, nonce, plaintext) + 16B auth tag
9.5

MsgError (0x05) - variable payload

Offset  Size  Field
0       1     error_code    See Section 4.4
1       N     message       UTF-8 string, human-readable description

SCP is an educational protocol. Do NOT use in production systems lmao. Implementation: github.com/pratiksingh94/scp