psa_cipher_update reports BUFFER_TOO_SMALL even if buffer size equals to PSA_CIPHER_UPDATE_OUTPUT_SIZE #592

Open
opened 2025-12-07 11:31:31 -06:00 by jnmeurisse · 10 comments
jnmeurisse commented 2025-12-07 11:31:31 -06:00 (Migrated from github.com)

Summary

psa_cipher_update returns BUFFER_TOO_SMALL status even if buffer is allocated with size equals to PSA_CIPHER_UPDATE_OUTPUT_SIZE. This error is reported when decrypting a buffer using PSA_ALG_CBC_PKCS7 algorithm.

System information

TF-PSA-Crypto version (number or commit id): 1.0.0
Operating system and version: Linux 5.15.0-161-generic
Configuration (if not default, please attach crypto_config.h): default crypto_config.h

Expected behavior

psa_cipher_update does not fail when we provide an output buffer sized with PSA_CIPHER_UPDATE_OUTPUT_SIZE.

Actual behavior

psa_cipher_update returns BUFFER_TOO_SMALL when psa_cipher_update is called more than once. In fact psa_cipher_update fails when the total input length is larger than the block size.

Steps to reproduce


[test_psa.c](https://github.com/user-attachments/files/24017405/test_psa.c)

### Summary psa_cipher_update returns BUFFER_TOO_SMALL status even if buffer is allocated with size equals to PSA_CIPHER_UPDATE_OUTPUT_SIZE. This error is reported when decrypting a buffer using PSA_ALG_CBC_PKCS7 algorithm. ### System information TF-PSA-Crypto version (number or commit id): 1.0.0 Operating system and version: Linux 5.15.0-161-generic Configuration (if not default, please attach `crypto_config.h`): default crypto_config.h ### Expected behavior psa_cipher_update does not fail when we provide an output buffer sized with PSA_CIPHER_UPDATE_OUTPUT_SIZE. ### Actual behavior psa_cipher_update returns BUFFER_TOO_SMALL when psa_cipher_update is called more than once. In fact psa_cipher_update fails when the total input length is larger than the block size. ### Steps to reproduce ``` [test_psa.c](https://github.com/user-attachments/files/24017405/test_psa.c) ```
yanesca commented 2025-12-08 10:33:28 -06:00 (Migrated from github.com)

Hi @jnmeurisse, thank you for your interest in TF-PSA-Crypto and for your bug report. I was trying to access the reproducer at https://github.com/user-attachments/files/24017405/test_psa.c, but I am getting a 404. Do you think you could paste it in a message or attach it in a different way?

Hi @jnmeurisse, thank you for your interest in TF-PSA-Crypto and for your bug report. I was trying to access the reproducer at https://github.com/user-attachments/files/24017405/test_psa.c, but I am getting a 404. Do you think you could paste it in a message or attach it in a different way?
gilles-peskine-arm commented 2025-12-08 11:42:13 -06:00 (Migrated from github.com)

CBC is based on blocks, so each update call may output a little less or a little more than the input, depending on the block alignment. CBC-PKCS7 decryption requires an output buffer that's larger than the actual data in some cases, so that its execution time doesn't depend on the amount of padding. But I thought that PSA_CIPHER_UPDATE_OUTPUT_SIZE and PSA_CIPHER_FINISH_OUTPUT_SIZE already accounted for these peculiarities. I guess we didn't get it right? But we would definitely need to see a reproducer.

CBC is based on blocks, so each update call may output a little less or a little more than the input, depending on the block alignment. CBC-PKCS7 decryption requires an output buffer that's larger than the actual data in some cases, so that its execution time doesn't depend on the amount of padding. But I thought that `PSA_CIPHER_UPDATE_OUTPUT_SIZE` and `PSA_CIPHER_FINISH_OUTPUT_SIZE` already accounted for these peculiarities. I guess we didn't get it right? But we would definitely need to see a reproducer.
jnmeurisse commented 2025-12-08 13:22:32 -06:00 (Migrated from github.com)

Here is the source code demonstrating the issue:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "psa/crypto.h"

static psa_status_t encrypt(
  psa_key_id_t key_id,
  uint8_t *iv,
  size_t iv_size,
  size_t *iv_length,
  uint8_t *input,
  size_t input_length,
  uint8_t *output,
  size_t *output_length
  )
{
  printf(".. encrypt len %lu\n", input_length);
  psa_key_attributes_t key_attr = psa_key_attributes_init();
  psa_cipher_operation_t op = psa_cipher_operation_init();
  psa_status_t status;

  status = psa_get_key_attributes(key_id, &key_attr);
  if (status != PSA_SUCCESS) {
    printf("psa_get_key_attributes status=%d\n", status);
    return status;
  }

  const size_t input_buffer_size = PSA_BLOCK_CIPHER_BLOCK_LENGTH(psa_get_key_type(&key_attr));
  const size_t output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_SIZE(
    psa_get_key_type(&key_attr),
    psa_get_key_algorithm(&key_attr),
    input_buffer_size);
  uint8_t input_buffer[input_buffer_size];
  uint8_t output_buffer[output_buffer_size];

  status = psa_cipher_encrypt_setup(&op, key_id, psa_get_key_algorithm(&key_attr));
  if (status != PSA_SUCCESS) {
    printf("psa_cipher_encrypt_setup status=%d\n", status);
    goto exit;
  }

  status = psa_cipher_generate_iv(&op, iv, iv_size, iv_length);
  if (status != PSA_SUCCESS) {
    printf("psa_cipher_generate_iv status=%d\n", status);
    goto exit;
  }

  size_t offset_in = 0;
  size_t offset_out = 0;
  while (offset_in < input_length) {
    size_t to_process = (input_length - offset_in > input_buffer_size)
                        ? input_buffer_size
                        : (input_length - offset_in);
    memcpy(input_buffer, input, to_process);

    printf("... update\n");
    printf("       input %lu bytes\n", to_process);

    size_t out_len = 0;
    status = psa_cipher_update(
        &op,
        input_buffer,
        to_process,
        output_buffer,
        to_process,
        &out_len
    );

    if (status != PSA_SUCCESS) {
      printf("psa_cipher_update status=%d\n", status);
      goto exit;
    }
    printf("       output %lu bytes\n", out_len);

    memcpy(output + offset_out, output_buffer, out_len);
    offset_in += to_process;
    offset_out += out_len;
  }

  size_t finish_len = 0;
  status = psa_cipher_finish(
      &op,
      output_buffer,
      output_buffer_size,
      &finish_len
  );
  printf("       finish %lu bytes\n", finish_len);

  if (status != PSA_SUCCESS) {
    printf("psa_cipher_finish status=%d\n", status);
    goto exit;
  }

  memcpy(output + offset_out, output_buffer, finish_len);
  *output_length = offset_out + finish_len;

  printf(".. output len %lu, status=%d\n", *output_length, status);

exit:
  psa_reset_key_attributes(&key_attr);
  psa_cipher_abort(&op);
  return status;
}

static psa_status_t decrypt(
  psa_key_id_t key_id,
  uint8_t *iv,
  size_t iv_size,
  uint8_t *input,
  size_t input_length,
  uint8_t *output,
  size_t *output_length
  )
{
  printf(".. decrypt len %lu\n", input_length);
  psa_key_attributes_t key_attr = psa_key_attributes_init();
  psa_cipher_operation_t op = psa_cipher_operation_init();
  psa_status_t status;

  status = psa_get_key_attributes(key_id, &key_attr);
  if (status != PSA_SUCCESS) {
    printf("psa_get_key_attributes status=%d\n", status);
    return status;
  }

  const size_t input_buffer_size = PSA_BLOCK_CIPHER_BLOCK_LENGTH(psa_get_key_type(&key_attr));
  const size_t output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_SIZE(
    psa_get_key_type(&key_attr),
    psa_get_key_algorithm(&key_attr),
    input_buffer_size);
  uint8_t input_buffer[input_buffer_size];
  uint8_t output_buffer[output_buffer_size];

  status = psa_cipher_decrypt_setup(&op, key_id, psa_get_key_algorithm(&key_attr));
  if (status != PSA_SUCCESS) {
    printf("psa_cipher_decrypt_setup status=%d\n", status);
    goto exit;
  }

  status = psa_cipher_set_iv(&op, iv, iv_size);
  if (status != PSA_SUCCESS) {
    printf("psa_cipher_set_iv status=%d\n", status);
    goto exit;
  }

  size_t offset_in = 0;
  size_t offset_out = 0;
  while (offset_in < input_length) {
    size_t to_process = (input_length - offset_in > input_buffer_size)
                        ? input_buffer_size
                        : (input_length - offset_in);


    memcpy(input_buffer, input, to_process);

    printf("... update\n");
    printf("       input %lu bytes\n", to_process);

    size_t out_len = 0;
    status = psa_cipher_update(
        &op,
        input_buffer,
        to_process,
        output_buffer,
        to_process,
        &out_len
    );

    if (status != PSA_SUCCESS) {
      printf("psa_cipher_update status=%d\n", status);
      goto exit;
    }
    printf("       output %lu bytes\n", out_len);

    memcpy(output + offset_out, output_buffer, out_len);
    offset_in += to_process;
    offset_out += out_len;
  }

  size_t finish_len = 0;
  status = psa_cipher_finish(
      &op,
      output_buffer,
      output_buffer_size,
      &finish_len
  );

  if (status != PSA_SUCCESS) {
    printf("psa_cipher_finish status=%d\n", status);
    goto exit;
  }
  printf("       finish %lu bytes\n", finish_len);

  memcpy(output + offset_out, output_buffer, finish_len);
  *output_length = offset_out + finish_len;

  printf(".. output len %lu, status=%d\n", *output_length, status);

exit:
  psa_reset_key_attributes(&key_attr);
  psa_cipher_abort(&op);
  return status;
}

static psa_status_t test_crypt_decrypt
(
  psa_key_id_t key_id,
  const size_t input_length,
  uint8_t *input,
  uint8_t *output
)
{
  printf("==> test crypt/decryp, plain text length=%lu\n", input_length);

  // Initialize plain text (all 0)
  memset(input, 0, input_length);

  // Allocate IV large enough
  const size_t iv_size = 512;
  size_t iv_length = 0;
  uint8_t *iv = malloc(iv_size);
  if (iv == NULL) {
    printf("IV malloc error");
    return PSA_ERROR_INSUFFICIENT_MEMORY;
  }

  size_t output_length = 0;
  psa_status_t status = encrypt(
    key_id,
    iv,
    iv_size,
    &iv_length,
    input,
    input_length,
    output,
    &output_length
    );
  if (status != PSA_SUCCESS) {
    printf("encrypt status=%d\n", status);
    goto exit;
  }

  memcpy(input, output, output_length);
  status = decrypt(
    key_id,
    iv,
    iv_length,
    input,
    output_length,
    output,
    &output_length
    );
  if (status != PSA_SUCCESS) {
    printf("encrypt status=%d\n", status);
    goto exit;
  }

  // Compare decrypt output with plain text
  if (input_length != output_length) {
    printf("** output_len not equal to input_len\n");
    status = PSA_ERROR_DATA_CORRUPT;
    goto exit;
  }

  for (int i = 0; i < output_length; i++) {
    if (output[i] != 0) {
      printf("** output[%d] not null\n", i);
      status = PSA_ERROR_DATA_CORRUPT;
      goto exit;
    }
  }

exit:
  free(iv);

  printf("\n");
  return status;
}


int main(int argc, char *argv[])
{
  const size_t BUFFER_SIZE = 8192;
  const int MAX_LEN = 18;

  psa_key_id_t key_id = 0;
  uint8_t *input = NULL;
  uint8_t *output = NULL;
  psa_key_attributes_t key_attr = psa_key_attributes_init();
  psa_status_t all_status[MAX_LEN];

  psa_status_t status = psa_crypto_init();
  if (status != PSA_SUCCESS) {
    printf("psa_crypto_init status=%d\n", status);
    return 0;
  }
  printf("%s initialized\n", TF_PSA_CRYPTO_VERSION_STRING_FULL);

  psa_set_key_type(&key_attr, PSA_KEY_TYPE_AES);
  psa_set_key_bits(&key_attr, 256);
  psa_set_key_algorithm(&key_attr, PSA_ALG_CBC_PKCS7);
  psa_set_key_lifetime(&key_attr, PSA_KEY_LIFETIME_VOLATILE);
  psa_set_key_usage_flags(&key_attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT );

  status = psa_generate_key(&key_attr, &key_id);
  if (status != PSA_SUCCESS) {
    printf("psa_generate_key status=%d\n", status);
    goto exit;
  }

  input = malloc(BUFFER_SIZE);
  output = malloc(BUFFER_SIZE);
  if (input == NULL || output == NULL) {
    printf("malloc error");
    goto exit;
  }

  for (int len=1; len < MAX_LEN; len++) {
    all_status[len] = test_crypt_decrypt(key_id, len, input, output);
  }

  printf("crypt/decrypt results\n");
  printf("=====================\n");
  for (int len=1; len < MAX_LEN; len++) {
    printf("len=%2.2d status=%d\n", len, all_status[len]);
  }

exit:
  psa_destroy_key(key_id);
  psa_reset_key_attributes(&key_attr);
  if (input)
    free(input);
  if (output)
    free(output);
  return 0;
}

Here is the source code demonstrating the issue: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include "psa/crypto.h" static psa_status_t encrypt( psa_key_id_t key_id, uint8_t *iv, size_t iv_size, size_t *iv_length, uint8_t *input, size_t input_length, uint8_t *output, size_t *output_length ) { printf(".. encrypt len %lu\n", input_length); psa_key_attributes_t key_attr = psa_key_attributes_init(); psa_cipher_operation_t op = psa_cipher_operation_init(); psa_status_t status; status = psa_get_key_attributes(key_id, &key_attr); if (status != PSA_SUCCESS) { printf("psa_get_key_attributes status=%d\n", status); return status; } const size_t input_buffer_size = PSA_BLOCK_CIPHER_BLOCK_LENGTH(psa_get_key_type(&key_attr)); const size_t output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_SIZE( psa_get_key_type(&key_attr), psa_get_key_algorithm(&key_attr), input_buffer_size); uint8_t input_buffer[input_buffer_size]; uint8_t output_buffer[output_buffer_size]; status = psa_cipher_encrypt_setup(&op, key_id, psa_get_key_algorithm(&key_attr)); if (status != PSA_SUCCESS) { printf("psa_cipher_encrypt_setup status=%d\n", status); goto exit; } status = psa_cipher_generate_iv(&op, iv, iv_size, iv_length); if (status != PSA_SUCCESS) { printf("psa_cipher_generate_iv status=%d\n", status); goto exit; } size_t offset_in = 0; size_t offset_out = 0; while (offset_in < input_length) { size_t to_process = (input_length - offset_in > input_buffer_size) ? input_buffer_size : (input_length - offset_in); memcpy(input_buffer, input, to_process); printf("... update\n"); printf(" input %lu bytes\n", to_process); size_t out_len = 0; status = psa_cipher_update( &op, input_buffer, to_process, output_buffer, to_process, &out_len ); if (status != PSA_SUCCESS) { printf("psa_cipher_update status=%d\n", status); goto exit; } printf(" output %lu bytes\n", out_len); memcpy(output + offset_out, output_buffer, out_len); offset_in += to_process; offset_out += out_len; } size_t finish_len = 0; status = psa_cipher_finish( &op, output_buffer, output_buffer_size, &finish_len ); printf(" finish %lu bytes\n", finish_len); if (status != PSA_SUCCESS) { printf("psa_cipher_finish status=%d\n", status); goto exit; } memcpy(output + offset_out, output_buffer, finish_len); *output_length = offset_out + finish_len; printf(".. output len %lu, status=%d\n", *output_length, status); exit: psa_reset_key_attributes(&key_attr); psa_cipher_abort(&op); return status; } static psa_status_t decrypt( psa_key_id_t key_id, uint8_t *iv, size_t iv_size, uint8_t *input, size_t input_length, uint8_t *output, size_t *output_length ) { printf(".. decrypt len %lu\n", input_length); psa_key_attributes_t key_attr = psa_key_attributes_init(); psa_cipher_operation_t op = psa_cipher_operation_init(); psa_status_t status; status = psa_get_key_attributes(key_id, &key_attr); if (status != PSA_SUCCESS) { printf("psa_get_key_attributes status=%d\n", status); return status; } const size_t input_buffer_size = PSA_BLOCK_CIPHER_BLOCK_LENGTH(psa_get_key_type(&key_attr)); const size_t output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_SIZE( psa_get_key_type(&key_attr), psa_get_key_algorithm(&key_attr), input_buffer_size); uint8_t input_buffer[input_buffer_size]; uint8_t output_buffer[output_buffer_size]; status = psa_cipher_decrypt_setup(&op, key_id, psa_get_key_algorithm(&key_attr)); if (status != PSA_SUCCESS) { printf("psa_cipher_decrypt_setup status=%d\n", status); goto exit; } status = psa_cipher_set_iv(&op, iv, iv_size); if (status != PSA_SUCCESS) { printf("psa_cipher_set_iv status=%d\n", status); goto exit; } size_t offset_in = 0; size_t offset_out = 0; while (offset_in < input_length) { size_t to_process = (input_length - offset_in > input_buffer_size) ? input_buffer_size : (input_length - offset_in); memcpy(input_buffer, input, to_process); printf("... update\n"); printf(" input %lu bytes\n", to_process); size_t out_len = 0; status = psa_cipher_update( &op, input_buffer, to_process, output_buffer, to_process, &out_len ); if (status != PSA_SUCCESS) { printf("psa_cipher_update status=%d\n", status); goto exit; } printf(" output %lu bytes\n", out_len); memcpy(output + offset_out, output_buffer, out_len); offset_in += to_process; offset_out += out_len; } size_t finish_len = 0; status = psa_cipher_finish( &op, output_buffer, output_buffer_size, &finish_len ); if (status != PSA_SUCCESS) { printf("psa_cipher_finish status=%d\n", status); goto exit; } printf(" finish %lu bytes\n", finish_len); memcpy(output + offset_out, output_buffer, finish_len); *output_length = offset_out + finish_len; printf(".. output len %lu, status=%d\n", *output_length, status); exit: psa_reset_key_attributes(&key_attr); psa_cipher_abort(&op); return status; } static psa_status_t test_crypt_decrypt ( psa_key_id_t key_id, const size_t input_length, uint8_t *input, uint8_t *output ) { printf("==> test crypt/decryp, plain text length=%lu\n", input_length); // Initialize plain text (all 0) memset(input, 0, input_length); // Allocate IV large enough const size_t iv_size = 512; size_t iv_length = 0; uint8_t *iv = malloc(iv_size); if (iv == NULL) { printf("IV malloc error"); return PSA_ERROR_INSUFFICIENT_MEMORY; } size_t output_length = 0; psa_status_t status = encrypt( key_id, iv, iv_size, &iv_length, input, input_length, output, &output_length ); if (status != PSA_SUCCESS) { printf("encrypt status=%d\n", status); goto exit; } memcpy(input, output, output_length); status = decrypt( key_id, iv, iv_length, input, output_length, output, &output_length ); if (status != PSA_SUCCESS) { printf("encrypt status=%d\n", status); goto exit; } // Compare decrypt output with plain text if (input_length != output_length) { printf("** output_len not equal to input_len\n"); status = PSA_ERROR_DATA_CORRUPT; goto exit; } for (int i = 0; i < output_length; i++) { if (output[i] != 0) { printf("** output[%d] not null\n", i); status = PSA_ERROR_DATA_CORRUPT; goto exit; } } exit: free(iv); printf("\n"); return status; } int main(int argc, char *argv[]) { const size_t BUFFER_SIZE = 8192; const int MAX_LEN = 18; psa_key_id_t key_id = 0; uint8_t *input = NULL; uint8_t *output = NULL; psa_key_attributes_t key_attr = psa_key_attributes_init(); psa_status_t all_status[MAX_LEN]; psa_status_t status = psa_crypto_init(); if (status != PSA_SUCCESS) { printf("psa_crypto_init status=%d\n", status); return 0; } printf("%s initialized\n", TF_PSA_CRYPTO_VERSION_STRING_FULL); psa_set_key_type(&key_attr, PSA_KEY_TYPE_AES); psa_set_key_bits(&key_attr, 256); psa_set_key_algorithm(&key_attr, PSA_ALG_CBC_PKCS7); psa_set_key_lifetime(&key_attr, PSA_KEY_LIFETIME_VOLATILE); psa_set_key_usage_flags(&key_attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT ); status = psa_generate_key(&key_attr, &key_id); if (status != PSA_SUCCESS) { printf("psa_generate_key status=%d\n", status); goto exit; } input = malloc(BUFFER_SIZE); output = malloc(BUFFER_SIZE); if (input == NULL || output == NULL) { printf("malloc error"); goto exit; } for (int len=1; len < MAX_LEN; len++) { all_status[len] = test_crypt_decrypt(key_id, len, input, output); } printf("crypt/decrypt results\n"); printf("=====================\n"); for (int len=1; len < MAX_LEN; len++) { printf("len=%2.2d status=%d\n", len, all_status[len]); } exit: psa_destroy_key(key_id); psa_reset_key_attributes(&key_attr); if (input) free(input); if (output) free(output); return 0; } ```
yanesca commented 2025-12-09 04:02:54 -06:00 (Migrated from github.com)

The test code passes the variable to_process as output_size to psa_cipher_update(). As far as I can tell to_process is not guaranteed to be appropriate as required my documentation. As far as I can tell, the library is working as intended.

The test code passes the variable `to_process` as `output_size` to [psa_cipher_update()](https://arm-software.github.io/psa-api/crypto/1.1/api/ops/ciphers.html#c.psa_cipher_update). As far as I can tell `to_process` is not guaranteed to be appropriate as required my documentation. As far as I can tell, the library is working as intended.
jnmeurisse commented 2025-12-09 05:08:32 -06:00 (Migrated from github.com)

It is a typo in my test program, sorry.

If you pass output_buffer_size instead of to_process as effective argument (lines 65 and 165), psa_cipher_update still reports error -138.

It is a typo in my test program, sorry. If you pass output_buffer_size instead of to_process as effective argument (lines 65 and 165), psa_cipher_update still reports error -138.
jnmeurisse commented 2025-12-12 10:37:32 -06:00 (Migrated from github.com)

Example scenario

Plaintext size: 16 bytes.

Encryption

The Encryption produces:
psa_cipher_update → 16 bytes
psa_cipher_finish → 16 bytes
Total ciphertext = 32 bytes.

Decryption

During decryption, processing the 32-byte ciphertext in two 16-byte chunks:

  • The first psa_cipher_update(16 bytes) succeeds.
  • The second psa_cipher_update(16 bytes) fails with BUFFER_TOO_SMALL despite the output buffer being 16 bytes (the value returned by PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(16)).

Cause:

mbedtls_psa_cipher_update() called from psa_cipher_update() computes the expected output size as:

    expected_output_size =
          (operation->ctx.cipher.unprocessed_len + input_length)
          / operation->block_length * operation->block_length;

At the second psa_cipher_update() call:
unprocessed_len = 16
input_length = 16
block_length = 16
Thus:
expected_output_size = (16 + 16) / 16 * 16 = 32 bytes.

Because psa_cipher_update() expects to emit 32 bytes in one call, but the caller only provides a 16-byte output buffer (as suggested by the PSA macros), the function returns PSA_ERROR_BUFFER_TOO_SMALL.

Source code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "psa/crypto.h"

int main(int argc, char *argv[])
{
  const size_t CHUNK = 16;

  psa_key_id_t key_id = 0;
  psa_cipher_operation_t encrypt_operation = psa_cipher_operation_init();
  psa_cipher_operation_t decrypt_operation = psa_cipher_operation_init();
  psa_key_attributes_t key_attr = psa_key_attributes_init();
  const uint8_t plain_text[16] = {0};
  uint8_t iv[512];
  size_t iv_length;
  uint8_t *encrypt_output_buffer = NULL;
  uint8_t *decrypt_output_buffer = NULL;
  uint8_t *finish_buffer = NULL;
  uint8_t *input_buffer = NULL;
  size_t encrypt_output_length;
  size_t decrypt_output_length;
  size_t finish_length;

  psa_status_t status = psa_crypto_init();
  if (status != PSA_SUCCESS) goto abort;

  /* Define and generate the key */
  psa_set_key_type(&key_attr, PSA_KEY_TYPE_AES);
  psa_set_key_bits(&key_attr, 128);
  psa_set_key_algorithm(&key_attr, PSA_ALG_CBC_PKCS7);
  psa_set_key_lifetime(&key_attr, PSA_KEY_LIFETIME_VOLATILE);
  psa_set_key_usage_flags(&key_attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT );
  status = psa_generate_key(&key_attr, &key_id);
  if (status != PSA_SUCCESS) goto abort;

  /* Allocate buffers */
  const size_t finish_buffer_size = PSA_CIPHER_FINISH_OUTPUT_MAX_SIZE;
  printf("   finish buffer size=%lu\n", finish_buffer_size);

  finish_buffer = malloc(finish_buffer_size);
  if (finish_buffer == NULL) {
    status = PSA_ERROR_INSUFFICIENT_MEMORY;
    goto abort;
  }

  /* Encryption phase */
  printf("** Encryption phase - plain text length = %lu\n", sizeof plain_text);
  const size_t encrypt_output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(sizeof plain_text);
  printf("   encryption output buffer size=%lu\n", encrypt_output_buffer_size);

  encrypt_output_buffer = malloc(encrypt_output_buffer_size);
  if (encrypt_output_buffer == NULL) {
    status = PSA_ERROR_INSUFFICIENT_MEMORY;
    goto abort;
  }

  status = psa_cipher_encrypt_setup(&encrypt_operation, key_id, psa_get_key_algorithm(&key_attr));
  if (status != PSA_SUCCESS) goto abort;
  status = psa_cipher_generate_iv(&encrypt_operation, iv, sizeof iv, &iv_length);
  if (status != PSA_SUCCESS) goto abort;

  status = psa_cipher_update(&encrypt_operation,
      plain_text, sizeof plain_text,
      encrypt_output_buffer, encrypt_output_buffer_size, &encrypt_output_length);
  if (status != PSA_SUCCESS) goto abort;

  status = psa_cipher_finish(&encrypt_operation, finish_buffer, finish_buffer_size, &finish_length);
  if (status != PSA_SUCCESS) goto abort;
  psa_cipher_abort(&encrypt_operation);

  /* Concatenate output and finish buffers */
  const size_t input_buffer_size = encrypt_output_length + finish_length;
  input_buffer = malloc(input_buffer_size);
  memcpy(input_buffer, encrypt_output_buffer, encrypt_output_length);
  memcpy(input_buffer + encrypt_output_length, finish_buffer, finish_length);

  /* Decryption phase */
  printf("** Decryption phase - input length = %lu\n", input_buffer_size);

  status = psa_cipher_decrypt_setup(&decrypt_operation, key_id, psa_get_key_algorithm(&key_attr));
  if (status != PSA_SUCCESS) goto abort;
  status = psa_cipher_set_iv(&decrypt_operation, iv, iv_length);
  if (status != PSA_SUCCESS) goto abort;

  const size_t decrypt_output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(CHUNK);
  printf("   decryption output buffer size=%lu\n", decrypt_output_buffer_size);
  decrypt_output_buffer = malloc(decrypt_output_buffer_size);
  if (decrypt_output_buffer == NULL) {
    status = PSA_ERROR_INSUFFICIENT_MEMORY;
    goto abort;
  }
  for (int idx = 0; idx < input_buffer_size; idx += CHUNK) {
    status = psa_cipher_update(&decrypt_operation,
          &input_buffer[idx], CHUNK,
          decrypt_output_buffer, decrypt_output_buffer_size, &decrypt_output_length);
    if (status != PSA_SUCCESS) goto abort;
  }

  status = psa_cipher_finish(&decrypt_operation, finish_buffer, finish_buffer_size, &finish_length);
  if (status != PSA_SUCCESS) goto abort;
  psa_cipher_abort(&decrypt_operation);

abort:
  free(input_buffer);
  free(encrypt_output_buffer);
  free(decrypt_output_buffer);
  free(finish_buffer);
  psa_cipher_abort(&encrypt_operation);
  psa_cipher_abort(&decrypt_operation);
  psa_destroy_key(key_id);
  printf("psa status=%d\n", status);
  return 0;
}

### Example scenario Plaintext size: 16 bytes. #### Encryption The Encryption produces: `psa_cipher_update` → 16 bytes `psa_cipher_finish` → 16 bytes Total ciphertext = 32 bytes. #### Decryption During decryption, processing the 32-byte ciphertext in two 16-byte chunks: - The first `psa_cipher_update(16 bytes)` succeeds. - The second `psa_cipher_update(16 bytes)` fails with BUFFER_TOO_SMALL despite the output buffer being 16 bytes (the value returned by PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(16)). #### Cause: `mbedtls_psa_cipher_update()` called from `psa_cipher_update()` computes the expected output size as: ``` expected_output_size = (operation->ctx.cipher.unprocessed_len + input_length) / operation->block_length * operation->block_length; ``` At the second psa_cipher_update() call: `unprocessed_len` = 16 `input_length` = 16 `block_length` = 16 Thus: `expected_output_size` = (16 + 16) / 16 * 16 = 32 bytes. Because psa_cipher_update() expects to emit 32 bytes in one call, but the caller only provides a 16-byte output buffer (as suggested by the PSA macros), the function returns PSA_ERROR_BUFFER_TOO_SMALL. ### Source code ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include "psa/crypto.h" int main(int argc, char *argv[]) { const size_t CHUNK = 16; psa_key_id_t key_id = 0; psa_cipher_operation_t encrypt_operation = psa_cipher_operation_init(); psa_cipher_operation_t decrypt_operation = psa_cipher_operation_init(); psa_key_attributes_t key_attr = psa_key_attributes_init(); const uint8_t plain_text[16] = {0}; uint8_t iv[512]; size_t iv_length; uint8_t *encrypt_output_buffer = NULL; uint8_t *decrypt_output_buffer = NULL; uint8_t *finish_buffer = NULL; uint8_t *input_buffer = NULL; size_t encrypt_output_length; size_t decrypt_output_length; size_t finish_length; psa_status_t status = psa_crypto_init(); if (status != PSA_SUCCESS) goto abort; /* Define and generate the key */ psa_set_key_type(&key_attr, PSA_KEY_TYPE_AES); psa_set_key_bits(&key_attr, 128); psa_set_key_algorithm(&key_attr, PSA_ALG_CBC_PKCS7); psa_set_key_lifetime(&key_attr, PSA_KEY_LIFETIME_VOLATILE); psa_set_key_usage_flags(&key_attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT ); status = psa_generate_key(&key_attr, &key_id); if (status != PSA_SUCCESS) goto abort; /* Allocate buffers */ const size_t finish_buffer_size = PSA_CIPHER_FINISH_OUTPUT_MAX_SIZE; printf(" finish buffer size=%lu\n", finish_buffer_size); finish_buffer = malloc(finish_buffer_size); if (finish_buffer == NULL) { status = PSA_ERROR_INSUFFICIENT_MEMORY; goto abort; } /* Encryption phase */ printf("** Encryption phase - plain text length = %lu\n", sizeof plain_text); const size_t encrypt_output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(sizeof plain_text); printf(" encryption output buffer size=%lu\n", encrypt_output_buffer_size); encrypt_output_buffer = malloc(encrypt_output_buffer_size); if (encrypt_output_buffer == NULL) { status = PSA_ERROR_INSUFFICIENT_MEMORY; goto abort; } status = psa_cipher_encrypt_setup(&encrypt_operation, key_id, psa_get_key_algorithm(&key_attr)); if (status != PSA_SUCCESS) goto abort; status = psa_cipher_generate_iv(&encrypt_operation, iv, sizeof iv, &iv_length); if (status != PSA_SUCCESS) goto abort; status = psa_cipher_update(&encrypt_operation, plain_text, sizeof plain_text, encrypt_output_buffer, encrypt_output_buffer_size, &encrypt_output_length); if (status != PSA_SUCCESS) goto abort; status = psa_cipher_finish(&encrypt_operation, finish_buffer, finish_buffer_size, &finish_length); if (status != PSA_SUCCESS) goto abort; psa_cipher_abort(&encrypt_operation); /* Concatenate output and finish buffers */ const size_t input_buffer_size = encrypt_output_length + finish_length; input_buffer = malloc(input_buffer_size); memcpy(input_buffer, encrypt_output_buffer, encrypt_output_length); memcpy(input_buffer + encrypt_output_length, finish_buffer, finish_length); /* Decryption phase */ printf("** Decryption phase - input length = %lu\n", input_buffer_size); status = psa_cipher_decrypt_setup(&decrypt_operation, key_id, psa_get_key_algorithm(&key_attr)); if (status != PSA_SUCCESS) goto abort; status = psa_cipher_set_iv(&decrypt_operation, iv, iv_length); if (status != PSA_SUCCESS) goto abort; const size_t decrypt_output_buffer_size = PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE(CHUNK); printf(" decryption output buffer size=%lu\n", decrypt_output_buffer_size); decrypt_output_buffer = malloc(decrypt_output_buffer_size); if (decrypt_output_buffer == NULL) { status = PSA_ERROR_INSUFFICIENT_MEMORY; goto abort; } for (int idx = 0; idx < input_buffer_size; idx += CHUNK) { status = psa_cipher_update(&decrypt_operation, &input_buffer[idx], CHUNK, decrypt_output_buffer, decrypt_output_buffer_size, &decrypt_output_length); if (status != PSA_SUCCESS) goto abort; } status = psa_cipher_finish(&decrypt_operation, finish_buffer, finish_buffer_size, &finish_length); if (status != PSA_SUCCESS) goto abort; psa_cipher_abort(&decrypt_operation); abort: free(input_buffer); free(encrypt_output_buffer); free(decrypt_output_buffer); free(finish_buffer); psa_cipher_abort(&encrypt_operation); psa_cipher_abort(&decrypt_operation); psa_destroy_key(key_id); printf("psa status=%d\n", status); return 0; } ```
yanesca commented 2025-12-15 01:25:38 -06:00 (Migrated from github.com)

@jnmeurisse Thank you very much for the reproducer and for the analysis. Indeed the output length will depend on the padding mode, expecting the same output length independently of the padding mode can't possibly be correct.

@jnmeurisse Thank you very much for the reproducer and for the analysis. Indeed the output length will depend on the padding mode, expecting the same output length independently of the padding mode can't possibly be correct.
gilles-peskine-arm commented 2025-12-15 04:02:48 -06:00 (Migrated from github.com)

I haven't yet dug through the code, but that strikes me as strange:

unprocessed_len = 16

16 bytes is a full block, why didn't the previous code emit it? From my memory of writing the initial version of that code (a long time ago, and it's been through a lot since then), I don't remember a reason to keep a full block in reserve, for any of the modes that we support. As the comment still says above the expected_output_size calculation:

Take the unprocessed partial block left over from previous update calls, if any

I haven't yet dug through the code, but that strikes me as strange: > unprocessed_len = 16 16 bytes is a full block, why didn't the previous code emit it? From my memory of writing the initial version of that code (a long time ago, and it's been through a lot since then), I don't remember a reason to keep a full block in reserve, for any of the modes that we support. As the comment still says above the `expected_output_size` calculation: > Take the unprocessed partial block left over from previous update calls, if any
yanesca commented 2025-12-15 05:51:25 -06:00 (Migrated from github.com)

In case there is padding, if you have a full block in the queue, update doesn't know if more data is coming and if it needs to emit all of it or just part of it, so keeps it until the next call.

In case there is padding, if you have a full block in the queue, update doesn't know if more data is coming and if it needs to emit all of it or just part of it, so keeps it until the next call.
gilles-peskine-arm commented 2026-02-09 10:18:15 -06:00 (Migrated from github.com)

To clarify and expand Janos's explanation:

Some ciphers process the input in fixed-size blocks (always 16 bytes in TF-PSA-Crypto, could be 8 bytes in Mbed TLS ≤3.6.x which supported DES). In these modes, psa_cipher_update() cannot emit output until it has a full block of input. So for example, if you pass 20 bytes of input to psa_cipher_update() and then 20 more bytes, then the first call outputs 16 bytes (and stores the last 4 bytes of input in the operation object) and the second call outputs 16 bytes (with 8 bytes left in the operation object). A At the end, psa_cipher_{encrypt,decrypt}_finish() emits the output corresponding to the input that is left in the operation object.

There is an additional complication, which is that there are ciphers where the last block requires special handling. Among the modes we support, the only such case is CBC-PKCS7 decryption, where the last block contains 0–15 bytes of payload, plus padding. (Encryption has no special handling of the last block of input, but the finish call may emit one extra block of output.) Since psa_cipher_update() doesn't know whether it's called for the last time, if it's doing CBC-PKCS7 decryption and its input ends on a block boundary, it must store the whole last block of input.

So let's look at the possible cases for a call to psa_cipher_update() for CBC-PKCS7 decryption in terms of input length n passed to the function and input length p in the operation object. In the operation object, there can be from 0 bytes (operation just started) to 16 bytes (the last call ended on a block boundary). The decryption process has (p + n) bytes of ciphertext available, and this starts on a block boundary. (It's an invariant that the start of the input left in the operation object is aligned on a block boundary.) If p + n > 16 * k then there are k blocks of plaintext available for output immediately (i.e. 16 * k bytes). Note that this is a strict inequality because the last full block available so far cannot be decrypted if it's the very last block of input. Given that psa_cipher_update doesn't know if there will be more input, it can only be sure that a block is not the last one if that block is not the last one so far, i.e. if there is at least one more byte of input already available. Let o be the output length:

  • If p + n == 0 (i.e. empty input on an operation that has never had input), emit nothing (o = 0) and store nothing. (The easiest way to deal with that is to quit early if n == 0.)
  • If p + n is a multiple of 16 but not 0, then emit o = p + n - 16 bytes of output and store 16 bytes in the operation. We have o = n + (p - 16) ≤ n.
  • If p + n is not a multiple of 16, then emit o = p + n - (p + n) % 16 bytes of output and store (p + n) % 16 bytes in the operation. We have o = n + (p - (p + n) % 16) ≤ n + 15 since p ≤ 16 and (p + n) % 16 ≤ 0.

In all cases, we have o ≤ n + block_size - 1. This fits the current definitions of PSA_CIPHER_UPDATE_OUTPUT_SIZE and PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE which allow o to be up to n + block_size.

The calculation of expected_output_size in psa_cipher_update is more conservative:

expected_output_size =
      (operation->ctx.cipher.unprocessed_len + input_length)
      / operation->block_length * operation->block_length;

It doesn't apply the - 16 part of the output length calculation when p + n is a multiple of 16. This calculation gives the naive result for block processing when each complete block can be processed immediately (and thus the operation object never stores a full block of unprocessed input), but it's off by one block when operation->ctx.cipher.unprocessed_len == operation->block_length.

To clarify and expand Janos's explanation: Some ciphers process the input in fixed-size blocks (always 16 bytes in TF-PSA-Crypto, could be 8 bytes in Mbed TLS ≤3.6.x which supported DES). In these modes, `psa_cipher_update()` cannot emit output until it has a full block of input. So for example, if you pass 20 bytes of input to `psa_cipher_update()` and then 20 more bytes, then the first call outputs 16 bytes (and stores the last 4 bytes of input in the operation object) and the second call outputs 16 bytes (with 8 bytes left in the operation object). A At the end, `psa_cipher_{encrypt,decrypt}_finish()` emits the output corresponding to the input that is left in the operation object. There is an additional complication, which is that there are ciphers where the last block requires special handling. Among the modes we support, the only such case is CBC-PKCS7 decryption, where the last block contains 0–15 bytes of payload, plus padding. (Encryption has no special handling of the last block of input, but the finish call may emit one extra block of output.) Since `psa_cipher_update()` doesn't know whether it's called for the last time, if it's doing CBC-PKCS7 decryption and its input ends on a block boundary, it must store the whole last block of input. So let's look at the possible cases for a call to `psa_cipher_update()` for CBC-PKCS7 decryption in terms of input length `n` passed to the function and input length `p` in the operation object. In the operation object, there can be from 0 bytes (operation just started) to 16 bytes (the last call ended on a block boundary). The decryption process has `(p + n)` bytes of ciphertext available, and this starts on a block boundary. (It's an invariant that the start of the input left in the operation object is aligned on a block boundary.) If `p + n > 16 * k` then there are `k` blocks of plaintext available for output immediately (i.e. `16 * k` bytes). Note that this is a strict inequality because the last full block available so far cannot be decrypted if it's the very last block of input. Given that `psa_cipher_update` doesn't know if there will be more input, it can only be sure that a block is not the last one if that block is not the last one so far, i.e. if there is at least one more byte of input already available. Let `o` be the output length: * If `p + n == 0` (i.e. empty input on an operation that has never had input), emit nothing (`o = 0`) and store nothing. (The easiest way to deal with that is to quit early if `n == 0`.) * If `p + n` is a multiple of 16 but not 0, then emit `o = p + n - 16` bytes of output and store 16 bytes in the operation. We have `o` = `n + (p - 16)` ≤ `n`. * If `p + n` is not a multiple of 16, then emit `o = p + n - (p + n) % 16` bytes of output and store `(p + n) % 16` bytes in the operation. We have `o` = `n + (p - (p + n) % 16)` ≤ `n + 15` since `p` ≤ 16 and `(p + n) % 16` ≤ 0. In all cases, we have `o` ≤ `n` + `block_size` - 1. This fits the current definitions of `PSA_CIPHER_UPDATE_OUTPUT_SIZE` and `PSA_CIPHER_UPDATE_OUTPUT_MAX_SIZE` which allow `o` to be up to `n + block_size`. The calculation of `expected_output_size` in `psa_cipher_update` is more conservative: expected_output_size = (operation->ctx.cipher.unprocessed_len + input_length) / operation->block_length * operation->block_length; It doesn't apply the `- 16` part of the output length calculation when `p + n` is a multiple of 16. This calculation gives the naive result for block processing when each complete block can be processed immediately (and thus the operation object never stores a full block of unprocessed input), but it's off by one block when `operation->ctx.cipher.unprocessed_len == operation->block_length`.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-Mbed-TLS/TF-PSA-Crypto#592
No description provided.