From d10008bc4dbc8103141470052e920f9c5de1ed33 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:47:36 +0300 Subject: [PATCH 1/3] cYAML: one non-ASCII byte no longer silences the whole output `char` is signed on x86 and ARM alike, so `*c < 32` in print_string() is true for every byte of a UTF-8 sequence, not just for control characters. Those bytes then fell into the \u expansion below, whose scratch buffer holds ten bytes: 0xe2 read as a negative int expands to eleven bytes with its terminator, so the TRY() fails and the failure propagates all the way out of cYAML_Print(), which returns NULL. The caller then prints nothing at all. Not one mangled string in an otherwise fine document -- no document. `ipctool` in its default output mode exits 0 having said nothing, which reads as "this tool found nothing to report" rather than as a failure, and JSON mode is unaffected, so the two modes disagree about whether the machine has any hardware. This is reachable from ordinary output: sensor names, U-Boot environments and vendor strings are read off the device and passed through verbatim. Compare as `unsigned char`. Bytes >= 128 then take the pass-through path, which is what we want -- YAML is UTF-8 and they need no escaping -- and control characters keep being expanded exactly as before. Two test cases cover both halves. The harness was also computing a per-case verdict and then unconditionally returning 0, so no regression in it could ever fail a build; it now exits non-zero when a case fails. --- src/cjson/cYAML.c | 16 ++++++++++++---- src/cjson/cYAML_test.c | 33 +++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/cjson/cYAML.c b/src/cjson/cYAML.c index ccb05f9..ca5ac1a 100644 --- a/src/cjson/cYAML.c +++ b/src/cjson/cYAML.c @@ -81,9 +81,15 @@ static bool print_string(string_buffer *buf, const char *s) { static const char *ESCAPES = "\"\\\b\f\n\r\t"; static const char *REPLACEMENTS = "\"\\bfnrt"; + /* Compared as unsigned: `char` is signed on x86 and ARM alike, so a plain + * `*c < 32` also catches every byte of a UTF-8 sequence. Those then fell + * into the \u expansion below, overran its 10-byte scratch, and failed the + * whole print — cYAML_Print returned NULL and ipctool printed nothing at + * all rather than one mangled string. Sensor names and U-Boot environments + * do carry non-ASCII, so this was reachable from ordinary output. */ bool needs_escaping = false; for (const char *c = s; *c; c++) { - if (*c < 32 || *c == ':' || index(ESCAPES, *c) != NULL) { + if ((unsigned char)*c < 32 || *c == ':' || index(ESCAPES, *c) != NULL) { needs_escaping = true; break; } @@ -104,10 +110,12 @@ static bool print_string(string_buffer *buf, const char *s) { continue; } - if (*c < 32) { - /* Expand non-printable characters. */ + if ((unsigned char)*c < 32) { + /* Expand non-printable characters. Bytes >= 128 are left alone: + * they are UTF-8, which YAML takes as-is. */ char repl[10]; - TRY(snprintf(repl, sizeof(repl), "\\u%04x", *c) < (int)sizeof(repl)); + TRY(snprintf(repl, sizeof(repl), "\\u%04x", (unsigned char)*c) < + (int)sizeof(repl)); TRY(strbuf_push(buf, repl)); continue; } diff --git a/src/cjson/cYAML_test.c b/src/cjson/cYAML_test.c index 017cd49..eb863b7 100644 --- a/src/cjson/cYAML_test.c +++ b/src/cjson/cYAML_test.c @@ -34,7 +34,9 @@ bool run_test(const char *name, const char *json, const char *wanted) { } int main(int argc, char *argv[]) { - run_test("top-level object", + bool ok = true; + + ok &= run_test("top-level object", "{ " " \"rom\": {" @@ -85,7 +87,7 @@ int main(int argc, char *argv[]) { "- item4\n" ); - run_test("top-level list", + ok &= run_test("top-level list", "[" " \"item1\"," @@ -119,7 +121,7 @@ int main(int argc, char *argv[]) { "- item4\n" ); - run_test("empty objects", + ok &= run_test("empty objects", "{ " " \"object\": {}," @@ -133,5 +135,28 @@ int main(int argc, char *argv[]) { "string: \"\"\n" ); - return 0; + /* Non-ASCII used to take down the whole print, not just one string: + * `char` is signed, so every UTF-8 byte tested as < 32, fell into the + * \u expansion and overran its scratch buffer, and cYAML_Print returned + * NULL. ipctool then emitted nothing at all in its default output mode. + * Sensor names and U-Boot environments do carry non-ASCII, so this was + * reachable in ordinary use. UTF-8 is valid YAML and passes through. */ + ok &= run_test("utf-8 passes through", + + "{ \"note\": \"em dash \\u2014 here\" }", + + "---\n" + "note: em dash \xe2\x80\x94 here\n" + ); + + /* Control characters still get expanded. */ + ok &= run_test("control characters are escaped", + + "{ \"note\": \"bell \\u0007 here\" }", + + "---\n" + "note: \"bell \\u0007 here\"\n" + ); + + return ok ? 0 : 1; } From 8f577618bcb9f400de2bea2b1876abdb27b1c761 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:48:14 +0300 Subject: [PATCH 2/3] cryptobench: AEAD seal rate at packet size, in software and in silicon Closes #186, which asks for a per-SoC probe of AES-128-GCM, AES-256-GCM and ChaCha20-Poly1305 on packet-sized buffers after ChaCha20 measured 2.3x AES-128-GCM on a GK7202V300. Adds `ipctool cryptobench`, beside cpubench and membw, which are the same kind of per-SoC probe. ipctool cryptobench [--json] [--bytes N] [--iters N] [--no-hw] Measured on a lab gk7205v200 (Cortex-A7, 1100-byte packets, majestic stopped), microseconds per packet: | | wall | cpu | MB/s | |---|---|---|---| | AES-128-GCM | 141.4 | 141.4 | 7.4 | | AES-256-GCM | 167.6 | 167.6 | 6.3 | | ChaCha20-Poly1305 | 45.1 | 45.1 | 23.3 | | engine AES-CTR, one packet per ioctl | 83.9 | 44.1 | 12.5 | | engine AES-CTR, 15 packets per ioctl | 49.7 | 12.3 | 21.1 | | engine, 16 bytes (the per-ioctl floor) | 37.4 | 34.1 | - | ChaCha20 wins by 3.14x here, so the reported ordering holds and is wider on this part. Written in C, in-tree. ipctool ships as a static musl binary (CMakeLists.txt:9-12, and release.yml builds with the default), musl's dlopen is a no-op when static, and all three CI toolchains carry only libmbedcrypto.so with no .a -- so the platform mbedTLS is not reachable from the shipped binary and linking it would trade the single static binary for a benchmark. The AES uses four runtime-generated T-tables and GHASH uses 4-bit tables, which is what mbedTLS itself does by default; a compact implementation would be several times slower and would inflate ChaCha20's lead into a number about this file rather than about the silicon. THE HARDWARE HALF REFRAMES THE PROPOSAL. The Cipher engine on HiSilicon and Goke parts cannot seal at all: gen-4 silicon has no GCM or CCM mode, and cryptobench establishes that by configuring a channel for AES-GCM and reporting what the driver answers, rather than by asserting it, so a part that does carry the mode reports itself. What the engine can do is AES-CTR, the confidentiality half; the authentication half would still be GHASH on the CPU. So ChaCha20's advantage on these cameras is not a software artifact waiting to be engineered away -- it is the answer. The engine rows also carry their own lesson, which is why the 16-byte row is there. Nearly half the cost of a 1100-byte packet is the trip, not the cipher, and a burst of 15 under one ioctl (openhisilicon#217) cuts CPU per packet from 44 us to 12 while wall clock barely moves. Three things the numbers depend on, all learned the expensive way: - Both clocks, every row. The hardware rows differ by ~2x between them because the driver sleeps on its completion interrupt and hands the core back; quoting either alone hides the point. cpu_wall near 1.00 on a hardware row means the run was contending for CPU. - Nothing is printed between timed blocks. A single printf to a serial console or an ssh pipe between two identical runs made the second one read twice as slow. - Every primitive is checked against a published vector before it is timed -- GCM test cases 2 and 14, RFC 8439 2.8.2, and for the engine NIST SP 800-38A F.5.1 plus a vector spanning a 32-bit counter wrap. A row that fails is reported unverified with no numbers rather than as a fast wrong answer. The wrap vector is not optional: an engine carrying only the low 32 bits of the counter agrees with software for every IV that is not near a wrap, so it would pass F.5.1, ship, and then disagree on one packet in millions. The /dev/cipher ABI is transcribed rather than linked, as majestic does it: libhi_cipher.so is a thin ioctl wrapper and most images do not ship it, so a DT_NEEDED on it would stop ipctool starting on the very cameras it is meant to inspect. Commands are derived from struct sizes with the vendor's own macro, so a struct that does not match the driver's yields a command it does not recognise rather than one it misreads. Degradation was tested on the board, not just written: no /dev/cipher reports `hardware: absent` and the software half still runs; a driver that rejects the batched command reports `batched: unsupported` and still measures the single-packet and floor rows. Cross-builds clean and still statically linked on all three CI targets, for 17-24 KB of .text (arm32 +17636, arm64 +20328, mips32 +24432). --- CMakeLists.txt | 10 + src/crypto/aes.c | 185 ++++++++++++++ src/crypto/aes.h | 43 ++++ src/crypto/chachapoly.c | 278 +++++++++++++++++++++ src/crypto/chachapoly.h | 24 ++ src/crypto/gcm.c | 131 ++++++++++ src/crypto/gcm.h | 36 +++ src/crypto/hisi_cipher.c | 245 ++++++++++++++++++ src/crypto/hisi_cipher.h | 74 ++++++ src/cryptobench.c | 523 +++++++++++++++++++++++++++++++++++++++ src/cryptobench.h | 6 + src/main.c | 6 + 12 files changed, 1561 insertions(+) create mode 100644 src/crypto/aes.c create mode 100644 src/crypto/aes.h create mode 100644 src/crypto/chachapoly.c create mode 100644 src/crypto/chachapoly.h create mode 100644 src/crypto/gcm.c create mode 100644 src/crypto/gcm.h create mode 100644 src/crypto/hisi_cipher.c create mode 100644 src/crypto/hisi_cipher.h create mode 100644 src/cryptobench.c create mode 100644 src/cryptobench.h diff --git a/CMakeLists.txt b/CMakeLists.txt index aff50df..73029be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,16 @@ set(IPCTOOL_SRC src/clocks.h src/cpubench.c src/cpubench.h + src/crypto/aes.c + src/crypto/aes.h + src/crypto/chachapoly.c + src/crypto/chachapoly.h + src/crypto/gcm.c + src/crypto/gcm.h + src/crypto/hisi_cipher.c + src/crypto/hisi_cipher.h + src/cryptobench.c + src/cryptobench.h src/dns.c src/dns.h src/ethernet.c diff --git a/src/crypto/aes.c b/src/crypto/aes.c new file mode 100644 index 0000000..a256363 --- /dev/null +++ b/src/crypto/aes.c @@ -0,0 +1,185 @@ +/* See aes.h for why this is T-table based and why the tables are generated + * rather than stored. */ + +#include + +#include "aes.h" + +/* Forward S-box, and the four T-tables built from it. FT0 holds + * MixColumns(SubBytes(x)) for one byte position; FT1..FT3 are FT0 rotated, so + * a round is four table lookups and three XORs per column. */ +static uint8_t FSb[256]; +static uint32_t FT0[256], FT1[256], FT2[256], FT3[256]; +static uint32_t RCON[10]; +static int tables_ready = 0; + +#define XTIME(x) (((x) << 1) ^ (((x) & 0x80) ? 0x1B : 0x00)) + +static uint32_t rotl8(uint32_t v) { return (v << 8) | (v >> 24); } + +static void gen_tables(void) { + /* Log/alog over GF(2^8) with generator 3, the usual way to get the + * multiplicative inverse the S-box is defined on. */ + uint8_t pow[256], log[256]; + uint8_t x = 1; + for (int i = 0; i < 256; i++) { + pow[i] = x; + log[x] = (uint8_t)i; + x = (uint8_t)(x ^ XTIME(x)); /* x *= 3 */ + } + + uint32_t rc = 1; + for (int i = 0; i < 10; i++) { + RCON[i] = rc; + rc = (uint32_t)((uint8_t)XTIME((uint8_t)rc)); + } + + FSb[0x00] = 0x63; + for (int i = 1; i < 256; i++) { + uint8_t inv = pow[255 - log[i]]; + uint8_t s = inv; + /* The affine transform: s ^= rotl(s,1..4), then ^ 0x63. */ + uint8_t t = s; + for (int j = 0; j < 4; j++) { + t = (uint8_t)((t << 1) | (t >> 7)); + s ^= t; + } + FSb[i] = (uint8_t)(s ^ 0x63); + } + + for (int i = 0; i < 256; i++) { + uint8_t s = FSb[i]; + uint8_t s2 = (uint8_t)XTIME(s); + uint8_t s3 = (uint8_t)(s2 ^ s); + /* One MixColumns column, little-endian word order to match the + * byte packing in encrypt_block below. */ + FT0[i] = ((uint32_t)s2) | ((uint32_t)s << 8) | ((uint32_t)s << 16) | + ((uint32_t)s3 << 24); + FT1[i] = rotl8(FT0[i]); + FT2[i] = rotl8(FT1[i]); + FT3[i] = rotl8(FT2[i]); + } + tables_ready = 1; +} + +static uint32_t get_u32_le(const uint8_t *p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void put_u32_le(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +int aes_setkey_enc(aes_ctx *ctx, const uint8_t *key, unsigned bits) { + if (!tables_ready) + gen_tables(); + + unsigned nk; + switch (bits) { + case 128: + ctx->rounds = 10; + nk = 4; + break; + case 256: + ctx->rounds = 14; + nk = 8; + break; + default: + return -1; + } + + for (unsigned i = 0; i < nk; i++) + ctx->rk[i] = get_u32_le(key + i * 4); + + const unsigned total = 4u * (unsigned)(ctx->rounds + 1); + for (unsigned i = nk; i < total; i++) { + uint32_t t = ctx->rk[i - 1]; + if (i % nk == 0) { + t = (t >> 8) | (t << 24); /* RotWord */ + t = ((uint32_t)FSb[t & 0xFF]) | + ((uint32_t)FSb[(t >> 8) & 0xFF] << 8) | + ((uint32_t)FSb[(t >> 16) & 0xFF] << 16) | + ((uint32_t)FSb[(t >> 24) & 0xFF] << 24); + t ^= RCON[i / nk - 1]; + } else if (nk > 6 && i % nk == 4) { + t = ((uint32_t)FSb[t & 0xFF]) | + ((uint32_t)FSb[(t >> 8) & 0xFF] << 8) | + ((uint32_t)FSb[(t >> 16) & 0xFF] << 16) | + ((uint32_t)FSb[(t >> 24) & 0xFF] << 24); + } + ctx->rk[i] = ctx->rk[i - nk] ^ t; + } + return 0; +} + +#define AES_FROUND(X0, X1, X2, X3, Y0, Y1, Y2, Y3) \ + do { \ + (X0) = *rk++ ^ FT0[(Y0) & 0xFF] ^ FT1[((Y1) >> 8) & 0xFF] ^ \ + FT2[((Y2) >> 16) & 0xFF] ^ FT3[((Y3) >> 24) & 0xFF]; \ + (X1) = *rk++ ^ FT0[(Y1) & 0xFF] ^ FT1[((Y2) >> 8) & 0xFF] ^ \ + FT2[((Y3) >> 16) & 0xFF] ^ FT3[((Y0) >> 24) & 0xFF]; \ + (X2) = *rk++ ^ FT0[(Y2) & 0xFF] ^ FT1[((Y3) >> 8) & 0xFF] ^ \ + FT2[((Y0) >> 16) & 0xFF] ^ FT3[((Y1) >> 24) & 0xFF]; \ + (X3) = *rk++ ^ FT0[(Y3) & 0xFF] ^ FT1[((Y0) >> 8) & 0xFF] ^ \ + FT2[((Y1) >> 16) & 0xFF] ^ FT3[((Y2) >> 24) & 0xFF]; \ + } while (0) + +void aes_encrypt_block(const aes_ctx *ctx, const uint8_t in[16], + uint8_t out[16]) { + const uint32_t *rk = ctx->rk; + uint32_t x0 = get_u32_le(in) ^ *rk++; + uint32_t x1 = get_u32_le(in + 4) ^ *rk++; + uint32_t x2 = get_u32_le(in + 8) ^ *rk++; + uint32_t x3 = get_u32_le(in + 12) ^ *rk++; + uint32_t y0, y1, y2, y3; + + for (int i = (ctx->rounds >> 1) - 1; i > 0; i--) { + AES_FROUND(y0, y1, y2, y3, x0, x1, x2, x3); + AES_FROUND(x0, x1, x2, x3, y0, y1, y2, y3); + } + AES_FROUND(y0, y1, y2, y3, x0, x1, x2, x3); + + /* Last round: SubBytes + ShiftRows, no MixColumns — so the S-box is read + * out of the T-table's byte lane rather than through a separate table. */ + x0 = *rk++ ^ ((uint32_t)FSb[y0 & 0xFF]) ^ + ((uint32_t)FSb[(y1 >> 8) & 0xFF] << 8) ^ + ((uint32_t)FSb[(y2 >> 16) & 0xFF] << 16) ^ + ((uint32_t)FSb[(y3 >> 24) & 0xFF] << 24); + x1 = *rk++ ^ ((uint32_t)FSb[y1 & 0xFF]) ^ + ((uint32_t)FSb[(y2 >> 8) & 0xFF] << 8) ^ + ((uint32_t)FSb[(y3 >> 16) & 0xFF] << 16) ^ + ((uint32_t)FSb[(y0 >> 24) & 0xFF] << 24); + x2 = *rk++ ^ ((uint32_t)FSb[y2 & 0xFF]) ^ + ((uint32_t)FSb[(y3 >> 8) & 0xFF] << 8) ^ + ((uint32_t)FSb[(y0 >> 16) & 0xFF] << 16) ^ + ((uint32_t)FSb[(y1 >> 24) & 0xFF] << 24); + x3 = *rk++ ^ ((uint32_t)FSb[y3 & 0xFF]) ^ + ((uint32_t)FSb[(y0 >> 8) & 0xFF] << 8) ^ + ((uint32_t)FSb[(y1 >> 16) & 0xFF] << 16) ^ + ((uint32_t)FSb[(y2 >> 24) & 0xFF] << 24); + + put_u32_le(out, x0); + put_u32_le(out + 4, x1); + put_u32_le(out + 8, x2); + put_u32_le(out + 12, x3); +} + +void aes_ctr_xcrypt(aes_ctx *ctx, uint8_t counter[16], uint8_t *buf, + size_t len) { + uint8_t ks[16]; + size_t off = 0; + while (off < len) { + aes_encrypt_block(ctx, counter, ks); + for (int i = 15; i >= 0; i--) + if (++counter[i] != 0) + break; + size_t n = len - off < 16 ? len - off : 16; + for (size_t i = 0; i < n; i++) + buf[off + i] ^= ks[i]; + off += n; + } +} diff --git a/src/crypto/aes.h b/src/crypto/aes.h new file mode 100644 index 0000000..c9ea969 --- /dev/null +++ b/src/crypto/aes.h @@ -0,0 +1,43 @@ +/* AES-128/256 block encryption, encrypt direction only. + * + * GCM and CTR both build their keystream out of forward AES, so the inverse + * cipher and its tables are not here — they would be a third of the code and + * a quarter of the tables for something nothing in ipctool decrypts. + * + * The tables are BUILT AT STARTUP rather than stored, which is what mbedTLS + * does by default (its MBEDTLS_AES_ROM_TABLES is off), and the reason matters + * for what this file is for: `ipctool cryptobench` exists to compare AES + * against ChaCha20 on cores with no AES instructions, so the AES here has to + * be about as fast as the AES a camera actually runs. A compact + * S-box-and-MixColumns implementation would be several times slower and would + * inflate ChaCha20's lead into a number about this file rather than about the + * silicon. Four 1 KB T-tables, generated once, is what the comparison needs. + */ + +#ifndef CRYPTO_AES_H +#define CRYPTO_AES_H + +#include +#include + +#define AES_BLOCK_SIZE 16 +#define AES_MAX_ROUNDS 14 + +typedef struct { + uint32_t rk[4 * (AES_MAX_ROUNDS + 1)]; + int rounds; +} aes_ctx; + +/* `bits` is 128 or 256. Returns 0, or -1 for an unsupported size. */ +int aes_setkey_enc(aes_ctx *ctx, const uint8_t *key, unsigned bits); + +/* One block, in != out is fine and in == out is fine. */ +void aes_encrypt_block(const aes_ctx *ctx, const uint8_t in[16], + uint8_t out[16]); + +/* AES-CTR over `len` bytes, in place, starting from `counter` and advancing + * it over the whole 128-bit block. `counter` is updated. */ +void aes_ctr_xcrypt(aes_ctx *ctx, uint8_t counter[16], uint8_t *buf, + size_t len); + +#endif /* CRYPTO_AES_H */ diff --git a/src/crypto/chachapoly.c b/src/crypto/chachapoly.c new file mode 100644 index 0000000..756afc5 --- /dev/null +++ b/src/crypto/chachapoly.c @@ -0,0 +1,278 @@ +/* ChaCha20-Poly1305 (RFC 8439), seal direction, one shot. See chachapoly.h. */ + +#include + +#include "chachapoly.h" + +/* ---- ChaCha20 ---------------------------------------------------------- */ + +static uint32_t rd_le32(const uint8_t *p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void wr_le32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static uint32_t rotl32(uint32_t v, int n) { return (v << n) | (v >> (32 - n)); } + +#define QR(a, b, c, d) \ + do { \ + a += b; \ + d = rotl32(d ^ a, 16); \ + c += d; \ + b = rotl32(b ^ c, 12); \ + a += b; \ + d = rotl32(d ^ a, 8); \ + c += d; \ + b = rotl32(b ^ c, 7); \ + } while (0) + +static void chacha20_block(const uint32_t in[16], uint8_t out[64]) { + uint32_t x[16]; + memcpy(x, in, sizeof(x)); + for (int i = 0; i < 10; i++) { + QR(x[0], x[4], x[8], x[12]); + QR(x[1], x[5], x[9], x[13]); + QR(x[2], x[6], x[10], x[14]); + QR(x[3], x[7], x[11], x[15]); + QR(x[0], x[5], x[10], x[15]); + QR(x[1], x[6], x[11], x[12]); + QR(x[2], x[7], x[8], x[13]); + QR(x[3], x[4], x[9], x[14]); + } + for (int i = 0; i < 16; i++) + wr_le32(out + i * 4, x[i] + in[i]); +} + +static void chacha20_init(uint32_t st[16], const uint8_t key[32], + const uint8_t nonce[12], uint32_t counter) { + st[0] = 0x61707865; + st[1] = 0x3320646e; + st[2] = 0x79622d32; + st[3] = 0x6b206574; + for (int i = 0; i < 8; i++) + st[4 + i] = rd_le32(key + i * 4); + st[12] = counter; + for (int i = 0; i < 3; i++) + st[13 + i] = rd_le32(nonce + i * 4); +} + +static void chacha20_xor(const uint8_t key[32], const uint8_t nonce[12], + uint32_t counter, uint8_t *buf, size_t len) { + uint32_t st[16]; + uint8_t ks[64]; + chacha20_init(st, key, nonce, counter); + size_t off = 0; + while (off < len) { + chacha20_block(st, ks); + st[12]++; + size_t n = len - off < 64 ? len - off : 64; + for (size_t i = 0; i < n; i++) + buf[off + i] ^= ks[i]; + off += n; + } +} + +/* ---- Poly1305 ---------------------------------------------------------- */ + +/* Five 26-bit limbs, the usual arrangement for a 32-bit core: every partial + * product fits a uint64 and the reduction is a shift and a multiply by 5. */ +typedef struct { + uint32_t r[5]; + uint32_t h[5]; + uint32_t pad[4]; +} poly1305; + +static void poly1305_init(poly1305 *st, const uint8_t key[32]) { + st->r[0] = (rd_le32(key + 0)) & 0x3ffffff; + st->r[1] = (rd_le32(key + 3) >> 2) & 0x3ffff03; + st->r[2] = (rd_le32(key + 6) >> 4) & 0x3ffc0ff; + st->r[3] = (rd_le32(key + 9) >> 6) & 0x3f03fff; + st->r[4] = (rd_le32(key + 12) >> 8) & 0x00fffff; + for (int i = 0; i < 5; i++) + st->h[i] = 0; + for (int i = 0; i < 4; i++) + st->pad[i] = rd_le32(key + 16 + i * 4); +} + +/* Whole 16-byte blocks only. Every block of the AEAD's MAC input is a full + * one — see poly1305_pad_update — so the short-last-block case that plain + * Poly1305 has does not arise here and the high bit is always implied. */ +static void poly1305_blocks(poly1305 *st, const uint8_t *m, size_t bytes) { + const uint32_t hibit = 1u << 24; + const uint32_t r0 = st->r[0], r1 = st->r[1], r2 = st->r[2], r3 = st->r[3], + r4 = st->r[4]; + const uint32_t s1 = r1 * 5, s2 = r2 * 5, s3 = r3 * 5, s4 = r4 * 5; + uint32_t h0 = st->h[0], h1 = st->h[1], h2 = st->h[2], h3 = st->h[3], + h4 = st->h[4]; + + while (bytes >= 16) { + h0 += (rd_le32(m + 0)) & 0x3ffffff; + h1 += (rd_le32(m + 3) >> 2) & 0x3ffffff; + h2 += (rd_le32(m + 6) >> 4) & 0x3ffffff; + h3 += (rd_le32(m + 9) >> 6) & 0x3ffffff; + h4 += (rd_le32(m + 12) >> 8) | hibit; + + uint64_t d0 = (uint64_t)h0 * r0 + (uint64_t)h1 * s4 + + (uint64_t)h2 * s3 + (uint64_t)h3 * s2 + (uint64_t)h4 * s1; + uint64_t d1 = (uint64_t)h0 * r1 + (uint64_t)h1 * r0 + + (uint64_t)h2 * s4 + (uint64_t)h3 * s3 + (uint64_t)h4 * s2; + uint64_t d2 = (uint64_t)h0 * r2 + (uint64_t)h1 * r1 + + (uint64_t)h2 * r0 + (uint64_t)h3 * s4 + (uint64_t)h4 * s3; + uint64_t d3 = (uint64_t)h0 * r3 + (uint64_t)h1 * r2 + + (uint64_t)h2 * r1 + (uint64_t)h3 * r0 + (uint64_t)h4 * s4; + uint64_t d4 = (uint64_t)h0 * r4 + (uint64_t)h1 * r3 + + (uint64_t)h2 * r2 + (uint64_t)h3 * r1 + (uint64_t)h4 * r0; + + uint32_t c = (uint32_t)(d0 >> 26); + h0 = (uint32_t)d0 & 0x3ffffff; + d1 += c; + c = (uint32_t)(d1 >> 26); + h1 = (uint32_t)d1 & 0x3ffffff; + d2 += c; + c = (uint32_t)(d2 >> 26); + h2 = (uint32_t)d2 & 0x3ffffff; + d3 += c; + c = (uint32_t)(d3 >> 26); + h3 = (uint32_t)d3 & 0x3ffffff; + d4 += c; + c = (uint32_t)(d4 >> 26); + h4 = (uint32_t)d4 & 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; + h0 &= 0x3ffffff; + h1 += c; + + m += 16; + bytes -= 16; + } + st->h[0] = h0; + st->h[1] = h1; + st->h[2] = h2; + st->h[3] = h3; + st->h[4] = h4; +} + +static void poly1305_finish(poly1305 *st, uint8_t mac[16]) { + uint32_t h0 = st->h[0], h1 = st->h[1], h2 = st->h[2], h3 = st->h[3], + h4 = st->h[4]; + + uint32_t c = h1 >> 26; + h1 &= 0x3ffffff; + h2 += c; + c = h2 >> 26; + h2 &= 0x3ffffff; + h3 += c; + c = h3 >> 26; + h3 &= 0x3ffffff; + h4 += c; + c = h4 >> 26; + h4 &= 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; + h0 &= 0x3ffffff; + h1 += c; + + /* h + -p, kept only if it did not borrow. */ + uint32_t g0 = h0 + 5; + c = g0 >> 26; + g0 &= 0x3ffffff; + uint32_t g1 = h1 + c; + c = g1 >> 26; + g1 &= 0x3ffffff; + uint32_t g2 = h2 + c; + c = g2 >> 26; + g2 &= 0x3ffffff; + uint32_t g3 = h3 + c; + c = g3 >> 26; + g3 &= 0x3ffffff; + uint32_t g4 = h4 + c - (1u << 26); + + uint32_t mask = (g4 >> 31) - 1; /* all ones when g >= 0 */ + g0 &= mask; + g1 &= mask; + g2 &= mask; + g3 &= mask; + g4 &= mask; + mask = ~mask; + h0 = (h0 & mask) | g0; + h1 = (h1 & mask) | g1; + h2 = (h2 & mask) | g2; + h3 = (h3 & mask) | g3; + h4 = (h4 & mask) | g4; + + /* Back to four 32-bit words, then + pad. */ + h0 = (h0 | (h1 << 26)) & 0xffffffff; + h1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffff; + h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; + h3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffff; + + uint64_t f = (uint64_t)h0 + st->pad[0]; + h0 = (uint32_t)f; + f = (uint64_t)h1 + st->pad[1] + (f >> 32); + h1 = (uint32_t)f; + f = (uint64_t)h2 + st->pad[2] + (f >> 32); + h2 = (uint32_t)f; + f = (uint64_t)h3 + st->pad[3] + (f >> 32); + h3 = (uint32_t)f; + + wr_le32(mac + 0, h0); + wr_le32(mac + 4, h1); + wr_le32(mac + 8, h2); + wr_le32(mac + 12, h3); +} + +/* One field of the MAC input: the data, then zeros to the next 16-byte + * boundary, exactly as RFC 8439 §2.8 pad16(). + * + * This is NOT Poly1305's own handling of a short final block, which appends a + * 0x01 byte and drops the implicit high bit. Getting those two confused is + * invisible on any message that happens to be a multiple of 16 and wrong on + * every other one — which is what the §2.8.2 vector, at 114 bytes, catches. */ +static void poly1305_pad_update(poly1305 *st, const uint8_t *data, size_t len) { + if (len == 0) + return; + size_t whole = len & ~(size_t)15; + if (whole) + poly1305_blocks(st, data, whole); + size_t rem = len - whole; + if (rem) { + uint8_t blk[16] = {0}; + memcpy(blk, data + whole, rem); + poly1305_blocks(st, blk, 16); + } +} + +/* ---- the AEAD ---------------------------------------------------------- */ + +void chachapoly_seal(const uint8_t key[32], const uint8_t nonce[12], + const uint8_t *aad, size_t aad_len, uint8_t *buf, + size_t len, uint8_t tag[16]) { + /* Block zero of the keystream is the one-time Poly1305 key; the data + * starts at block one. */ + uint8_t polykey[64]; + uint32_t st0[16]; + chacha20_init(st0, key, nonce, 0); + chacha20_block(st0, polykey); + + chacha20_xor(key, nonce, 1, buf, len); + + poly1305 p; + poly1305_init(&p, polykey); + poly1305_pad_update(&p, aad, aad_len); + poly1305_pad_update(&p, buf, len); + + uint8_t lenblk[16]; + uint64_t a = aad_len, c = len; + for (int i = 0; i < 8; i++) { + lenblk[i] = (uint8_t)(a >> (8 * i)); + lenblk[8 + i] = (uint8_t)(c >> (8 * i)); + } + poly1305_blocks(&p, lenblk, 16); + poly1305_finish(&p, tag); +} diff --git a/src/crypto/chachapoly.h b/src/crypto/chachapoly.h new file mode 100644 index 0000000..17005b8 --- /dev/null +++ b/src/crypto/chachapoly.h @@ -0,0 +1,24 @@ +/* ChaCha20-Poly1305 sealing, RFC 8439. + * + * The reason this is in ipctool at all: on a core with no AES instructions + * and no carry-less multiply, ChaCha20's 32-bit add-rotate-xor and + * Poly1305's 26-bit-limb arithmetic are both things the CPU is actually good + * at, while AES-GCM needs table lookups for the cipher and 32 more for every + * block of GHASH. That is the comparison `ipctool cryptobench` measures. + * + * Seal only, one shot, which is what a packet-sized benchmark needs. + */ + +#ifndef CRYPTO_CHACHAPOLY_H +#define CRYPTO_CHACHAPOLY_H + +#include +#include + +/* Encrypt `len` bytes in place under the 32-byte key and 12-byte nonce, and + * write the 16-byte tag. `aad` may be NULL when `aad_len` is 0. */ +void chachapoly_seal(const uint8_t key[32], const uint8_t nonce[12], + const uint8_t *aad, size_t aad_len, uint8_t *buf, + size_t len, uint8_t tag[16]); + +#endif /* CRYPTO_CHACHAPOLY_H */ diff --git a/src/crypto/gcm.c b/src/crypto/gcm.c new file mode 100644 index 0000000..1046833 --- /dev/null +++ b/src/crypto/gcm.c @@ -0,0 +1,131 @@ +/* See gcm.h. GHASH follows the 4-bit-table method mbedTLS uses, so the + * throughput this reports is comparable with a camera's own TLS stack. */ + +#include + +#include "gcm.h" + +/* Reduction values for the low nibble shifted out of the accumulator, i.e. + * x^128 + x^7 + x^2 + x + 1 folded back four bits at a time. */ +static const uint16_t last4[16] = { + 0x0000, 0x1c20, 0x3840, 0x2460, 0x7080, 0x6ca0, 0x48c0, 0x54e0, + 0xe100, 0xfd20, 0xd940, 0xc560, 0x9180, 0x8da0, 0xa9c0, 0xb5e0}; + +static uint64_t get_u64_be(const uint8_t *p) { + uint64_t v = 0; + for (int i = 0; i < 8; i++) + v = (v << 8) | p[i]; + return v; +} + +static void put_u64_be(uint8_t *p, uint64_t v) { + for (int i = 7; i >= 0; i--) { + p[i] = (uint8_t)v; + v >>= 8; + } +} + +int gcm_setkey(gcm_ctx *ctx, const uint8_t *key, unsigned bits) { + if (aes_setkey_enc(&ctx->aes, key, bits) != 0) + return -1; + + uint8_t h[16] = {0}; + aes_encrypt_block(&ctx->aes, h, h); + + uint64_t hi = get_u64_be(h); + uint64_t lo = get_u64_be(h + 8); + + ctx->HL[0] = 0; + ctx->HH[0] = 0; + ctx->HH[8] = hi; + ctx->HL[8] = lo; + + for (int i = 4; i > 0; i >>= 1) { + uint32_t t = (uint32_t)(lo & 1) * 0xe1000000u; + lo = (lo >> 1) | (hi << 63); + hi = (hi >> 1) ^ ((uint64_t)t << 32); + ctx->HL[i] = lo; + ctx->HH[i] = hi; + } + for (int i = 2; i <= 8; i *= 2) { + uint64_t vh = ctx->HH[i], vl = ctx->HL[i]; + for (int j = 1; j < i; j++) { + ctx->HH[i + j] = vh ^ ctx->HH[j]; + ctx->HL[i + j] = vl ^ ctx->HL[j]; + } + } + return 0; +} + +/* acc = acc * H over GF(2^128), acc in place. */ +static void ghash_mult(const gcm_ctx *ctx, uint8_t acc[16]) { + uint8_t lo = acc[15] & 0x0f; + uint64_t zh = ctx->HH[lo], zl = ctx->HL[lo]; + + for (int i = 15; i >= 0; i--) { + lo = acc[i] & 0x0f; + uint8_t hi = (uint8_t)((acc[i] >> 4) & 0x0f); + uint8_t rem; + + if (i != 15) { + rem = (uint8_t)(zl & 0x0f); + zl = (zh << 60) | (zl >> 4); + zh = zh >> 4; + zh ^= (uint64_t)last4[rem] << 48; + zh ^= ctx->HH[lo]; + zl ^= ctx->HL[lo]; + } + rem = (uint8_t)(zl & 0x0f); + zl = (zh << 60) | (zl >> 4); + zh = zh >> 4; + zh ^= (uint64_t)last4[rem] << 48; + zh ^= ctx->HH[hi]; + zl ^= ctx->HL[hi]; + } + put_u64_be(acc, zh); + put_u64_be(acc + 8, zl); +} + +static void ghash_update(const gcm_ctx *ctx, uint8_t acc[16], + const uint8_t *data, size_t len) { + while (len > 0) { + size_t n = len < 16 ? len : 16; + for (size_t i = 0; i < n; i++) + acc[i] ^= data[i]; + ghash_mult(ctx, acc); + data += n; + len -= n; + } +} + +void gcm_seal(gcm_ctx *ctx, const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, uint8_t *buf, size_t len, uint8_t tag[16]) { + /* 96-bit nonce: J0 is the nonce with a counter of 1 appended. */ + uint8_t j0[16]; + memcpy(j0, nonce, 12); + j0[12] = 0; + j0[13] = 0; + j0[14] = 0; + j0[15] = 1; + + /* The data runs from counter 2; counter 1 is kept for the tag. */ + uint8_t ctr[16]; + memcpy(ctr, j0, 16); + ctr[15] = 2; + aes_ctr_xcrypt(&ctx->aes, ctr, buf, len); + + uint8_t acc[16] = {0}; + if (aad_len > 0) + ghash_update(ctx, acc, aad, aad_len); + ghash_update(ctx, acc, buf, len); + + uint8_t lenblk[16]; + put_u64_be(lenblk, (uint64_t)aad_len * 8); + put_u64_be(lenblk + 8, (uint64_t)len * 8); + ghash_update(ctx, acc, lenblk, 16); + + uint8_t s[16]; + aes_encrypt_block(&ctx->aes, j0, s); + for (int i = 0; i < 16; i++) + tag[i] = (uint8_t)(s[i] ^ acc[i]); +} diff --git a/src/crypto/gcm.h b/src/crypto/gcm.h new file mode 100644 index 0000000..76c2d48 --- /dev/null +++ b/src/crypto/gcm.h @@ -0,0 +1,36 @@ +/* AES-GCM sealing. + * + * Only the seal (encrypt-and-tag) direction, and only 96-bit nonces — the one + * shape every protocol that matters here uses, and the one the proposal in + * ipctool#186 asks about. + * + * GHASH is the interesting half on these parts. A Cortex-A7 or a MIPS 24K has + * no carry-less multiply, so the GF(2^128) product is done with 4-bit tables + * — 32 table lookups and shifts per block — and it is why AES-GCM loses to + * ChaCha20-Poly1305 on this hardware even though AES itself is not + * catastrophic. Same method mbedTLS uses, so the number is comparable to what + * a camera's own TLS stack would get. + */ + +#ifndef CRYPTO_GCM_H +#define CRYPTO_GCM_H + +#include +#include + +#include "aes.h" + +typedef struct { + aes_ctx aes; + uint64_t HL[16], HH[16]; +} gcm_ctx; + +/* `bits` is 128 or 256. Returns 0, or -1 on an unsupported key size. */ +int gcm_setkey(gcm_ctx *ctx, const uint8_t *key, unsigned bits); + +/* Encrypt `len` bytes in place and write the 16-byte tag. + * `nonce` is 12 bytes. `aad` may be NULL when `aad_len` is 0. */ +void gcm_seal(gcm_ctx *ctx, const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, uint8_t *buf, size_t len, uint8_t tag[16]); + +#endif /* CRYPTO_GCM_H */ diff --git a/src/crypto/hisi_cipher.c b/src/crypto/hisi_cipher.c new file mode 100644 index 0000000..be3c2d6 --- /dev/null +++ b/src/crypto/hisi_cipher.c @@ -0,0 +1,245 @@ +/* See hisi_cipher.h. */ + +#include +#include +#include +#include +#include + +#include "hisi_cipher.h" + +/* ---- the ABI ----------------------------------------------------------- */ + +#define HISI_IOC_W 1U +#define HISI_IOC_R 2U +#define hisi_ioc(dir, nr, size) \ + (((dir) << 30) | ((unsigned)(size) << 16) | (0x4Du << 8) | (unsigned)(nr)) + +/* HI_UNF_CIPHER_* enumerators. */ +#define HISI_ALG_AES 2 +#define HISI_MODE_CTR 4 +#define HISI_MODE_GCM 6 +#define HISI_WIDTH_128 3 +#define HISI_KEYLEN_128 0 +#define HISI_IV_SET 1 +#define HISI_OP_ENCRYPT_VIRT 0x10 + +typedef union { + void *p; + const void *cp; + unsigned long long phy; + unsigned int word[2]; +} hisi_addr; + +typedef struct { + uint32_t id, reserve; +} hisi_symc_create; + +typedef struct { + uint32_t id, reserve; +} hisi_symc_destroy; + +typedef struct { + uint32_t id, hard_key, alg, mode, width, klen, sm1_round_num; + uint8_t fkey[32], skey[32], iv[16]; + uint32_t ivlen, iv_usage, reserve; + hisi_addr aad; + uint32_t alen, tlen; +} hisi_symc_cfg; + +typedef struct { + uint32_t id, len, operation, last; + hisi_addr in, out; +} hisi_symc_encrypt; + +/* OpenIPC extension (openhisilicon baf0af2): a burst under one ioctl with an + * IV per package, at user virtual addresses. */ +typedef struct { + hisi_addr src; + hisi_addr dst; + uint32_t length; + uint32_t reserve; + uint8_t iv[16]; +} hisi_via_pkg; + +typedef struct { + uint32_t id; + hisi_addr pkg; + uint32_t pkg_num; + uint32_t operation; +} hisi_encrypt_via_multi; + +#define HISI_CMD_CREATE hisi_ioc(HISI_IOC_R, 0x00, sizeof(hisi_symc_create)) +#define HISI_CMD_DESTROY hisi_ioc(HISI_IOC_W, 0x01, sizeof(hisi_symc_destroy)) +#define HISI_CMD_CONFIG hisi_ioc(HISI_IOC_W, 0x02, sizeof(hisi_symc_cfg)) +#define HISI_CMD_ENCRYPT hisi_ioc(HISI_IOC_W, 0x03, sizeof(hisi_symc_encrypt)) +#define HISI_CMD_ENCRYPT_VIA_MULTI \ + hisi_ioc(HISI_IOC_W, 0x10, sizeof(hisi_encrypt_via_multi)) + +/* ---- plumbing ---------------------------------------------------------- */ + +/* Scratch for padding a short packet up to the block size. The engine takes + * whole blocks; CTR is a stream cipher, so the padding changes nothing about + * the bytes the caller gets back. */ +static uint8_t g_in[HISI_CIPHER_MAX_LEN + 16] __attribute__((aligned(64))); +static uint8_t g_out[HISI_CIPHER_MAX_LEN + 16] __attribute__((aligned(64))); + +/* musl declares the request as `int`, glibc as `unsigned long`. Every command + * here has bit 31 set (the direction field sits at bit 30), so casting the + * wrong way round matters: `(int)` on glibc would sign-extend to a 64-bit + * request the driver never sees. OpenIPC images are musl, but glibc OEM + * firmware is exactly where someone would want to run this. */ +#ifdef __GLIBC__ +typedef unsigned long ioctl_req_t; +#else +typedef int ioctl_req_t; +#endif + +static int xioctl(int fd, unsigned long req, void *arg) { + int r; + do { + r = ioctl(fd, (ioctl_req_t)req, arg); + } while (r == -1 && errno == EINTR); + return r; +} + +static bool chan_config(hisi_cipher *c, const uint8_t key[16], + const uint8_t iv[16]) { + hisi_symc_cfg cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.id = c->chan; + cfg.alg = HISI_ALG_AES; + cfg.mode = HISI_MODE_CTR; + cfg.width = HISI_WIDTH_128; + cfg.klen = HISI_KEYLEN_128; + memcpy(cfg.fkey, key, 16); + memcpy(cfg.iv, iv, 16); + cfg.ivlen = 16; + cfg.iv_usage = HISI_IV_SET; + return xioctl(c->fd, HISI_CMD_CONFIG, &cfg) == 0; +} + +bool hisi_cipher_ctr(hisi_cipher *c, const uint8_t key[16], + const uint8_t iv[16], uint8_t *buf, size_t len) { + if (c->fd < 0 || len == 0 || len > HISI_CIPHER_MAX_LEN) + return false; + + const size_t padded = (len + 15) / 16 * 16; + memcpy(g_in, buf, len); + if (padded > len) + memset(g_in + len, 0, padded - len); + + if (!chan_config(c, key, iv)) + return false; + + hisi_symc_encrypt e; + memset(&e, 0, sizeof(e)); + e.id = c->chan; + e.len = (uint32_t)padded; + e.operation = HISI_OP_ENCRYPT_VIRT; + e.in.cp = g_in; + e.out.p = g_out; + if (xioctl(c->fd, HISI_CMD_ENCRYPT, &e) != 0) + return false; + + memcpy(buf, g_out, len); + return true; +} + +bool hisi_cipher_ctr_batch(hisi_cipher *c, const uint8_t key[16], + const hisi_cipher_job *jobs, size_t count) { + if (c->fd < 0 || count == 0 || count > HISI_CIPHER_BATCH_MAX) + return false; + + hisi_via_pkg pkg[HISI_CIPHER_BATCH_MAX]; + memset(pkg, 0, sizeof(pkg)); + for (size_t i = 0; i < count; i++) { + if (jobs[i].len == 0 || jobs[i].len > HISI_CIPHER_MAX_LEN) + return false; + pkg[i].src.cp = jobs[i].buf; + pkg[i].dst.p = jobs[i].buf; + pkg[i].length = (uint32_t)jobs[i].len; + memcpy(pkg[i].iv, jobs[i].iv, 16); + } + + /* One config for the whole burst — its IV field is written and then + * ignored, because every package carries its own. */ + if (!chan_config(c, key, pkg[0].iv)) + return false; + + hisi_encrypt_via_multi req; + memset(&req, 0, sizeof(req)); + req.id = c->chan; + req.pkg.p = pkg; + req.pkg_num = (uint32_t)count; + req.operation = 0; + return xioctl(c->fd, HISI_CMD_ENCRYPT_VIA_MULTI, &req) == 0; +} + +bool hisi_cipher_supports_gcm(hisi_cipher *c) { + if (c->fd < 0) + return false; + + /* Configuring the channel is enough to settle it: the driver looks up an + * implementation for the (alg, mode) pair and refuses here when there is + * none, long before any data is submitted. The channel is put back to + * CTR afterwards so a failed probe leaves nothing behind. */ + hisi_symc_cfg cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.id = c->chan; + cfg.alg = HISI_ALG_AES; + cfg.mode = HISI_MODE_GCM; + cfg.width = HISI_WIDTH_128; + cfg.klen = HISI_KEYLEN_128; + cfg.ivlen = 12; + cfg.iv_usage = HISI_IV_SET; + const bool ok = xioctl(c->fd, HISI_CMD_CONFIG, &cfg) == 0; + + uint8_t zero[16] = {0}; + (void)chan_config(c, zero, zero); + return ok; +} + +bool hisi_cipher_open(hisi_cipher *c) { + c->fd = -1; + c->chan = 0; + c->batch_max = 0; + + c->fd = open(HISI_CIPHER_DEV, O_RDWR | O_CLOEXEC); + if (c->fd < 0) + return false; + + hisi_symc_create cr; + memset(&cr, 0, sizeof(cr)); + if (xioctl(c->fd, HISI_CMD_CREATE, &cr) != 0) { + close(c->fd); + c->fd = -1; + return false; + } + c->chan = cr.id; + + /* Probe the batched command by trying it. A driver without it answers + * EINVAL, which is the common case in the field and not a fault. Two + * packets rather than one, because one would also succeed on a driver + * that ignored the per-package IV entirely. */ + uint8_t key[16] = {0}, iv[16] = {0}, a[16] = {0}, b[16] = {0}; + hisi_cipher_job probe[2] = { + {iv, a, sizeof(a)}, + {iv, b, sizeof(b)}, + }; + if (hisi_cipher_ctr_batch(c, key, probe, 2)) + c->batch_max = HISI_CIPHER_BATCH_MAX; + + return true; +} + +void hisi_cipher_close(hisi_cipher *c) { + if (c->fd < 0) + return; + hisi_symc_destroy d; + memset(&d, 0, sizeof(d)); + d.id = c->chan; + (void)xioctl(c->fd, HISI_CMD_DESTROY, &d); + close(c->fd); + c->fd = -1; +} diff --git a/src/crypto/hisi_cipher.h b/src/crypto/hisi_cipher.h new file mode 100644 index 0000000..0f09fde --- /dev/null +++ b/src/crypto/hisi_cipher.h @@ -0,0 +1,74 @@ +/* The HiSilicon/Goke Cipher engine through /dev/cipher. + * + * TRANSCRIBED, NOT LINKED. libhi_cipher.so is a thin wrapper over these + * ioctls and most firmware images do not ship it, so a DT_NEEDED on it would + * stop ipctool starting on the very cameras it is meant to inspect. Going + * straight to the device also keeps the single static binary intact, which + * dlopen would not. + * + * WHAT THE SILICON CAN AND CANNOT DO, because it decides what is worth + * measuring: the gen-4 block does AES in ECB/CBC/CTR/CFB/OFB and carries a + * separate hash engine, a TRNG and RSA — but NOT GCM or CCM. + * CHIP_AES_CCM_GCM_SUPPORT is defined only for hi3569v100 in the vendor + * sources, not for hi3516ev200 or hi3516cv500, so on every part OpenIPC ships + * there is no hardware AEAD to benchmark at all. What can be measured is + * AES-CTR, which is the confidentiality half of AES-GCM; the authentication + * half would still be GHASH on the CPU, and the engine's own hash measured + * five times slower than software. + * + * The command encoding is the vendor's own, not Linux's: direction in the top + * two bits, then the payload size, then type 0x4D and the number. Because the + * size is part of the number, a struct that does not match the driver's + * produces a command it does not recognise rather than one it misreads. + */ + +#ifndef CRYPTO_HISI_CIPHER_H +#define CRYPTO_HISI_CIPHER_H + +#include +#include +#include + +#define HISI_CIPHER_DEV "/dev/cipher" + +/* Matches KAPI_SYMC_BATCH_MAX_PKG / KAPI_SYMC_BATCH_MAX_LEN in the driver. */ +#define HISI_CIPHER_BATCH_MAX 15 +#define HISI_CIPHER_MAX_LEN 2048 + +typedef struct { + const uint8_t *iv; /* 16 bytes */ + uint8_t *buf; + size_t len; +} hisi_cipher_job; + +typedef struct { + int fd; + uint32_t chan; + unsigned batch_max; /* 0 when the driver predates the batched ioctl */ +} hisi_cipher; + +/* Whether the block will accept an AEAD mode, asked rather than assumed. + * + * The answer on every part OpenIPC ships today is no — CHIP_AES_CCM_GCM_SUPPORT + * is defined for hi3569v100 alone — but asking keeps that a measurement, so a + * future part that does carry the mode reports itself instead of inheriting a + * hardcoded "unsupported". */ +bool hisi_cipher_supports_gcm(hisi_cipher *c); + +/* Opens the device and takes a channel. False when the node is absent — the + * ordinary state on most cameras, and not an error. */ +bool hisi_cipher_open(hisi_cipher *c); +void hisi_cipher_close(hisi_cipher *c); + +/* One packet, AES-128-CTR, in place. Lengths that are not a multiple of the + * block are padded internally and only `len` bytes are written back, which is + * invisible in CTR. */ +bool hisi_cipher_ctr(hisi_cipher *c, const uint8_t key[16], + const uint8_t iv[16], uint8_t *buf, size_t len); + +/* A burst under one ioctl, each job with its own IV. False if the driver has + * no batched command, which `batch_max == 0` reports up front. */ +bool hisi_cipher_ctr_batch(hisi_cipher *c, const uint8_t key[16], + const hisi_cipher_job *jobs, size_t count); + +#endif /* CRYPTO_HISI_CIPHER_H */ diff --git a/src/cryptobench.c b/src/cryptobench.c new file mode 100644 index 0000000..6113471 --- /dev/null +++ b/src/cryptobench.c @@ -0,0 +1,523 @@ +/* `ipctool cryptobench` — what crypto this SoC can sustain at packet size. + * + * Asked for in ipctool#186, after ChaCha20-Poly1305 measured 2.3x AES-128-GCM + * on a GK7202V300 — the opposite of the server-side ordering, because these + * cores have no AES instructions and no carry-less multiply. Two things + * decide the answer on this hardware and neither is visible from a bulk + * throughput figure: + * + * - AES-GCM pays for GHASH, which without a carry-less multiply is 32 table + * lookups and shifts per block, on top of an AES that is itself + * table-driven. ChaCha20 and Poly1305 are 32-bit add-rotate-xor and + * 26-bit-limb arithmetic, which is what these cores are good at. + * - Packet-sized buffers, not bulk. At ~1200 bytes the per-call setup is a + * real share of the cost, and that is the size a streamer actually seals. + * + * THE HARDWARE HALF IS THE OTHER HALF OF THE ANSWER. HiSilicon and Goke parts + * carry a Cipher engine on /dev/cipher, and the obvious hope is that it makes + * AES-GCM cheap again. It does not: the gen-4 block has no GCM or CCM mode at + * all, which this probes rather than assumes. What it can do is AES-CTR, the + * confidentiality half — so the engine cannot seal, and the authentication + * half would still be GHASH on the CPU. Measuring AES-CTR anyway is worth it + * because it says how much of AES-GCM could ever be offloaded, and because + * the per-ioctl floor (the 16-byte row) explains why the engine is a poor + * bargain per packet and a good one per burst. + * + * READING THE OUTPUT. Every row carries both clocks. CLOCK_MONOTONIC answers + * "how long did it take"; CLOCK_THREAD_CPUTIME_ID answers "how much core did + * it cost", and on the hardware rows they differ by about a factor of two + * because the driver sleeps on its completion interrupt and hands the core + * back. Quoting one of them alone hides the entire point. `cpu_wall` near + * 1.00 on a hardware row means the run was fighting something else for the + * CPU — stop majestic and try again. + * + * Nothing is printed between timed blocks: buffering the results and printing + * once at the end is not tidiness, it is because a single printf to a serial + * console or an ssh pipe between two identical runs has been measured making + * the second one read twice as slow. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "cjson/cJSON.h" +#include "cjson/cYAML.h" +#include "crypto/chachapoly.h" +#include "crypto/gcm.h" +#include "crypto/hisi_cipher.h" +#include "cryptobench.h" +#include "tools.h" + +/* The engine's own per-packet ceiling, which the software rows share so that + * every row in one run is the same size. */ +#define MAX_PACKET HISI_CIPHER_MAX_LEN + +static double now_wall(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (double)t.tv_sec + (double)t.tv_nsec / 1e9; +} + +static double now_cpu(void) { + struct timespec t; + clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t); + return (double)t.tv_sec + (double)t.tv_nsec / 1e9; +} + +/* Keeps the optimiser from deleting work whose result nothing reads. */ +static volatile uint8_t g_sink; + +/* ---- known answers ----------------------------------------------------- */ + +/* A benchmark of a wrong implementation is worth nothing, and a wrong one is + * invisible from the outside because ciphertext is supposed to look like + * noise. Each primitive is checked against a published vector before it is + * timed, and a row that fails is reported unverified with no numbers rather + * than as a fast wrong answer. + * + * The tag covers the ciphertext in all three constructions, so checking the + * tag checks both halves. */ + +static bool verify_gcm128(void) { + /* GCM spec test case 2: zero key, zero IV, one zero block. */ + static const uint8_t key[16] = {0}; + static const uint8_t nonce[12] = {0}; + static const uint8_t want_ct[16] = {0x03, 0x88, 0xda, 0xce, 0x60, 0xb6, + 0xa3, 0x92, 0xf3, 0x28, 0xc2, 0xb9, + 0x71, 0xb2, 0xfe, 0x78}; + static const uint8_t want_tag[16] = {0xab, 0x6e, 0x47, 0xd4, 0x2c, 0xec, + 0x13, 0xbd, 0xf5, 0x3a, 0x67, 0xb2, + 0x12, 0x57, 0xbd, 0xdf}; + gcm_ctx c; + uint8_t buf[16] = {0}, tag[16]; + if (gcm_setkey(&c, key, 128) != 0) + return false; + gcm_seal(&c, nonce, NULL, 0, buf, sizeof(buf), tag); + return memcmp(buf, want_ct, 16) == 0 && memcmp(tag, want_tag, 16) == 0; +} + +static bool verify_gcm256(void) { + /* GCM spec test case 14: zero 256-bit key, zero IV, one zero block. */ + static const uint8_t key[32] = {0}; + static const uint8_t nonce[12] = {0}; + static const uint8_t want_ct[16] = {0xce, 0xa7, 0x40, 0x3d, 0x4d, 0x60, + 0x6b, 0x6e, 0x07, 0x4e, 0xc5, 0xd3, + 0xba, 0xf3, 0x9d, 0x18}; + static const uint8_t want_tag[16] = {0xd0, 0xd1, 0xc8, 0xa7, 0x99, 0x99, + 0x6b, 0xf0, 0x26, 0x5b, 0x98, 0xb5, + 0xd4, 0x8a, 0xb9, 0x19}; + gcm_ctx c; + uint8_t buf[16] = {0}, tag[16]; + if (gcm_setkey(&c, key, 256) != 0) + return false; + gcm_seal(&c, nonce, NULL, 0, buf, sizeof(buf), tag); + return memcmp(buf, want_ct, 16) == 0 && memcmp(tag, want_tag, 16) == 0; +} + +static bool verify_chachapoly(void) { + /* RFC 8439 section 2.8.2. */ + static const uint8_t key[32] = { + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, + 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f}; + static const uint8_t nonce[12] = {0x07, 0x00, 0x00, 0x00, 0x40, 0x41, + 0x42, 0x43, 0x44, 0x45, 0x46, 0x47}; + static const uint8_t aad[12] = {0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, + 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7}; + static const char pt[] = + "Ladies and Gentlemen of the class of '99: If I could offer you only " + "one tip for the future, sunscreen would be it."; + static const uint8_t want_tag[16] = {0x1a, 0xe1, 0x0b, 0x59, 0x4f, 0x09, + 0xe2, 0x6a, 0x7e, 0x90, 0x2e, 0xcb, + 0xd0, 0x60, 0x06, 0x91}; + uint8_t buf[128], tag[16]; + const size_t len = sizeof(pt) - 1; + memcpy(buf, pt, len); + chachapoly_seal(key, nonce, aad, sizeof(aad), buf, len, tag); + return memcmp(tag, want_tag, 16) == 0; +} + +/* The engine, against NIST SP 800-38A F.5.1 and then across a 32-bit counter + * wrap. The second one is not optional: a block that carries only the low 32 + * bits of the counter agrees with software for every IV that is not near a + * wrap, which is nearly all of them, so it would pass the first vector, ship, + * and then disagree on one packet in millions with nothing to say so. */ +static bool verify_hw_ctr(hisi_cipher *hw) { + static const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, + 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, + 0x09, 0xcf, 0x4f, 0x3c}; + static const uint8_t iv[16] = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, + 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, + 0xfc, 0xfd, 0xfe, 0xff}; + static const uint8_t plain[16] = {0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, + 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, + 0x73, 0x93, 0x17, 0x2a}; + static const uint8_t want[16] = {0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, + 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, + 0x99, 0x0d, 0xb6, 0xce}; + uint8_t buf[16]; + memcpy(buf, plain, sizeof(buf)); + if (!hisi_cipher_ctr(hw, key, iv, buf, sizeof(buf))) + return false; + if (memcmp(buf, want, sizeof(want)) != 0) + return false; + + static const uint8_t wrap_iv[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, + 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, + 0xff, 0xff, 0xff, 0xff}; + static const uint8_t wrap_want[32] = { + 0xa9, 0xd5, 0xcc, 0x9c, 0xa3, 0x90, 0xbf, 0x56, 0x0f, 0x26, 0x0d, + 0x21, 0x31, 0x95, 0xf5, 0xa3, 0xb3, 0x34, 0x2b, 0xcf, 0xf2, 0x44, + 0x50, 0xc2, 0xfa, 0x5c, 0x65, 0x9a, 0x07, 0x96, 0x30, 0x22}; + uint8_t wrap[32]; + memset(wrap, 0, sizeof(wrap)); + if (!hisi_cipher_ctr(hw, key, wrap_iv, wrap, sizeof(wrap))) + return false; + return memcmp(wrap, wrap_want, sizeof(wrap_want)) == 0; +} + +/* ---- rows -------------------------------------------------------------- */ + +struct row { + bool verified; + bool ran; + double wall_us; /* per packet */ + double cpu_us; + double mb_per_sec; /* wall, payload bytes only */ +}; + +/* These are timings, not measurements of a constant: the run-to-run spread is + * percents, so printing a double's full 17 significant digits would suggest a + * precision that is not there and makes the table unreadable when pasted. */ +static double rnd(double v, int places) { + double scale = 1; + for (int i = 0; i < places; i++) + scale *= 10; + return (double)(long long)(v * scale + (v < 0 ? -0.5 : 0.5)) / scale; +} + +static cJSON *row_to_json(const struct row *r) { + cJSON *j_inner = cJSON_CreateObject(); + cJSON_AddItemToObject(j_inner, "verified", cJSON_CreateBool(r->verified)); + if (!r->ran) + return j_inner; + ADD_PARAM_NUM("wall_us", rnd(r->wall_us, 3)); + ADD_PARAM_NUM("cpu_us", rnd(r->cpu_us, 3)); + ADD_PARAM_NUM("cpu_wall", + rnd(r->wall_us > 0 ? r->cpu_us / r->wall_us : 0, 3)); + ADD_PARAM_NUM("mb_per_sec", rnd(r->mb_per_sec, 2)); + return j_inner; +} + +static cJSON *batch_row_json(const struct row *r, unsigned depth) { + cJSON *j_inner = row_to_json(r); + ADD_PARAM_NUM("depth", depth); + return j_inner; +} + +static cJSON *batch_absent_json(void) { + cJSON *j_inner = cJSON_CreateObject(); + ADD_PARAM("status", "unsupported"); + ADD_PARAM("note", + "driver predates the batched ioctl (OpenIPC/openhisilicon#217)"); + return j_inner; +} + +static void finish_row(struct row *r, double w0, double c0, double w1, + double c1, unsigned iters, size_t bytes) { + const double wall = (w1 - w0) / iters; + const double cpu = (c1 - c0) / iters; + r->ran = true; + r->wall_us = wall * 1e6; + r->cpu_us = cpu * 1e6; + r->mb_per_sec = wall > 0 ? (double)bytes / wall / 1048576.0 : 0; +} + +/* ---- software ---------------------------------------------------------- */ + +static void bench_gcm(struct row *r, unsigned bits, uint8_t *buf, size_t bytes, + unsigned iters) { + r->verified = (bits == 128) ? verify_gcm128() : verify_gcm256(); + if (!r->verified) + return; + + uint8_t key[32]; + memset(key, 0x2b, sizeof(key)); + uint8_t nonce[12]; + memset(nonce, 0x07, sizeof(nonce)); + uint8_t tag[16]; + gcm_ctx c; + gcm_setkey(&c, key, bits); + + const double w0 = now_wall(), c0 = now_cpu(); + for (unsigned i = 0; i < iters; i++) + gcm_seal(&c, nonce, NULL, 0, buf, bytes, tag); + const double c1 = now_cpu(), w1 = now_wall(); + g_sink = tag[0]; + finish_row(r, w0, c0, w1, c1, iters, bytes); +} + +static void bench_chachapoly(struct row *r, uint8_t *buf, size_t bytes, + unsigned iters) { + r->verified = verify_chachapoly(); + if (!r->verified) + return; + + uint8_t key[32]; + memset(key, 0x2b, sizeof(key)); + uint8_t nonce[12]; + memset(nonce, 0x07, sizeof(nonce)); + uint8_t tag[16]; + + const double w0 = now_wall(), c0 = now_cpu(); + for (unsigned i = 0; i < iters; i++) + chachapoly_seal(key, nonce, NULL, 0, buf, bytes, tag); + const double c1 = now_cpu(), w1 = now_wall(); + g_sink = tag[0]; + finish_row(r, w0, c0, w1, c1, iters, bytes); +} + +/* ---- hardware ---------------------------------------------------------- */ + +static void bench_hw_single(struct row *r, hisi_cipher *hw, uint8_t *buf, + size_t bytes, unsigned iters) { + static const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, + 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, + 0x09, 0xcf, 0x4f, 0x3c}; + uint8_t iv[16]; + memset(iv, 0x11, sizeof(iv)); + + const double w0 = now_wall(), c0 = now_cpu(); + for (unsigned i = 0; i < iters; i++) { + if (!hisi_cipher_ctr(hw, key, iv, buf, bytes)) { + r->ran = false; + return; + } + } + const double c1 = now_cpu(), w1 = now_wall(); + g_sink = buf[0]; + finish_row(r, w0, c0, w1, c1, iters, bytes); +} + +static void bench_hw_batch(struct row *r, hisi_cipher *hw, size_t bytes, + unsigned iters, unsigned depth) { + static const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, + 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, + 0x09, 0xcf, 0x4f, 0x3c}; + static uint8_t bufs[HISI_CIPHER_BATCH_MAX][MAX_PACKET]; + static uint8_t ivs[HISI_CIPHER_BATCH_MAX][16]; + hisi_cipher_job jobs[HISI_CIPHER_BATCH_MAX]; + + for (unsigned i = 0; i < depth; i++) { + memset(bufs[i], (int)i + 1, bytes); + memset(ivs[i], (int)i + 0x40, sizeof(ivs[i])); + jobs[i].iv = ivs[i]; + jobs[i].buf = bufs[i]; + jobs[i].len = bytes; + } + + const unsigned rounds = iters / depth ? iters / depth : 1; + const double w0 = now_wall(), c0 = now_cpu(); + for (unsigned i = 0; i < rounds; i++) { + if (!hisi_cipher_ctr_batch(hw, key, jobs, depth)) { + r->ran = false; + return; + } + } + const double c1 = now_cpu(), w1 = now_wall(); + g_sink = bufs[0][0]; + /* Per packet, not per call — that is the number the software rows are in. + */ + finish_row(r, w0, c0, w1, c1, rounds * depth, bytes); +} + +static cJSON *build_hw_json(size_t bytes, unsigned iters) { + hisi_cipher hw; + if (!hisi_cipher_open(&hw)) { + cJSON *j_inner = cJSON_CreateObject(); + ADD_PARAM("device", "absent"); + ADD_PARAM("note", "no " HISI_CIPHER_DEV + " — no cipher engine, or its module is not loaded"); + return j_inner; + } + + const bool aead = hisi_cipher_supports_gcm(&hw); + + cJSON *j_inner = cJSON_CreateObject(); + ADD_PARAM("device", HISI_CIPHER_DEV); + ADD_PARAM("aead", aead ? "supported" : "unsupported"); + if (!aead) + ADD_PARAM("aead_note", + "block refuses AES-GCM, so there is no hardware sealing on " + "this part and only the AES-CTR half could be offloaded"); + + struct row single = {false, false, 0, 0, 0}; + struct row floor = {false, false, 0, 0, 0}; + struct row batch = {false, false, 0, 0, 0}; + + single.verified = verify_hw_ctr(&hw); + floor.verified = single.verified; + batch.verified = single.verified; + + if (single.verified) { + static uint8_t buf[MAX_PACKET]; + memset(buf, 0x5a, sizeof(buf)); + bench_hw_single(&single, &hw, buf, bytes, iters); + /* One block through the same path: nearly all of it is the syscall + * and the channel programming, so this is the fixed cost per trip + * and the number to reason with before writing any code against + * this engine. */ + bench_hw_single(&floor, &hw, buf, 16, iters); + if (hw.batch_max > 0) + bench_hw_batch(&batch, &hw, bytes, iters, hw.batch_max); + } + + cJSON *ctr = cJSON_CreateObject(); + cJSON_AddItemToObject(ctr, "single", row_to_json(&single)); + cJSON_AddItemToObject(ctr, "ioctl_floor_16b", row_to_json(&floor)); + cJSON_AddItemToObject(ctr, "batched", + hw.batch_max > 0 + ? batch_row_json(&batch, hw.batch_max) + : batch_absent_json()); + cJSON_AddItemToObject(j_inner, "aes_128_ctr", ctr); + + hisi_cipher_close(&hw); + return j_inner; +} + +/* ---- the command ------------------------------------------------------- */ + +static cJSON *software_json(const struct row *aes128, const struct row *aes256, + const struct row *chacha) { + cJSON *j_inner = cJSON_CreateObject(); + cJSON_AddItemToObject(j_inner, "aes_128_gcm", row_to_json(aes128)); + cJSON_AddItemToObject(j_inner, "aes_256_gcm", row_to_json(aes256)); + cJSON_AddItemToObject(j_inner, "chacha20_poly1305", row_to_json(chacha)); + if (aes128->ran && chacha->ran && chacha->wall_us > 0) + /* The ratio ipctool#186 is about: above 1 means ChaCha20 wins. */ + ADD_PARAM_NUM("chacha_vs_aes128", + rnd(aes128->wall_us / chacha->wall_us, 2)); + return j_inner; +} + +static cJSON *build_cryptobench_json(size_t bytes, unsigned iters, + bool want_hw) { + static uint8_t buf[MAX_PACKET]; + memset(buf, 0x5a, sizeof(buf)); + + struct row aes128 = {false, false, 0, 0, 0}; + struct row aes256 = {false, false, 0, 0, 0}; + struct row chacha = {false, false, 0, 0, 0}; + + bench_gcm(&aes128, 128, buf, bytes, iters); + bench_gcm(&aes256, 256, buf, bytes, iters); + bench_chachapoly(&chacha, buf, bytes, iters); + + cJSON *j_inner = cJSON_CreateObject(); + ADD_PARAM_NUM("packet_bytes", (double)bytes); + ADD_PARAM_NUM("iters", (double)iters); + + cJSON_AddItemToObject(j_inner, "software", + software_json(&aes128, &aes256, &chacha)); + + if (want_hw) + cJSON_AddItemToObject(j_inner, "hardware", build_hw_json(bytes, iters)); + return j_inner; +} + +static void print_cryptobench_usage(void) { + printf( + "Usage: ipctool cryptobench [--json] [--bytes N] [--iters N] " + "[--no-hw]\n" + "\n" + "Seal throughput at packet size, in software and on the SoC's cipher\n" + "engine where it has one.\n" + "\n" + "Software rows are AES-128-GCM, AES-256-GCM and ChaCha20-Poly1305,\n" + "each checked against a published test vector before it is timed; a\n" + "row that fails its vector is reported unverified with no numbers.\n" + "\n" + "Hardware rows come from /dev/cipher and are AES-CTR only, because\n" + "the gen-4 HiSilicon/Goke block has no GCM or CCM mode — which the\n" + "probe asks the driver rather than assuming. The 16-byte row is the\n" + "per-ioctl floor: nearly all of it is syscall and channel setup, so\n" + "it is the number to reason with before building on this engine.\n" + "\n" + "Every row reports both clocks. cpu_wall well below 1 on a hardware\n" + "row is the driver sleeping on its completion interrupt and handing\n" + "the core back, which is the point of it; cpu_wall near 1 there means\n" + "the run was contending for CPU — stop majestic and repeat.\n" + "\n" + "Output is YAML by default; --json emits JSON.\n" + "Defaults: --bytes 1200 (a packet, not bulk), --iters 20000.\n"); +} + +int cryptobench_cmd(int argc, char **argv) { + bool want_json = false, want_hw = true; + size_t bytes = 1200; + unsigned iters = 20000; + + const struct option long_options[] = { + {"json", no_argument, NULL, 'j'}, + {"bytes", required_argument, NULL, 'b'}, + {"iters", required_argument, NULL, 'i'}, + {"no-hw", no_argument, NULL, 'n'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0}, + }; + int opt; + optind = 1; + while ((opt = getopt_long(argc, argv, "jb:i:nh", long_options, NULL)) != + -1) { + switch (opt) { + case 'j': + want_json = true; + break; + case 'b': + bytes = (size_t)strtoul(optarg, NULL, 10); + if (bytes < 16 || bytes > MAX_PACKET) { + fprintf(stderr, + "cryptobench: --bytes must be 16..%d (the engine's " + "own limit)\n", + MAX_PACKET); + return EXIT_FAILURE; + } + break; + case 'i': + iters = (unsigned)strtoul(optarg, NULL, 10); + if (iters < 100) { + fprintf(stderr, "cryptobench: --iters must be >= 100\n"); + return EXIT_FAILURE; + } + break; + case 'n': + want_hw = false; + break; + case 'h': + print_cryptobench_usage(); + return EXIT_SUCCESS; + default: + print_cryptobench_usage(); + return EXIT_FAILURE; + } + } + + cJSON *bench = build_cryptobench_json(bytes, iters, want_hw); + cJSON *root = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "cryptobench", bench); + + char *out = want_json ? cJSON_Print(root) : cYAML_Print(root); + if (out) { + printf("%s", out); + if (want_json) + printf("\n"); + free(out); + } + cJSON_Delete(root); + return EXIT_SUCCESS; +} diff --git a/src/cryptobench.h b/src/cryptobench.h new file mode 100644 index 0000000..9718e12 --- /dev/null +++ b/src/cryptobench.h @@ -0,0 +1,6 @@ +#ifndef CRYPTOBENCH_H +#define CRYPTOBENCH_H + +int cryptobench_cmd(int argc, char **argv); + +#endif /* CRYPTOBENCH_H */ diff --git a/src/main.c b/src/main.c index 75294d2..11dc1af 100644 --- a/src/main.c +++ b/src/main.c @@ -19,6 +19,7 @@ #include "cjson/cYAML.h" #include "clocks.h" #include "cpubench.h" +#include "cryptobench.h" #include "ethernet.h" #include "firmware.h" #include "hal/hisi/hal_hisi.h" @@ -109,6 +110,9 @@ void print_usage() { " cpubench [--json] [--loops N]\n" " triangulate CPU clock by running three\n" " tight inline-asm patterns (ARM only)\n" + " cryptobench [--json] [--bytes N] [--iters N] [--no-hw]\n" + " AEAD seal rate at packet size, in\n" + " software and on the SoC cipher engine\n" " membw [--size MB] [--iters N] [--ops set,...] [--json]\n" " DDR bandwidth probe (memset / read scan " "/\n" @@ -195,6 +199,8 @@ int main(int argc, char *argv[]) { return clocks_cmd(argc - 1, argv + 1); else if (!strcmp(argv[1], "cpubench")) return cpubench_cmd(argc - 1, argv + 1); + else if (!strcmp(argv[1], "cryptobench")) + return cryptobench_cmd(argc - 1, argv + 1); else if (!strcmp(argv[1], "membw")) return membw_cmd(argc - 1, argv + 1); else if (!strcmp(argv[1], "bootrom")) From ecca9ce4af94ef360f08d8d28137597972f6c377 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:01:58 +0300 Subject: [PATCH 3/3] cryptobench: verify the batched path, and four smaller review fixes The batch probe was the real defect. Its own comment claimed it used "two packets rather than one, because one would also succeed on a driver that ignored the per-package IV entirely" -- and then submitted two packets with the SAME zero IV and never looked at the output. It checked that the ioctl existed and nothing else, while the row it enabled inherited `verified` from the single-packet vector, so a driver that accepted the burst and applied the channel IV to all fifteen packages would have been reported as a verified 21 MB/s. There is now a known-answer test for the batched command specifically: fifteen packages, each with a distinct IV and distinct plaintext, every one compared against the software AES-CTR in this same binary, which F.5.1 has already pinned to the standard. `batched.verified` stands on that vector rather than inheriting, and the row is not measured unless it passes. On a gk7205v200 all fifteen match, so openhisilicon#217 does honour per-package IVs -- previously assumed, now checked. That test also settles a question raised in review about the batch path handing unrounded lengths to the driver while the single-packet path pads: it runs at the benchmark's own length, which is 1100 bytes by default on that board and not a multiple of the block. It passes, so the driver rounds the descriptor itself and the two paths agree. Padding in the batch path would need a bounce buffer per package and would make the benchmark measure that copy instead of the engine; the asymmetry is now documented as deliberate rather than left to look like an oversight. Also: - Tag the output with `chip:` from getchipname(), as membw does. #186 asks for a table with a row per SoC, and a row of numbers that does not say which SoC is not a row. - `--bytes`/`--iters` went through strtoul() with no endptr and no errno check, then narrowed to unsigned before a minimum-only bound. `--iters -1` became UINT_MAX and `--iters 100junk` silently became 100. Require the whole argument to parse and range-check before narrowing. - A part that does accept AES-GCM reported `aead: supported` and then measured only AES-CTR. No such part was available to test against -- CHIP_AES_CCM_GCM_SUPPORT is hi3569v100 alone -- so rather than ship a sealing path that has never run, it now reports "supported, not measured" and asks for an issue. An unverifiable number is worse than a missing one. - cYAML: validate UTF-8 rather than assuming every byte >= 128 is part of a well-formed sequence. U-Boot environments reach the printer as raw flash bytes, so latin-1 or truncated input could produce a stream that no parser accepts. Valid sequences still pass through unescaped; invalid bytes, overlong forms, surrogates and out-of-range code points become \u00XX, which round-trips losslessly. One more test. Not changed: review also asked that `--bytes` be constrained so a run cannot report a size other than ~1200. Choosing the size is the feature -- the default is 1200, every run states its `packet_bytes`, and the 16-byte row exists precisely to show how much of the cost is per-call. A benchmark that refuses to measure what it is asked to measure would answer a narrower question than #186 poses. --- src/cjson/cYAML.c | 93 +++++++++++++++++++---- src/cjson/cYAML_test.c | 11 +++ src/crypto/hisi_cipher.c | 19 ++++- src/cryptobench.c | 158 ++++++++++++++++++++++++++++++++------- 4 files changed, 234 insertions(+), 47 deletions(-) diff --git a/src/cjson/cYAML.c b/src/cjson/cYAML.c index ca5ac1a..8ea0f17 100644 --- a/src/cjson/cYAML.c +++ b/src/cjson/cYAML.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,50 @@ static bool strbuf_push(string_buffer *buf, const char *s) { return true; } +/* Length of the well-formed UTF-8 sequence starting at `c`, or 0 if what is + * there is not one. + * + * ipctool does not get to assume its input is UTF-8: U-Boot environments, + * sensor names and vendor strings are read as raw bytes off flash and passed + * through untranscoded, so a high byte may be latin-1, a truncated sequence, + * or nothing in particular. Valid sequences must survive unescaped (that is + * the point of a UTF-8 document format); invalid ones must not reach the + * output, or the whole stream stops parsing because of one bad byte in one + * value. Rejecting overlong forms, surrogates and out-of-range code points + * matters for the same reason -- a parser is entitled to refuse them. */ +static int utf8_seq_len(const unsigned char *c) { + if (c[0] < 0x80) + return 1; + + unsigned len; + uint32_t cp; + if ((c[0] & 0xe0) == 0xc0) { + len = 2; + cp = c[0] & 0x1fu; + } else if ((c[0] & 0xf0) == 0xe0) { + len = 3; + cp = c[0] & 0x0fu; + } else if ((c[0] & 0xf8) == 0xf0) { + len = 4; + cp = c[0] & 0x07u; + } else { + return 0; /* continuation byte or 0xfe/0xff, never a lead */ + } + + for (unsigned i = 1; i < len; i++) { + if ((c[i] & 0xc0) != 0x80) + return 0; /* also catches the terminator: 0 is not a continuation */ + cp = (cp << 6) | (c[i] & 0x3fu); + } + + static const uint32_t min_cp[5] = {0, 0, 0x80, 0x800, 0x10000}; + if (cp < min_cp[len] || cp > 0x10ffff) + return 0; /* overlong, or past the last code point */ + if (cp >= 0xd800 && cp <= 0xdfff) + return 0; /* surrogate half, not a scalar value */ + return (int)len; +} + static bool print_string(string_buffer *buf, const char *s) { if (!s || !*s) { TRY(strbuf_push(buf, "\"\"")); @@ -81,18 +126,20 @@ static bool print_string(string_buffer *buf, const char *s) { static const char *ESCAPES = "\"\\\b\f\n\r\t"; static const char *REPLACEMENTS = "\"\\bfnrt"; - /* Compared as unsigned: `char` is signed on x86 and ARM alike, so a plain - * `*c < 32` also catches every byte of a UTF-8 sequence. Those then fell - * into the \u expansion below, overran its 10-byte scratch, and failed the - * whole print — cYAML_Print returned NULL and ipctool printed nothing at - * all rather than one mangled string. Sensor names and U-Boot environments - * do carry non-ASCII, so this was reachable from ordinary output. */ + /* Every test here compares as `unsigned char`. `char` is signed on x86 and + * ARM alike, so a plain `*c < 32` was also true for every byte of a UTF-8 + * sequence; those fell into the \u expansion below, overran its 10-byte + * scratch, and failed the whole print -- cYAML_Print returned NULL and + * ipctool printed nothing at all rather than one mangled string. */ bool needs_escaping = false; - for (const char *c = s; *c; c++) { - if ((unsigned char)*c < 32 || *c == ':' || index(ESCAPES, *c) != NULL) { + for (const unsigned char *c = (const unsigned char *)s; *c;) { + const int seq = utf8_seq_len(c); + if (seq == 0 || *c < 32 || *c == ':' || + index(ESCAPES, (char)*c) != NULL) { needs_escaping = true; break; } + c += seq; } if (!needs_escaping) { @@ -101,28 +148,44 @@ static bool print_string(string_buffer *buf, const char *s) { } TRY(strbuf_push(buf, "\"")); - for (const char *c = s; *c; c++) { - char *found = index(ESCAPES, *c); + for (const unsigned char *c = (const unsigned char *)s; *c;) { + char *found = *c < 0x80 ? index(ESCAPES, (char)*c) : NULL; if (found != NULL) { char repl[] = "\\_"; repl[1] = REPLACEMENTS[found - ESCAPES]; TRY(strbuf_push(buf, repl)); + c++; + continue; + } + + const int seq = utf8_seq_len(c); + if (seq > 1) { + /* A valid multi-byte sequence goes out as it came in. */ + for (int i = 0; i < seq; i++) { + char raw[] = "_"; + raw[0] = (char)c[i]; + TRY(strbuf_push(buf, raw)); + } + c += seq; continue; } - if ((unsigned char)*c < 32) { - /* Expand non-printable characters. Bytes >= 128 are left alone: - * they are UTF-8, which YAML takes as-is. */ + if (seq == 0 || *c < 32) { + /* Control characters, and bytes that are not UTF-8 at all. \u00XX + * is a lossless spelling of the byte: it round-trips through a + * parser as U+0000..U+00FF and keeps the document readable. */ char repl[10]; - TRY(snprintf(repl, sizeof(repl), "\\u%04x", (unsigned char)*c) < + TRY(snprintf(repl, sizeof(repl), "\\u%04x", (unsigned)*c) < (int)sizeof(repl)); TRY(strbuf_push(buf, repl)); + c++; continue; } char repl[] = "_"; - repl[0] = *c; + repl[0] = (char)*c; TRY(strbuf_push(buf, repl)); + c++; } TRY(strbuf_push(buf, "\"")); diff --git a/src/cjson/cYAML_test.c b/src/cjson/cYAML_test.c index eb863b7..2f5c0c3 100644 --- a/src/cjson/cYAML_test.c +++ b/src/cjson/cYAML_test.c @@ -149,6 +149,17 @@ int main(int argc, char *argv[]) { "note: em dash \xe2\x80\x94 here\n" ); + /* Bytes that are not valid UTF-8 reach cYAML from raw flash -- U-Boot + * environments are passed through untranscoded -- and must not be emitted + * raw, or one bad byte in one value stops the whole document parsing. */ + ok &= run_test("invalid utf-8 is escaped, not passed through", + + "{ \"note\": \"latin1 \xe9 here\" }", + + "---\n" + "note: \"latin1 \\u00e9 here\"\n" + ); + /* Control characters still get expanded. */ ok &= run_test("control characters are escaped", diff --git a/src/crypto/hisi_cipher.c b/src/crypto/hisi_cipher.c index be3c2d6..ada7a9c 100644 --- a/src/crypto/hisi_cipher.c +++ b/src/crypto/hisi_cipher.c @@ -146,6 +146,14 @@ bool hisi_cipher_ctr(hisi_cipher *c, const uint8_t key[16], return true; } +/* Lengths go to the driver unrounded, unlike the single-packet path above, + * which pads into its own scratch. That asymmetry is deliberate and measured, + * not an oversight: the batched command encrypts in place at the caller's + * buffers, so padding here would mean a bounce buffer per package and would + * make the benchmark measure that copy instead of the engine. The batched + * driver rounds the descriptor itself, which the caller's known-answer test + * confirms at a length that is not a multiple of the block -- verified on a + * gk7205v200 at 1100 bytes, where all fifteen packages match software CTR. */ bool hisi_cipher_ctr_batch(hisi_cipher *c, const uint8_t key[16], const hisi_cipher_job *jobs, size_t count) { if (c->fd < 0 || count == 0 || count > HISI_CIPHER_BATCH_MAX) @@ -219,9 +227,14 @@ bool hisi_cipher_open(hisi_cipher *c) { c->chan = cr.id; /* Probe the batched command by trying it. A driver without it answers - * EINVAL, which is the common case in the field and not a fault. Two - * packets rather than one, because one would also succeed on a driver - * that ignored the per-package IV entirely. */ + * EINVAL, which is the common case in the field and not a fault. + * + * This asks only whether the command EXISTS. It deliberately does not + * check the ciphertext, because a probe that submitted one IV and one + * plaintext could not tell a correct driver from one that ignored the + * per-package IV, and a probe that looked like it had checked would be + * worse than one that plainly has not. Correctness is established by the + * caller's known-answer test, which uses a distinct IV per package. */ uint8_t key[16] = {0}, iv[16] = {0}, a[16] = {0}, b[16] = {0}; hisi_cipher_job probe[2] = { {iv, a, sizeof(a)}, diff --git a/src/cryptobench.c b/src/cryptobench.c index 6113471..05047e3 100644 --- a/src/cryptobench.c +++ b/src/cryptobench.c @@ -37,7 +37,9 @@ * the second one read twice as slow. */ +#include #include +#include #include #include #include @@ -45,8 +47,10 @@ #include #include +#include "chipid.h" #include "cjson/cJSON.h" #include "cjson/cYAML.h" +#include "crypto/aes.h" #include "crypto/chachapoly.h" #include "crypto/gcm.h" #include "crypto/hisi_cipher.h" @@ -142,15 +146,18 @@ static bool verify_chachapoly(void) { return memcmp(tag, want_tag, 16) == 0; } +/* The AES-128 key SP 800-38A uses, shared by the engine's vectors and its + * benchmark rows. */ +static const uint8_t g_hw_key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, + 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, + 0x09, 0xcf, 0x4f, 0x3c}; + /* The engine, against NIST SP 800-38A F.5.1 and then across a 32-bit counter * wrap. The second one is not optional: a block that carries only the low 32 * bits of the counter agrees with software for every IV that is not near a * wrap, which is nearly all of them, so it would pass the first vector, ship, * and then disagree on one packet in millions with nothing to say so. */ static bool verify_hw_ctr(hisi_cipher *hw) { - static const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, - 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, - 0x09, 0xcf, 0x4f, 0x3c}; static const uint8_t iv[16] = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff}; @@ -162,7 +169,7 @@ static bool verify_hw_ctr(hisi_cipher *hw) { 0x99, 0x0d, 0xb6, 0xce}; uint8_t buf[16]; memcpy(buf, plain, sizeof(buf)); - if (!hisi_cipher_ctr(hw, key, iv, buf, sizeof(buf))) + if (!hisi_cipher_ctr(hw, g_hw_key, iv, buf, sizeof(buf))) return false; if (memcmp(buf, want, sizeof(want)) != 0) return false; @@ -176,11 +183,67 @@ static bool verify_hw_ctr(hisi_cipher *hw) { 0x50, 0xc2, 0xfa, 0x5c, 0x65, 0x9a, 0x07, 0x96, 0x30, 0x22}; uint8_t wrap[32]; memset(wrap, 0, sizeof(wrap)); - if (!hisi_cipher_ctr(hw, key, wrap_iv, wrap, sizeof(wrap))) + if (!hisi_cipher_ctr(hw, g_hw_key, wrap_iv, wrap, sizeof(wrap))) return false; return memcmp(wrap, wrap_want, sizeof(wrap_want)) == 0; } +/* The batched command needs a known answer of its own, and specifically one + * with a DIFFERENT IV PER PACKAGE. A driver that accepted the burst but + * applied the channel's single IV to every package would return correct + * ciphertext for package 0 and silent nonsense for the other fourteen — which + * is exactly the shape of the batch benchmark's real jobs, and would have been + * reported as a fast verified row. Each package is checked against the + * software AES-CTR in this same binary, which the F.5.1 vector above has + * already pinned to the standard. + * + * `len` is the length the benchmark will actually use, and is deliberately not + * required to be a multiple of the block: the single-packet path pads a short + * tail itself while this one hands the length straight to the driver, so any + * disagreement between them about a partial final block surfaces here as a + * failed vector rather than as a wrong number in the table. */ +static bool verify_hw_ctr_batch(hisi_cipher *hw, size_t len) { + if (hw->batch_max == 0 || len == 0 || len > MAX_PACKET) + return false; + + static uint8_t bufs[HISI_CIPHER_BATCH_MAX][MAX_PACKET]; + static uint8_t want[HISI_CIPHER_BATCH_MAX][MAX_PACKET]; + static uint8_t ivs[HISI_CIPHER_BATCH_MAX][16]; + hisi_cipher_job jobs[HISI_CIPHER_BATCH_MAX]; + + aes_ctx aes; + if (aes_setkey_enc(&aes, g_hw_key, 128) != 0) + return false; + + const unsigned depth = hw->batch_max; + for (unsigned i = 0; i < depth; i++) { + /* Distinct plaintext as well as distinct IV, so a driver that + * processed the same package fifteen times cannot pass either. */ + for (size_t j = 0; j < len; j++) + bufs[i][j] = (uint8_t)(j + i * 31u); + memcpy(want[i], bufs[i], len); + + memset(ivs[i], 0, sizeof(ivs[i])); + ivs[i][0] = (uint8_t)(0xa0 + i); + ivs[i][15] = (uint8_t)(i * 7u + 1u); + + uint8_t counter[16]; + memcpy(counter, ivs[i], sizeof(counter)); + aes_ctr_xcrypt(&aes, counter, want[i], len); + + jobs[i].iv = ivs[i]; + jobs[i].buf = bufs[i]; + jobs[i].len = len; + } + + if (!hisi_cipher_ctr_batch(hw, g_hw_key, jobs, depth)) + return false; + for (unsigned i = 0; i < depth; i++) + if (memcmp(bufs[i], want[i], len) != 0) + return false; + return true; +} + /* ---- rows -------------------------------------------------------------- */ struct row { @@ -286,15 +349,12 @@ static void bench_chachapoly(struct row *r, uint8_t *buf, size_t bytes, static void bench_hw_single(struct row *r, hisi_cipher *hw, uint8_t *buf, size_t bytes, unsigned iters) { - static const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, - 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, - 0x09, 0xcf, 0x4f, 0x3c}; uint8_t iv[16]; memset(iv, 0x11, sizeof(iv)); const double w0 = now_wall(), c0 = now_cpu(); for (unsigned i = 0; i < iters; i++) { - if (!hisi_cipher_ctr(hw, key, iv, buf, bytes)) { + if (!hisi_cipher_ctr(hw, g_hw_key, iv, buf, bytes)) { r->ran = false; return; } @@ -306,9 +366,6 @@ static void bench_hw_single(struct row *r, hisi_cipher *hw, uint8_t *buf, static void bench_hw_batch(struct row *r, hisi_cipher *hw, size_t bytes, unsigned iters, unsigned depth) { - static const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, - 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, - 0x09, 0xcf, 0x4f, 0x3c}; static uint8_t bufs[HISI_CIPHER_BATCH_MAX][MAX_PACKET]; static uint8_t ivs[HISI_CIPHER_BATCH_MAX][16]; hisi_cipher_job jobs[HISI_CIPHER_BATCH_MAX]; @@ -324,7 +381,7 @@ static void bench_hw_batch(struct row *r, hisi_cipher *hw, size_t bytes, const unsigned rounds = iters / depth ? iters / depth : 1; const double w0 = now_wall(), c0 = now_cpu(); for (unsigned i = 0; i < rounds; i++) { - if (!hisi_cipher_ctr_batch(hw, key, jobs, depth)) { + if (!hisi_cipher_ctr_batch(hw, g_hw_key, jobs, depth)) { r->ran = false; return; } @@ -350,11 +407,15 @@ static cJSON *build_hw_json(size_t bytes, unsigned iters) { cJSON *j_inner = cJSON_CreateObject(); ADD_PARAM("device", HISI_CIPHER_DEV); - ADD_PARAM("aead", aead ? "supported" : "unsupported"); - if (!aead) - ADD_PARAM("aead_note", - "block refuses AES-GCM, so there is no hardware sealing on " - "this part and only the AES-CTR half could be offloaded"); + ADD_PARAM("aead", aead ? "supported, not measured" : "unsupported"); + ADD_PARAM("aead_note", + aead ? "this part accepts AES-GCM, which no part available when " + "this was written did; the sealing benchmark is not " + "implemented, because an unverifiable number is worse " + "than a missing one. Please open an issue" + : "block refuses AES-GCM, so there is no hardware sealing " + "on this part and only the AES-CTR half could be " + "offloaded"); struct row single = {false, false, 0, 0, 0}; struct row floor = {false, false, 0, 0, 0}; @@ -362,7 +423,10 @@ static cJSON *build_hw_json(size_t bytes, unsigned iters) { single.verified = verify_hw_ctr(&hw); floor.verified = single.verified; - batch.verified = single.verified; + /* Not inherited: the batched path is a different command with different + * per-package state, and passing the single-packet vector says nothing + * about it. */ + batch.verified = single.verified && verify_hw_ctr_batch(&hw, bytes); if (single.verified) { static uint8_t buf[MAX_PACKET]; @@ -373,7 +437,7 @@ static cJSON *build_hw_json(size_t bytes, unsigned iters) { * and the number to reason with before writing any code against * this engine. */ bench_hw_single(&floor, &hw, buf, 16, iters); - if (hw.batch_max > 0) + if (batch.verified) bench_hw_batch(&batch, &hw, bytes, iters, hw.batch_max); } @@ -430,6 +494,28 @@ static cJSON *build_cryptobench_json(size_t bytes, unsigned iters, return j_inner; } +/* strtoul() alone would take "-1" as ULONG_MAX and "100junk" as 100, and the + * narrowing to unsigned then hides the first of those: --iters -1 becomes + * UINT_MAX and the tool sits there for a week. Require the whole argument to + * be a number and range-check before narrowing. */ +static bool parse_bounded(const char *arg, unsigned long lo, unsigned long hi, + unsigned long *out) { + if (!arg || !*arg) + return false; + errno = 0; + char *end = NULL; + const unsigned long v = strtoul(arg, &end, 10); + if (errno != 0 || end == arg || *end != '\0') + return false; + /* strtoul() accepts a leading '-' and wraps it; reject the sign itself. */ + if (strchr(arg, '-')) + return false; + if (v < lo || v > hi) + return false; + *out = v; + return true; +} + static void print_cryptobench_usage(void) { printf( "Usage: ipctool cryptobench [--json] [--bytes N] [--iters N] " @@ -478,23 +564,29 @@ int cryptobench_cmd(int argc, char **argv) { case 'j': want_json = true; break; - case 'b': - bytes = (size_t)strtoul(optarg, NULL, 10); - if (bytes < 16 || bytes > MAX_PACKET) { + case 'b': { + unsigned long v; + if (!parse_bounded(optarg, 16, MAX_PACKET, &v)) { fprintf(stderr, - "cryptobench: --bytes must be 16..%d (the engine's " - "own limit)\n", + "cryptobench: --bytes must be a number 16..%d (the " + "engine's own limit)\n", MAX_PACKET); return EXIT_FAILURE; } + bytes = (size_t)v; break; - case 'i': - iters = (unsigned)strtoul(optarg, NULL, 10); - if (iters < 100) { - fprintf(stderr, "cryptobench: --iters must be >= 100\n"); + } + case 'i': { + unsigned long v; + if (!parse_bounded(optarg, 100, 100000000UL, &v)) { + fprintf(stderr, + "cryptobench: --iters must be a number 100..%lu\n", + 100000000UL); return EXIT_FAILURE; } + iters = (unsigned)v; break; + } case 'n': want_hw = false; break; @@ -508,6 +600,14 @@ int cryptobench_cmd(int argc, char **argv) { } cJSON *bench = build_cryptobench_json(bytes, iters, want_hw); + + /* A row of numbers is only useful in the per-SoC table #186 asks for if it + * says which SoC. Same tagging membw does; absent on a host build, where + * chip detection never ran. */ + const char *chip = getchipname(); + if (chip) + cJSON_AddItemToObject(bench, "chip", cJSON_CreateString(chip)); + cJSON *root = cJSON_CreateObject(); cJSON_AddItemToObject(root, "cryptobench", bench);