PSS is a protocol that provides socket security via preshared keys.
This protocol can be used on any reliable transport layer. For example, TCP and WebSocket are both viable transport layers for PSS.
Messages of this protocol use the variable-length encodings u64_dyn_v2 and i64_dyn_v2, referred to as U64DynV2
and I64DynV2
.
As this protocol relies on pre-shared keys, it is assumed that the connecting party ("client") already knows the public key of the machine it is connecting to ("server").
The first step of the PSS handshake is the client encrypting 32 random bytes against the server's public key. For RSA, OAEP is used if bit 0 of flags is set, PKCS#1 v1.5 otherwise. The encrypted blob is transmitted as follows:
flags: U64DynV2 = 0,
size: U64DynV2,
data: u8[size],
The server then verifies that all flags are known, the padding is intact, and the data is indeed 32 bytes, closing the connection on failure.
The 32-byte shared secret will then be split into two 16-byte keys to encrypt further traffic. The first key is used by the client to encrypt messages and by the server to decrypt them, and vice-versa for the second key. The encryption scheme is 128-bit AES-GCM, with its variables being transmitted like so:
size: I64DynV2,
explicit_iv: u8[4],
ciphertext: u8[size],
auth_tag: u8[16],
Note that the size is not allowed to be negative, but instead the sign bit is used to differentiate between PSS and application messages; PSS messages have their size inverted bitwise.
The 12-byte IV is constructed as follows:
explicit_iv: u8[4],
seq_num: U64BigEndian,
- The sequence number is directionally dependent and starts at 0. When sending/receiving a message, the relevant sequence number needs to be incremented.
- Sequence numbers are also not allowed to be negative, so overflows must be prevented. To keep the sequence numbers low, use the rekeying process.
The aadata is equal to the I64DynV2-encoded size
, including the PSS message flag.
Forward secrecy is an optional feature. A server implementing this MUST support the FS negotiation being interleaved with authentication.
To initiate it, the client generates a Curve25519 keypair and sends the request:
msg_type: u8 = 128, // PSS_X25519_REQUEST
pubkey: u8[32],
Upon receipt, a supporting server generates its own keypair and responds:
msg_type: u8 = 129, // PSS_X25519_RESPONSE
pubkey: u8[32],
- After sending this, the server derives a 32-byte shared secret via x25519, sets its send key to the last 16 bytes of it, and resets its send sequence number.
- After receiving this, the client can also derive the 32-byte shared secret, and just needs to rekey itself, first sending:
msg_type: u8 = 130, // PSS_X25519_REKEY
Finally, the client uses the first 16 bytes of the shared secret as its send key, and also resets its send sequence number.
msg_type: u8 = 6, // PSS_REKEY
new_key: u8[16],
- After sending, the send key is replaced and send sequence number reset to 0.
- Upon receipt, the receive key is replaced and receive sequence number reset to 0.
To close the secure layer, byte 5 (PSS_CLOSE) is sent on it. Clients MUST NOT send further messages on it, e.g. by closing the transport layer connection. Servers MUST NOT process further PSS messages after this.
To finish the PSS handshake, the client has to send byte 7 (PSS_NO_AUTH) or use one of the following authentication methods. Be aware that if authentication is skipped and forward secrecy is also not used, there is no replay protection.
For an initial setup, it may be necessary to receive a connection from a yet-to-be-trusted client. In this situation, the server can generate 8 random bytes to be used as a passcode and share it along with other relevant data such as its public key. A different passcode size could theoretically also be negotiated.
To authenticate this way, the client simply sends:
msg_type: u8 = 0, // PSS_AUTH_PASSCODE
passcode: u8[8],
- After sending, any subsequent application messages requiring authentication can already be pre-sent.
- Upon receipt, if passcode mode is active and the value matches, passcode mode is disabled, byte 4 (PSS_AUTHED) is sent and the connection is considered authenticated. Otherwise, byte 7 (PSS_NO_AUTH) is sent.
Passcode mode MUST NOT be enabled permanently, for example by imposing limitations on time or incorrect guesses.
To begin public key authentication, the client sends byte 1 (PSS_AUTH_PUBKEY_REQUEST). Upon receipt, the server generates 32 random bytes and responds:
msg_type: u8 = 2, // PSS_AUTH_PUBKEY_CHALLENGE
challenge: u8[32],
For an RSA public key, the challenge is signed using SHA256 with the PKCS#1 v1.5 scheme, and the signature is transmitted along with the public key. For RSA, the public key is the n
component only, as e
is assumed to be 65537 (0x10001
).
msg_type: u8 = 3, // PSS_AUTH_PUBKEY_FINISH
pubkey_size: U64DynV2,
pubkey: u8[pubkey_size],
sig_size: U64DynV2,
sig: u8[sig_size],
- After sending, any subsequent application messages requiring authentication can already be pre-sent.
- Upon receipt, the public key is checked against the list of known/trusted keys and the signature is validated. On success, byte 4 (PSS_AUTHED) is sent and the connection is considered authenticated. Otherwise, byte 7 (PSS_NO_AUTH) is sent.
PSS_AUTH_PASSCODE = 0,
PSS_AUTH_PUBKEY_REQUEST = 1,
PSS_AUTH_PUBKEY_CHALLENGE = 2,
PSS_AUTH_PUBKEY_FINISH = 3,
PSS_AUTHED = 4,
PSS_CLOSE = 5,
PSS_REKEY = 6,
PSS_NO_AUTH = 7,
PSS_X25519_REQUEST = 128,
PSS_X25519_RESPONSE = 129,
PSS_X25519_REKEY = 130,
If an unknown or unhandled message type below 128 is received, the connection MUST be closed.