450 lines
15 KiB
Markdown
450 lines
15 KiB
Markdown
# SecLink -- Secure Serial Link Library
|
|
|
|
SecLink is the top-level API for the DVX serial/networking stack. It
|
|
composes three lower-level libraries into a single interface for reliable,
|
|
optionally encrypted, channel-multiplexed serial communication:
|
|
|
|
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
|
|
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
|
|
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
|
|
|
|
SecLink adds channel multiplexing and per-packet encryption control on
|
|
top of the packet layer's reliable delivery.
|
|
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Application
|
|
|
|
|
| secLinkSend() send data on a channel, optionally encrypted
|
|
| secLinkPoll() receive, decrypt, deliver to callback
|
|
| secLinkHandshake() DH key exchange (blocking)
|
|
|
|
|
[SecLink] channel header, encrypt/decrypt, key management
|
|
|
|
|
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
|
|
|
|
[RS232] ISR-driven UART, 2048-byte ring buffers
|
|
|
|
|
UART Hardware
|
|
```
|
|
|
|
### Channel Multiplexing
|
|
|
|
SecLink prepends a one-byte header to every packet's payload before
|
|
handing it to the packet layer:
|
|
|
|
```
|
|
Bit 7 Bits 6..0
|
|
----- ---------
|
|
Encrypt Channel (0-127)
|
|
```
|
|
|
|
This allows up to 128 independent logical channels over a single serial
|
|
link. Each channel can carry a different type of traffic (terminal data,
|
|
file transfer, control messages, etc.) without needing separate framing
|
|
or sequencing per stream. The receive callback includes the channel
|
|
number so the application can dispatch accordingly.
|
|
|
|
The encrypt flag (bit 7) tells the receiver whether the payload portion
|
|
of this packet is encrypted. The channel header byte itself is always
|
|
sent in the clear.
|
|
|
|
### Mixed Clear and Encrypted Traffic
|
|
|
|
Unencrypted packets can be sent before or after the DH handshake. This
|
|
enables a startup protocol (version negotiation, capability exchange)
|
|
before keys are established. Encrypted packets require a completed
|
|
handshake -- attempting to send an encrypted packet before the handshake
|
|
returns `SECLINK_ERR_NOT_READY`.
|
|
|
|
On the receive side, encrypted packets arriving before the handshake is
|
|
complete are silently dropped. Cleartext packets are delivered regardless
|
|
of handshake state.
|
|
|
|
|
|
## Lifecycle
|
|
|
|
```
|
|
secLinkOpen() Open COM port and packet layer
|
|
secLinkHandshake() DH key exchange (blocks until both sides complete)
|
|
secLinkSend() Send data on a channel (encrypted or cleartext)
|
|
secLinkPoll() Receive and deliver packets to callback
|
|
secLinkClose() Tear down everything (ciphers, packet, COM port)
|
|
```
|
|
|
|
### Handshake Protocol
|
|
|
|
The DH key exchange uses the packet layer's reliable delivery, so lost
|
|
packets are automatically retransmitted. Both sides can send their public
|
|
key simultaneously -- there is no initiator/responder distinction.
|
|
|
|
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
|
|
2. Both sides send their 128-byte public key as a single packet
|
|
3. On receiving the remote's public key, each side immediately computes
|
|
the shared secret (`remote^private mod p`)
|
|
4. Each side derives separate TX and RX cipher keys from the master key
|
|
5. Cipher contexts are created and the link transitions to READY state
|
|
6. The DH context (containing the private key) is destroyed immediately
|
|
|
|
**Directional key derivation:**
|
|
|
|
The side with the lexicographically lower public key uses
|
|
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
|
|
side uses the reverse assignment. This is critical for CTR mode security:
|
|
if both sides used the same key and counter, they would produce identical
|
|
keystreams, and XORing two ciphertexts would reveal the XOR of the
|
|
plaintexts. The XOR-derived directional keys ensure each direction has a
|
|
unique keystream even though both sides start their counters at zero.
|
|
|
|
**Forward secrecy:**
|
|
|
|
The DH context (containing the private key and shared secret) is
|
|
destroyed immediately after deriving the session cipher keys. Even if
|
|
the application's long-term state is compromised later, past session
|
|
keys cannot be recovered from memory.
|
|
|
|
|
|
## Payload Size
|
|
|
|
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
|
|
(254 bytes). This is the packet layer's 255-byte maximum minus the
|
|
1-byte channel header that SecLink prepends.
|
|
|
|
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
|
|
automatically splits the data into 254-byte chunks and sends each one
|
|
with blocking delivery.
|
|
|
|
|
|
## API Reference
|
|
|
|
### Types
|
|
|
|
```c
|
|
// Receive callback -- delivers plaintext with channel number
|
|
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
|
|
uint8_t channel);
|
|
|
|
// Opaque connection handle
|
|
typedef struct SecLinkS SecLinkT;
|
|
```
|
|
|
|
The receive callback is invoked from `secLinkPoll()` for each incoming
|
|
packet. Encrypted packets are decrypted before delivery -- the callback
|
|
always receives plaintext regardless of whether encryption was used on
|
|
the wire. The `data` pointer is valid only during the callback.
|
|
|
|
### Constants
|
|
|
|
| Name | Value | Description |
|
|
|-------------------------|-------|---------------------------------------|
|
|
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
|
|
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
|
|
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
|
|
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
|
|
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
|
|
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
|
|
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
|
|
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
|
|
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
|
|
|
|
### Functions
|
|
|
|
#### secLinkOpen
|
|
|
|
```c
|
|
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
|
|
int stopBits, int handshake,
|
|
SecLinkRecvT callback, void *ctx);
|
|
```
|
|
|
|
Opens the COM port via rs232, creates the packet layer with default
|
|
window size (4), and returns a link handle. The callback is invoked from
|
|
`secLinkPoll()` for each received packet (decrypted if applicable).
|
|
|
|
Returns `NULL` on failure (serial port error, packet layer allocation
|
|
error, or memory allocation failure). On failure, all partially
|
|
initialized resources are cleaned up.
|
|
|
|
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
|
|
- `bps` -- baud rate (50 through 115200)
|
|
- `dataBits` -- 5, 6, 7, or 8
|
|
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
|
|
- `stopBits` -- 1 or 2
|
|
- `handshake` -- `RS232_HANDSHAKE_*` constant
|
|
- `callback` -- receive callback function
|
|
- `ctx` -- user pointer passed through to the callback
|
|
|
|
#### secLinkClose
|
|
|
|
```c
|
|
void secLinkClose(SecLinkT *link);
|
|
```
|
|
|
|
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
|
|
destroys the DH context if still present, closes the packet layer, closes
|
|
the COM port, zeroes the link structure, and frees memory.
|
|
|
|
#### secLinkHandshake
|
|
|
|
```c
|
|
int secLinkHandshake(SecLinkT *link);
|
|
```
|
|
|
|
Performs a Diffie-Hellman key exchange. Blocks until both sides have
|
|
exchanged public keys and derived cipher keys. The RNG must be seeded
|
|
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
|
|
|
|
Internally:
|
|
1. Creates a DH context and generates keys
|
|
2. Sends the 128-byte public key via the packet layer (blocking)
|
|
3. Polls the packet layer in a loop until the remote's public key arrives
|
|
4. Computes the shared secret and derives directional cipher keys
|
|
5. Destroys the DH context (forward secrecy)
|
|
6. Transitions the link to READY state
|
|
|
|
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
|
|
(DH key generation failure, send failure, or serial disconnect during
|
|
the exchange).
|
|
|
|
#### secLinkSend
|
|
|
|
```c
|
|
int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
|
|
uint8_t channel, bool encrypt, bool block);
|
|
```
|
|
|
|
Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel.
|
|
|
|
- `channel` -- logical channel number (0-127)
|
|
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
|
|
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
|
|
- `block` -- if `true`, waits for transmit window space. If `false`,
|
|
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
|
|
|
|
**Cipher counter safety:** The function checks transmit window space
|
|
BEFORE encrypting the payload. If it encrypted first and then the send
|
|
failed, the cipher counter would advance without the data being sent,
|
|
permanently desynchronizing the TX cipher state from the remote's RX
|
|
cipher. This ordering is critical for correctness.
|
|
|
|
The channel header byte is prepended to the data, and only the payload
|
|
portion (not the header) is encrypted.
|
|
|
|
Returns `SECLINK_SUCCESS` or an error code.
|
|
|
|
#### secLinkSendBuf
|
|
|
|
```c
|
|
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
|
|
uint8_t channel, bool encrypt);
|
|
```
|
|
|
|
Sends an arbitrarily large buffer by splitting it into
|
|
`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all
|
|
data is sent. The receiver sees multiple packets on the same channel and
|
|
must reassemble if needed.
|
|
|
|
Returns `SECLINK_SUCCESS` or the first error encountered.
|
|
|
|
#### secLinkPoll
|
|
|
|
```c
|
|
int secLinkPoll(SecLinkT *link);
|
|
```
|
|
|
|
Delegates to `pktPoll()` to read serial data, process frames, handle
|
|
ACKs and retransmits. Received packets are routed through an internal
|
|
callback that:
|
|
|
|
- During handshake: expects a 128-byte DH public key
|
|
- When ready: strips the channel header, decrypts the payload if the
|
|
encrypt flag is set, and forwards plaintext to the user callback
|
|
|
|
Returns the number of packets delivered, or a negative error code.
|
|
|
|
Must be called frequently (every iteration of your main loop).
|
|
|
|
#### secLinkGetPending
|
|
|
|
```c
|
|
int secLinkGetPending(SecLinkT *link);
|
|
```
|
|
|
|
Returns the number of unacknowledged packets in the transmit window.
|
|
Delegates directly to `pktGetPending()`. Useful for non-blocking send
|
|
loops to determine when there is room to send more data.
|
|
|
|
#### secLinkIsReady
|
|
|
|
```c
|
|
bool secLinkIsReady(SecLinkT *link);
|
|
```
|
|
|
|
Returns `true` if the DH handshake is complete and the link is ready for
|
|
encrypted communication. Cleartext sends do not require the link to be
|
|
ready.
|
|
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Encrypted Link
|
|
|
|
```c
|
|
#include "secLink.h"
|
|
#include "../security/security.h"
|
|
|
|
void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
|
|
// handle received plaintext on 'channel'
|
|
}
|
|
|
|
int main(void) {
|
|
// Seed the RNG before handshake
|
|
uint8_t entropy[16];
|
|
secRngGatherEntropy(entropy, sizeof(entropy));
|
|
secRngSeed(entropy, sizeof(entropy));
|
|
|
|
// Open link on COM1 at 115200 8N1, no flow control
|
|
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
|
|
RS232_HANDSHAKE_NONE, onRecv, NULL);
|
|
if (!link) {
|
|
return 1;
|
|
}
|
|
|
|
// DH key exchange (blocks until both sides complete)
|
|
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
|
|
secLinkClose(link);
|
|
return 1;
|
|
}
|
|
|
|
// Send encrypted data on channel 0
|
|
const char *msg = "Hello, secure world!";
|
|
secLinkSend(link, (const uint8_t *)msg, strlen(msg), 0, true, true);
|
|
|
|
// Main loop
|
|
while (1) {
|
|
secLinkPoll(link);
|
|
}
|
|
|
|
secLinkClose(link);
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
### Mixed Encrypted and Cleartext Channels
|
|
|
|
```c
|
|
#define CHAN_CONTROL 0 // cleartext control channel
|
|
#define CHAN_DATA 1 // encrypted data channel
|
|
|
|
// Cleartext status message (no handshake needed)
|
|
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
|
|
|
|
// Encrypted payload (requires completed handshake)
|
|
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
|
|
```
|
|
|
|
### Non-Blocking File Transfer
|
|
|
|
```c
|
|
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
|
|
uint8_t channel, bool encrypt) {
|
|
int offset = 0;
|
|
int bytesLeft = fileSize;
|
|
|
|
while (bytesLeft > 0) {
|
|
secLinkPoll(link); // process ACKs, free window slots
|
|
|
|
if (secLinkGetPending(link) < 4) { // window has room
|
|
int chunk = bytesLeft;
|
|
if (chunk > SECLINK_MAX_PAYLOAD) {
|
|
chunk = SECLINK_MAX_PAYLOAD;
|
|
}
|
|
|
|
int rc = secLinkSend(link, fileData + offset, chunk,
|
|
channel, encrypt, false);
|
|
if (rc == SECLINK_SUCCESS) {
|
|
offset += chunk;
|
|
bytesLeft -= chunk;
|
|
}
|
|
// SECLINK_ERR_SEND means window full, retry next iteration
|
|
}
|
|
|
|
// Application can do other work here:
|
|
// update progress bar, check for cancel, etc.
|
|
}
|
|
|
|
// Drain remaining ACKs
|
|
while (secLinkGetPending(link) > 0) {
|
|
secLinkPoll(link);
|
|
}
|
|
|
|
return SECLINK_SUCCESS;
|
|
}
|
|
```
|
|
|
|
### Blocking Bulk Transfer
|
|
|
|
```c
|
|
// Send an entire file in one call (blocks until complete)
|
|
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
|
|
```
|
|
|
|
|
|
## Internal State Machine
|
|
|
|
SecLink maintains a three-state internal state machine:
|
|
|
|
| State | Value | Description |
|
|
|--------------|-------|----------------------------------------------|
|
|
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
|
|
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
|
|
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
|
|
|
|
Transitions:
|
|
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
|
|
- `HANDSHAKE -> READY`: when the remote's public key is received and
|
|
cipher keys are derived
|
|
- Any state -> cleanup: when `secLinkClose()` is called
|
|
|
|
Cleartext packets can be sent and received in any state. Encrypted
|
|
packets require `STATE_READY`.
|
|
|
|
|
|
## Building
|
|
|
|
```
|
|
make # builds ../lib/libseclink.a
|
|
make clean # removes objects and library
|
|
```
|
|
|
|
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
|
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
|
|
|
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
|
|
|
|
Link against all four libraries in this order:
|
|
|
|
```
|
|
-lseclink -lpacket -lsecurity -lrs232
|
|
```
|
|
|
|
|
|
## Files
|
|
|
|
- `secLink.h` -- Public API header (types, constants, function prototypes)
|
|
- `secLink.c` -- Complete implementation (handshake, send, receive, state
|
|
machine)
|
|
- `Makefile` -- DJGPP cross-compilation build rules
|
|
|
|
|
|
## Dependencies
|
|
|
|
SecLink requires these libraries (all built into `../lib/`):
|
|
|
|
| Library | Purpose |
|
|
|------------------|---------------------------------------------|
|
|
| `librs232.a` | Serial port driver (ISR, ring buffers) |
|
|
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
|
|
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
|