W tym poście chciałbym opisać sprzętową akcelerację niektórych usług kryptograficznych na układzie STM32H735 oraz sposób ich integracji z biblioteką mbedTLS.
Projekt wykorzystuje takie bloki sprzętowe jak CRYP, HASH oraz RNG. Dzięki nim można przenieść część operacji kryptograficznych z rdzenia Cortex-M7 do odpowiednich bloków sprzętowych.
Taka implementacja pozwoli na wykorzystanie tych możliwości bez dodatkowych zmian interfejsów w bibliotece mbedTLS. Dostęp do akceleracji sprzętowej będzie realizowany przez standardowe funkcje biblioteki.
CRYP:
Blok CRYP odpowiada za sprzętową realizację operacji szyfrowania AES. W projekcie wykorzystuje go do AES-ECB, AES-CBC oraz AES-GCM.
W przypadku AES-ECB realizowane jest szyfrowanie i deszyfrowanie pojedynczych bloków danych o długości 128 bitów.
Dla AES-CBC przekazywany jest cały bufor danych, bez dodatkowych operacji szyfrowania dla każdego bloku na poziomie aplikacji. Do transferu danych pomiędzy pamięcią a modułem CRYP korzystam z DMA.
W trybie AES-GCM peryferium realizuje szyfrowanie AES w trybie licznikowym oraz operacje GHASH. Jest ono wykorzystywane podczas obliczania taga uwierzytelniającego.
HASH:
Blok HASH wykorzystywany jest do sprzętowego obliczania funkcji skrótu (SHA-224, SHA-256). Dane mogą być przekazywane strumieniowo. Dzięki temu nie ma buforowania całej wiadomości w pamięci RAM.
Nie korzystam tutaj z DMA. W tym przypadku jest to znacznie trudniejsze. Bibliotek mbedTLS obsługuje kilka niezależnych kontekstów SHA-256, które współdzielą jeden blok HASH. Takie działanie w trybie DMA byłoby utrudnione z powodu konieczności synchronizacji końcówki transferu czy współdzielenia HASH.
Polling pozwala dokładnie określić kiedy obliczenia zostały zakończone. Bezpiecznie zapisać stan tego peryferium. Wykorzystanie DMA w takim przypadku mogłoby wygenerować więcej problemów niż korzyści z jego zastosowania.
RNG:
Tutaj stosowany jest jako sprzętowe źródło entropii dla mbedTLS.
Dane przesyłane z RNG podawane są do generowania liczb pseudolosowych CTR_DRBG. Wykorzystywane jest to np. do nawiązywania połączenia TLS czy wykonywania operacji kryptograficznych wymagających losowości.
Konfiguracja MBED-TLS:
Pierwszym krokiem jest wybranie odpowiednich elementów, które odpowiadają za alternatywną implementację:
Otwieramy plik:
- MBEDTLS/App/mbedtls_config.h
Gdzie uruchamiamy następujące definicje:
- #define MBEDTLS_GCM_ALT
- #define MBEDTLS_SHA256_ALT
- #define MBEDTLS_AES_SETKEY_ENC_ALT
- #define MBEDTLS_AES_SETKEY_DEC_ALT
- #define MBEDTLS_AES_ENCRYPT_ALT
- #define MBEDTLS_AES_DECRYPT_ALT
- #define MBEDTLS_STM32_AES_CBC_HW
- #define MBEDTLS_ENTROPY_HARDWARE_ALT
Zostawiamy standardowe moduły z biblioteki uruchomione:
- #define MBEDTLS_AES_C
- #define MBEDTLS_GCM_C
- #define MBEDTLS_SHA256_C
- #define MBEDTLS_ENTROPY_C
- #define MBEDTLS_CTR_DRBG_C
Powyższe definicje odpowiadają za włączenie poszczególnych modułów w bibliotece mbedTLS.
Definicje z końcówka _ALT pozwalają na zastąpienie standardowej implementacji całego modułu czy wybranych funkcji własną implementacją.
Blok CRYP operacje AES-ECB:
STM32H735 posiada sprzętowy moduł CRYP. Pozwala on na wykonywanie operacji AES odciążając tym samym rdzeń układu Cortex-M7.
Dołożone zostały cztery podstawowe funkcje:
- mbedtls_aes_setkey_enc()
- mbedtls_aes_setkey_dec()
- mbedtls_internal_aes_encrypt()
- mbedtls_internal_aes_decrypt()
Odpowiadają one wcześniejszym definicjom:
- #define MBEDTLS_AES_SETKEY_ENC_ALT
- #define MBEDTLS_AES_SETKEY_DEC_ALT
- #define MBEDTLS_AES_ENCRYPT_ALT
- #define MBEDTLS_AES_DECRYPT_ALT
Przyjrzyjmy się tym funkcjom:
- #if defined(MBEDTLS_AES_SETKEY_ENC_ALT)
- static int stm32_aes_rounds_from_keybits(unsigned int keybits) {
- switch (keybits) {
- case 128U: return (10);
- case 192U: return (12);
- case 256U: return (14);
- default: return (-1);
- }
- }
- int mbedtls_aes_setkey_enc(mbedtls_aes_context *ctx,
- const unsigned char *key,
- unsigned int keybits)
- {
- stm32_mbedtls_aes_hw_ctx_t *hw_ctx;
- int rounds;
- if ((ctx == NULL) || (key == NULL)) {
- return (MBEDTLS_ERR_AES_BAD_INPUT_DATA);
- }
- //Wyznaczenie liczby rund AES na podstawie długości klucza
- rounds = stm32_aes_rounds_from_keybits(keybits);
- if (rounds < 0) {
- return (MBEDTLS_ERR_AES_INVALID_KEY_LENGTH);
- }
- memset(ctx, 0, sizeof(*ctx));
- //Zapis liczby rund oraz bufora
- ctx->nr = rounds;
- ctx->rk = ctx->buf;
- hw_ctx = stm32_aes_ctx_get(ctx);
- hw_ctx->keybits = keybits;
- //Zapisywanie klucza
- memcpy(hw_ctx->key_bytes, key, keybits / 8U);
- return (0);
- }
- #endif
Powyższa funkcja przygotowuje dane kontekstu AES wykorzystywanego do operacji szyfrowania. Zapamiętywany jest klucz wraz z długością. Operacje AES będę wykonywane później przez blok sprzętowy CRYP.
Klucz do operacji deszyfrowania jest przygotowany przez tą samą funkcję. Z funkcji mbedtls_aes_setkey_dec(...) jest wywoływana funckja mbedtls_aes_setkey_enc(...). Wykorzystywany będzie ten sam klucz. Wybór operacji w bloku CRYP określi czy będzie wykonywana operacja szyfrowania czy deszyfrowania.
Teraz funkcja wykonująca szyfrowanie pojedynczego bloku AES:
- #if defined(MBEDTLS_AES_ENCRYPT_ALT)
- static int stm32_aes_prepare_hw(const stm32_mbedtls_aes_hw_ctx_t *hw_ctx)
- {
- stm32_aes_prepare_key_words((uint32_t *)hw_ctx->key_words, hw_ctx->key_bytes,
- hw_ctx->keybits, g_stm32_aes_swap_key_words);
- return stm32_aes_prepare_hw_words((uint32_t *)hw_ctx->key_words, hw_ctx->keybits,
- g_stm32_aes_data_type, CRYP_AES_ECB, NULL);
- }
- static void stm32_aes_lock(void)
- {
- if (g_mbedTlsMutex != NULL)
- {
- (void)xSemaphoreTakeRecursive(g_mbedTlsMutex, portMAX_DELAY);
- }
- }
- int mbedtls_internal_aes_encrypt(mbedtls_aes_context *ctx,
- const unsigned char input[16],
- unsigned char output[16])
- {
- HAL_StatusTypeDef status;
- int ret;
- if ((ctx == NULL) || (input == NULL) || (output == NULL)) {
- return (MBEDTLS_ERR_AES_BAD_INPUT_DATA);
- }
- //Blokowanie dostępu do modułu przez
- //ustawienie mutexa
- stm32_aes_lock();
- //Sprawdzenie działania
- stm32_aes_run_selftest_once();
- //Przygotowanie modułu do działania:
- //ustawienie klucza, jego długości, kolejności
- ret = stm32_aes_prepare_hw(stm32_aes_ctx_get(ctx));
- if (ret == 0)
- {
- //Wykonanie szyfrowania. Operacja na bloku 128 bitowym (16 bajtów)
- status = HAL_CRYP_Encrypt(&hcryp, (uint32_t *)input, 16U,
- (uint32_t *)output, HAL_MAX_DELAY);
- //Konwersja kodu błędu
- ret = (status == HAL_OK)
- ? 0
- : MBEDTLS_ERR_AES_HW_ACCEL_FAILED;
- if (ret != 0) {
- //Wypisanie błędu na konsole.
- stm32_aes_log_block("enc", input, output,
- stm32_aes_ctx_get(ctx)->keybits, status);
- }
- }
- else {
- printf("[AESHW] enc prep ERROR ret=%d\r\n", ret);
- }
- //Zwolnienie dostępu
- stm32_aes_unlock();
- return ret;
- }
- #endif
Do funkcji przekazywany jest pojedynczy blok AES (16 bajtów) . Po zakończeniu operacji zwracany jest 16-bajtowy blok zaszyfrowanych danych.
Funkcja deszyfrująca jest podobna więc nie będę jej już tutaj wklejał.
Głównym elementem sprzętowej implementacji są funkcje HAL_CRYP_Encrypt(...) oraz HAL_CRYP_Decrypt(). Moduł CRYP skonfigurowany jest w trybie CRYP_AES_ECB. Do peryferium przekazywany jest pojedynczy 128-bitowy blok danych. Właściwe operacje AES wykonywane są przez sprzętowy akcelerator kryptograficzny. Rdzeń Cortex-M7 nie musi programowo wykonywać kolejnych rund algorytmu AES.
Sprzętowa realizacja AES-CBC:
Peryferium konfigurujemy jako CRYP_AES_CBC. Moduł otrzymuje klucz AES, IV, bufor wejściowy oraz długość danych. Blok CRYP wykonuje operacje AES oraz mechanizm łańcuchowania charakterystyczny dla CBC.
Po stronie programu konieczne jest poprawne przygotowanie konfiguracji modułu. Cała funkcja wygląda następująco:
- int stm32_mbedtls_aes_crypt_cbc(mbedtls_aes_context *ctx,
- int mode,
- size_t length,
- unsigned char iv[16],
- const unsigned char *input,
- unsigned char *output)
- {
- stm32_mbedtls_aes_hw_ctx_t *hw_ctx;
- uint32_t iv_words[4];
- unsigned char last_cipher[16];
- size_t chunk_length;
- HAL_StatusTypeDef status;
- int ret;
- if ((ctx == NULL) || (iv == NULL) || (input == NULL) || (output == NULL) ||
- ((mode != MBEDTLS_AES_ENCRYPT) && (mode != MBEDTLS_AES_DECRYPT)) ||
- ((length % 16U) != 0U))
- {
- return MBEDTLS_ERR_AES_BAD_INPUT_DATA;
- }
- if (length == 0U)
- {
- return 0;
- }
- stm32_aes_lock();
- stm32_aes_run_selftest_once();
- ret = stm32_aes_run_cbc_selftest_once();
- if (ret != 0)
- {
- stm32_aes_unlock();
- return ret;
- }
- hw_ctx = stm32_aes_ctx_get(ctx);
- stm32_aes_prepare_key_words(hw_ctx->key_words, hw_ctx->key_bytes,
- hw_ctx->keybits, g_stm32_aes_swap_key_words);
- while (length != 0U)
- {
- chunk_length = (length > 65520U) ? 65520U : length;
- stm32_aes_prepare_iv_words(iv_words, iv);
- ret = stm32_aes_prepare_hw_words(hw_ctx->key_words, hw_ctx->keybits,
- g_stm32_aes_data_type, CRYP_AES_CBC,
- iv_words);
- if (ret != 0)
- {
- break;
- }
- if (mode == MBEDTLS_AES_DECRYPT)
- {
- memcpy(last_cipher, input + chunk_length - sizeof(last_cipher),
- sizeof(last_cipher));
- }
- if ((chunk_length >= STM32_AES_DMA_MIN_LENGTH) &&
- ((((uintptr_t)input | (uintptr_t)output) & 3U) == 0U) &&
- (stm32_aes_dma_addressable(input, chunk_length) != 0) &&
- (stm32_aes_dma_addressable(output, chunk_length) != 0))
- {
- status = stm32_aes_cbc_dma(mode, input, output,
- (uint16_t)chunk_length);
- }
- else if (mode == MBEDTLS_AES_DECRYPT)
- {
- status = HAL_CRYP_Decrypt(&hcryp, (uint32_t *)input,
- (uint16_t)chunk_length, (uint32_t *)output,
- HAL_MAX_DELAY);
- }
- else
- {
- status = HAL_CRYP_Encrypt(&hcryp, (uint32_t *)input,
- (uint16_t)chunk_length, (uint32_t *)output,
- HAL_MAX_DELAY);
- }
- if (status != HAL_OK)
- {
- stm32_aes_log_block("CBC", input, output, hw_ctx->keybits, status);
- ret = MBEDTLS_ERR_AES_HW_ACCEL_FAILED;
- break;
- }
- if (mode == MBEDTLS_AES_DECRYPT)
- {
- memcpy(iv, last_cipher, sizeof(last_cipher));
- }
- else
- {
- memcpy(iv, output + chunk_length - sizeof(last_cipher),
- sizeof(last_cipher));
- }
- input += chunk_length;
- output += chunk_length;
- length -= chunk_length;
- }
- stm32_aes_unlock();
- return ret;
- }
Istotnym elementem powyższej funkcji jest:
- ret = stm32_aes_prepare_hw_words(
- hw_ctx->key_words,
- hw_ctx->keybits,
- g_stm32_aes_data_type,
- CRYP_AES_CBC,
- iv_words);
Blok CRYP zostaje ustawiony w trybie CRYP_AES_CBC.
Po skonfigurowaniu modułu CRYP dane można przekazywać do HAL_CRYP_Encrypt() czy HAL_CRYP_Decrypt(). Ścieżka DMA jest wykorzystywana jak zostaną spełnione warunki długości danych, wyrównania oraz obszaru pamięci.
Powyższa funkcja zarządza całym kontekstem, IV, dzieleniem dużych buforów oraz wyborem ścieżki DMA bądź pollingu.
Sprzętowa realizacja AES-GCM:
Standardowe funkcje GCM z mbedTLS zostały wyłączone. Zostały zastąpione własną implementację z wykorzystaniem bloku CRYP:
- mbedtls_gcm_setkey();
- mbedtls_gcm_starts();
- mbedtls_gcm_update();
- mbedtls_gcm_finish();
- mbedtls_gcm_crypt_and_tag();
- mbedtls_gcm_auth_decrypt();
Na samym początku należy przygotować klucz.
- int mbedtls_gcm_setkey(mbedtls_gcm_context *ctx, mbedtls_cipher_id_t cipher,
- const unsigned char *key, unsigned int keybits)
- {
- if ((ctx == NULL) || (key == NULL) || (cipher != MBEDTLS_CIPHER_ID_AES) ||
- ((keybits != 128U) && (keybits != 192U) && (keybits != 256U)))
- {
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- gcm_abort(ctx);
- memset(ctx->key, 0, sizeof(ctx->key));
- memcpy(ctx->key, key, keybits / 8U);
- gcm_key_words(ctx->key_words, ctx->key, keybits);
- ctx->keybits = keybits;
- return 0;
- }
Powyższa funkcja sprawdza długość klucza, zapisuje wartość w kontekście GCM oraz przygotowuje słowa kluczowe. Wszystko w formacie wymaganym przeze peryferiom CRYP.
W następnym etapie jest wywołanie funkcji mbedtls_gcm_starts().
- int mbedtls_gcm_starts(mbedtls_gcm_context *ctx, int mode,
- const unsigned char *iv, size_t iv_len,
- const unsigned char *add, size_t add_len)
- {
- int ret;
- if (ctx == NULL)
- {
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- gcm_abort(ctx);
- if (((mode != MBEDTLS_GCM_ENCRYPT) &&
- (mode != MBEDTLS_GCM_DECRYPT)) || (iv == NULL) ||
- (iv_len != 12U) || ((add_len != 0U) && (add == NULL)) ||
- (add_len > UINT32_MAX) || (ctx->keybits == 0U))
- {
- return (iv_len != 12U) ? MBEDTLS_ERR_PLATFORM_FEATURE_UNSUPPORTED :
- MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- ctx->mode = mode;
- gcm_initial_counter(ctx->iv_words, iv);
- gcm_lock(ctx);
- ctx->trace_id = ++g_gcm_trace_sequence;
- GCM_TRACE("[GCMHW] start id=%lu mode=%s aad=%lu\r\n",
- (unsigned long)ctx->trace_id,
- (mode == MBEDTLS_GCM_ENCRYPT) ? "enc" : "dec",
- (unsigned long)add_len);
- ret = gcm_selftest_once();
- if (ret == 0)
- {
- ret = gcm_start_hardware(ctx, add, add_len);
- }
- if (ret != 0)
- {
- gcm_abort(ctx);
- return ret;
- }
- ctx->active = 1U;
- return 0;
- }
Powyższa funkcja przygotowuje kontekst operacji, IV. Implementacja przyjmuje IV o długości 12 bajtów. Dalej przygotowywany jest początkowy licznik GCM oraz uruchamiana jest konfiguracja sprzętowego CRYP.
Sprzętowe uruchomienie wygląda następująco:
- static int gcm_start_hardware(mbedtls_gcm_context *ctx,
- const unsigned char *add, size_t add_len)
- {
- static const unsigned char empty[1] = { 0U };
- const unsigned char *header = add;
- uint32_t key_size;
- HAL_StatusTypeDef status;
- if (gcm_key_size(ctx->keybits, &key_size) != 0)
- {
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- hcryp.Instance = CRYP;
- hcryp.Init.DataType = CRYP_BYTE_SWAP;
- hcryp.Init.KeySize = key_size;
- hcryp.Init.pKey = ctx->key_words;
- hcryp.Init.Algorithm = CRYP_AES_GCM;
- hcryp.Init.pInitVect = ctx->iv_words;
- if ((add_len > 0U) && (add_len <= sizeof(ctx->header_words)))
- {
- memset(ctx->header_words, 0, sizeof(ctx->header_words));
- memcpy(ctx->header_words, add, add_len);
- header = (const unsigned char *)ctx->header_words;
- }
- hcryp.Init.Header = (uint32_t *)header;
- hcryp.Init.HeaderSize = (uint32_t)add_len;
- hcryp.Init.B0 = NULL;
- hcryp.Init.DataWidthUnit = CRYP_DATAWIDTHUNIT_BYTE;
- hcryp.Init.HeaderWidthUnit = CRYP_HEADERWIDTHUNIT_BYTE;
- hcryp.Init.KeyIVConfigSkip = CRYP_KEYIVCONFIG_ONCE;
- if (HAL_CRYP_Init(&hcryp) != HAL_OK)
- {
- return MBEDTLS_ERR_GCM_HW_ACCEL_FAILED;
- }
- status = (ctx->mode == MBEDTLS_GCM_ENCRYPT) ?
- HAL_CRYP_Encrypt(&hcryp, (uint32_t *)empty, 0U,
- (uint32_t *)empty, HAL_MAX_DELAY) :
- HAL_CRYP_Decrypt(&hcryp, (uint32_t *)empty, 0U,
- (uint32_t *)empty, HAL_MAX_DELAY);
- return (status == HAL_OK) ? 0 : MBEDTLS_ERR_GCM_HW_ACCEL_FAILED;
- }
Jak widać powyżej ustawione jest w trybie CRYP_AES_GCM.
Właściwe szyfrowanie czy deszyfrowanie realizowane jest przez funkcje mbedtls_gcm_update().
- int mbedtls_gcm_update(mbedtls_gcm_context *ctx, size_t length,
- const unsigned char *input, unsigned char *output)
- {
- HAL_StatusTypeDef status;
- if ((ctx == NULL) || (ctx->active == 0U))
- {
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- if (((length != 0U) && ((input == NULL) || (output == NULL))) ||
- (length > UINT16_MAX) || ((ctx->final_input_seen != 0U) && (length != 0U)))
- {
- gcm_abort(ctx);
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- if (length == 0U)
- {
- return 0;
- }
- GCM_TRACE("[GCMHW] update id=%lu mode=%s len=%lu\r\n",
- (unsigned long)ctx->trace_id,
- (ctx->mode == MBEDTLS_GCM_ENCRYPT) ? "enc" : "dec",
- (unsigned long)length);
- size_t dma_length = length & ~(size_t)15U;
- if ((dma_length >= GCM_DMA_MIN_LENGTH) && (hcryp.hdmain != NULL) &&
- (g_mbedTlsMutex != NULL) &&
- (hcryp.hdmaout != NULL) && (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) &&
- stm32_crypto_dma_addressable(g_gcm_dma_input, sizeof(g_gcm_dma_input)) &&
- stm32_crypto_dma_addressable(g_gcm_dma_output, sizeof(g_gcm_dma_output)))
- {
- status = gcm_dma_payload(ctx->mode, input, output, dma_length);
- }
- else
- {
- dma_length = 0U;
- status = HAL_OK;
- }
- /* HAL's polling implementation handles partial blocks and silicon workarounds. */
- if ((status == HAL_OK) && (dma_length < length))
- {
- status = (ctx->mode == MBEDTLS_GCM_ENCRYPT) ?
- HAL_CRYP_Encrypt(&hcryp, (uint32_t *)(input + dma_length),
- (uint16_t)(length - dma_length),
- (uint32_t *)(output + dma_length), HAL_MAX_DELAY) :
- HAL_CRYP_Decrypt(&hcryp, (uint32_t *)(input + dma_length),
- (uint16_t)(length - dma_length),
- (uint32_t *)(output + dma_length), HAL_MAX_DELAY);
- }
- if (status != HAL_OK)
- {
- gcm_abort(ctx);
- return MBEDTLS_ERR_GCM_HW_ACCEL_FAILED;
- }
- ctx->payload_length += length;
- if ((length % 16U) != 0U)
- {
- ctx->final_input_seen = 1U;
- }
- return 0;
- }
Dla większych fragmentów danych wykorzystywana jest ścieżka DMA. Minimalna długość danych wynosi 256 bajtów. DMA korzysta z fragmentów do 1024 bajtów. Poprawne przetworzenie danych zwiększa całkowitą długość danych o przetworzony blok.
Transfer danych do DMA wykonywany jest przez funkcję gcm_dma_transfer().
- static HAL_StatusTypeDef gcm_dma_transfer(int mode, const unsigned char *input,
- unsigned char *output, uint16_t length,
- size_t output_cache_length)
- {
- HAL_StatusTypeDef status;
- uint32_t start;
- if (g_gcm_dma_semaphore == NULL)
- {
- g_gcm_dma_semaphore = xSemaphoreCreateBinaryStatic(&g_gcm_dma_semaphore_storage);
- }
- (void)xSemaphoreTake(g_gcm_dma_semaphore, 0);
- stm32_crypto_dma_clean(input, length);
- stm32_crypto_dma_clean(output, output_cache_length);
- g_gcm_dma_waiting = 1U;
- status = (mode == MBEDTLS_GCM_ENCRYPT) ?
- HAL_CRYP_Encrypt_DMA(&hcryp, (uint32_t *)input, length, (uint32_t *)output) :
- HAL_CRYP_Decrypt_DMA(&hcryp, (uint32_t *)input, length, (uint32_t *)output);
- if ((status == HAL_OK) &&
- (xSemaphoreTake(g_gcm_dma_semaphore,
- pdMS_TO_TICKS(STM32_CRYPTO_DMA_TIMEOUT_MS)) != pdTRUE))
- {
- status = HAL_TIMEOUT;
- }
- start = HAL_GetTick();
- while ((status == HAL_OK) && (HAL_CRYP_GetError(&hcryp) == HAL_CRYP_ERROR_NONE) &&
- ((HAL_CRYP_GetState(&hcryp) == HAL_CRYP_STATE_BUSY) ||
- (HAL_DMA_GetState(hcryp.hdmain) == HAL_DMA_STATE_BUSY) ||
- (HAL_DMA_GetState(hcryp.hdmaout) == HAL_DMA_STATE_BUSY)))
- {
- if ((HAL_GetTick() - start) >= STM32_CRYPTO_DMA_TIMEOUT_MS)
- {
- status = HAL_TIMEOUT;
- break;
- }
- taskYIELD();
- }
- g_gcm_dma_waiting = 0U;
- if ((status != HAL_OK) || (HAL_CRYP_GetError(&hcryp) != HAL_CRYP_ERROR_NONE))
- {
- CLEAR_BIT(hcryp.Instance->DMACR, CRYP_DMACR_DIEN | CRYP_DMACR_DOEN);
- __HAL_CRYP_DISABLE(&hcryp);
- (void)HAL_DMA_Abort(hcryp.hdmain);
- (void)HAL_DMA_Abort(hcryp.hdmaout);
- hcryp.State = HAL_CRYP_STATE_READY;
- __HAL_UNLOCK(&hcryp);
- if (status == HAL_OK) status = HAL_ERROR;
- }
- stm32_crypto_dma_invalidate(output, output_cache_length);
- return status;
- }
Przed rozpoczęciem DMA czyszczony jest D-Cache. Po zakończeniu transferu bufor wynikowy jest niszczony. Ta operacja jest konieczna ponieważ bez odpowiedniej obsługi cache DMA mógłby zapisać nowe dane do pamięci RAM. Natomiast procesor dalej korzystałby ze starej kopii, która została umieszczona w D-Cache.
Jeśli bufor wejściowy lub wyjściowy nie spełnia wymagań dostępności dla DMA czy wyrównania dane zostają skopiowane do buforów pomocniczych. Zostają przeniesione do sekcji .crypto_dma_buffers. Gdzie następuje ich wyrównanie do 32 bajtów.
Jak już cała wiadomość jest przetworzona. to przechodzimy do generowania taga uwierzytelniającego.
- int mbedtls_gcm_finish(mbedtls_gcm_context *ctx, unsigned char *tag, size_t tag_len)
- {
- uint32_t tag_words[4] = { 0U };
- unsigned char tag_bytes[16];
- size_t payload_length;
- int mode;
- HAL_StatusTypeDef status;
- if ((ctx == NULL) || (ctx->active == 0U))
- {
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- if ((tag == NULL) || (tag_len < 4U) || (tag_len > sizeof(tag_bytes)))
- {
- gcm_abort(ctx);
- return MBEDTLS_ERR_GCM_BAD_INPUT;
- }
- status = HAL_CRYPEx_AESGCM_GenerateAuthTAG(&hcryp, tag_words, HAL_MAX_DELAY);
- if (status == HAL_OK)
- {
- gcm_tag_to_bytes(tag_bytes, tag_words);
- memcpy(tag, tag_bytes, tag_len);
- }
- payload_length = ctx->payload_length;
- mode = ctx->mode;
- gcm_abort(ctx);
- GCM_TRACE("[GCMHW] finish id=%lu mode=%s payload=%lu status=%d\r\n",
- (unsigned long)ctx->trace_id,
- (mode == MBEDTLS_GCM_ENCRYPT) ? "enc" : "dec",
- (unsigned long)payload_length, (int)status);
- mbedtls_platform_zeroize(tag_bytes, sizeof(tag_bytes));
- return (status == HAL_OK) ? 0 : MBEDTLS_ERR_GCM_HW_ACCEL_FAILED;
- }
Funkcja HAL_CRYPex_AESGCM_GenerateAuthTAG(..) generuje tag uwierzytelniający. Jak już tag jest gotowy to następuje jego konwersja do postaci bajtowej.
MbedTLS wykonuje cały powyższy proces przez funkcje mbedtls_gmc_crypt_and_tag().
Blok CRYP wykonuje znacznie więcej operacji dla AES-GCM niż dla AES-ECB czy AES-CBC. Sprzęt odpowiada za szyfrowanie danych w trybie GCM oraz za operacje związane z wyznaczeniem wartości uwierzytelniającej.
Sprzętowa realizacja SHA-224 oraz SHA-256:
Teraz przechodzimy do bloku HASH. Obsługuje on w projekcie SHA-224 oraz SHA-256.
Skupimy się tutaj tylko na jednym z nich czyli SHA-256. Najważniejsze funkcje to mbedtls_sha256_starts_ret(), mbedtls_sha256_update_ret(), mbedtls_sha256_finish_ret(). Należy także pamiętać o mechanizmie zapisywania i odtwarzania sanu sprzętowego HASH. Moduł ten może być współdzielony przez kilka kontekstów mbedTLS.
Obliczenia są rozpoczynane od funkcji mbedtls_sha256_starts_ret:
- int mbedtls_sha256_starts_ret(mbedtls_sha256_context *ctx, int is224)
- {
- if (ctx == NULL)
- {
- return MBEDTLS_ERR_SHA256_BAD_INPUT_DATA;
- }
- memset(ctx, 0, sizeof(*ctx));
- ctx->is224 = is224 ? 1 : 0;
- return 0;
- }
W niej następuje zerowanie kontekstu oraz określenie który z algorytmów zostanie użyty.
Jak już wspomniałem istotne jest współdzielenie modułu. Tutaj z pomocą przychodzi następująca funkcja:
- static HAL_StatusTypeDef sha256_alt_prepare(mbedtls_sha256_context *ctx)
- {
- hhash.Init.DataType = HASH_DATATYPE_8B;
- if (HAL_HASH_Init(&hhash) != HAL_OK)
- {
- return HAL_ERROR;
- }
- if (ctx->hw_started != 0U)
- {
- HAL_HASH_ContextRestoring(&hhash, (const uint8_t *)ctx->hw_context);
- hhash.Phase = HAL_HASH_PHASE_PROCESS;
- hhash.State = HAL_HASH_STATE_READY;
- hhash.Status = HAL_OK;
- hhash.Accumulation = 1U;
- }
- return HAL_OK;
- }
Wykonywana jest przed każdym rozpoczęciem operacji. Jeśli dany kontekst był już używany to odtwarzamy jego poprzedni stan. Po przetworzeniu kolejnej partii danych stan sprzętowego mudułu jest zapisywany:
- static void sha256_alt_save(mbedtls_sha256_context *ctx)
- {
- HAL_HASH_ContextSaving(&hhash, (const uint8_t *)ctx->hw_context);
- ctx->hw_started = 1U;
- }
Przy odtwarzaniu nie ma konieczności ponownego przeliczania danych dla przywróconego kontekstu.
Najważniejszą funkcją wykorzystywaną przy przetwarzaniu danych jest update_ret:
- int mbedtls_sha256_update_ret(mbedtls_sha256_context *ctx,
- const unsigned char *input,
- size_t ilen)
- {
- unsigned char block[4];
- size_t aligned_length;
- size_t offset = 0U;
- int ret;
- if ((ctx == NULL) || ((ilen > 0U) && (input == NULL)))
- {
- return MBEDTLS_ERR_SHA256_BAD_INPUT_DATA;
- }
- if (ctx->tail_length != 0U)
- {
- size_t needed = 4U - ctx->tail_length;
- if (ilen < needed)
- {
- memcpy(ctx->tail + ctx->tail_length, input, ilen);
- ctx->tail_length += (uint8_t)ilen;
- return 0;
- }
- memcpy(block, ctx->tail, ctx->tail_length);
- memcpy(block + ctx->tail_length, input, needed);
- ret = (sha256_alt_accumulate(ctx, block, sizeof(block)) == HAL_OK) ?
- 0 : MBEDTLS_ERR_SHA256_HW_ACCEL_FAILED;
- if (ret != 0)
- {
- return ret;
- }
- ctx->tail_length = 0U;
- offset = needed;
- }
- aligned_length = (ilen - offset) & ~(size_t)0x3U;
- if (aligned_length != 0U)
- {
- if (aligned_length > UINT32_MAX)
- {
- return MBEDTLS_ERR_SHA256_BAD_INPUT_DATA;
- }
- ret = (sha256_alt_accumulate(ctx, input + offset,
- (uint32_t)aligned_length) == HAL_OK) ?
- 0 : MBEDTLS_ERR_SHA256_HW_ACCEL_FAILED;
- if (ret != 0)
- {
- return ret;
- }
- offset += aligned_length;
- }
- if (offset < ilen)
- {
- ctx->tail_length = (uint8_t)(ilen - offset);
- memcpy(ctx->tail, input + offset, ctx->tail_length);
- }
- return 0;
- }
Funkcja przekazuje dane strumieniowo. Nie musi on być trzymany w jednym buforze. Do bloku HASH przekazywane są dane po 32 bity. Pozostałe 1-3 bajty są przechowywane w tail.
Funkcja korzysta z sha256_alt_accumulate, która przekazuje dane do HASH:
- static HAL_StatusTypeDef sha256_alt_accumulate(mbedtls_sha256_context *ctx,
- const unsigned char *input,
- uint32_t length)
- {
- HAL_StatusTypeDef status;
- if (length == 0U)
- {
- return HAL_OK;
- }
- sha256_alt_lock();
- if (sha256_alt_prepare(ctx) != HAL_OK)
- {
- sha256_alt_unlock();
- return HAL_ERROR;
- }
- if (ctx->is224 != 0)
- {
- status = HAL_HASHEx_SHA224_Accmlt(&hhash, input, length);
- }
- else
- {
- status = HAL_HASHEx_SHA256_Accmlt(&hhash, input, length);
- }
- if (status == HAL_OK)
- {
- status = sha256_alt_wait_idle();
- }
- if (status == HAL_OK)
- {
- sha256_alt_save(ctx);
- }
- sha256_alt_unlock();
- return status;
- }
SHA-224 wykorzystuje funkcje HAL_HASHEx_SHA224_Accmlt. SHA-256 korzysta z HAL_HASHEx_SHA256_Accnkt(). Funkcja może przetworzyć kolejny fragment wiadomości bez kończenia całej operacji SHA. Aktualny stan sprzętowego HASH jest zapisywany.
Obliczenia są kończone przez funkcję mbedtls_sha256_finish_ret:
- int mbedtls_sha256_finish_ret(mbedtls_sha256_context *ctx,
- unsigned char output[32])
- {
- static const unsigned char zero_byte[1] = { 0U };
- const unsigned char *tail;
- HAL_StatusTypeDef status;
- if ((ctx == NULL) || (output == NULL))
- {
- return MBEDTLS_ERR_SHA256_BAD_INPUT_DATA;
- }
- tail = (ctx->tail_length != 0U) ? ctx->tail : zero_byte;
- sha256_alt_lock();
- status = sha256_alt_finish(
- ctx,
- tail,
- ctx->tail_length,
- output);
- sha256_alt_unlock();
- if (status != HAL_OK)
- {
- return MBEDTLS_ERR_SHA256_HW_ACCEL_FAILED;
- }
- ctx->tail_length = 0U;
- ctx->hw_started = 0U;
- return 0;
- }
Powyższa funkcja przekazuje ostatnie bajty danych po czym kończy obliczenia. Powyższa funkcja wywołuje funkcję pomocniczą sha256_alt_finish:
- static HAL_StatusTypeDef sha256_alt_finish(mbedtls_sha256_context *ctx,
- const unsigned char *input,
- uint32_t length,
- unsigned char output[32])
- {
- HAL_StatusTypeDef status;
- if (sha256_alt_prepare(ctx) != HAL_OK) {
- return HAL_ERROR;
- }
- if (ctx->is224 != 0) {
- status = HAL_HASHEx_SHA224_Accmlt_End(&hhash, input, length, output, HAL_MAX_DELAY);
- } else {
- status = HAL_HASHEx_SHA256_Accmlt_End(&hhash, input, length, output, HAL_MAX_DELAY);
- }
- return status;
- }
Operacje kończone są przez funkcje z biblioteki HAL w zależności od rodzaju wybranej biblioteki. Wynik operacji jest przekazywany do output. Dla SHA-256 długość wynosi 32 bajty. W przypadku SHA-224 wykorzystywane jest pierwsze 28 bajtów wyniku.
Najważniejszą częścią tej implementacji jest możliwość strumieniowego przekazywania danych wraz z zapisem stanu sprzętowego HASH pomiędzy wywołaniami.
Synchronizacja dostępu do CRYP i HASH:
Sprzętowe moduły kryptograficzne są pojedynczymi peryferiami sprzętowymi. Biblioteka mbedTLS może być wykorzystywana przez kilka zadań systemowych np. HTTPS czy klient TLS.
Przeskakiwanie pomiędzy wywołaniami może jednocześnie zmieniać konfigurację tego samego peryferium. Może to doprowadzić do uszkodzenia kontekstu kryptograficznego bądź do otrzymania niepoprawnego wyniku.
Z tego powodu dobrze jest zabezpieczać dostęp do bloków sprzętowych przez mutexy
W konfiguracji FreeRTOS została włączona obsługa mutexów rekurencyjnych (configUSE_RECURSIVE_MUTEXES). Tworzenie mutexu odbywa się w następujący sposób:
- g_mbedTlsMutex = xSemaphoreCreateRecursiveMutex();
Przed rozpoczęciem operacji sprzętowej ustawiany jest ten mutex przez funkcje stm32_aes_lock. Jego zwolnienie obsługiwane jest przez funkcję stm32_aes_unlock:
- static void stm32_aes_lock(void)
- {
- if (g_mbedTlsMutex != NULL)
- {
- (void)xSemaphoreTakeRecursive(
- g_mbedTlsMutex,
- portMAX_DELAY);
- }
- }
- static void stm32_aes_unlock(void)
- {
- if (g_mbedTlsMutex != NULL)
- {
- (void)xSemaphoreGiveRecursive(
- g_mbedTlsMutex);
- }
- }
Operacje są podobne dla modułu HASH.
Jeśli jakieś zadanie w tym samym czasie chce skorzystać z tego samego bloku, to musi poczekać na jego zwolnienie.
Wykorzystywanie mutexu rekurecyjnego jest tutaj szczególnie istotne. Funckja wyższego poziomu może mieć już założoną blokadę. Chce następie wywołać kolejną funkcję, która również potrzebuje dostępu do tego samego mutexu. Zwykły mutex mógłby tu wywołać zaklesczenie zadania. Mutex rekurencyjny pozwala temu samemu zadaniu wielokrotnie przejąć tę samą blokadę. Oczywiście musi ją później zwolnić.
Dzięki takim operacjom moduły CRYP oraz HASH mogą być wielokrotnie współdzielone przez kilka operacji kryptograficznych.
Zakończenie:
Przedstawiona implementacja wykorzystuje sprzętowe bloku kryptograficzne. Pozwala to znacząco przyśpieszyć wybrane operacje wykonywane przez bibliotekę mbedTLS.
Blok CRYP realizuje operacje AES w trybach ECB, CBC oraz GCM. W przypadku CBC możliwe jest przetwarzanie większych buforów oraz wykorzystanie DMA. GCM dodatkowo korzysta ze sprzętowej obsługi mechanizmu uwierzytelniania danych.
Blok HASH oblicza SHA-224 oraz SHA-256. Dane mogą być przekazane strumieniowo. Stan peryferium jest zapisywany i odtwarzany pomiędzy kolejnymi operacjami.
Największą zaletą takiej integracji jest zachowanie standardowego interfejsu biblioteki.
Zastosowanie sprzętowych peryferiów dostępny w STM32H735 pozwala na odciążenie rdzenia Cortex-M7.