Use single pass ChaCha20 setup and session key schedule in sodium backend - #26
Conversation
c301b9b to
bbfc33d
Compare
|
|
esphbot
left a comment
There was a problem hiding this comment.
Tip
No blocking issues found — ready to merge.
bbfc33d to
81e16ac
Compare
|
Addressed the review: the gate is now versioned (SODIUM_ESPHOME_NOISE_FAST_PATH is an integer the port bumps on any semantics change, consumed as >= 1) and NOISE_DISABLE_SODIUM_FAST_PATH forces the fallback. On test reachability, the RFC 7539 A.5 cipherstate vector now runs on a host build linked against the patched port with the fast path confirmed in the binary, and it passes; the reproduction recipe is in the PR description. On the decrypt asymmetry, the counter continuation is a documented contract of the session API (block0_xor always advances past block 0) and the versioned gate is the guard against cross repo drift; an explicit _ic variant would add API surface for one call site, happy to add it if preferred. One correction to the review: the memory note compares against the struct before #20; on current main the cipher state is parent plus a 32 byte key, so holding the 64 byte key schedule grows it by 32 bytes as the description says. |
There was a problem hiding this comment.
Pull request overview
This PR adds a capability-gated fast path in the libsodium ChaCha20-Poly1305 backend for the ESPHome libsodium port, using session-persistent ChaCha20 setup and consolidated MAC computation to reduce per-message fixed overhead while keeping the existing implementation as a fallback for stock libsodium.
Changes:
- Detect ESPHome libsodium support via
sodium/sodium_esphome.hand enable aSODIUM_ESPHOME_NOISE_FAST_PATH-gated implementation (with an opt-out macro). - Cache a session-persistent ChaCha20 key schedule in the cipher state, rewriting only nonce/counter per message.
- Use one-pass cipher processing and a single
crypto_onetimeauth_poly1305_aead_maccall to cover the full transcript.
Suppressed comments (1)
src/backend/sodium/cipher-chachapoly.c:94
- In the fast-path decrypt,
blockcontains the derived Poly1305 key and is not wiped on either the success or MAC-failure path. This increases the lifetime of key material on the stack and differs from the fallback implementation’s explicit scratch wiping. Consider wipingblock(andmac) before returning, including on MAC failure.
uint8_t block[64] __attribute__((aligned(4)));
uint8_t mac[16];
crypto_stream_chacha20_ietf_session_block0_xor
(&(st->chacha_st), block, NULL, NULL, 0, state->n);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/backend/sodium/cipher-chachapoly.c:96
blockis only wiped for the first 32 bytes. The remaining bytes still hold ChaCha20 block-0 keystream derived from the session key, and unlike the fallback path they remain on the stack after return. Wipe the entire block; while here, consider also clearing the computedmacon the success path for consistent cleanup.
crypto_stream_chacha20_ietf_session_block0_xor
(&(st->chacha_st), block, NULL, NULL, 0, state->n);
crypto_onetimeauth_poly1305_aead_mac(mac, ad, ad_len, data, len, block);
sodium_memzero(block, 32);
if (!noise_is_equal(mac, data + len, 16)) {
src/backend/sodium/cipher-chachapoly.c:81
- Only the first 32 bytes of
blockare wiped, but the remaining 32 bytes still contain ChaCha20 block-0 keystream derived from the session key. Consider wiping the full 64-byte block to avoid leaving key-dependent material on the stack (the fallback path wipes the full scratch struct).
This issue also appears on line 92 of the same file.
/* Poly1305 key generation and payload encryption share one cipher
setup; the key in block is cleared as soon as the MAC is done */
crypto_stream_chacha20_ietf_session_block0_xor
(&(st->chacha_st), block, data, data, len, state->n);
crypto_onetimeauth_poly1305_aead_mac
(data + len, ad, ad_len, data, len, block);
sodium_memzero(block, 32);
return NOISE_ERROR_NONE;
46cd39e to
ea8e6a4
Compare
Versioned gate and opt-out confirmed in the diff. Both prior blockers closed. Your memory correction is right; my note was wrong. Verified Decrypt asymmetry: recorded as a note, not a request. No Also checked One small thing: fast path drops the fallback's |
|
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/backend/sodium/cipher-chachapoly.c:95
crypto_stream_chacha20_ietf_session_block0_xor()andcrypto_onetimeauth_poly1305_aead_mac()are invoked with NULL pointers (data/ad) when the corresponding lengths are 0. The Noise API allowsad == NULLwhenad_len == 0, but C APIs commonly assume pointers are non-NULL even for zero-length inputs. Consider always passing non-NULL pointers to avoid undefined behavior if the esphome libsodium implementation doesn’t special-case NULL.
(NoiseCipherState *state, const uint8_t *ad, size_t ad_len,
uint8_t *data, size_t len)
{
NoiseChaChaPolyState *st = (NoiseChaChaPolyState *)state;
src/backend/sodium/cipher-chachapoly.c:80
- In the fast path,
crypto_onetimeauth_poly1305_aead_mac()is called unconditionally withad, but the Noise API allowsad == NULLwhenad_len == 0(seesrc/protocol/cipherstate.c:304-305). To preserve that contract and avoid relying on libsodium accepting NULL pointers for zero-length inputs, pass a non-NULL dummy pointer whenad_lenis 0.
This issue also appears on line 92 of the same file.
uint8_t *data, size_t len)
{
NoiseChaChaPolyState *st = (NoiseChaChaPolyState *)state;
uint8_t block[64] __attribute__((aligned(4)));
c61f4b5 to
922a477
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/backend/sodium/cipher-chachapoly.c:101
crypto_stream_chacha20_ietf_session_block0_xor()is called withNULLinput/output pointers (len=0). Unless that API explicitly documents accepting NULL pointers, this is fragile and can crash if the implementation validates pointers even whenlenis zero. Passing non-NULL pointers keeps the behavior identical while avoiding potential UB.
crypto_stream_chacha20_ietf_session_block0_xor
(&(st->chacha_st), block, NULL, NULL, 0, state->n);
crypto_onetimeauth_poly1305_aead_mac(mac, ad, ad_len, data, len, block);
|
|
esphbot
left a comment
There was a problem hiding this comment.
Warning
Important issues found.
- Versioned gate uses
>= 1, so a semantics-changing port bump does NOT downgrade to the fallback
|
Both taken: the gate is now an exact match (SODIUM_ESPHOME_NOISE_FAST_PATH == 1) so a port version bump really does downgrade this file to the fallback until it is updated deliberately, and the comment says so; the block0_xor NULL in and out with len 0 contract is now documented in the fast path header comment alongside the ad rule. |
Confirmed at One nit left, non-blocking: |
PR Review — Use single pass ChaCha20 setup and session key schedule in sodium backendBoth prior findings are resolved at Verified against the PR head, not just the diff:
Not re-raised: Copilot's wipe-all-64 and ✅ Resolved since last review (1)Previously-flagged issues verified fixed
🟢 Suggestions
1. Exact-match gate now fails silently on a port version bump — consider a visible diagnostic
|
esphbot
left a comment
There was a problem hiding this comment.
Tip
No blocking issues found — ready to merge.
Summary
Adds a fast path for the esphome libsodium port (esphome-libs/libsodium#32) in the sodium ChaChaPoly backend, feature detected via
sodium/sodium_esphome.h; the current implementation stays as the fallback for stock libsodium.The main consumer of this path today is log message streaming over the encrypted API, small messages where the per message fixed cost dominates, which is what these changes target. OTA over noise is planned eventually; that shifts the focus to bulk throughput, where larger noise frames amortize the fixed cost and a hand tuned chacha loop remains as a follow up.
The changes:
crypto_stream_chacha20_ietf_session_state) loaded once ininit_key, instead of running a full key setup twice per message; per message only the nonce and counter words are rewritten.crypto_onetimeauth_poly1305_aead_maccall instead of five dispatched init/update/final calls.Benchmarks
ESP32 (Xtensa LX6, 240 MHz, ESP-IDF),
noise_cipherstate_encryptwith the same pattern asAPINoiseFrameHelper::write_protobuf_messages, together with the libsodium patches:ESP8266 (Xtensa LX106, 80 MHz, Arduino), same benchmark:
Memory
Measured on the same builds: ESP32 flash grows 124 bytes and static RAM is unchanged; ESP8266 flash shrinks 956 bytes and static RAM shrinks 52 bytes (the fast path uses fewer libsodium entry points, so more code is dead stripped). Each cipher state grows 32 bytes (key schedule instead of raw key), 64 bytes per connection. Peak stack per crypto call drops from about 880 to about 528 bytes because the per operation scratch, the stacked cipher context and two wrapper frames are gone.
Verification
The RFC 7539 A.5 ChaChaPoly vector (
tests/unit/test-cipherstate.c, nonzero nonce, nonzero ad, byte exact ciphertext and tag, decrypt round trip and MAC failure paths) passes on a host build linked against the patched libsodium port with the fast path confirmed in the binary; to reproduce, add_subdirectory both repos with the port's patches applied (the port exports thesodiumCMake target that this repo's CMakeLists links) and build the cipherstate test againstnoise_c. An encrypted API session (handshake plus bidirectional traffic) also works on ESP32 and ESP8266; the MAC transcript is unchanged, so it interoperates with existing peers; building against stock libsodium keeps the previous implementation via the capability guard, andNOISE_DISABLE_SODIUM_FAST_PATHforces it.Depends on a libsodium release containing esphome-libs/libsodium#32; the dependency pin bump can follow the usual flow once that ships.