all repos — TinyCrypT @ 18220c954688befc03aacf31bfe51d4bfe3dc1aa

Short and sweet classical cryptographic primitives

Optimized and fixed UB in ChaCha20-Poly1305
Juniper Beatitudes [email protected]
Sat, 04 Jul 2026 13:46:40 -0500
commit

18220c954688befc03aacf31bfe51d4bfe3dc1aa

parent

7c2240df23ac2daabdc18b6de31df5eeb7f7022e

M flake.nixflake.nix

@@ -31,6 +31,7 @@ clang-tools

valgrind bear tokei + cbmc ]; }; teiresia_shl = pkgs.mkShell {
M meson.buildmeson.build

@@ -30,6 +30,10 @@ '-O3',

'-ffreestanding', ] +if target_machine.cpu_family() == 'x86_64' + build_args += '-march=native' +endif + target = static_library('tinycrypt', sources, c_args: build_args, include_directories: incdir) project_dep = declare_dependency(
M test.ctest.c

@@ -1,37 +1,23 @@

-#include "tinycrypt/x25519.h" +#include "tinycrypt/chacha20_poly1305.h" #include <stdint.h> #include <stdio.h> int main (int argc, char **argv) { - const uint8_t SCALAR[] - = { 0x4b, 0x66, 0xe9, 0xd4, 0xd1, 0xb4, 0x67, 0x3c, 0x5a, 0xd2, 0x26, - 0x91, 0x95, 0x7d, 0x6a, 0xf5, 0xc1, 0x1b, 0x64, 0x21, 0xe0, 0xea, - 0x01, 0xd4, 0x2c, 0xa4, 0x16, 0x9e, 0x79, 0x18, 0xba, 0x0d }; - const uint8_t U[] - = { 0xe5, 0x21, 0x0f, 0x12, 0x78, 0x68, 0x11, 0xd3, 0xf4, 0xb7, 0x95, - 0x9d, 0x05, 0x38, 0xae, 0x2c, 0x31, 0xdb, 0xe7, 0x10, 0x6f, 0xc0, - 0x3c, 0x3e, 0xfc, 0x4c, 0xd5, 0x49, 0xc7, 0x15, 0xa4, 0x93 }; - const uint8_t EXPECTED[] - = { 0x95, 0xcb, 0xde, 0x94, 0x76, 0xe8, 0x90, 0x7d, 0x7a, 0xad, 0xe4, - 0x5c, 0xb4, 0xb8, 0x73, 0xf8, 0x8b, 0x59, 0x5a, 0x68, 0x79, 0x9f, - 0xa1, 0x52, 0xe6, 0xf8, 0xf7, 0x64, 0x7a, 0xac, 0x79, 0x57 }; - uint8_t actual[sizeof (EXPECTED)]; - for (unsigned int i = 0; i < 10000; ++i) - { - tct_x25519 (SCALAR, U, actual); - } - for (unsigned int i = 0; i < sizeof (actual); ++i) - { - printf ("%02x", actual[i]); - } - printf ("\n"); - - for (unsigned int i = 0; i < sizeof (actual); ++i) + const uint8_t KEY[] + = { 0x42, 0x90, 0xbc, 0xb1, 0x54, 0x17, 0x35, 0x31, 0xf3, 0x14, 0xaf, + 0x57, 0xf3, 0xbe, 0x3b, 0x50, 0x06, 0xda, 0x37, 0x1e, 0xce, 0x27, + 0x2a, 0xfa, 0x1b, 0x5d, 0xbd, 0xd1, 0x10, 0x0a, 0x10, 0x07 }; + const uint8_t INPUT[128 * 1024]; + const uint8_t NONCE[] = { 0xcd, 0x7c, 0xf6, 0x7b, 0xe3, 0x9c, 0x79, 0x4a }; + const uint8_t AAD[] = { 0x0 }; + uint8_t actual_output[sizeof (INPUT) + 16]; + for (uint64_t i = 0; i < 1024; ++i) { - printf ("%02x", EXPECTED[i]); + tct_aead_chacha20_poly1305_encrypt (AAD, sizeof (AAD), KEY, NONCE, INPUT, + sizeof (INPUT), actual_output, + actual_output + sizeof (INPUT)); } - printf ("\n"); return 0; }
M tests/src/chacha20-poly1305/all.cpptests/src/chacha20-poly1305/all.cpp

@@ -5,6 +5,7 @@ }

#include "gtest/gtest.h" #include <cstdint> #include <cstring> +#include <vector> TEST (ChaCha20_Poly1305, cc20_p1305_full) {

@@ -27,4 +28,161 @@ sizeof (INPUT), actual_output,

actual_output + sizeof (INPUT)); EXPECT_EQ (memcmp (actual_output, EXPECTED_OUTPUT, sizeof (EXPECTED_OUTPUT)), 0); +} + +// Encrypt-then-decrypt round trip across a spread of message lengths. This +// exercises the multi-block keystream paths (the 8-block AVX2 and 4-block SIMD +// cores kick in at 512 and 256 bytes respectively) as well as the +// partial-block tail, none of which the single fixed vector above reaches. +TEST (ChaCha20_Poly1305, round_trip_lengths) +{ + const uint8_t KEY[32] + = { 0x42, 0x90, 0xbc, 0xb1, 0x54, 0x17, 0x35, 0x31, 0xf3, 0x14, 0xaf, + 0x57, 0xf3, 0xbe, 0x3b, 0x50, 0x06, 0xda, 0x37, 0x1e, 0xce, 0x27, + 0x2a, 0xfa, 0x1b, 0x5d, 0xbd, 0xd1, 0x10, 0x0a, 0x10, 0x07 }; + const uint8_t NONCE[8] = { 0xcd, 0x7c, 0xf6, 0x7b, 0xe3, 0x9c, 0x79, 0x4a }; + const uint8_t AAD[10] + = { 0x87, 0xe2, 0x29, 0xd4, 0x50, 0x08, 0x45, 0xa0, 0x79, 0xc0 }; + + // Lengths straddling block (64) and vector-width (256, 512) boundaries. + const size_t lengths[] + = { 0, 1, 63, 64, 65, 127, 128, 255, 256, + 257, 320, 511, 512, 513, 576, 1024, 2048, 4096 }; + + for (size_t aad_len = 0; aad_len <= sizeof (AAD); aad_len += sizeof (AAD)) + { + for (size_t len : lengths) + { + std::vector<uint8_t> plaintext (len), cipher (len), recovered (len); + for (size_t i = 0; i < len; ++i) + plaintext[i] = static_cast<uint8_t> (i * 31 + 7); + + uint8_t mac[16]; + tct_aead_chacha20_poly1305_encrypt (AAD, aad_len, KEY, NONCE, + plaintext.data (), len, + cipher.data (), mac); + + // ciphertext must differ from plaintext where there is data + if (len > 0) + { + EXPECT_NE (memcmp (cipher.data (), plaintext.data (), len), 0) + << "len=" << len; + } + + // frame = ciphertext || 16-byte tag + std::vector<uint8_t> frame (len + 16); + memcpy (frame.data (), cipher.data (), len); + memcpy (frame.data () + len, mac, 16); + + bool ok = tct_aead_chacha20_poly1305_decrypt_and_verify ( + AAD, aad_len, KEY, NONCE, frame.data (), len, recovered.data ()); + EXPECT_TRUE (ok) << "verify failed: aad_len=" << aad_len + << " len=" << len; + EXPECT_EQ (memcmp (recovered.data (), plaintext.data (), len), 0) + << "aad_len=" << aad_len << " len=" << len; + + // a single flipped ciphertext bit must fail verification + if (len > 0) + { + frame[0] ^= 0x01; + EXPECT_FALSE (tct_aead_chacha20_poly1305_decrypt_and_verify ( + AAD, aad_len, KEY, NONCE, frame.data (), len, + recovered.data ())) + << "tamper not detected: len=" << len; + } + } + } +} + +// ---- [email protected] ---- + +static std::vector<uint8_t> +unhex (const char *h) +{ + std::vector<uint8_t> v; + for (size_t i = 0; h[i] && h[i + 1]; i += 2) + { + auto nib = [] (char c) { + return c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10; + }; + v.push_back (static_cast<uint8_t> (nib (h[i]) << 4 | nib (h[i + 1]))); + } + return v; +} + +// build the packet the reference used: 2-byte LE length + 2 zero bytes, then +// payload[i] = i*11 + 1 +static std::vector<uint8_t> +make_packet (uint32_t plen) +{ + std::vector<uint8_t> p (4 + plen); + p[0] = plen & 0xff; + p[1] = (plen >> 8) & 0xff; + for (uint32_t i = 0; i < plen; ++i) + p[4 + i] = static_cast<uint8_t> (i * 11 + 1); + return p; +} + +// Golden vectors from an independent reference implementation that is itself +// anchored against the canonical RFC 8439 ChaCha20 and Poly1305 test vectors. +TEST (ChaCha20_Poly1305, openssh_golden) +{ + auto key = unhex ("030a11181f262d343b424950575e656c737a81888f969da4abb2b9c0" + "c7ced5dce3eaf1f8ff060d141b222930373e454c535a61686f767d84" + "8b9299a0a7aeb5bc"); + auto seq = unhex ("0000000000000005"); + struct + { + uint32_t plen; + const char *out; + } golden[] = { + { 8, "fe5bcdbc9fdefa8cf3de24c316be2bd5af1fdef8f4730379b7fc0df0" }, + { 60, + "ca5bcdbc9fdefa8cf3de24c3f9cfb43fdb24a92f38ad88ebf7209eef95b0a44c" + "f0d8c30674fdaf749bfcd12bb622551bfa0a25bdf1be24d96ef9df544547c240" + "e25732916c0370aea37d9fdb8d374605" }, + }; + for (auto &g : golden) + { + auto packet = make_packet (g.plen); + auto expected = unhex (g.out); + std::vector<uint8_t> out (4 + g.plen + 16); + tct_chacha20_poly1305_openssh_seal (seq.data (), key.data (), + packet.data (), g.plen, out.data ()); + EXPECT_EQ (out, expected) << "seal mismatch, plen=" << g.plen; + } +} + +TEST (ChaCha20_Poly1305, openssh_round_trip) +{ + auto key = unhex ("030a11181f262d343b424950575e656c737a81888f969da4abb2b9c0" + "c7ced5dce3eaf1f8ff060d141b222930373e454c535a61686f767d84" + "8b9299a0a7aeb5bc"); + const uint8_t seq[8] = { 0, 0, 0, 0, 0, 0, 0, 5 }; + + for (uint32_t plen : { 0u, 1u, 8u, 63u, 64u, 65u, 255u, 256u, 512u, 1000u, 4096u }) + { + auto packet = make_packet (plen); + std::vector<uint8_t> frame (4 + plen + 16), recovered (4 + plen); + tct_chacha20_poly1305_openssh_seal (seq, key.data (), packet.data (), + plen, frame.data ()); + + // length field decrypts to the real length + uint8_t declen[4]; + tct_chacha20_poly1305_openssh_decrypt_length (seq, key.data (), + frame.data (), declen); + EXPECT_EQ (memcmp (declen, packet.data (), 4), 0) << "plen=" << plen; + + // open verifies and recovers the packet + EXPECT_TRUE (tct_chacha20_poly1305_openssh_open ( + seq, key.data (), frame.data (), plen, recovered.data ())) + << "plen=" << plen; + EXPECT_EQ (recovered, packet) << "plen=" << plen; + + // any tampered ciphertext byte fails authentication + frame[0] ^= 0x40; + EXPECT_FALSE (tct_chacha20_poly1305_openssh_open ( + seq, key.data (), frame.data (), plen, recovered.data ())) + << "tamper undetected, plen=" << plen; + } }
M tinycrypt/chacha20_poly1305.htinycrypt/chacha20_poly1305.h

@@ -17,4 +17,43 @@ const uint8_t *aad, const uint64_t aad_len, const uint8_t *key,

const uint8_t *nonce, const uint8_t *frame, const uint64_t ciphertext_len, uint8_t *plaintext_out); +/// Seals an SSH packet with the [email protected] cipher (RFC- +/// incompatible; matches OpenSSH's PROTOCOL.chacha20poly1305). +/// +/// \param seqnr 8-byte packet sequence number (the ChaCha20 nonce). +/// \param key 64-byte key: K_2 = key[0..32) encrypts the payload and +/// derives the Poly1305 key, K_1 = key[32..64) encrypts the +/// 4-byte length field. +/// \param packet 4-byte packet length followed by \p payload_len bytes. +/// \param payload_len number of payload bytes after the length field. +/// \param out receives 4 + \p payload_len ciphertext bytes followed by +/// a 16-byte Poly1305 tag. +void tct_chacha20_poly1305_openssh_seal (const uint8_t *seqnr, + const uint8_t *key, + const uint8_t *packet, + uint32_t payload_len, uint8_t *out); + +/// Decrypts only the 4-byte encrypted length field (with K_1), so a reader can +/// size the packet before authenticating it. +void tct_chacha20_poly1305_openssh_decrypt_length (const uint8_t *seqnr, + const uint8_t *key, + const uint8_t *enc_len, + uint8_t *len_out); + +/// Verifies the tag and opens a [email protected] packet. +/// +/// \param frame 4-byte encrypted length, \p payload_len encrypted payload +/// bytes, then the 16-byte tag. +/// \param payload_len payload length (obtain via +/// tct_chacha20_poly1305_openssh_decrypt_length). +/// \param packet_out on success receives 4 + \p payload_len plaintext bytes +/// (length field followed by payload). +/// \return true and writes \p packet_out on success; false (writing nothing) if +/// authentication fails. +bool tct_chacha20_poly1305_openssh_open (const uint8_t *seqnr, + const uint8_t *key, + const uint8_t *frame, + uint32_t payload_len, + uint8_t *packet_out); + #endif
M tinycrypt/x86_64_aarch64/chacha20_poly1305.ctinycrypt/x86_64_aarch64/chacha20_poly1305.c

@@ -4,9 +4,44 @@

#include "tinycrypt/chacha20_poly1305.h" typedef uint32_t dwpacked __attribute__ ((vector_size (16))); +typedef uint8_t bytes16 __attribute__ ((vector_size (16))); +/* On SSSE3 / NEON the 16- and 8-bit left-rotations each lower to a single + byte permute (pshufb / tbl). On plain SSE2 that permute is emulated poorly + and is markedly slower than shift-or, so fall back to shift-or there. The + 12- and 7-bit rotations are always shift-or. */ +#if defined(__SSSE3__) || defined(__ARM_NEON) || defined(__ARM_NEON__) static dwpacked -rotl_32 (dwpacked x, int c) +rotl16 (dwpacked x) +{ + bytes16 b = (bytes16)x; + b = __builtin_shufflevector (b, b, 2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9, 14, + 15, 12, 13); + return (dwpacked)b; +} +static dwpacked +rotl8 (dwpacked x) +{ + bytes16 b = (bytes16)x; + b = __builtin_shufflevector (b, b, 3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, + 12, 13, 14); + return (dwpacked)b; +} +#else +static dwpacked +rotl16 (dwpacked x) +{ + return (x << 16) | (x >> 16); +} +static dwpacked +rotl8 (dwpacked x) +{ + return (x << 8) | (x >> 24); +} +#endif + +static dwpacked +rotl_n (dwpacked x, int c) { return (x << c) | (x >> (32 - c)); }

@@ -30,85 +65,350 @@ u >>= 8;

} } -#define CC20_QR(state, a, b, c, d) \ +/* Quarter-round on whole vectors. The shape is layout agnostic, so the same + macro drives the single-block (row) core and the vertical multi-block + cores below. */ +#define CC20_QR(a, b, c, d) \ do \ { \ - state[a] += state[b]; \ - state[d] ^= state[a]; \ - state[d] = rotl_32 (state[d], 16); \ - state[c] += state[d]; \ - state[b] ^= state[c]; \ - state[b] = rotl_32 (state[b], 12); \ - state[a] += state[b]; \ - state[d] ^= state[a]; \ - state[d] = rotl_32 (state[d], 8); \ - state[c] += state[d]; \ - state[b] ^= state[c]; \ - state[b] = rotl_32 (state[b], 7); \ + a += b; \ + d ^= a; \ + d = rotl16 (d); \ + c += d; \ + b ^= c; \ + b = rotl_n (b, 12); \ + a += b; \ + d ^= a; \ + d = rotl8 (d); \ + c += d; \ + b ^= c; \ + b = rotl_n (b, 7); \ } \ - while (false); + while (false) + +static dwpacked +splat (uint32_t v) +{ + return (dwpacked){ v, v, v, v }; +} + +/* Counter/nonce row in the DJB / OpenSSH layout: a 64-bit block counter in + words 12/13 (little-endian) and a 64-bit nonce in words 14/15. This matches + standard ChaCha20 as used by [email protected]. */ +static dwpacked +cc20_counter_row (uint64_t counter, uint32_t n0, uint32_t n1) +{ + return (dwpacked){ (uint32_t)counter, (uint32_t)(counter >> 32), n0, n1 }; +} + +/* One ChaCha20 block; keystream returned in four vectors (row layout). */ +static void +chacha_core (dwpacked s0, dwpacked s1, dwpacked s2, dwpacked s3, + dwpacked out[4]) +{ + dwpacked x0 = s0, x1 = s1, x2 = s2, x3 = s3; + for (uint32_t i = 0; i < 10; ++i) + { + CC20_QR (x0, x1, x2, x3); + x1 = __builtin_shufflevector (x1, x1, 1, 2, 3, 0); + x2 = __builtin_shufflevector (x2, x2, 2, 3, 0, 1); + x3 = __builtin_shufflevector (x3, x3, 3, 0, 1, 2); + CC20_QR (x0, x1, x2, x3); + x1 = __builtin_shufflevector (x1, x1, 3, 0, 1, 2); + x2 = __builtin_shufflevector (x2, x2, 2, 3, 0, 1); + x3 = __builtin_shufflevector (x3, x3, 1, 2, 3, 0); + } + out[0] = x0 + s0; + out[1] = x1 + s1; + out[2] = x2 + s2; + out[3] = x3 + s3; +} +/* Serializing single-block wrapper, used for the Poly1305 one-time key. */ static void -cc20_block (const uint8_t *key, const uint16_t counter, const uint8_t *nonce, +cc20_block (const uint8_t *key, const uint64_t counter, const uint8_t *nonce, uint8_t *state_out) { - dwpacked initial_state[4] - = { { 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 } }; - initial_state[1] = (dwpacked){ from_le32 (key), from_le32 (key + 4), - from_le32 (key + 8), from_le32 (key + 12) }; - initial_state[2] = (dwpacked){ from_le32 (key + 16), from_le32 (key + 20), - from_le32 (key + 24), from_le32 (key + 28) }; - initial_state[3] = (dwpacked){ counter & 0xff, (counter >> 8) & 0xff, - from_le32 (nonce), from_le32 (nonce + 4) }; - dwpacked state[4]; - for (uint32_t i = 0; i < 4; ++i) + dwpacked s0 = { 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 }; + dwpacked s1 = { from_le32 (key), from_le32 (key + 4), from_le32 (key + 8), + from_le32 (key + 12) }; + dwpacked s2 = { from_le32 (key + 16), from_le32 (key + 20), + from_le32 (key + 24), from_le32 (key + 28) }; + dwpacked s3 + = cc20_counter_row (counter, from_le32 (nonce), from_le32 (nonce + 4)); + dwpacked ks[4]; + chacha_core (s0, s1, s2, s3, ks); + for (unsigned int i = 0; i < 16; ++i) + to_le32 (ks[i / 4][i % 4], state_out + 4 * i); +} + +/* XOR a full 64-byte block; keystream stays in registers. Assumes a + little-endian target (x86-64 / aarch64, the platforms this file targets). */ +static void +cc20_xor64 (const uint8_t *in, const dwpacked ks[4], uint8_t *out) +{ + for (unsigned int k = 0; k < 4; ++k) + { + dwpacked p; + __builtin_memcpy (&p, in + 16 * k, 16); + p ^= ks[k]; + __builtin_memcpy (out + 16 * k, &p, 16); + } +} + +/* Transpose a 4x4 word matrix held in four vectors (rows <-> columns). */ +static void +transpose4 (dwpacked *r0, dwpacked *r1, dwpacked *r2, dwpacked *r3) +{ + dwpacked a0 = __builtin_shufflevector (*r0, *r1, 0, 4, 1, 5); + dwpacked a1 = __builtin_shufflevector (*r0, *r1, 2, 6, 3, 7); + dwpacked a2 = __builtin_shufflevector (*r2, *r3, 0, 4, 1, 5); + dwpacked a3 = __builtin_shufflevector (*r2, *r3, 2, 6, 3, 7); + *r0 = __builtin_shufflevector (a0, a2, 0, 1, 4, 5); + *r1 = __builtin_shufflevector (a0, a2, 2, 3, 6, 7); + *r2 = __builtin_shufflevector (a1, a3, 0, 1, 4, 5); + *r3 = __builtin_shufflevector (a1, a3, 2, 3, 6, 7); +} + +/* Encrypt exactly four blocks held in vertical layout (lane = block) in + v[16]. Removes the per-round diagonalization shuffles the row core needs. */ +static void +cc20_xor_x4 (dwpacked v[16], const uint8_t *pt, uint8_t *out) +{ + dwpacked in[16]; + for (unsigned int k = 0; k < 16; ++k) + in[k] = v[k]; + for (unsigned int r = 0; r < 10; ++r) { - state[i] = initial_state[i]; + CC20_QR (v[0], v[4], v[8], v[12]); + CC20_QR (v[1], v[5], v[9], v[13]); + CC20_QR (v[2], v[6], v[10], v[14]); + CC20_QR (v[3], v[7], v[11], v[15]); + CC20_QR (v[0], v[5], v[10], v[15]); + CC20_QR (v[1], v[6], v[11], v[12]); + CC20_QR (v[2], v[7], v[8], v[13]); + CC20_QR (v[3], v[4], v[9], v[14]); } - for (uint32_t i = 0; i < 10; ++i) + for (unsigned int k = 0; k < 16; ++k) + v[k] += in[k]; + transpose4 (&v[0], &v[1], &v[2], &v[3]); + transpose4 (&v[4], &v[5], &v[6], &v[7]); + transpose4 (&v[8], &v[9], &v[10], &v[11]); + transpose4 (&v[12], &v[13], &v[14], &v[15]); + for (unsigned int j = 0; j < 4; ++j) { - CC20_QR (state, 0, 1, 2, 3); - state[1] = __builtin_shufflevector (state[1], state[1], 1, 2, 3, 0); - state[2] = __builtin_shufflevector (state[2], state[2], 2, 3, 0, 1); - state[3] = __builtin_shufflevector (state[3], state[3], 3, 0, 1, 2); - CC20_QR (state, 0, 1, 2, 3); - state[1] = __builtin_shufflevector (state[1], state[1], 3, 0, 1, 2); - state[2] = __builtin_shufflevector (state[2], state[2], 2, 3, 0, 1); - state[3] = __builtin_shufflevector (state[3], state[3], 1, 2, 3, 0); + const dwpacked ks[4] = { v[j], v[4 + j], v[8 + j], v[12 + j] }; + cc20_xor64 (pt + 64 * j, ks, out + 64 * j); } +} - for (uint32_t i = 0; i < 4; ++i) +#ifdef __AVX2__ +typedef uint32_t qwpacked __attribute__ ((vector_size (32))); +typedef uint8_t bytes32 __attribute__ ((vector_size (32))); + +static qwpacked +rotl16_x8 (qwpacked x) +{ + bytes32 b = (bytes32)x; + b = __builtin_shufflevector (b, b, 2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9, 14, + 15, 12, 13, 18, 19, 16, 17, 22, 23, 20, 21, 26, + 27, 24, 25, 30, 31, 28, 29); + return (qwpacked)b; +} +static qwpacked +rotl8_x8 (qwpacked x) +{ + bytes32 b = (bytes32)x; + b = __builtin_shufflevector (b, b, 3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, + 12, 13, 14, 19, 16, 17, 18, 23, 20, 21, 22, 27, + 24, 25, 26, 31, 28, 29, 30); + return (qwpacked)b; +} +static qwpacked +rotl_n_x8 (qwpacked x, int c) +{ + return (x << c) | (x >> (32 - c)); +} + +#define CC20_QR8(a, b, c, d) \ + do \ + { \ + a += b; \ + d ^= a; \ + d = rotl16_x8 (d); \ + c += d; \ + b ^= c; \ + b = rotl_n_x8 (b, 12); \ + a += b; \ + d ^= a; \ + d = rotl8_x8 (d); \ + c += d; \ + b ^= c; \ + b = rotl_n_x8 (b, 7); \ + } \ + while (false) + +/* AVX2 (per-128-bit-lane) building blocks for an 8x8 word transpose. */ +#define CC20_UNPACKLO32(a, b) \ + __builtin_shufflevector (a, b, 0, 8, 1, 9, 4, 12, 5, 13) +#define CC20_UNPACKHI32(a, b) \ + __builtin_shufflevector (a, b, 2, 10, 3, 11, 6, 14, 7, 15) +#define CC20_UNPACKLO64(a, b) \ + __builtin_shufflevector (a, b, 0, 1, 8, 9, 4, 5, 12, 13) +#define CC20_UNPACKHI64(a, b) \ + __builtin_shufflevector (a, b, 2, 3, 10, 11, 6, 7, 14, 15) +#define CC20_PERM128_LO(a, b) \ + __builtin_shufflevector (a, b, 0, 1, 2, 3, 8, 9, 10, 11) +#define CC20_PERM128_HI(a, b) \ + __builtin_shufflevector (a, b, 4, 5, 6, 7, 12, 13, 14, 15) + +static void +transpose8 (qwpacked r[8]) +{ + qwpacked t0 = CC20_UNPACKLO32 (r[0], r[1]), t1 = CC20_UNPACKHI32 (r[0], r[1]); + qwpacked t2 = CC20_UNPACKLO32 (r[2], r[3]), t3 = CC20_UNPACKHI32 (r[2], r[3]); + qwpacked t4 = CC20_UNPACKLO32 (r[4], r[5]), t5 = CC20_UNPACKHI32 (r[4], r[5]); + qwpacked t6 = CC20_UNPACKLO32 (r[6], r[7]), t7 = CC20_UNPACKHI32 (r[6], r[7]); + qwpacked u0 = CC20_UNPACKLO64 (t0, t2), u1 = CC20_UNPACKHI64 (t0, t2); + qwpacked u2 = CC20_UNPACKLO64 (t1, t3), u3 = CC20_UNPACKHI64 (t1, t3); + qwpacked u4 = CC20_UNPACKLO64 (t4, t6), u5 = CC20_UNPACKHI64 (t4, t6); + qwpacked u6 = CC20_UNPACKLO64 (t5, t7), u7 = CC20_UNPACKHI64 (t5, t7); + r[0] = CC20_PERM128_LO (u0, u4); + r[4] = CC20_PERM128_HI (u0, u4); + r[1] = CC20_PERM128_LO (u1, u5); + r[5] = CC20_PERM128_HI (u1, u5); + r[2] = CC20_PERM128_LO (u2, u6); + r[6] = CC20_PERM128_HI (u2, u6); + r[3] = CC20_PERM128_LO (u3, u7); + r[7] = CC20_PERM128_HI (u3, u7); +} + +static qwpacked +splat8 (uint32_t v) +{ + return (qwpacked){ v, v, v, v, v, v, v, v }; +} + +/* Encrypt exactly eight blocks held in vertical layout in v[16]. */ +static void +cc20_xor_x8 (qwpacked v[16], const uint8_t *pt, uint8_t *out) +{ + qwpacked in[16]; + for (unsigned int k = 0; k < 16; ++k) + in[k] = v[k]; + for (unsigned int r = 0; r < 10; ++r) { - state[i] += initial_state[i]; + CC20_QR8 (v[0], v[4], v[8], v[12]); + CC20_QR8 (v[1], v[5], v[9], v[13]); + CC20_QR8 (v[2], v[6], v[10], v[14]); + CC20_QR8 (v[3], v[7], v[11], v[15]); + CC20_QR8 (v[0], v[5], v[10], v[15]); + CC20_QR8 (v[1], v[6], v[11], v[12]); + CC20_QR8 (v[2], v[7], v[8], v[13]); + CC20_QR8 (v[3], v[4], v[9], v[14]); } - for (unsigned int i = 0; i < 16; ++i) + for (unsigned int k = 0; k < 16; ++k) + v[k] += in[k]; + /* words 0..7 and 8..15 transposed independently -> lane j is block j */ + transpose8 (&v[0]); + transpose8 (&v[8]); + for (unsigned int j = 0; j < 8; ++j) { - to_le32 (state[i / 4][i % 4], state_out + 4 * i); + qwpacked p0, p1; + __builtin_memcpy (&p0, pt + 64 * j, 32); + __builtin_memcpy (&p1, pt + 64 * j + 32, 32); + p0 ^= v[j]; + p1 ^= v[8 + j]; + __builtin_memcpy (out + 64 * j, &p0, 32); + __builtin_memcpy (out + 64 * j + 32, &p1, 32); } } +#endif /* __AVX2__ */ static void -cc20_encrypt (const uint8_t *key, const uint32_t counter, const uint8_t *nonce, +cc20_encrypt (const uint8_t *key, const uint64_t counter, const uint8_t *nonce, const uint8_t *plaintext, const uint32_t plaintext_len, uint8_t *encrypted_out) { - uint8_t key_stream[64]; - for (uint32_t i = 0; i < plaintext_len / 64; ++i) + /* Constant/key/nonce state is invariant across blocks; build it once. */ + const dwpacked s0 = { 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 }; + const dwpacked s1 = { from_le32 (key), from_le32 (key + 4), + from_le32 (key + 8), from_le32 (key + 12) }; + const dwpacked s2 = { from_le32 (key + 16), from_le32 (key + 20), + from_le32 (key + 24), from_le32 (key + 28) }; + const uint32_t n0 = from_le32 (nonce), n1 = from_le32 (nonce + 4); + const uint32_t full = plaintext_len / 64; + uint32_t i = 0; + +#ifdef __AVX2__ + { + const qwpacked b[12] + = { splat8 (s0[0]), splat8 (s0[1]), splat8 (s0[2]), splat8 (s0[3]), + splat8 (s1[0]), splat8 (s1[1]), splat8 (s1[2]), splat8 (s1[3]), + splat8 (s2[0]), splat8 (s2[1]), splat8 (s2[2]), splat8 (s2[3]) }; + const qwpacked bn0 = splat8 (n0), bn1 = splat8 (n1); + for (; i + 8 <= full; i += 8) + { + uint64_t c[8]; + for (unsigned int j = 0; j < 8; ++j) + c[j] = counter + i + j; + qwpacked lo + = { (uint32_t)c[0], (uint32_t)c[1], (uint32_t)c[2], (uint32_t)c[3], + (uint32_t)c[4], (uint32_t)c[5], (uint32_t)c[6], (uint32_t)c[7] }; + qwpacked hi = { (uint32_t)(c[0] >> 32), (uint32_t)(c[1] >> 32), + (uint32_t)(c[2] >> 32), (uint32_t)(c[3] >> 32), + (uint32_t)(c[4] >> 32), (uint32_t)(c[5] >> 32), + (uint32_t)(c[6] >> 32), (uint32_t)(c[7] >> 32) }; + qwpacked v[16] = { b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + b[8], b[9], b[10], b[11], lo, hi, bn0, bn1 }; + cc20_xor_x8 (v, plaintext + 64 * i, encrypted_out + 64 * i); + } + } +#endif + + dwpacked bc[12]; + for (unsigned int j = 0; j < 4; ++j) + { + bc[j] = splat (s0[j]); + bc[4 + j] = splat (s1[j]); + bc[8 + j] = splat (s2[j]); + } + const dwpacked bn0 = splat (n0), bn1 = splat (n1); + for (; i + 4 <= full; i += 4) + { + uint64_t c0 = counter + i, c1 = counter + i + 1, c2 = counter + i + 2, + c3 = counter + i + 3; + dwpacked v[16] + = { bc[0], bc[1], bc[2], bc[3], bc[4], bc[5], bc[6], bc[7], bc[8], + bc[9], bc[10], bc[11], + (dwpacked){ (uint32_t)c0, (uint32_t)c1, (uint32_t)c2, + (uint32_t)c3 }, + (dwpacked){ (uint32_t)(c0 >> 32), (uint32_t)(c1 >> 32), + (uint32_t)(c2 >> 32), (uint32_t)(c3 >> 32) }, + bn0, bn1 }; + cc20_xor_x4 (v, plaintext + 64 * i, encrypted_out + 64 * i); + } + + /* Remaining whole blocks (fewer than the vector width), one at a time. */ + for (; i < full; ++i) { - cc20_block (key, counter + i, nonce, key_stream); - for (uint32_t j = 0; j < 64; ++j) - { - encrypted_out[i * 64 + j] = plaintext[i * 64 + j] ^ key_stream[j]; - } + dwpacked s3 = cc20_counter_row (counter + i, n0, n1); + dwpacked ks[4]; + chacha_core (s0, s1, s2, s3, ks); + cc20_xor64 (plaintext + 64 * i, ks, encrypted_out + 64 * i); } + + /* Trailing partial block. */ if ((plaintext_len % 64) != 0) { - uint32_t i = plaintext_len / 64; - cc20_block (key, counter + i, nonce, key_stream); + dwpacked s3 = cc20_counter_row (counter + full, n0, n1); + dwpacked ks[4]; + chacha_core (s0, s1, s2, s3, ks); + uint8_t key_stream[64]; + for (unsigned int k = 0; k < 16; ++k) + to_le32 (ks[k / 4][k % 4], key_stream + 4 * k); for (uint32_t j = 0; j < plaintext_len % 64; ++j) - { - encrypted_out[i * 64 + j] = plaintext[i * 64 + j] ^ key_stream[j]; - } + encrypted_out[64 * full + j] = plaintext[64 * full + j] ^ key_stream[j]; } }

@@ -337,7 +637,7 @@ buf[i % 16] = frame[i - aad_len - 8];

} else if (i < aad_len + 16 + ciphertext_len) { - buf[i % 16] = ciphertext_len_le[i - aad_len - 16 - ciphertext_len]; + buf[i % 16] = ciphertext_len_le[i - aad_len - 8 - ciphertext_len]; } if (i % 16 == 15) {

@@ -392,7 +692,7 @@ buf[i % 16] = cipher_out[i - aad_len - 8];

} else if (i < aad_len + 16 + plaintext_len) { - buf[i % 16] = plaintext_len_le[i - aad_len - 16 - plaintext_len]; + buf[i % 16] = plaintext_len_le[i - aad_len - 8 - plaintext_len]; } if (i % 16 == 15) {

@@ -408,4 +708,78 @@ for (unsigned int i = 0; i < 2; ++i)

{ to_le64 (mac_chunked[i], mac_out + 8 * i); } -}+} + +/* ------------------------------------------------------------------ */ +/* [email protected] */ +/* */ +/* Unlike the RFC 8439 AEAD above, OpenSSH's SSH transport cipher */ +/* uses two ChaCha20 keys, encrypts the 4-byte packet length */ +/* separately, and authenticates the raw ciphertext with a plain */ +/* Poly1305 (no AAD, no length framing). The 8-byte nonce is the SSH */ +/* packet sequence number. See PROTOCOL.chacha20poly1305. */ +/* ------------------------------------------------------------------ */ + +/* Poly1305 tag over a contiguous message (the standard construction). */ +static void +poly1305_tag (const uint8_t *otk, const uint8_t *msg, uint64_t len, + uint8_t tag[16]) +{ + uint64_t acc[4]; + poly1305_mac_init (acc); + uint64_t i = 0; + for (; i + 16 <= len; i += 16) + poly1305_mac_rolling (msg + i, otk, 16, acc); + if (i < len) + poly1305_mac_rolling (msg + i, otk, (uint8_t)(len - i), acc); + poly1305_mac_finish (acc, otk); + to_le64 (acc[0], tag); + to_le64 (acc[1], tag + 8); +} + +void +tct_chacha20_poly1305_openssh_seal (const uint8_t *seqnr, const uint8_t *key, + const uint8_t *packet, uint32_t payload_len, + uint8_t *out) +{ + const uint8_t *k_payload = key; /* K_2 */ + const uint8_t *k_length = key + 32; /* K_1 */ + uint8_t otk[64]; + cc20_block (k_payload, 0, seqnr, otk); /* Poly1305 key @ ctr 0 */ + cc20_encrypt (k_length, 0, seqnr, packet, 4, out); /* length @ ctr 0 */ + cc20_encrypt (k_payload, 1, seqnr, packet + 4, payload_len, out + 4); + poly1305_tag (otk, out, 4 + (uint64_t)payload_len, out + 4 + payload_len); +} + +void +tct_chacha20_poly1305_openssh_decrypt_length (const uint8_t *seqnr, + const uint8_t *key, + const uint8_t *enc_len, + uint8_t *len_out) +{ + cc20_encrypt (key + 32, 0, seqnr, enc_len, 4, len_out); /* K_1 @ ctr 0 */ +} + +bool +tct_chacha20_poly1305_openssh_open (const uint8_t *seqnr, const uint8_t *key, + const uint8_t *frame, uint32_t payload_len, + uint8_t *packet_out) +{ + const uint8_t *k_payload = key; /* K_2 */ + const uint8_t *k_length = key + 32; /* K_1 */ + uint8_t otk[64]; + cc20_block (k_payload, 0, seqnr, otk); + const uint64_t ct_len = 4 + (uint64_t)payload_len; + uint8_t tag[16]; + poly1305_tag (otk, frame, ct_len, tag); + /* constant-time tag comparison before touching the payload */ + if (((from_le64 (frame + ct_len) ^ from_le64 (tag)) + | (from_le64 (frame + ct_len + 8) ^ from_le64 (tag + 8))) + != 0) + { + return false; + } + cc20_encrypt (k_length, 0, seqnr, frame, 4, packet_out); + cc20_encrypt (k_payload, 1, seqnr, frame + 4, payload_len, packet_out + 4); + return true; +}