diff --git a/.github/workflows/afalg.yml b/.github/workflows/afalg.yml new file mode 100644 index 00000000000..2b47ac75013 --- /dev/null +++ b/.github/workflows/afalg.yml @@ -0,0 +1,198 @@ +name: AF_ALG Tests + +# START OF COMMON SECTION +on: + push: + branches: [ 'release/**' ] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [ '*' ] + # Weekday-morning cron (10:00 UTC) seeds the master-scoped ccache that PR runs + # restore: re-runs --build-only (compile only, no tests) on the + # default branch. PR runs are read-only (see ccache-setup). + schedule: + - cron: '2 10 * * 1-5' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read +# END OF COMMON SECTION + +jobs: + # The Linux AF_ALG port (wolfcrypt/src/port/af_alg/) offloads AES and SHA-256 + # to the kernel crypto API over AF_ALG sockets. Note that Docker's default + # seccomp profile blocks socket(AF_ALG). + # + # Both configs build on one runner via .github/scripts/parallel-make-check.py + # (see os-check.yml for the full pattern): each builds in its own out-of-tree + # ("VPATH") build directory off one checkout/autogen, on a pool of one-per-CPU + # worker threads, longest first. + make_check: + name: make check + if: ${{ (github.repository_owner == 'wolfssl') && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }} + runs-on: ubuntu-24.04 + # Generous for a cold ccache; warm reruns finish in a fraction. + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + name: Checkout wolfSSL + + - name: Install dependencies + uses: ./.github/actions/install-apt-deps + with: + packages: autoconf automake libtool build-essential bubblewrap + ghcr-debs-tag: ubuntu-24.04-minimal + + # Ubuntu 24.04 can restrict unprivileged user namespaces via AppArmor, + # which would stop the test scripts from re-execing under + # bwrap --unshare-net (their port-isolation mechanism). + - name: Allow unprivileged user namespaces (for bwrap) + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true + + # The socket families the port binds through (algif_hash, algif_skcipher, + # algif_aead) are loadable modules, normally autoloaded on bind() via + # their module aliases. On the -azure kernels the hosted runners boot, + # algif_aead is not in the installed linux-modules-azure package, and + # Ubuntu's generated blacklist neutralizes it with an + # "install algif_aead /bin/false" rule -- which defeats the bind-time + # autoload too, so aead/gcm(aes) is unreachable until it is loaded by + # hand. The underlying cipher is not the problem: gcm(aes) is already + # registered by aesni_intel. + # + # --ignore-install skips that /bin/false rule; linux-modules-extra + # supplies the .ko if the base package really lacks it (that package is + # often absent from the mirrors for the runner's exact kernel revision, + # so it is a best-effort second try, not a dependency). Failures stay + # non-fatal here: the probe below is what turns a missing algorithm into + # a red check, and it names the algorithm when it does. + - name: Load AF_ALG kernel modules + run: | + uname -r + missing= + for m in algif_hash algif_skcipher algif_aead gcm; do + if sudo modprobe --ignore-install "$m"; then + echo "modprobe $m: ok" + else + echo "modprobe $m: not loadable, will retry after modules-extra" + missing="$missing $m" + fi + done + if [ -n "$missing" ]; then + grep -rn 'algif_' /etc/modprobe.d /lib/modprobe.d || true + sudo apt-get update -qq || true + sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true + for m in $missing; do + if sudo modprobe --ignore-install "$m"; then + echo "modprobe $m: ok after modules-extra" + else + echo "modprobe $m: still not loadable" + fi + done + fi + echo '--- registered AES/SHA-256 algorithms (name/driver) ---' + awk '/^name/ { n = $3 } /^driver/ { print n "\t" $3 }' /proc/crypto \ + | grep -E 'aes|sha256' | sort -u || true + + # Preflight: bind every (type, name) pair wolfcrypt/src/port/af_alg/ uses, + # so a runner image without one of them fails here with the missing + # algorithm named, rather than deep inside testwolfcrypt. Deliberately a + # hard failure and not a skip: a green check that exercised no AF_ALG code + # would be worse than a red one. The set mirrors afalg_hash.c (sha256) and + # afalg_aes.c (cbc/ecb/ctr/gcm); extend it when the port grows an + # algorithm. + - name: Verify the kernel provides the algorithms the port needs + run: | + cat > "$RUNNER_TEMP/afalg-probe.c" <<'EOF' + #include + #include + #include + #include + #include + + static const char* types[] = { + "hash", "skcipher", "skcipher", "skcipher", "aead" + }; + static const char* names[] = { + "sha256", "cbc(aes)", "ecb(aes)", "ctr(aes)", "gcm(aes)" + }; + + int main(void) + { + struct sockaddr_alg sa; + size_t i; + int fd; + int missing = 0; + + for (i = 0; i < sizeof(types) / sizeof(types[0]); i++) { + fd = socket(AF_ALG, SOCK_SEQPACKET, 0); + if (fd < 0) { + printf("::error::socket(AF_ALG) unavailable on this kernel\n"); + return 1; + } + memset(&sa, 0, sizeof(sa)); + sa.salg_family = AF_ALG; + strncpy((char*)sa.salg_type, types[i], sizeof(sa.salg_type) - 1); + strncpy((char*)sa.salg_name, names[i], sizeof(sa.salg_name) - 1); + if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + printf("::error::kernel is missing %s/%s\n", types[i], names[i]); + missing = 1; + } + else { + printf("ok: %s/%s\n", types[i], names[i]); + } + close(fd); + } + return missing; + } + EOF + gcc -Wall -Werror -o "$RUNNER_TEMP/afalg-probe" "$RUNNER_TEMP/afalg-probe.c" + "$RUNNER_TEMP/afalg-probe" + + # ccache via the cross-platform composite; the script passes the + # compiler to configure as CC="ccache gcc" (or a per-config "cc"). + - name: Set up ccache + uses: ./.github/actions/ccache-setup + with: + workflow-id: afalg + read-only: ${{ github.event_name == 'pull_request' }} + max-size: 100M + + - name: Build all configs (parallel, out-of-tree) + run: | + cat > "$RUNNER_TEMP/afalg-configs.json" <<'EOF' + [ + {"name": "defaults-afalg", "minutes": 2, + "configure": ["--enable-afalg"], + "cflags": "-pedantic -Wdeclaration-after-statement -Wnull-dereference -Wno-overlength-strings"}, + {"name": "all-afalg", "minutes": 5, + "configure": ["--enable-all", "--enable-testcert", "--enable-acert", + "--enable-dtls13", "--enable-dtls-mtu", "--enable-dtls-frag-ch", + "--enable-dtlscid", "--enable-quic", "--enable-afalg", + "--disable-srtp", "--disable-sha224", "--disable-hashflags", + "--disable-cryptocb", "--disable-aesgcm-stream"], + "cflags": "-pedantic -Wdeclaration-after-statement -Wnull-dereference -Wno-overlength-strings"} + ] + EOF + .github/scripts/parallel-make-check.py \ + ${{ github.event_name == 'schedule' && '--build-only' || '' }} \ + --private-dir=certs \ + "$RUNNER_TEMP/afalg-configs.json" + + - name: ccache stats + if: always() + run: ccache -s || true + + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v6 + with: + retention-days: 7 + name: afalg-logs + path: | + build-*/make-check.log + build-*/test-suite.log + build-*/config.log + if-no-files-found: ignore diff --git a/doc/dox_comments/header_files/aes.h b/doc/dox_comments/header_files/aes.h index 194f833dc1e..99891837f2c 100644 --- a/doc/dox_comments/header_files/aes.h +++ b/doc/dox_comments/header_files/aes.h @@ -368,6 +368,17 @@ int wc_AesGcmSetKey(Aes* aes, const byte* key, word32 len); It also encodes the input authentication vector, authIn, into the authentication tag, authTag. + \note When built with WOLFSSL_AFALG_XILINX_AES, the Xilinx AF_ALG kernel + interface operates on a combined cipher text + tag buffer, so this function + does not honor the exact-size buffer contract described below. Both in and + out must be allocated with WC_AES_BLOCK_SIZE (16) bytes of room beyond sz: + sz + 16 bytes are sent to the kernel from in (the trailing 16 bytes are + scratch space for the tag and their contents are irrelevant), and sz + 16 + bytes are read back into out. The tag is additionally copied out to authTag + as usual. Both buffers should also be aligned to WOLFSSL_XILINX_ALIGN; an + unaligned in is staged through a temporary allocation, or rejected with + BAD_ALIGN_E if NO_WOLFSSL_ALLOC_ALIGN is defined. + \return 0 On successfully encrypting the input message \param aes - pointer to the AES object used to encrypt data @@ -420,6 +431,17 @@ int wc_AesGcmEncrypt(Aes* aes, byte* out, the output data is undefined. However, callers must unconditionally zeroize the output buffer to guard against leakage of cleartext data. + \note When built with WOLFSSL_AFALG_XILINX_AES, the Xilinx AF_ALG kernel + interface operates on a combined cipher text + tag buffer, so this function + does not honor the exact-size buffer contract described below. Both in and + out must be allocated with WC_AES_BLOCK_SIZE (16) bytes of room beyond sz. + The tag to check against is written into in + sz by this function, which + means the in buffer is modified even though it is declared const, and + sz + 16 bytes are read back into out. Both buffers should also be aligned + to WOLFSSL_XILINX_ALIGN; an unaligned in is staged through a temporary + allocation, or rejected with BAD_ALIGN_E if NO_WOLFSSL_ALLOC_ALIGN is + defined. + \return 0 On successfully decrypting and authenticating the input message \return AES_GCM_AUTH_E If the authentication tag does not match the supplied authentication code vector, authTag. diff --git a/wolfcrypt/src/aes.c b/wolfcrypt/src/aes.c index 9bbbe3d6857..fcae1e67ab9 100644 --- a/wolfcrypt/src/aes.c +++ b/wolfcrypt/src/aes.c @@ -16073,6 +16073,11 @@ int wc_AesGetKeySize(Aes* aes, word32* keySize) #elif defined(WOLFSSL_AFALG) /* implemented in wolfcrypt/src/port/af_alg/afalg_aes.c */ + #define _AesEcbEncrypt(aes, out, in, sz) wc_AesEcbEncrypt(aes, out, in, sz) + #ifdef HAVE_AES_DECRYPT + #define _AesEcbDecrypt(aes, out, in, sz) \ + wc_AesEcbDecrypt(aes, out, in, sz) + #endif #elif defined(WOLFSSL_DEVCRYPTO_AES) /* implemented in wolfcrypt/src/port/devcrypt/devcrypto_aes.c */ diff --git a/wolfcrypt/src/port/af_alg/afalg_aes.c b/wolfcrypt/src/port/af_alg/afalg_aes.c index efbf6358594..532b0516b65 100644 --- a/wolfcrypt/src/port/af_alg/afalg_aes.c +++ b/wolfcrypt/src/port/af_alg/afalg_aes.c @@ -585,10 +585,12 @@ int wc_AesGcmSetKey(Aes* aes, const byte* key, word32 len) /* Performs AES-GCM encryption and returns 0 on success * - * Warning: If using Xilinx hardware acceleration it is assumed that the out - * buffer is large enough to hold both cipher text and tag. That is - * sz | 16 bytes. The input and output buffer is expected to be 64 bit - * aligned + * Warning: If using Xilinx hardware acceleration it is assumed that both the in + * and out buffers are large enough to hold cipher text and tag. That is + * sz | 16 bytes. sz | 16 bytes are sent to the kernel from the in + * buffer, with the trailing 16 bytes being scratch space for the tag, + * and sz | 16 bytes are read back into the out buffer. The input and + * output buffer is expected to be 64 bit aligned * */ int wc_AesGcmEncrypt(Aes* aes, byte* out, const byte* in, word32 sz, @@ -657,6 +659,16 @@ int wc_AesGcmEncrypt(Aes* aes, byte* out, const byte* in, word32 sz, WOLFSSL_MSG("CMSG_FIRSTHDR() in wc_AesGcmEncrypt() returned NULL unexpectedly."); return SYSLIB_FAILED_E; } + + /* Always set the operation. The same Aes structure, and with it the same + * AF_ALG socket, can be used for both encrypt and decrypt calls, so the + * operation currently stored in the control message could be left over + * from a previous call in the other direction. */ + if (wc_Afalg_SetOp(cmsg, AES_ENCRYPTION) < 0) { + WOLFSSL_MSG("Error with setting AF_ALG operation"); + return WC_AFALG_SOCK_E; + } + cmsg = CMSG_NXTHDR(msg, cmsg); if (cmsg == NULL) { WOLFSSL_MSG("CMSG_NEXTHDR() in wc_AesGcmEncrypt() returned NULL unexpectedly."); @@ -789,10 +801,12 @@ int wc_AesGcmEncrypt(Aes* aes, byte* out, const byte* in, word32 sz, #if defined(HAVE_AES_DECRYPT) || defined(HAVE_AESGCM_DECRYPT) /* Performs AES-GCM decryption and returns 0 on success * - * Warning: If using Xilinx hardware acceleration it is assumed that the in - * buffer is large enough to hold both cipher text and tag. That is + * Warning: If using Xilinx hardware acceleration it is assumed that both the in + * and out buffers are large enough to hold cipher text and tag. That is * sz | 16 bytes. The in buffer has tag appended even though it is - * const for this wolfSSL API. + * const for this wolfSSL API, and sz | 16 bytes are read back into the + * out buffer. The input and output buffer is expected to be 64 bit + * aligned. */ int wc_AesGcmDecrypt(Aes* aes, byte* out, const byte* in, word32 sz, const byte* iv, word32 ivSz, @@ -831,7 +845,10 @@ int wc_AesGcmDecrypt(Aes* aes, byte* out, const byte* in, word32 sz, return ret; if (aes->rdFd == WC_SOCK_NOTSET) { - aes->dir = AES_DECRYPTION; + /* aes->dir is not changed here, the operation used with the socket is + * set on every call below. It is left as AES_ENCRYPTION, the value set + * by wc_AesGcmSetKey, so that the software tag handling can still make + * use of wc_AesEncryptDirect. */ if ((ret = wc_AesSetup(aes, WC_TYPE_AEAD, WC_NAME_AESGCM, ivSz, authInSz)) != 0) { WOLFSSL_MSG("Error with first time setup of AF_ALG socket"); @@ -855,7 +872,9 @@ int wc_AesGcmDecrypt(Aes* aes, byte* out, const byte* in, word32 sz, if ((cmsg = CMSG_FIRSTHDR(msg)) == NULL) { return WC_AFALG_SOCK_E; } - if (wc_Afalg_SetOp(cmsg, aes->dir) < 0) { + /* Always set the operation. The socket could have been created by a + * previous wc_AesGcmEncrypt call made with this same Aes structure. */ + if (wc_Afalg_SetOp(cmsg, AES_DECRYPTION) < 0) { WOLFSSL_MSG("Error with setting AF_ALG operation"); return WC_AFALG_SOCK_E; } diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 41c2e71e334..da9e027f623 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -19271,8 +19271,142 @@ static wc_test_ret_t aesgcm_default_test_helper(byte* key, int keySz, byte* iv, /* tests that only use 12 byte IV and 16 or less byte AAD * test vectors are from NIST SP 800-38D * https://csrc.nist.gov/Projects/Cryptographic-Algorithm-Validation-Program/CAVP-TESTING-BLOCK-CIPHER-MODES*/ +/* Encrypt, decrypt and then encrypt again using one Aes structure, without + * setting the key again in between. Ports that hold on to state tied to the + * operation direction (AF_ALG keeps an open socket with the operation stored + * in a control message) have to update that state on every call. */ +static wc_test_ret_t aesgcm_reuse_ctx_test(void) +{ + wc_test_ret_t ret = 0; +#if defined(WOLFSSL_AES_256) && defined(HAVE_AES_DECRYPT) && \ + !defined(HAVE_RENESAS_SYNC) && !defined(WOLFSSL_XILINX_CRYPT) && \ + !defined(WOLFSSL_AFALG_XILINX_AES) +#if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) + Aes *enc = NULL; +#else + Aes enc[1]; +#endif + WOLFSSL_SMALL_STACK_STATIC const byte k1[] = + { + 0xfe, 0xff, 0xe9, 0x92, 0x86, 0x65, 0x73, 0x1c, + 0x6d, 0x6a, 0x8f, 0x94, 0x67, 0x30, 0x83, 0x08, + 0xfe, 0xff, 0xe9, 0x92, 0x86, 0x65, 0x73, 0x1c, + 0x6d, 0x6a, 0x8f, 0x94, 0x67, 0x30, 0x83, 0x08 + }; + WOLFSSL_SMALL_STACK_STATIC const byte p[] = + { + 0xd9, 0x31, 0x32, 0x25, 0xf8, 0x84, 0x06, 0xe5, + 0xa5, 0x59, 0x09, 0xc5, 0xaf, 0xf5, 0x26, 0x9a, + 0x86, 0xa7, 0xa9, 0x53, 0x15, 0x34, 0xf7, 0xda, + 0x2e, 0x4c, 0x30, 0x3d, 0x8a, 0x31, 0x8a, 0x72, + 0x1c, 0x3c, 0x0c, 0x95, 0x95, 0x68, 0x09, 0x53, + 0x2f, 0xcf, 0x0e, 0x24, 0x49, 0xa6, 0xb5, 0x25, + 0xb1, 0x6a, 0xed, 0xf5, 0xaa, 0x0d, 0xe6, 0x57, + 0xba, 0x63, 0x7b, 0x39 + }; + WOLFSSL_SMALL_STACK_STATIC const byte a[] = + { + 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad, 0xbe, 0xef, + 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad, 0xbe, 0xef, + 0xab, 0xad, 0xda, 0xd2 + }; + WOLFSSL_SMALL_STACK_STATIC const byte iv1[] = + { + 0xca, 0xfe, 0xba, 0xbe, 0xfa, 0xce, 0xdb, 0xad, + 0xde, 0xca, 0xf8, 0x88 + }; + WOLFSSL_SMALL_STACK_STATIC const byte c1[] = + { + 0x52, 0x2d, 0xc1, 0xf0, 0x99, 0x56, 0x7d, 0x07, + 0xf4, 0x7f, 0x37, 0xa3, 0x2a, 0x84, 0x42, 0x7d, + 0x64, 0x3a, 0x8c, 0xdc, 0xbf, 0xe5, 0xc0, 0xc9, + 0x75, 0x98, 0xa2, 0xbd, 0x25, 0x55, 0xd1, 0xaa, + 0x8c, 0xb0, 0x8e, 0x48, 0x59, 0x0d, 0xbb, 0x3d, + 0xa7, 0xb0, 0x8b, 0x10, 0x56, 0x82, 0x88, 0x38, + 0xc5, 0xf6, 0x1e, 0x63, 0x93, 0xba, 0x7a, 0x0a, + 0xbc, 0xc9, 0xf6, 0x62 + }; + WOLFSSL_SMALL_STACK_STATIC const byte t1[] = + { + 0x76, 0xfc, 0x6e, 0xce, 0x0f, 0x4e, 0x17, 0x68, + 0xcd, 0xdf, 0x88, 0x53, 0xbb, 0x2d, 0x55, 0x1b + }; + byte resultT[sizeof(t1)]; + byte resultC[sizeof(p) + WC_AES_BLOCK_SIZE]; + byte resultP[sizeof(p) + WC_AES_BLOCK_SIZE]; + + XMEMSET(resultT, 0, sizeof(resultT)); + XMEMSET(resultC, 0, sizeof(resultC)); + XMEMSET(resultP, 0, sizeof(resultP)); + +#if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) + enc = test_AesGcmNew(HEAP_HINT, devId, &ret); + if (enc == NULL) + return WC_TEST_RET_ENC_EC(ret); +#else + XMEMSET(enc, 0, sizeof(Aes)); + ret = test_AesGcmInit(enc, HEAP_HINT, devId); + if (ret != 0) + return WC_TEST_RET_ENC_EC(ret); +#endif + + ret = wc_AesGcmSetKey(enc, k1, (word32)sizeof(k1)); + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + + ret = wc_AesGcmEncrypt(enc, resultC, p, sizeof(p), iv1, sizeof(iv1), + resultT, sizeof(resultT), a, sizeof(a)); +#if defined(WOLFSSL_ASYNC_CRYPT) + ret = wc_AsyncWait(ret, &enc->asyncDev, WC_ASYNC_FLAG_NONE); +#endif + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + if (XMEMCMP(c1, resultC, sizeof(c1))) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + if (XMEMCMP(t1, resultT, sizeof(t1))) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + /* decrypt with the same structure that was just used to encrypt */ + ret = wc_AesGcmDecrypt(enc, resultP, resultC, sizeof(p), iv1, sizeof(iv1), + resultT, sizeof(resultT), a, sizeof(a)); +#if defined(WOLFSSL_ASYNC_CRYPT) + ret = wc_AsyncWait(ret, &enc->asyncDev, WC_ASYNC_FLAG_NONE); +#endif + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + if (XMEMCMP(p, resultP, sizeof(p))) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + /* and back to encrypt with the same structure again */ + XMEMSET(resultT, 0, sizeof(resultT)); + XMEMSET(resultC, 0, sizeof(resultC)); + ret = wc_AesGcmEncrypt(enc, resultC, p, sizeof(p), iv1, sizeof(iv1), + resultT, sizeof(resultT), a, sizeof(a)); +#if defined(WOLFSSL_ASYNC_CRYPT) + ret = wc_AsyncWait(ret, &enc->asyncDev, WC_ASYNC_FLAG_NONE); +#endif + if (ret != 0) + ERROR_OUT(WC_TEST_RET_ENC_EC(ret), out); + if (XMEMCMP(c1, resultC, sizeof(c1))) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + if (XMEMCMP(t1, resultT, sizeof(t1))) + ERROR_OUT(WC_TEST_RET_ENC_NC, out); + + ret = 0; + out: +#if defined(WOLFSSL_SMALL_STACK) && !defined(WOLFSSL_NO_MALLOC) + wc_AesDelete(enc, &enc); +#else + wc_AesFree(enc); +#endif +#endif + return ret; +} + WOLFSSL_TEST_SUBROUTINE wc_test_ret_t aesgcm_default_test(void) { + wc_test_ret_t ret; + #ifdef WOLFSSL_AES_128 byte key1[] = { 0x29, 0x8e, 0xfa, 0x1c, 0xcf, 0x29, 0xcf, 0x62, @@ -19348,7 +19482,6 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t aesgcm_default_test(void) 0x11, 0x64, 0xb2, 0xff }; - wc_test_ret_t ret; WOLFSSL_ENTER("aesgcm_default_test"); ret = aesgcm_default_test_helper(key1, sizeof(key1), iv1, sizeof(iv1), @@ -19371,6 +19504,13 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t aesgcm_default_test(void) } #endif + /* checked here rather than in aesgcm_test() so that it also runs for the + * ports that aesgcm_test() is skipped for, AF_ALG among them */ + ret = aesgcm_reuse_ctx_test(); + if (ret != 0) { + return ret; + } + return 0; }