feat(crypto): portable constant-time AES-GCM software fallback (rv2 8 phase C)

- no-intrinsics AES: S-box = GF(2^8) inverse via a fixed-exponent power ladder
  (constant-time in the input, no tables), constant-time gf8_mul, byte-oriented
  ShiftRows/MixColumns/key-expansion (AES-128 and AES-256)
- constant-time GHASH: bit-by-bit GF(2^128) multiply (mask-driven, no tables)
- aes_gcm_seal/open now dispatch: AES-NI path when present (and not forced
  software), else this portable fallback -> AES-GCM works on ANY CPU, so the
  phase-B no-AES-NI trap is retired
- wo_aes_force_software test hook; both hw and sw paths verified against NIST
  SP 800-38D cases 4 (AES-128) and 16 (AES-256) byte-for-byte; test_crypto 48/0;
  ASan/UBSan clean; full runtime battery green
- ARMv8 crypto-extension hardware path deferred (untestable on x86-64 host)

(cherry picked from commit dccf650899798401a9adac8489f34c85ed9304af)
This commit is contained in:
shoney.arickathil 2026-09-08 13:47:26 +02:00
parent 6a38ad7cb0
commit 94d5f176f7
3 changed files with 224 additions and 15 deletions

View file

@ -620,33 +620,219 @@ static int aes_gcm_core(const uint8_t *key, size_t keylen, const uint8_t nonce[1
int wo_aes_gcm_available(void) { return 0; }
#endif
/* ---- Portable constant-time AES-GCM (rv2 8 phase C software fallback) ------
* No intrinsics, no lookup tables: the S-box is the GF(2^8) inverse via a
* fixed-exponent power ladder (constant-time in the input), GHASH is the
* bit-by-bit GF(2^128) multiply (constant-time, mask-driven). Slower than the
* AES-NI path; its job is portability and side-channel safety, not speed. The
* ARMv8 crypto-extension hardware path is deferred (untestable on the x86-64
* dev host). */
int wo_aes_force_software = 0; /* test hook: force the software path */
static uint8_t gf8_mul(uint8_t a, uint8_t b) {
uint8_t p = 0;
for (int i = 0; i < 8; i++) {
p ^= (uint8_t)(-(b & 1)) & a;
uint8_t hi = (uint8_t)(-((a >> 7) & 1));
a = (uint8_t)((a << 1)) ^ (uint8_t)(hi & 0x1b);
b = (uint8_t)(b >> 1);
}
return p;
}
static uint8_t aes_sbox_ct(uint8_t x) {
/* inverse = x^254 in GF(2^8); the exponent is a compile-time constant so
* the ladder is a fixed op sequence — constant-time in x. inv(0) = 0. */
uint8_t base = x, inv = 1;
for (int i = 0; i < 8; i++) {
if ((254u >> i) & 1u) inv = gf8_mul(inv, base);
base = gf8_mul(base, base);
}
/* affine: out_i = inv_i ^ inv_{i+4} ^ inv_{i+5} ^ inv_{i+6} ^ inv_{i+7}, ^0x63 */
uint8_t s = 0;
for (int i = 0; i < 8; i++) {
uint8_t bit = (uint8_t)(((inv >> i) ^ (inv >> ((i + 4) & 7)) ^
(inv >> ((i + 5) & 7)) ^ (inv >> ((i + 6) & 7)) ^
(inv >> ((i + 7) & 7))) & 1u);
s |= (uint8_t)(bit << i);
}
return (uint8_t)(s ^ 0x63);
}
static uint32_t subword(uint32_t w) {
return ((uint32_t)aes_sbox_ct(w >> 24) << 24) |
((uint32_t)aes_sbox_ct((w >> 16) & 0xff) << 16) |
((uint32_t)aes_sbox_ct((w >> 8) & 0xff) << 8) |
(uint32_t)aes_sbox_ct(w & 0xff);
}
static void aes_expand_sw(const uint8_t *key, size_t keylen, uint8_t *rk,
int *nr_out) {
int nk = (int)(keylen / 4); /* 4 (AES-128) or 8 (AES-256) */
int nr = nk + 6; /* 10 or 14 */
int total = 4 * (nr + 1);
uint32_t w[60];
for (int i = 0; i < nk; i++)
w[i] = ((uint32_t)key[4 * i] << 24) | ((uint32_t)key[4 * i + 1] << 16) |
((uint32_t)key[4 * i + 2] << 8) | (uint32_t)key[4 * i + 3];
uint8_t rcon = 1;
for (int i = nk; i < total; i++) {
uint32_t t = w[i - 1];
if (i % nk == 0) {
t = (t << 8) | (t >> 24); /* RotWord */
t = subword(t); /* SubWord */
t ^= (uint32_t)rcon << 24;
rcon = gf8_mul(rcon, 2);
} else if (nk > 6 && i % nk == 4) {
t = subword(t);
}
w[i] = w[i - nk] ^ t;
}
for (int i = 0; i < total; i++) {
rk[4 * i] = (uint8_t)(w[i] >> 24);
rk[4 * i + 1] = (uint8_t)(w[i] >> 16);
rk[4 * i + 2] = (uint8_t)(w[i] >> 8);
rk[4 * i + 3] = (uint8_t)w[i];
}
*nr_out = nr;
}
static void aes_block_sw(const uint8_t *rk, int nr, const uint8_t in[16],
uint8_t out[16]) {
uint8_t s[16];
memcpy(s, in, 16);
for (int i = 0; i < 16; i++) s[i] ^= rk[i];
for (int round = 1; round <= nr; round++) {
for (int i = 0; i < 16; i++) s[i] = aes_sbox_ct(s[i]);
uint8_t t[16]; /* ShiftRows: state is column-major, byte = row + 4*col */
for (int r = 0; r < 4; r++)
for (int c = 0; c < 4; c++) t[r + 4 * c] = s[r + 4 * ((c + r) & 3)];
memcpy(s, t, 16);
if (round != nr) {
for (int c = 0; c < 4; c++) {
uint8_t *col = s + 4 * c;
uint8_t a0 = col[0], a1 = col[1], a2 = col[2], a3 = col[3];
col[0] = gf8_mul(a0, 2) ^ gf8_mul(a1, 3) ^ a2 ^ a3;
col[1] = a0 ^ gf8_mul(a1, 2) ^ gf8_mul(a2, 3) ^ a3;
col[2] = a0 ^ a1 ^ gf8_mul(a2, 2) ^ gf8_mul(a3, 3);
col[3] = gf8_mul(a0, 3) ^ a1 ^ a2 ^ gf8_mul(a3, 2);
}
}
for (int i = 0; i < 16; i++) s[i] ^= rk[16 * round + i];
}
memcpy(out, s, 16);
}
/* GF(2^128) multiply, GCM bit order, constant-time (mask-driven, no tables). */
static void gf128_mul(const uint8_t X[16], const uint8_t Y[16], uint8_t out[16]) {
uint8_t Z[16] = { 0 }, V[16];
memcpy(V, Y, 16);
for (int i = 0; i < 128; i++) {
uint8_t bit = (uint8_t)((X[i >> 3] >> (7 - (i & 7))) & 1);
uint8_t m = (uint8_t)(-bit);
for (int j = 0; j < 16; j++) Z[j] ^= (uint8_t)(V[j] & m);
uint8_t lsb = (uint8_t)(V[15] & 1);
for (int j = 15; j > 0; j--)
V[j] = (uint8_t)((V[j] >> 1) | (V[j - 1] << 7));
V[0] = (uint8_t)(V[0] >> 1);
V[0] ^= (uint8_t)(0xe1 & (uint8_t)(-lsb));
}
memcpy(out, Z, 16);
}
static void ghash_sw(const uint8_t H[16], const uint8_t *data, size_t len,
uint8_t T[16]) {
size_t off = 0;
while (off < len) {
uint8_t blk[16];
size_t n = len - off < 16 ? len - off : 16;
memset(blk, 0, 16);
memcpy(blk, data + off, n);
for (int j = 0; j < 16; j++) T[j] ^= blk[j];
uint8_t tmp[16];
gf128_mul(T, H, tmp);
memcpy(T, tmp, 16);
off += n;
}
}
static int aes_gcm_core_sw(const uint8_t *key, size_t keylen,
const uint8_t nonce[12], const uint8_t *aad,
size_t aadlen, const uint8_t *in, size_t inlen,
uint8_t *out, uint8_t tag_out[16],
const uint8_t *tag_in) {
uint8_t rk[15 * 16];
int nr;
aes_expand_sw(key, keylen, rk, &nr);
uint8_t H[16] = { 0 };
aes_block_sw(rk, nr, H, H); /* H = AES(0) */
uint8_t j0[16];
memcpy(j0, nonce, 12);
j0[12] = 0; j0[13] = 0; j0[14] = 0; j0[15] = 1;
uint8_t ej0[16];
aes_block_sw(rk, nr, j0, ej0);
uint8_t T[16] = { 0 };
ghash_sw(H, aad, aadlen, T);
if (tag_in) ghash_sw(H, in, inlen, T); /* open: GHASH ciphertext first */
uint32_t ctr = 2;
size_t off = 0;
while (off < inlen) {
uint8_t cb[16];
memcpy(cb, nonce, 12);
cb[12] = (uint8_t)(ctr >> 24); cb[13] = (uint8_t)(ctr >> 16);
cb[14] = (uint8_t)(ctr >> 8); cb[15] = (uint8_t)ctr;
uint8_t ks[16];
aes_block_sw(rk, nr, cb, ks);
size_t n = inlen - off < 16 ? inlen - off : 16;
for (size_t i = 0; i < n; i++) out[off + i] = in[off + i] ^ ks[i];
off += n; ctr++;
}
if (!tag_in) ghash_sw(H, out, inlen, T); /* seal: GHASH produced ciphertext */
uint8_t lb[16];
uint64_t aB = (uint64_t)aadlen * 8, cB = (uint64_t)inlen * 8;
for (int i = 0; i < 8; i++) lb[i] = (uint8_t)(aB >> (56 - 8 * i));
for (int i = 0; i < 8; i++) lb[8 + i] = (uint8_t)(cB >> (56 - 8 * i));
ghash_sw(H, lb, 16, T);
uint8_t tag[16];
for (int i = 0; i < 16; i++) tag[i] = (uint8_t)(T[i] ^ ej0[i]);
if (tag_in) {
uint8_t d = 0;
for (int i = 0; i < 16; i++) d |= (uint8_t)(tag[i] ^ tag_in[i]);
return d == 0 ? 0 : 1;
}
memcpy(tag_out, tag, 16);
return 0;
}
/* Public seal/open. keylen 16 (AES-128) or 32 (AES-256), nonce 12 bytes.
* Returns 0 ok, 1 auth failure (open), -2 when no hardware AES is available. */
* Dispatches to the AES-NI path when available (and not forced software),
* else the portable constant-time fallback. Returns 0 ok, 1 auth failure. */
int wo_aes_gcm_seal(const uint8_t *key, size_t keylen, const uint8_t nonce[12],
const uint8_t *aad, size_t aadlen, const uint8_t *pt,
size_t ptlen, uint8_t *out) {
#if defined(__x86_64__)
if (!wo_aes_gcm_available()) return -2;
return aes_gcm_core(key, keylen, nonce, aad, aadlen, pt, ptlen, out,
out + ptlen, NULL);
#else
(void)key; (void)keylen; (void)nonce; (void)aad; (void)aadlen;
(void)pt; (void)ptlen; (void)out;
return -2;
if (wo_aes_gcm_available() && !wo_aes_force_software)
return aes_gcm_core(key, keylen, nonce, aad, aadlen, pt, ptlen, out,
out + ptlen, NULL);
#endif
return aes_gcm_core_sw(key, keylen, nonce, aad, aadlen, pt, ptlen, out,
out + ptlen, NULL);
}
int wo_aes_gcm_open(const uint8_t *key, size_t keylen, const uint8_t nonce[12],
const uint8_t *aad, size_t aadlen, const uint8_t *ct,
size_t ctlen, const uint8_t tag[16], uint8_t *out) {
#if defined(__x86_64__)
if (!wo_aes_gcm_available()) return -2;
return aes_gcm_core(key, keylen, nonce, aad, aadlen, ct, ctlen, out, NULL,
tag);
#else
(void)key; (void)keylen; (void)nonce; (void)aad; (void)aadlen; (void)ct;
(void)ctlen; (void)tag; (void)out;
return -2;
if (wo_aes_gcm_available() && !wo_aes_force_software)
return aes_gcm_core(key, keylen, nonce, aad, aadlen, ct, ctlen, out,
NULL, tag);
#endif
return aes_gcm_core_sw(key, keylen, nonce, aad, aadlen, ct, ctlen, out, NULL,
tag);
}
/* The VM half: Bytes in, fresh Bytes out. Wrong class id traps

View file

@ -30,6 +30,7 @@ int wo_chacha20poly1305_open(const uint8_t key[32], const uint8_t nonce[12],
* nonce 12 bytes. seal writes out[ptlen] || tag[16]. Returns 0 ok, 1 auth
* failure (open), -2 when no hardware AES is available (phase C fallback). */
int wo_aes_gcm_available(void);
extern int wo_aes_force_software; /* test hook: force the portable AES path */
int wo_aes_gcm_seal(const uint8_t *key, size_t keylen, const uint8_t nonce[12],
const uint8_t *aad, size_t aadlen, const uint8_t *pt,
size_t ptlen, uint8_t *out);

View file

@ -229,5 +229,27 @@ int main(void) {
"76fc6ece0f4e1768cddf8853bb2d551b");
}
/* rv2 8 phase C — the portable constant-time software path, forced on
* (works regardless of hardware), against the same NIST vectors. */
wo_aes_force_software = 1;
t_aesgcm("feffe9928665731c6d6a8f9467308308",
"cafebabefacedbaddecaf888",
"feedfacedeadbeeffeedfacedeadbeefabaddad2",
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721"
"c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39",
"42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e2"
"1d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091"
"5bc94fbc3221a5db94fae95ae7121a47");
t_aesgcm("feffe9928665731c6d6a8f9467308308"
"feffe9928665731c6d6a8f9467308308",
"cafebabefacedbaddecaf888",
"feedfacedeadbeeffeedfacedeadbeefabaddad2",
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721"
"c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39",
"522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8"
"cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662"
"76fc6ece0f4e1768cddf8853bb2d551b");
wo_aes_force_software = 0;
return t_report("test_crypto");
}