Add a library that implements the per-packet part of IPsec ESP (RFC 4303) with AES-GCM (RFC 4106), so that the userspace datapath can encrypt and decrypt tunnel traffic itself. A follow-up patch uses it for an 'esp' tunnel port type.
A security association (SA) holds the key, the salt and the sequence number state for one direction. SAs are kept in a global SA database, a cmap, where datapath threads look them up without locks: an outbound SA by its SPI and the peer's address, which are both in the header the datapath pushes, and an inbound SA by its SPI alone. esp_seal() and esp_open() are safe to call concurrently for the same SA: - Outbound sequence numbers are taken with an atomic increment. The sequence number must not cycle, so packets are refused once it is exhausted. Extended (64-bit) Sequence Numbers are supported. - The explicit IV is the sequence number XORed with a per-SA value from the system's entropy source, so a manually keyed SA that restarts at sequence number 1 does not reuse a nonce. - The anti-replay window (up to 4096 packets, 64 by default) is checked before decryption and updated after authentication, under a per-SA mutex. For ESN the high half of the sequence number is inferred as in RFC 4303 appendix A. Encryption uses OpenSSL's EVP interface, which uses AES-NI where available. Each thread caches a few cipher contexts with expanded keys, so a packet costs no allocation and no key expansion. Without OpenSSL, SAs cannot be created. The unit test checks decryption against vectors produced by an independent AES-GCM implementation, with and without ESN, as well as round trips for all key sizes, tampering, replay and sequence number wraparound. Assisted-by: claude-opus-5-5, Claude Code Signed-off-by: Thilak Raj Surendra Babu <[email protected]> --- lib/automake.mk | 2 + lib/esp.c | 685 ++++++++++++++++++++++++++++++++++++++++++++++ lib/esp.h | 97 +++++++ lib/packets.h | 4 +- tests/automake.mk | 1 + tests/library.at | 6 + tests/test-esp.c | 387 ++++++++++++++++++++++++++ 7 files changed, 1180 insertions(+), 2 deletions(-) create mode 100644 lib/esp.c create mode 100644 lib/esp.h create mode 100644 tests/test-esp.c diff --git a/lib/automake.mk b/lib/automake.mk index 2dfca79da..1e3a800fe 100644 --- a/lib/automake.mk +++ b/lib/automake.mk @@ -104,6 +104,8 @@ lib_libopenvswitch_la_SOURCES = \ lib/dynamic-string.c \ lib/entropy.c \ lib/entropy.h \ + lib/esp.c \ + lib/esp.h \ lib/fat-rwlock.c \ lib/fat-rwlock.h \ lib/fatal-signal.c \ diff --git a/lib/esp.c b/lib/esp.c new file mode 100644 index 000000000..a6a364356 --- /dev/null +++ b/lib/esp.c @@ -0,0 +1,685 @@ +/* + * Copyright (c) 2026 Thilak Raj Surendra Babu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <config.h> +#include "esp.h" + +#include <errno.h> +#include <string.h> + +#ifdef HAVE_OPENSSL +#include <openssl/crypto.h> +#include <openssl/evp.h> +#endif + +#include "byte-order.h" +#include "cmap.h" +#include "coverage.h" +#include "entropy.h" +#include "hash.h" +#include "ovs-atomic.h" +#include "ovs-rcu.h" +#include "ovs-thread.h" +#include "util.h" +#include "openvswitch/vlog.h" + +VLOG_DEFINE_THIS_MODULE(esp); + +#ifdef HAVE_OPENSSL +COVERAGE_DEFINE(esp_tx_seq_exhausted); +COVERAGE_DEFINE(esp_tx_crypto_error); +COVERAGE_DEFINE(esp_rx_malformed); +COVERAGE_DEFINE(esp_rx_no_sa); +COVERAGE_DEFINE(esp_rx_replay); +COVERAGE_DEFINE(esp_rx_auth_failed); +#endif + +struct esp_sa { + struct cmap_node node; /* In 'esp_sad', for inbound SAs. */ + struct esp_sa_params params; + uint64_t id; /* Unique, never 0. */ +#ifdef HAVE_OPENSSL + const EVP_CIPHER *cipher; +#endif + + /* Outbound. The explicit IV of a packet is its sequence number XORed + * with 'iv_salt', which is random per SA and comes from the system's + * entropy source, so that a manually keyed SA that restarts its sequence + * numbers does not reuse IVs. */ + atomic_uint64_t seq; /* Last sequence number sent. */ + uint64_t iv_salt; + + /* Inbound anti-replay window (RFC 4303 section 3.4.3 and appendix A). + * Bit (N % replay_size) of 'replay_bitmap' is set if sequence number N + * has been received, for N in (replay_top - replay_size, replay_top]. */ + struct ovs_mutex replay_mutex; + uint64_t replay_top OVS_GUARDED; + uint32_t replay_size; /* In packets, multiple of 64. */ + uint64_t *replay_bitmap; /* Its contents are guarded. */ +}; + +#ifdef HAVE_OPENSSL +#define OVS_UNUSED_WITHOUT_OPENSSL +#else +#define OVS_UNUSED_WITHOUT_OPENSSL OVS_UNUSED +#endif + +/* SA database, indexed by SPI and destination. Only written by the main + * thread. */ +static struct cmap esp_sad = CMAP_INITIALIZER; + +bool +esp_is_supported(void) +{ +#ifdef HAVE_OPENSSL + return true; +#else + return false; +#endif +} + +/* Parses 's', an AES-GCM key followed by a 4-byte salt as hexadecimal digits + * with an optional "0x" prefix, into 'params'. This is the format used by + * "ip xfrm state ... aead 'rfc4106(gcm(aes))' <key> 128". + * + * Returns NULL if successful, otherwise a malloc()'d error message. */ +char * +esp_parse_key(const char *s, struct esp_sa_params *params) +{ + size_t n_digits, key_len; + + if (!strncmp(s, "0x", 2) || !strncmp(s, "0X", 2)) { + s += 2; + } + + n_digits = strlen(s); + key_len = n_digits / 2; + if (n_digits % 2 + || (key_len != 16 + ESP_SALT_LEN && key_len != 24 + ESP_SALT_LEN + && key_len != 32 + ESP_SALT_LEN)) { + return xasprintf("key must be 20, 28 or 36 bytes (an AES-128, -192 " + "or -256 key followed by a 4-byte salt), " + "got %"PRIuSIZE" hex digits", n_digits); + } + + for (size_t i = 0; i < key_len; i++) { + bool ok; + + params->key[i] = hexits_value(&s[2 * i], 2, &ok); + if (!ok) { + return xstrdup("key contains a non-hexadecimal digit"); + } + } + params->key_len = key_len; + + return NULL; +} + +bool +esp_sa_params_equal(const struct esp_sa_params *a, + const struct esp_sa_params *b) +{ + return (a->spi == b->spi + && ipv6_addr_equals(&a->dst, &b->dst) + && a->key_len == b->key_len + && !memcmp(a->key, b->key, a->key_len) + && a->esn == b->esn + && a->replay_window == b->replay_window); +} + +/* Returns a new SA for 'params', or NULL if ESP is not supported or 'params' + * is invalid. */ +struct esp_sa * +esp_sa_create(const struct esp_sa_params *params OVS_UNUSED_WITHOUT_OPENSSL) +{ +#ifdef HAVE_OPENSSL + static atomic_uint64_t next_id = 1; + const EVP_CIPHER *cipher; + struct esp_sa *sa; + + switch (params->key_len - ESP_SALT_LEN) { + case 16: + cipher = EVP_aes_128_gcm(); + break; + case 24: + cipher = EVP_aes_192_gcm(); + break; + case 32: + cipher = EVP_aes_256_gcm(); + break; + default: + return NULL; + } + + /* An inbound SA needs the anti-replay window to infer the high half of + * extended sequence numbers. */ + bool inbound = !ipv6_addr_is_set(¶ms->dst); + if (params->replay_window > ESP_MAX_REPLAY_WINDOW + || (inbound && params->esn && !params->replay_window)) { + return NULL; + } + + sa = xzalloc(sizeof *sa); + sa->params = *params; + atomic_add_relaxed(&next_id, 1, &sa->id); + sa->cipher = cipher; + atomic_init(&sa->seq, 0); + get_entropy_or_die(&sa->iv_salt, sizeof sa->iv_salt); + + ovs_mutex_init(&sa->replay_mutex); + sa->replay_size = ROUND_UP(params->replay_window, 64); + if (sa->replay_size) { + sa->replay_bitmap = xzalloc(sa->replay_size / 8); + } + + return sa; +#else + return NULL; +#endif +} + +void +esp_sa_destroy(struct esp_sa *sa) +{ + if (sa) { + ovs_mutex_destroy(&sa->replay_mutex); + free(sa->replay_bitmap); +#ifdef HAVE_OPENSSL + OPENSSL_cleanse(sa->params.key, sizeof sa->params.key); +#endif + free(sa); + } +} + +/* Destroys 'sa' once no thread can be using it anymore. */ +void +esp_sa_destroy_postponed(struct esp_sa *sa) +{ + if (sa) { + ovsrcu_postpone(esp_sa_destroy, sa); + } +} + +const struct esp_sa_params * +esp_sa_get_params(const struct esp_sa *sa) +{ + return &sa->params; +} + +/* Makes the next packet sent on outbound SA 'sa' use sequence number 'seq'. + * For testing only. */ +void +esp_sa_set_next_seq(struct esp_sa *sa, uint64_t seq) +{ + atomic_store_relaxed(&sa->seq, seq - 1); +} + +/* SA database. */ + +static uint32_t +esp_sad_hash(ovs_be32 spi, const struct in6_addr *dst) +{ + return hash_bytes(dst, sizeof *dst, (OVS_FORCE uint32_t) spi); +} + +/* Adds 'sa' to the SAD. Returns 0 if successful, EEXIST if an SA with the + * same SPI and destination is already present. */ +int +esp_sad_insert(struct esp_sa *sa) +{ + const struct esp_sa_params *p = &sa->params; + + if (esp_sad_lookup(p->spi, &p->dst)) { + return EEXIST; + } + cmap_insert(&esp_sad, &sa->node, esp_sad_hash(p->spi, &p->dst)); + return 0; +} + +/* Replaces 'old', which must be in the SAD, by 'new'. Their SPIs and + * destinations must be the same. Packets processed meanwhile use one or the + * other. */ +void +esp_sad_replace(struct esp_sa *old, struct esp_sa *new) +{ + const struct esp_sa_params *p = &new->params; + + ovs_assert(old->params.spi == p->spi + && ipv6_addr_equals(&old->params.dst, &p->dst)); + cmap_replace(&esp_sad, &old->node, &new->node, + esp_sad_hash(p->spi, &p->dst)); +} + +void +esp_sad_remove(struct esp_sa *sa) +{ + const struct esp_sa_params *p = &sa->params; + + cmap_remove(&esp_sad, &sa->node, esp_sad_hash(p->spi, &p->dst)); +} + +/* Returns the SA with 'spi' and 'dst' from the SAD, or NULL. 'dst' is the + * peer's address for an outbound SA, all-zeros for an inbound SA. */ +struct esp_sa * +esp_sad_lookup(ovs_be32 spi, const struct in6_addr *dst) +{ + struct esp_sa *sa; + + CMAP_FOR_EACH_WITH_HASH (sa, node, esp_sad_hash(spi, dst), &esp_sad) { + if (sa->params.spi == spi && ipv6_addr_equals(&sa->params.dst, dst)) { + return sa; + } + } + return NULL; +} + +#ifdef HAVE_OPENSSL +/* Anti-replay window. */ + +/* Determines the full sequence number of a packet whose transmitted (low 32) + * bits are 'seq_lo', per RFC 4303 appendix A.2.1. Returns false if it would + * be outside the 64-bit sequence number space. */ +static bool +esp_replay_infer_seq(const struct esp_sa *sa, uint32_t seq_lo, + uint64_t *seqp) + OVS_REQUIRES(sa->replay_mutex) +{ + uint32_t top_lo = sa->replay_top; + uint32_t top_hi = sa->replay_top >> 32; + uint32_t bottom = top_lo - sa->replay_size + 1; + uint32_t seq_hi; + + if (!sa->params.esn) { + *seqp = seq_lo; + return true; + } + + if (top_lo >= sa->replay_size - 1) { + /* The window lies within one 2**32 subspace. */ + if (seq_lo >= bottom) { + seq_hi = top_hi; + } else if (top_hi == UINT32_MAX) { + return false; + } else { + seq_hi = top_hi + 1; + } + } else { + /* The window spans two subspaces. */ + if (seq_lo >= bottom) { + if (!top_hi) { + return false; + } + seq_hi = top_hi - 1; + } else { + seq_hi = top_hi; + } + } + + *seqp = ((uint64_t) seq_hi << 32) | seq_lo; + return true; +} + +static bool +esp_replay_bit(const struct esp_sa *sa, uint64_t seq) + OVS_REQUIRES(sa->replay_mutex) +{ + uint32_t bit = seq % sa->replay_size; + + return sa->replay_bitmap[bit / 64] & (UINT64_C(1) << (bit % 64)); +} + +static bool +esp_replay_check__(const struct esp_sa *sa, uint64_t seq) + OVS_REQUIRES(sa->replay_mutex) +{ + if (!seq) { + return false; + } else if (seq > sa->replay_top) { + return true; + } else if (sa->replay_top - seq >= sa->replay_size) { + return false; + } else { + return !esp_replay_bit(sa, seq); + } +} + +/* Checks, before authenticating it, whether a packet with transmitted + * sequence number 'seq_lo' may be accepted. If so, stores its full sequence + * number in '*seqp' and returns true. */ +static bool +esp_replay_check(struct esp_sa *sa, uint32_t seq_lo, uint64_t *seqp) +{ + bool ok; + + if (!sa->replay_size) { + *seqp = seq_lo; + return true; + } + + ovs_mutex_lock(&sa->replay_mutex); + ok = (esp_replay_infer_seq(sa, seq_lo, seqp) + && esp_replay_check__(sa, *seqp)); + ovs_mutex_unlock(&sa->replay_mutex); + + return ok; +} + +/* Records that the authenticated packet with sequence number 'seq' has been + * received. Returns false if another thread received it meanwhile. */ +static bool +esp_replay_update(struct esp_sa *sa, uint64_t seq) +{ + uint32_t bit; + bool ok; + + if (!sa->replay_size) { + return true; + } + + ovs_mutex_lock(&sa->replay_mutex); + ok = esp_replay_check__(sa, seq); + if (ok) { + if (seq > sa->replay_top) { + uint64_t diff = seq - sa->replay_top; + + if (diff >= sa->replay_size) { + memset(sa->replay_bitmap, 0, sa->replay_size / 8); + } else { + for (uint64_t s = sa->replay_top + 1; s < seq; s++) { + bit = s % sa->replay_size; + sa->replay_bitmap[bit / 64] &= + ~(UINT64_C(1) << (bit % 64)); + } + } + sa->replay_top = seq; + } + bit = seq % sa->replay_size; + sa->replay_bitmap[bit / 64] |= UINT64_C(1) << (bit % 64); + } + ovs_mutex_unlock(&sa->replay_mutex); + + return ok; +} + +#endif /* HAVE_OPENSSL */ + +/* Packet processing. */ + +/* Returns the number of bytes that must follow a payload of 'payload_len' + * bytes: padding to a 4-byte boundary, the ESP trailer and the ICV. */ +size_t +esp_trailer_len(size_t payload_len) +{ + size_t pad_len = (4 - (payload_len + ESP_TRAILER_LEN) % 4) % 4; + + return pad_len + ESP_TRAILER_LEN + ESP_ICV_LEN; +} + +#ifdef HAVE_OPENSSL +/* Each thread keeps a few cipher contexts with their keys already expanded, + * indexed by SA and direction. A collision only costs a key expansion. */ +#define ESP_CTX_CACHE_SIZE 16 + +struct esp_ctx_slot { + uint64_t sa_id; /* 0 if unused. */ + bool enc; + EVP_CIPHER_CTX *ctx; +}; + +struct esp_ctx_cache { + struct esp_ctx_slot slots[ESP_CTX_CACHE_SIZE]; +}; + +static ovsthread_key_t esp_ctx_cache_key; + +static void +esp_ctx_cache_destroy(void *cache_) +{ + struct esp_ctx_cache *cache = cache_; + + for (size_t i = 0; i < ESP_CTX_CACHE_SIZE; i++) { + EVP_CIPHER_CTX_free(cache->slots[i].ctx); + } + free(cache); +} + +static EVP_CIPHER_CTX * +esp_get_ctx(const struct esp_sa *sa, bool enc) +{ + static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER; + struct esp_ctx_cache *cache; + struct esp_ctx_slot *slot; + + if (ovsthread_once_start(&once)) { + ovsthread_key_create(&esp_ctx_cache_key, esp_ctx_cache_destroy); + ovsthread_once_done(&once); + } + + cache = ovsthread_getspecific(esp_ctx_cache_key); + if (!cache) { + cache = xzalloc(sizeof *cache); + ovsthread_setspecific(esp_ctx_cache_key, cache); + } + + slot = &cache->slots[(sa->id * 2 + enc) % ESP_CTX_CACHE_SIZE]; + + if (!slot->ctx) { + slot->ctx = EVP_CIPHER_CTX_new(); + if (!slot->ctx) { + return NULL; + } + } + if (slot->sa_id != sa->id || slot->enc != enc) { + slot->sa_id = 0; + if (!EVP_CipherInit_ex(slot->ctx, sa->cipher, NULL, sa->params.key, + NULL, enc)) { + return NULL; + } + slot->sa_id = sa->id; + slot->enc = enc; + } + return slot->ctx; +} + +/* Builds the AES-GCM nonce and additional authenticated data for a packet + * with ESP header 'esp' and full sequence number 'seq' (RFC 4106 section 5). + * Returns the length of the AAD. */ +static size_t +esp_gcm_prepare(const struct esp_sa *sa, const struct esp_header *esp, + uint64_t seq, uint8_t nonce[ESP_SALT_LEN + ESP_IV_LEN], + uint8_t aad[12]) +{ + ovs_be32 spi = get_16aligned_be32(&esp->spi); + ovs_be32 seq_hi = htonl(seq >> 32); + ovs_be32 seq_lo = htonl(seq); + + memcpy(nonce, &sa->params.key[sa->params.key_len - ESP_SALT_LEN], + ESP_SALT_LEN); + memcpy(nonce + ESP_SALT_LEN, esp + 1, ESP_IV_LEN); + + memcpy(aad, &spi, 4); + if (sa->params.esn) { + memcpy(aad + 4, &seq_hi, 4); + memcpy(aad + 8, &seq_lo, 4); + return 12; + } else { + memcpy(aad + 4, &seq_lo, 4); + return 8; + } +} +#endif + +/* Encrypts a packet for outbound SA 'sa'. + * + * 'esp' must be followed by ESP_IV_LEN bytes of space for the IV, then by + * 'payload_len' bytes of payload whose type is 'next_hdr' (an IP protocol + * number), then by esp_trailer_len(payload_len) bytes of space. Fills in the + * ESP header, IV, padding, trailer and ICV, and encrypts the payload, padding + * and trailer in place. + * + * Returns 0 if successful, otherwise a positive errno value. */ +int +esp_seal(struct esp_sa *sa OVS_UNUSED_WITHOUT_OPENSSL, + struct esp_header *esp OVS_UNUSED_WITHOUT_OPENSSL, + size_t payload_len OVS_UNUSED_WITHOUT_OPENSSL, + uint8_t next_hdr OVS_UNUSED_WITHOUT_OPENSSL) +{ +#ifdef HAVE_OPENSSL + uint8_t *payload = (uint8_t *) esp + ESP_PREFIX_LEN; + size_t ct_len = esp_trailer_len(payload_len) - ESP_ICV_LEN + payload_len; + size_t pad_len = ct_len - payload_len - ESP_TRAILER_LEN; + uint8_t nonce[ESP_SALT_LEN + ESP_IV_LEN]; + uint8_t *trailer = payload + payload_len; + EVP_CIPHER_CTX *ctx; + uint8_t aad[12]; + size_t aad_len; + uint64_t seq; + ovs_be64 iv; + int len; + + atomic_add_relaxed(&sa->seq, 1, &seq); + seq++; + if (OVS_UNLIKELY(sa->params.esn ? !seq : seq > UINT32_MAX)) { + /* RFC 4303 section 3.3.3: the sequence number must not cycle. With + * ESN, this only catches the wrap to zero, not later numbers, but + * sending 2**64 packets is out of reach. */ + COVERAGE_INC(esp_tx_seq_exhausted); + return ERANGE; + } + + for (size_t i = 0; i < pad_len; i++) { + trailer[i] = i + 1; + } + trailer[pad_len] = pad_len; + trailer[pad_len + 1] = next_hdr; + + put_16aligned_be32(&esp->spi, sa->params.spi); + put_16aligned_be32(&esp->seq_no, htonl(seq)); + iv = htonll(seq ^ sa->iv_salt); + memcpy(esp + 1, &iv, ESP_IV_LEN); + + aad_len = esp_gcm_prepare(sa, esp, seq, nonce, aad); + + ctx = esp_get_ctx(sa, true); + if (OVS_UNLIKELY(!ctx + || !EVP_CipherInit_ex(ctx, NULL, NULL, NULL, nonce, 1) + || !EVP_CipherUpdate(ctx, NULL, &len, aad, aad_len) + || !EVP_CipherUpdate(ctx, payload, &len, payload, ct_len) + || !EVP_CipherFinal_ex(ctx, payload + len, &len) + || !EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, + ESP_ICV_LEN, + payload + ct_len))) { + COVERAGE_INC(esp_tx_crypto_error); + return EIO; + } + return 0; +#else + return EOPNOTSUPP; +#endif +} + +/* Authenticates and decrypts the 'len'-byte ESP packet that starts at 'esp', + * using the inbound SA for its SPI, and checks it against the SA's + * anti-replay window. + * + * If successful, stores the SPI in '*spi', the protocol of the payload in + * '*next_hdr' and its length in '*payload_len', and returns 0. The payload + * starts ESP_PREFIX_LEN bytes after 'esp'. Otherwise, returns a positive + * errno value. */ +int +esp_open(struct esp_header *esp OVS_UNUSED_WITHOUT_OPENSSL, + size_t len OVS_UNUSED_WITHOUT_OPENSSL, + ovs_be32 *spi OVS_UNUSED_WITHOUT_OPENSSL, + uint8_t *next_hdr OVS_UNUSED_WITHOUT_OPENSSL, + size_t *payload_len OVS_UNUSED_WITHOUT_OPENSSL) +{ +#ifdef HAVE_OPENSSL + uint8_t *payload = (uint8_t *) esp + ESP_PREFIX_LEN; + uint8_t nonce[ESP_SALT_LEN + ESP_IV_LEN]; + EVP_CIPHER_CTX *ctx; + uint8_t *trailer; + struct esp_sa *sa; + uint8_t *pad; + uint8_t aad[12]; + size_t aad_len; + size_t ct_len; + size_t pad_len; + uint32_t seq_lo; + uint64_t seq; + int out_len; + + if (OVS_UNLIKELY(len < ESP_PREFIX_LEN + ESP_TRAILER_LEN + ESP_ICV_LEN)) { + COVERAGE_INC(esp_rx_malformed); + return EINVAL; + } + ct_len = len - ESP_PREFIX_LEN - ESP_ICV_LEN; + + *spi = get_16aligned_be32(&esp->spi); + sa = esp_sad_lookup(*spi, &in6addr_any); + if (OVS_UNLIKELY(!sa)) { + COVERAGE_INC(esp_rx_no_sa); + return ENOENT; + } + + seq_lo = ntohl(get_16aligned_be32(&esp->seq_no)); + if (OVS_UNLIKELY(!esp_replay_check(sa, seq_lo, &seq))) { + COVERAGE_INC(esp_rx_replay); + return EALREADY; + } + + aad_len = esp_gcm_prepare(sa, esp, seq, nonce, aad); + + ctx = esp_get_ctx(sa, false); + if (OVS_UNLIKELY(!ctx + || !EVP_CipherInit_ex(ctx, NULL, NULL, NULL, nonce, 0) + || !EVP_CipherUpdate(ctx, NULL, &out_len, aad, aad_len) + || !EVP_CipherUpdate(ctx, payload, &out_len, payload, + ct_len) + || !EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, + ESP_ICV_LEN, payload + ct_len) + || EVP_CipherFinal_ex(ctx, payload + out_len, + &out_len) <= 0)) { + COVERAGE_INC(esp_rx_auth_failed); + return EBADMSG; + } + + if (OVS_UNLIKELY(!esp_replay_update(sa, seq))) { + COVERAGE_INC(esp_rx_replay); + return EALREADY; + } + + trailer = payload + ct_len - ESP_TRAILER_LEN; + pad_len = trailer[0]; + if (OVS_UNLIKELY(pad_len + ESP_TRAILER_LEN > ct_len)) { + COVERAGE_INC(esp_rx_malformed); + return EINVAL; + } + /* RFC 4303 section 2.4: the padding bytes are 1, 2, 3, ... */ + pad = trailer - pad_len; + for (size_t i = 0; i < pad_len; i++) { + if (OVS_UNLIKELY(pad[i] != i + 1)) { + COVERAGE_INC(esp_rx_malformed); + return EINVAL; + } + } + + *next_hdr = trailer[1]; + *payload_len = ct_len - pad_len - ESP_TRAILER_LEN; + return 0; +#else + return EOPNOTSUPP; +#endif +} diff --git a/lib/esp.h b/lib/esp.h new file mode 100644 index 000000000..be4558c30 --- /dev/null +++ b/lib/esp.h @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026 Thilak Raj Surendra Babu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ESP_H +#define ESP_H 1 + +/* IPsec Encapsulating Security Payload (ESP, RFC 4303) with AES-GCM + * (RFC 4106), for tunnels in the userspace datapath. + * + * A Security Association (SA) holds the keys and sequence number state for + * one direction of a tunnel. SAs are published in a global SA database + * (SAD), where datapath threads look them up: + * + * - An outbound SA by its SPI and the address of the peer, which are both + * in the tunnel header that the datapath pushes. + * + * - An inbound SA by its SPI alone, as this host chose the SPI. Received + * packets are authenticated and decrypted before it is known which tunnel + * port they belong to. + * + * esp_seal() and esp_open() may be called from any thread, including PMD + * threads, concurrently for the same SA. All other functions must be called + * from a single thread (the main thread in ovs-vswitchd). An SA that has + * been removed from the SAD or is no longer published to other threads must + * be freed with esp_sa_destroy_postponed(). */ + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include "compiler.h" +#include "openvswitch/types.h" +#include "packets.h" + +/* ESP_HEADER_LEN, struct esp_header and ESP_TRAILER_LEN are in packets.h. */ +#define ESP_IV_LEN 8 +#define ESP_ICV_LEN 16 +#define ESP_SALT_LEN 4 + +/* Bytes between the start of the ESP header and the encrypted payload. */ +#define ESP_PREFIX_LEN (ESP_HEADER_LEN + ESP_IV_LEN) + +/* An AES-256 key followed by the 4-byte salt, as in RFC 4106. */ +#define ESP_MAX_KEY_LEN (32 + ESP_SALT_LEN) + +#define ESP_DEFAULT_REPLAY_WINDOW 64 +#define ESP_MAX_REPLAY_WINDOW 4096 + +struct esp_sa_params { + ovs_be32 spi; + struct in6_addr dst; /* Outbound: peer address. Inbound: 0. */ + uint8_t key[ESP_MAX_KEY_LEN]; /* AES key followed by the salt. */ + uint8_t key_len; /* Including the salt. */ + bool esn; /* Extended (64-bit) Sequence Numbers. */ + uint16_t replay_window; /* Inbound only, in packets; 0 = off, + * which is invalid with 'esn'. */ +}; + +bool esp_is_supported(void); + +char *esp_parse_key(const char *, struct esp_sa_params *) + OVS_WARN_UNUSED_RESULT; +bool esp_sa_params_equal(const struct esp_sa_params *, + const struct esp_sa_params *); + +struct esp_sa *esp_sa_create(const struct esp_sa_params *); +void esp_sa_destroy(struct esp_sa *); +void esp_sa_destroy_postponed(struct esp_sa *); +const struct esp_sa_params *esp_sa_get_params(const struct esp_sa *); + +int esp_sad_insert(struct esp_sa *); +void esp_sad_replace(struct esp_sa *old, struct esp_sa *new); +void esp_sad_remove(struct esp_sa *); +struct esp_sa *esp_sad_lookup(ovs_be32 spi, const struct in6_addr *dst); + +size_t esp_trailer_len(size_t payload_len); +int esp_seal(struct esp_sa *, struct esp_header *, size_t payload_len, + uint8_t next_hdr); +int esp_open(struct esp_header *, size_t len, ovs_be32 *spi, + uint8_t *next_hdr, size_t *payload_len); + +/* For testing only. */ +void esp_sa_set_next_seq(struct esp_sa *, uint64_t seq); + +#endif /* esp.h */ diff --git a/lib/packets.h b/lib/packets.h index ea0bda840..b9d770581 100644 --- a/lib/packets.h +++ b/lib/packets.h @@ -878,8 +878,8 @@ BUILD_ASSERT_DECL(UDP_HEADER_LEN == sizeof(struct udp_header)); #define ESP_HEADER_LEN 8 struct esp_header { - ovs_be32 spi; - ovs_be32 seq_no; + ovs_16aligned_be32 spi; + ovs_16aligned_be32 seq_no; }; BUILD_ASSERT_DECL(ESP_HEADER_LEN == sizeof(struct esp_header)); diff --git a/tests/automake.mk b/tests/automake.mk index 2f3e4f7ef..4dc3754b1 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -496,6 +496,7 @@ tests_ovstest_SOURCES = \ tests/test-reconnect.c \ tests/test-rstp.c \ tests/test-sflow.c \ + tests/test-esp.c \ tests/test-sha1.c \ tests/test-skiplist.c \ tests/test-stp.c \ diff --git a/tests/library.at b/tests/library.at index 80ebe6ed8..dc6ec3000 100644 --- a/tests/library.at +++ b/tests/library.at @@ -63,6 +63,12 @@ AT_CHECK([ovstest test-sha1], [0], [.................... ]) AT_CLEANUP +AT_SETUP([IPsec ESP]) +AT_KEYWORDS([esp ipsec]) +AT_SKIP_IF([test "$HAVE_OPENSSL" = no]) +AT_CHECK([ovstest test-esp]) +AT_CLEANUP + AT_SETUP([test skiplist]) AT_KEYWORDS([skiplist]) AT_CHECK([ovstest test-skiplist], [0], [skiplist insert diff --git a/tests/test-esp.c b/tests/test-esp.c new file mode 100644 index 000000000..e5fd9013d --- /dev/null +++ b/tests/test-esp.c @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2026 Thilak Raj Surendra Babu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <config.h> +#include "esp.h" +#include <errno.h> +#include <string.h> +#include "byte-order.h" +#include "ovstest.h" +#include "random.h" +#include "util.h" + +#define TEST_KEY "101112131415161718191a1b1c1d1e1fcafebabe" +#define TEST_PAYLOAD "OVS ESP known answer test vector" + +/* Produced with the 'cryptography' Python module from TEST_KEY, IV + * 0x0011223344556677, payload TEST_PAYLOAD and next header 4. + * - SPI 0x1000, sequence number 7. + * - SPI 0x1001, extended sequence number 0x100000005. */ +static const char *kat_seq7 = + "00001000000000070011223344556677" + "6b646566a4cb5ffb8ae9320d838a07cc83405ce68e7003193ade723b8a3b01b8" + "2246cdb5397447f5217f7a2a0edea54dcab9d890"; +static const char *kat_esn = + "00001001000000050011223344556677" + "6b646566a4cb5ffb8ae9320d838a07cc83405ce68e7003193ade723b8a3b01b8" + "2246cdb5d03e0fb3b624b02969575ee723f923a5"; + +struct test_pkt { + uint8_t buf[256]; + size_t len; +}; + +static struct esp_sa_params +make_params(uint32_t spi, const char *key, bool esn, uint16_t window) +{ + struct esp_sa_params params; + char *error; + + memset(¶ms, 0, sizeof params); + params.spi = htonl(spi); + params.esn = esn; + params.replay_window = window; + error = esp_parse_key(key, ¶ms); + ovs_assert(!error); + return params; +} + +static void +from_hex(struct test_pkt *pkt, const char *hex) +{ + pkt->len = strlen(hex) / 2; + ovs_assert(pkt->len && pkt->len <= sizeof pkt->buf); + for (size_t i = 0; i < pkt->len; i++) { + bool ok; + + pkt->buf[i] = hexits_value(&hex[2 * i], 2, &ok); + ovs_assert(ok); + } +} + +static struct esp_header * +pkt_esp(struct test_pkt *pkt) +{ + return ALIGNED_CAST(struct esp_header *, pkt->buf); +} + +static int +seal(struct esp_sa *sa, struct test_pkt *pkt, const void *payload, + size_t payload_len) +{ + memcpy(pkt->buf + ESP_PREFIX_LEN, payload, payload_len); + pkt->len = ESP_PREFIX_LEN + payload_len + esp_trailer_len(payload_len); + ovs_assert(pkt->len <= sizeof pkt->buf); + return esp_seal(sa, pkt_esp(pkt), payload_len, IPPROTO_IPIP); +} + +static int +open_pkt(struct test_pkt *pkt, size_t *payload_len) +{ + uint8_t next_hdr; + ovs_be32 spi; + + return esp_open(pkt_esp(pkt), pkt->len, &spi, &next_hdr, payload_len); +} + +/* Seals a packet with sequence number 'seq' on 'out' and returns the result + * of opening it. */ +static int +send_seq(struct esp_sa *out, uint64_t seq) +{ + struct test_pkt pkt; + size_t len; + + esp_sa_set_next_seq(out, seq); + ovs_assert(!seal(out, &pkt, "x", 1)); + return open_pkt(&pkt, &len); +} + +static void +test_parse_key(void) +{ + struct esp_sa_params params; + char *error; + + error = esp_parse_key("0x" TEST_KEY, ¶ms); + ovs_assert(!error); + ovs_assert(params.key_len == 20); + ovs_assert(params.key[0] == 0x10 && params.key[19] == 0xbe); + + error = esp_parse_key(TEST_KEY TEST_KEY, ¶ms); + ovs_assert(error); + free(error); + + error = esp_parse_key("101112131415161718191a1b1c1d1e1fcafebabz", ¶ms); + ovs_assert(error); + free(error); + + error = esp_parse_key("", ¶ms); + ovs_assert(error); + free(error); +} + +static void +test_known_answer(void) +{ + struct esp_sa_params params = make_params(0x1000, TEST_KEY, false, 64); + struct esp_sa *sa = esp_sa_create(¶ms); + struct test_pkt pkt; + uint8_t next_hdr; + size_t len; + ovs_be32 spi; + + ovs_assert(!esp_sad_insert(sa)); + + /* Corrupted ICV, payload, or authenticated sequence number. These must + * not advance the anti-replay window. */ + from_hex(&pkt, kat_seq7); + pkt.buf[pkt.len - 1] ^= 1; + ovs_assert(open_pkt(&pkt, &len) == EBADMSG); + from_hex(&pkt, kat_seq7); + pkt.buf[ESP_PREFIX_LEN] ^= 1; + ovs_assert(open_pkt(&pkt, &len) == EBADMSG); + from_hex(&pkt, kat_seq7); + pkt.buf[7] = 8; + ovs_assert(open_pkt(&pkt, &len) == EBADMSG); + + /* Unknown SPI and truncated packet. */ + from_hex(&pkt, kat_seq7); + pkt.buf[3] = 0x99; + ovs_assert(open_pkt(&pkt, &len) == ENOENT); + from_hex(&pkt, kat_seq7); + pkt.len = ESP_PREFIX_LEN + ESP_ICV_LEN + 1; + ovs_assert(open_pkt(&pkt, &len) == EINVAL); + + from_hex(&pkt, kat_seq7); + ovs_assert(!esp_open(pkt_esp(&pkt), pkt.len, &spi, &next_hdr, &len)); + ovs_assert(spi == htonl(0x1000)); + ovs_assert(next_hdr == 4); + ovs_assert(len == strlen(TEST_PAYLOAD)); + ovs_assert(!memcmp(pkt.buf + ESP_PREFIX_LEN, TEST_PAYLOAD, len)); + + /* Replayed. */ + from_hex(&pkt, kat_seq7); + ovs_assert(open_pkt(&pkt, &len) == EALREADY); + + esp_sad_remove(sa); + esp_sa_destroy(sa); +} + +static void +test_round_trip(void) +{ + static const char *keys[] = { + TEST_KEY, + "000102030405060708090a0b0c0d0e0f1011121314151617deadbeef", + ("000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1fdeadbeef"), + }; + + for (size_t k = 0; k < ARRAY_SIZE(keys); k++) { + for (int esn = 0; esn < 2; esn++) { + struct esp_sa_params params = make_params(0x2000, keys[k], + esn, 64); + struct esp_sa *out = esp_sa_create(¶ms); + struct esp_sa *in = esp_sa_create(¶ms); + uint8_t payload[100]; + + ovs_assert(out && in); + ovs_assert(!esp_sad_insert(in)); + for (size_t i = 0; i < sizeof payload; i++) { + payload[i] = i * 7; + } + + for (size_t n = 0; n <= sizeof payload; n++) { + struct test_pkt pkt; + size_t len; + + ovs_assert(!seal(out, &pkt, payload, n)); + ovs_assert((pkt.len - ESP_PREFIX_LEN - ESP_ICV_LEN) % 4 == 0); + if (n >= 16) { + ovs_assert(memcmp(pkt.buf + ESP_PREFIX_LEN, payload, n)); + } + ovs_assert(!open_pkt(&pkt, &len)); + ovs_assert(len == n); + ovs_assert(!memcmp(pkt.buf + ESP_PREFIX_LEN, payload, n)); + } + + esp_sad_remove(in); + esp_sa_destroy(in); + esp_sa_destroy(out); + } + } +} + +static void +test_replay_window(void) +{ + struct esp_sa_params params = make_params(0x3000, TEST_KEY, false, 64); + struct esp_sa *out = esp_sa_create(¶ms); + struct esp_sa *in = esp_sa_create(¶ms); + + ovs_assert(!esp_sad_insert(in)); + + ovs_assert(send_seq(out, 0) == EALREADY); /* 0 is never valid. */ + ovs_assert(!send_seq(out, 200)); + ovs_assert(!send_seq(out, 137)); /* Oldest in window. */ + ovs_assert(send_seq(out, 136) == EALREADY); /* Too old. */ + ovs_assert(send_seq(out, 137) == EALREADY); /* Duplicate. */ + ovs_assert(!send_seq(out, 199)); + ovs_assert(!send_seq(out, 263)); /* Slides by 63. */ + ovs_assert(send_seq(out, 199) == EALREADY); + ovs_assert(!send_seq(out, 200 + 64)); + ovs_assert(!send_seq(out, 10000)); /* Slides beyond the window. */ + ovs_assert(!send_seq(out, 9999)); + + /* Without ESN the sequence number must not cycle. */ + ovs_assert(!send_seq(out, UINT32_MAX)); + struct test_pkt pkt; + ovs_assert(seal(out, &pkt, "x", 1) == ERANGE); + + esp_sad_remove(in); + esp_sa_destroy(in); + esp_sa_destroy(out); + + /* Anti-replay disabled. */ + params = make_params(0x3001, TEST_KEY, false, 0); + out = esp_sa_create(¶ms); + in = esp_sa_create(¶ms); + ovs_assert(!esp_sad_insert(in)); + ovs_assert(!send_seq(out, 5)); + ovs_assert(!send_seq(out, 5)); + esp_sad_remove(in); + esp_sa_destroy(in); + esp_sa_destroy(out); +} + +static void +test_esn(void) +{ + struct esp_sa_params params = make_params(0x1001, TEST_KEY, true, 64); + struct esp_sa *out = esp_sa_create(¶ms); + struct esp_sa *in = esp_sa_create(¶ms); + const uint64_t wrap = UINT64_C(1) << 32; + struct test_pkt pkt; + size_t len; + + ovs_assert(!esp_sad_insert(in)); + + /* A receiver cannot follow a jump of nearly 2**32. */ + ovs_assert(send_seq(out, wrap - 10) == EALREADY); + + ovs_assert(!send_seq(out, wrap / 2)); + ovs_assert(!send_seq(out, wrap - 16)); + ovs_assert(!send_seq(out, wrap - 1)); + ovs_assert(!send_seq(out, wrap + 1)); /* Low 32 bits wrap. */ + ovs_assert(!send_seq(out, wrap - 2)); /* Still in the window. */ + ovs_assert(send_seq(out, wrap - 1) == EALREADY); + + /* The known-answer packet has sequence number 2**32 + 5. */ + from_hex(&pkt, kat_esn); + ovs_assert(!open_pkt(&pkt, &len)); + ovs_assert(len == strlen(TEST_PAYLOAD)); + ovs_assert(!memcmp(pkt.buf + ESP_PREFIX_LEN, TEST_PAYLOAD, len)); + + /* Too far ahead: authenticated as 2**32 + 7, which fails. */ + ovs_assert(send_seq(out, 3 * wrap + 7) == EBADMSG); + + esp_sad_remove(in); + esp_sa_destroy(in); + esp_sa_destroy(out); + + /* ESN requires an anti-replay window. */ + params.replay_window = 0; + ovs_assert(!esp_sa_create(¶ms)); +} + +static void +test_sad(void) +{ + struct esp_sa_params p1 = make_params(0x4000, TEST_KEY, false, 64); + struct esp_sa_params p2 = make_params(0x4000, TEST_KEY, true, 64); + struct esp_sa_params p3 = make_params(0x4000, TEST_KEY, false, 0); + const ovs_be32 spi = htonl(0x4000); + struct esp_sa *sa1, *sa2, *sa3; + struct in6_addr peer; + + /* An outbound SA with the same SPI, for a peer. */ + in6_addr_set_mapped_ipv4(&peer, htonl(0x01010101)); + p3.dst = peer; + + sa1 = esp_sa_create(&p1); + sa2 = esp_sa_create(&p2); + sa3 = esp_sa_create(&p3); + + ovs_assert(!esp_sa_params_equal(&p1, &p2)); + ovs_assert(!esp_sad_lookup(spi, &in6addr_any)); + ovs_assert(!esp_sad_insert(sa1)); + ovs_assert(esp_sad_insert(sa2) == EEXIST); + ovs_assert(!esp_sad_insert(sa3)); + ovs_assert(esp_sad_lookup(spi, &in6addr_any) == sa1); + ovs_assert(esp_sad_lookup(spi, &peer) == sa3); + esp_sad_replace(sa1, sa2); + ovs_assert(esp_sad_lookup(spi, &in6addr_any) == sa2); + esp_sad_remove(sa2); + ovs_assert(!esp_sad_lookup(spi, &in6addr_any)); + ovs_assert(esp_sad_lookup(spi, &peer) == sa3); + esp_sad_remove(sa3); + ovs_assert(!esp_sad_lookup(spi, &peer)); + + esp_sa_destroy(sa1); + esp_sa_destroy(sa2); + esp_sa_destroy(sa3); +} + +/* A manually keyed SA restarts its sequence numbers with the same key when + * ovs-vswitchd restarts, so its IVs must not depend on OVS's seeded generator: + * two SAs from the same seed must still use different IVs. */ +static void +test_iv_salt(void) +{ + struct esp_sa_params params = make_params(0x1000, TEST_KEY, false, 0); + uint8_t ivs[2][ESP_IV_LEN]; + + for (size_t i = 0; i < ARRAY_SIZE(ivs); i++) { + struct test_pkt pkt; + struct esp_sa *sa; + + random_set_seed(1); + sa = esp_sa_create(¶ms); + ovs_assert(!seal(sa, &pkt, TEST_PAYLOAD, strlen(TEST_PAYLOAD))); + memcpy(ivs[i], pkt.buf + ESP_HEADER_LEN, ESP_IV_LEN); + esp_sa_destroy(sa); + } + ovs_assert(memcmp(ivs[0], ivs[1], ESP_IV_LEN)); +} + +static void +test_esp_main(int argc OVS_UNUSED, char *argv[] OVS_UNUSED) +{ + if (!esp_is_supported()) { + exit(77); + } + + test_parse_key(); + test_known_answer(); + test_round_trip(); + test_replay_window(); + test_esn(); + test_sad(); + test_iv_salt(); +} + +OVSTEST_REGISTER("test-esp", test_esp_main); -- 2.43.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
