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/cjson/cYAML.c b/src/cjson/cYAML.c index ccb05f9..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,12 +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"; + /* 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 (*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) { @@ -95,26 +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 (*c < 32) { - /* Expand non-printable characters. */ + 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", *c) < (int)sizeof(repl)); + 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 017cd49..2f5c0c3 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,39 @@ 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" + ); + + /* 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", + + "{ \"note\": \"bell \\u0007 here\" }", + + "---\n" + "note: \"bell \\u0007 here\"\n" + ); + + return ok ? 0 : 1; } 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..ada7a9c --- /dev/null +++ b/src/crypto/hisi_cipher.c @@ -0,0 +1,258 @@ +/* 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; +} + +/* 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) + 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. + * + * 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)}, + {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..05047e3 --- /dev/null +++ b/src/cryptobench.c @@ -0,0 +1,623 @@ +/* `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 +#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" +#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 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 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, g_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, 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 { + 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) { + 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, g_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 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, g_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, 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}; + struct row batch = {false, false, 0, 0, 0}; + + single.verified = verify_hw_ctr(&hw); + floor.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]; + 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 (batch.verified) + 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; +} + +/* 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] " + "[--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': { + unsigned long v; + if (!parse_bounded(optarg, 16, MAX_PACKET, &v)) { + fprintf(stderr, + "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': { + 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; + case 'h': + print_cryptobench_usage(); + return EXIT_SUCCESS; + default: + print_cryptobench_usage(); + return EXIT_FAILURE; + } + } + + 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); + + 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"))