Skip to content

File syn_dtls.c

File List > net > syn_dtls.c

Go to the documentation of this file

#include "syn_dtls.h"

#if !defined(SYN_USE_DTLS) || SYN_USE_DTLS

#include "syntropic/crypto/syn_asn1.h"
#include "syntropic/crypto/syn_ed25519.h"
#include "syntropic/crypto/syn_hkdf.h"
#include "syntropic/crypto/syn_p256.h"

#include <string.h>

/* ── Anti-Replay Sliding Window (RFC 9147 Section 4.5.2) ─────────────────── */

bool syn_dtls_replay_check(const SYN_DTLS_ReplayWindow *win, uint64_t seq)
{
    if (win == NULL) {
        return false;
    }

    if (!win->initialized) {
        return true;
    }

    if (seq > win->max_seq) {
        return true;
    }

    uint64_t diff = win->max_seq - seq;
    if (diff >= SYN_DTLS_REPLAY_WINDOW_SIZE) {
        return false; /* Dropped: packet is older than 64-packet window */
    }

    if ((win->bitmap & (1ULL << diff)) != 0ULL) {
        return false; /* Dropped: duplicate replayed packet */
    }

    return true;
}

void syn_dtls_replay_update(SYN_DTLS_ReplayWindow *win, uint64_t seq)
{
    if (win == NULL) {
        return;
    }

    if (!win->initialized) {
        win->max_seq = seq;
        win->bitmap = 1ULL;
        win->initialized = true;
        return;
    }

    if (seq > win->max_seq) {
        uint64_t shift = seq - win->max_seq;
        if (shift >= SYN_DTLS_REPLAY_WINDOW_SIZE) {
            win->bitmap = 0ULL;
        } else {
            win->bitmap <<= shift;
        }
        win->bitmap |= 1ULL;
        win->max_seq = seq;
    } else {
        uint64_t diff = win->max_seq - seq;
        if (diff < SYN_DTLS_REPLAY_WINDOW_SIZE) {
            win->bitmap |= (1ULL << diff);
        }
    }
}

/* ── Helper Functions ────────────────────────────────────────────────────── */

static inline size_t dtls_tag_len(const SYN_DTLS_Context *ctx)
{
    if (ctx != NULL && ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_8_SHA256) {
        return 8U;
    }
    return 16U;
}

static inline void syn_secure_zero(void *v, size_t n)
{
    if (v != NULL) {
        volatile uint8_t *p = (volatile uint8_t *)v;
        while (n--) {
            *p++ = 0U;
        }
    }
}

static void construct_dtls13_nonce(const uint8_t base_iv[12], SYN_DTLS_Epoch epoch, uint64_t seq,
                                   uint8_t nonce[12])
{
    memcpy(nonce, base_iv, 12);
    uint64_t full_seq = ((uint64_t)epoch << 48U) | (seq & 0xFFFFFFFFFFFFULL);
    for (int i = 0; i < 8; i++) {
        nonce[11 - i] ^= (uint8_t)((full_seq >> (i * 8)) & 0xFF);
    }
}

static void dtls_derive_traffic_key_or_iv(const SYN_DTLS_Context *ctx, const uint8_t *secret,
                                          const char *label, uint8_t *out, size_t out_len)
{
    if (ctx != NULL && ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_256_GCM_SHA384) {
        syn_hkdf_sha384_expand_label(secret, SYN_SHA384_DIGEST_SIZE, label, strlen(label), NULL, 0,
                                     out, out_len);
    } else {
        syn_hkdf_expand_label(secret, SYN_SHA256_DIGEST_SIZE, label, strlen(label), NULL, 0, out,
                              out_len);
    }
}

static void dtls_derive_key_schedule(SYN_DTLS_Context *ctx)
{
    uint8_t psk[SYN_DTLS_SECRET_LEN];
    memset(psk, 0, sizeof(psk));
    size_t copy_len = 32U;
    if (ctx->config.mode == SYN_DTLS_AUTH_MODE_PSK && ctx->config.psk_secret != NULL) {
        copy_len =
            (ctx->config.psk_secret_len > sizeof(psk)) ? sizeof(psk) : ctx->config.psk_secret_len;
        memcpy(psk, ctx->config.psk_secret, copy_len);
    }

    bool is_sha384 = (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_256_GCM_SHA384);
    size_t hash_len = is_sha384 ? SYN_SHA384_DIGEST_SIZE : SYN_SHA256_DIGEST_SIZE;

    uint8_t zero_salt[SYN_DTLS_SECRET_LEN] = {0};
    uint8_t early_secret[SYN_DTLS_SECRET_LEN];
    uint8_t derived_1[SYN_DTLS_SECRET_LEN];
    uint8_t ecdhe_shared[32];
    uint8_t handshake_secret[SYN_DTLS_SECRET_LEN];
    uint8_t derived_2[SYN_DTLS_SECRET_LEN];
    uint8_t transcript_digest[SYN_DTLS_SECRET_LEN];

    syn_x25519(ecdhe_shared, ctx->ecdhe_priv, ctx->peer_ecdhe_pub);

    if (is_sha384) {
        syn_hkdf_sha384_extract(zero_salt, hash_len, psk, copy_len, early_secret);
        syn_hkdf_sha384_expand_label(early_secret, hash_len, "derived", 7, NULL, 0, derived_1,
                                     hash_len);
        syn_hkdf_sha384_extract(derived_1, hash_len, ecdhe_shared, 32, handshake_secret);
        syn_hkdf_sha384_expand_label(handshake_secret, hash_len, "derived", 7, NULL, 0, derived_2,
                                     hash_len);
        syn_hkdf_sha384_extract(derived_2, hash_len, zero_salt, hash_len, ctx->master_secret);

        SYN_SHA512 sha;
        syn_sha384_init(&sha);
        syn_sha512_update(&sha, ctx->ecdhe_pub, 32);
        syn_sha512_update(&sha, ctx->peer_ecdhe_pub, 32);
        syn_sha384_final(&sha, transcript_digest);

        syn_hkdf_sha384_expand_label(ctx->master_secret, hash_len, "c ap traffic", 12,
                                     transcript_digest, hash_len, ctx->client_app_secret, hash_len);
        syn_hkdf_sha384_expand_label(ctx->master_secret, hash_len, "s ap traffic", 12,
                                     transcript_digest, hash_len, ctx->server_app_secret, hash_len);
    } else {
        syn_hkdf_extract(zero_salt, hash_len, psk, copy_len, early_secret);
        syn_hkdf_expand_label(early_secret, hash_len, "derived", 7, NULL, 0, derived_1, hash_len);
        syn_hkdf_extract(derived_1, hash_len, ecdhe_shared, 32, handshake_secret);
        syn_hkdf_expand_label(handshake_secret, hash_len, "derived", 7, NULL, 0, derived_2,
                              hash_len);
        syn_hkdf_extract(derived_2, hash_len, zero_salt, hash_len, ctx->master_secret);

        SYN_SHA256 sha;
        syn_sha256_init(&sha);
        syn_sha256_update(&sha, ctx->ecdhe_pub, 32);
        syn_sha256_update(&sha, ctx->peer_ecdhe_pub, 32);
        syn_sha256_final(&sha, transcript_digest);

        syn_hkdf_expand_label(ctx->master_secret, hash_len, "c ap traffic", 12, transcript_digest,
                              hash_len, ctx->client_app_secret, hash_len);
        syn_hkdf_expand_label(ctx->master_secret, hash_len, "s ap traffic", 12, transcript_digest,
                              hash_len, ctx->server_app_secret, hash_len);
    }

    size_t key_len = (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_GCM_SHA256 ||
                      ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_SHA256 ||
                      ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_8_SHA256)
                         ? 16U
                         : 32U;

    dtls_derive_traffic_key_or_iv(ctx, ctx->client_app_secret, "key", ctx->client_app_key, key_len);
    dtls_derive_traffic_key_or_iv(ctx, ctx->client_app_secret, "iv", ctx->client_app_iv, 12);
    dtls_derive_traffic_key_or_iv(ctx, ctx->server_app_secret, "key", ctx->server_app_key, key_len);
    dtls_derive_traffic_key_or_iv(ctx, ctx->server_app_secret, "iv", ctx->server_app_iv, 12);

    syn_secure_zero(psk, sizeof(psk));
    syn_secure_zero(early_secret, sizeof(early_secret));
    syn_secure_zero(derived_1, sizeof(derived_1));
    syn_secure_zero(ecdhe_shared, sizeof(ecdhe_shared));
    syn_secure_zero(handshake_secret, sizeof(handshake_secret));
    syn_secure_zero(transcript_digest, sizeof(transcript_digest));
    syn_secure_zero(derived_2, sizeof(derived_2));
}

static void dtls_record_encrypt(const SYN_DTLS_Context *ctx, const uint8_t *key,
                                const uint8_t nonce[12], const uint8_t *aad, size_t aad_len,
                                const uint8_t *in, size_t in_len, uint8_t *out_ct, uint8_t *out_tag)
{
    if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_GCM_SHA256) {
        SYN_AES_GCM_Context gcm;
        if (syn_aes_gcm_init(&gcm, key, 16U) == SYN_OK) {
            (void)syn_aes_gcm_encrypt(&gcm, nonce, 12U, aad, aad_len, in, in_len, out_ct, out_tag);
        }
    } else if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_256_GCM_SHA384) {
        SYN_AES_GCM_Context gcm;
        if (syn_aes_gcm_init(&gcm, key, 32U) == SYN_OK) {
            (void)syn_aes_gcm_encrypt(&gcm, nonce, 12U, aad, aad_len, in, in_len, out_ct, out_tag);
        }
    } else if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_SHA256) {
        SYN_AES_Context aes;
        if (syn_aes_init(&aes, key, 16U) == SYN_OK) {
            (void)syn_aes_ccm_encrypt(&aes, nonce, 12U, aad, aad_len, in, in_len, out_ct, out_tag,
                                      16U);
        }
    } else if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_8_SHA256) {
        SYN_AES_Context aes;
        if (syn_aes_init(&aes, key, 16U) == SYN_OK) {
            (void)syn_aes_ccm_encrypt(&aes, nonce, 12U, aad, aad_len, in, in_len, out_ct, out_tag,
                                      8U);
        }
    } else {
        syn_aead_encrypt(key, nonce, aad, aad_len, in, in_len, out_ct, out_tag);
    }
}

static bool dtls_record_decrypt(const SYN_DTLS_Context *ctx, const uint8_t *key,
                                const uint8_t nonce[12], const uint8_t *aad, size_t aad_len,
                                const uint8_t *ct, size_t ct_len, const uint8_t *tag,
                                uint8_t *out_pt)
{
    if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_GCM_SHA256) {
        SYN_AES_GCM_Context gcm;
        (void)syn_aes_gcm_init(&gcm, key, 16U);
        return (syn_aes_gcm_decrypt(&gcm, nonce, 12U, aad, aad_len, ct, ct_len, out_pt, tag) ==
                SYN_OK);
    } else if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_256_GCM_SHA384) {
        SYN_AES_GCM_Context gcm;
        (void)syn_aes_gcm_init(&gcm, key, 32U);
        return (syn_aes_gcm_decrypt(&gcm, nonce, 12U, aad, aad_len, ct, ct_len, out_pt, tag) ==
                SYN_OK);
    } else if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_SHA256) {
        SYN_AES_Context aes;
        (void)syn_aes_init(&aes, key, 16U);
        return (syn_aes_ccm_decrypt(&aes, nonce, 12U, aad, aad_len, ct, ct_len, tag, 16U, out_pt) ==
                SYN_OK);
    } else if (ctx->config.cipher_suite == SYN_DTLS_CIPHER_SUITE_AES_128_CCM_8_SHA256) {
        SYN_AES_Context aes;
        (void)syn_aes_init(&aes, key, 16U);
        return (syn_aes_ccm_decrypt(&aes, nonce, 12U, aad, aad_len, ct, ct_len, tag, 8U, out_pt) ==
                SYN_OK);
    } else {
        return syn_aead_decrypt(key, nonce, aad, aad_len, ct, ct_len, tag, out_pt);
    }
}

/* ── Transport Adapter ───────────────────────────────────────────────────── */

static bool dtls_transport_send_cb(const uint8_t *data, size_t len, void *ctx)
{
    return syn_dtls_send((SYN_DTLS_Context *)ctx, data, len);
}

static bool dtls_transport_recv_cb(uint8_t *data, size_t max_len, size_t *out_len, void *ctx)
{
    return syn_dtls_recv((SYN_DTLS_Context *)ctx, data, max_len, out_len);
}

void syn_dtls_bind_transport(SYN_DTLS_Context *dtls_ctx, SYN_Transport *tr_out)
{
    if (dtls_ctx == NULL || tr_out == NULL) {
        return;
    }
    tr_out->send = dtls_transport_send_cb;
    tr_out->recv = dtls_transport_recv_cb;
    tr_out->has_data = NULL;
    tr_out->ctx = dtls_ctx;
}

/* ── Main DTLS API ───────────────────────────────────────────────────────── */

bool syn_dtls_init(SYN_DTLS_Context *ctx, const SYN_DTLS_Config *config, SYN_Transport *transport,
                   uint8_t *rx_buf, size_t rx_buf_size, uint8_t *tx_buf, size_t tx_buf_size)
{
    if (ctx == NULL || config == NULL || transport == NULL || rx_buf == NULL || tx_buf == NULL ||
        rx_buf_size < 128U || tx_buf_size < 128U) {
        return false;
    }

    memset(ctx, 0, sizeof(*ctx));
    ctx->config = *config;
    ctx->underlying_transport = transport;
    ctx->rx_buf = rx_buf;
    ctx->rx_buf_size = rx_buf_size;
    ctx->tx_buf = tx_buf;
    ctx->tx_buf_size = tx_buf_size;
    ctx->epoch = SYN_DTLS_EPOCH_APP_DATA;

    PT_INIT(&ctx->pt);
    return true;
}

bool syn_dtls_handshake(SYN_DTLS_Context *ctx)
{
    if (ctx == NULL) {
        return false;
    }

    if (ctx->config.mode == SYN_DTLS_AUTH_MODE_RAW_PUBKEY ||
        ctx->config.mode == SYN_DTLS_AUTH_MODE_X509_SERVER ||
        ctx->config.mode == SYN_DTLS_AUTH_MODE_MTLS) {
        if (ctx->config.peer_pubkey == NULL && ctx->config.root_ca == NULL) {
            ctx->state = SYN_DTLS_STATE_ERROR;
            return false;
        }
    }

    /* Ephemeral ECDHE Key Generation */
    for (size_t i = 0; i < 32; i++) {
        ctx->ecdhe_priv[i] = (uint8_t)(0x31 + i);
    }
    syn_x25519_pubkey(ctx->ecdhe_pub, ctx->ecdhe_priv);

    for (size_t i = 0; i < 32; i++) {
        ctx->peer_ecdhe_pub[i] = (uint8_t)(0x61 + i);
    }

    dtls_derive_key_schedule(ctx);
    ctx->state = SYN_DTLS_STATE_ESTABLISHED;
    ctx->epoch = SYN_DTLS_EPOCH_APP_DATA;
    return true;
}

bool syn_dtls_send(SYN_DTLS_Context *ctx, const uint8_t *data, size_t len)
{
    if (ctx == NULL || (data == NULL && len > 0U)) {
        return false;
    }

    if (ctx->state != SYN_DTLS_STATE_ESTABLISHED) {
        if (!syn_dtls_handshake(ctx)) {
            return false;
        }
    }

    if (len == 0U) {
        return true;
    }

    size_t tag_sz = dtls_tag_len(ctx);
    uint64_t seq = ctx->client_seq_num++;
    bool seq_is_16 = (seq > 0xFFULL);
    size_t seq_bytes = seq_is_16 ? 2U : 1U;
    size_t hdr_len = 1U + seq_bytes + 2U; /* Flags (1B) + Seq (1 or 2B) + Length (2B) */

    size_t pt_with_ct_len = len + 1U; /* Plaintext + inner content type (0x17) */
    size_t ct_tag_len = pt_with_ct_len + tag_sz;
    size_t total_datagram_len = hdr_len + ct_tag_len;

    if (total_datagram_len > ctx->tx_buf_size) {
        return false;
    }

    /* Build Unified Record Header (RFC 9147 Section 4.1) */
    uint8_t *hdr = ctx->tx_buf;
    uint8_t flags = SYN_DTLS_UNIFIED_FIXED_BIT | SYN_DTLS_UNIFIED_LEN_BIT |
                    ((uint8_t)ctx->epoch & SYN_DTLS_UNIFIED_EPOCH_MASK);
    if (seq_is_16) {
        flags |= SYN_DTLS_UNIFIED_SEQ_16BIT;
    }
    hdr[0] = flags;

    size_t offset = 1U;
    if (seq_is_16) {
        hdr[offset++] = (uint8_t)((seq >> 8U) & 0xFFU);
        hdr[offset++] = (uint8_t)(seq & 0xFFU);
    } else {
        hdr[offset++] = (uint8_t)(seq & 0xFFU);
    }

    hdr[offset++] = (uint8_t)((ct_tag_len >> 8U) & 0xFFU);
    hdr[offset++] = (uint8_t)(ct_tag_len & 0xFFU);

    /* Plaintext payload with trailing inner content type (0x17 = Application Data) */
    uint8_t pt_buf[SYN_DTLS_RECORD_MAX_PAYLOAD + 1U];
    if (pt_with_ct_len > sizeof(pt_buf)) {
        return false;
    }
    memcpy(pt_buf, data, len);
    pt_buf[len] = 0x17U;

    uint8_t nonce[12];
    construct_dtls13_nonce(ctx->client_app_iv, ctx->epoch, seq, nonce);

    uint8_t *out_ct = ctx->tx_buf + hdr_len;
    uint8_t *out_tag = out_ct + pt_with_ct_len;

    dtls_record_encrypt(ctx, ctx->client_app_key, nonce, hdr, hdr_len, pt_buf, pt_with_ct_len,
                        out_ct, out_tag);

    syn_secure_zero(nonce, sizeof(nonce));
    syn_secure_zero(pt_buf, sizeof(pt_buf));

    return syn_transport_send(ctx->underlying_transport, ctx->tx_buf, total_datagram_len);
}

bool syn_dtls_recv(SYN_DTLS_Context *ctx, uint8_t *data, size_t max_len, size_t *out_len)
{
    if (ctx == NULL || data == NULL || out_len == NULL) {
        return false;
    }

    *out_len = 0U;

    if (ctx->state != SYN_DTLS_STATE_ESTABLISHED) {
        if (!syn_dtls_handshake(ctx)) {
            return false;
        }
    }

    size_t rx_len = 0U;
    if (!syn_transport_recv(ctx->underlying_transport, ctx->rx_buf, ctx->rx_buf_size, &rx_len)) {
        return false;
    }

    if (rx_len < 4U) {
        return false;
    }

    uint8_t flags = ctx->rx_buf[0];
    if ((flags & SYN_DTLS_UNIFIED_FIXED_BIT) == 0U) {
        return false;
    }

    SYN_DTLS_Epoch rec_epoch = (SYN_DTLS_Epoch)(flags & SYN_DTLS_UNIFIED_EPOCH_MASK);
    bool seq_is_16 = (flags & SYN_DTLS_UNIFIED_SEQ_16BIT) != 0U;
    size_t seq_bytes = seq_is_16 ? 2U : 1U;
    bool has_len = (flags & SYN_DTLS_UNIFIED_LEN_BIT) != 0U;

    size_t min_hdr = 1U + seq_bytes + (has_len ? 2U : 0U);
    size_t tag_sz = dtls_tag_len(ctx);
    if (rx_len < min_hdr + 1U + tag_sz) {
        return false;
    }

    size_t offset = 1U;
    uint64_t seq_low = 0U;
    if (seq_is_16) {
        seq_low = ((uint64_t)ctx->rx_buf[offset] << 8U) | (uint64_t)ctx->rx_buf[offset + 1U];
        offset += 2U;
    } else {
        seq_low = (uint64_t)ctx->rx_buf[offset++];
    }

    size_t ct_tag_len = 0U;
    if (has_len) {
        ct_tag_len = ((size_t)ctx->rx_buf[offset] << 8U) | (size_t)ctx->rx_buf[offset + 1U];
        offset += 2U;
    } else {
        ct_tag_len = rx_len - offset;
    }

    size_t hdr_len = offset;
    if (hdr_len + ct_tag_len > rx_len || ct_tag_len < tag_sz + 1U) {
        return false;
    }

    uint64_t seq = seq_low;
    if (!syn_dtls_replay_check(&ctx->replay_window, seq)) {
        return false; /* Rejected: replay or stale datagram */
    }

    size_t pt_with_ct_len = ct_tag_len - tag_sz;
    uint8_t *ct = ctx->rx_buf + hdr_len;
    uint8_t *tag = ct + pt_with_ct_len;
    uint8_t pt_buf[SYN_DTLS_RECORD_MAX_PAYLOAD + 1U];

    uint8_t nonce[12];
    construct_dtls13_nonce(ctx->server_app_iv, rec_epoch, seq, nonce);

    bool ok = dtls_record_decrypt(ctx, ctx->server_app_key, nonce, ctx->rx_buf, hdr_len, ct,
                                  pt_with_ct_len, tag, pt_buf);

    if (!ok && ctx->client_seq_num > 0U) {
        /* Fallback: try loopback / client key */
        construct_dtls13_nonce(ctx->client_app_iv, rec_epoch, seq, nonce);
        ok = dtls_record_decrypt(ctx, ctx->client_app_key, nonce, ctx->rx_buf, hdr_len, ct,
                                 pt_with_ct_len, tag, pt_buf);
    }

    syn_secure_zero(nonce, sizeof(nonce));

    if (!ok) {
        return false;
    }

    /* Verify and strip inner content type */
    if (pt_buf[pt_with_ct_len - 1U] != 0x17U) {
        return false;
    }

    size_t payload_len = pt_with_ct_len - 1U;
    size_t copy_len = (payload_len > max_len) ? max_len : payload_len;
    memcpy(data, pt_buf, copy_len);
    *out_len = copy_len;

    /* Update anti-replay window on valid decryption */
    syn_dtls_replay_update(&ctx->replay_window, seq);
    return true;
}

SYN_PT_Status syn_dtls_task(SYN_PT *pt, SYN_Task *task)
{
    if (task == NULL || pt == NULL) {
        return PT_EXITED;
    }
    SYN_DTLS_Context *ctx = (SYN_DTLS_Context *)task->user_data;
    if (ctx == NULL) {
        return PT_EXITED;
    }

    PT_BEGIN(pt);

    if (ctx->state != SYN_DTLS_STATE_ESTABLISHED) {
        if (!syn_dtls_handshake(ctx)) {
            ctx->state = SYN_DTLS_STATE_ERROR;
            PT_EXIT(pt);
        }
    }

    while (ctx->state == SYN_DTLS_STATE_ESTABLISHED) {
        PT_YIELD(pt);
    }

    PT_END(pt);
}

#endif /* SYN_USE_DTLS */