NIST's AES CTR random number generator in rust

✍️ Written on 2021-12-31 in 7449 words.
Part of cs IT-security pqcrypto

Update 2022-02-24: In randombytes, I limited the copy_from_slice to size chunk.len() since chunk need not have 16 bytes uniformly.

Motivation

The NIST PQC candidates use the same RNG (written by Bassham & Lawrence E in 2017) in their reference implementations to fetch random number based on a seed. The interface is trivial:

int
randombytes(unsigned char *x, unsigned long long xlen);

void
randombytes_init(unsigned char *entropy_input,
                 unsigned char *personalization_string,
                 int security_strength)

What does an equivalent implementation in rust look like?

The C implementation

The following implementation is provided in Classic McEliece and is more compact than others.

/*
   rng.c
   Created by Bassham, Lawrence E (Fed) on 8/29/17.
   Copyright © 2017 Bassham, Lawrence E (Fed). All rights reserved.
*/

#include <string.h>
#include "rng.h"
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>

AES256_CTR_DRBG_struct  DRBG_ctx;

void    AES256_ECB(unsigned char *key, unsigned char *ctr, unsigned char *buffer);

/*
 seedexpander_init()
  ctx            - stores the current state of an instance of the seed expander
  seed           - a 32 byte random value
  diversifier    - an 8 byte diversifier
  maxlen         - maximum number of bytes (less than 2**32) generated under this seed and diversifier
 */
int
seedexpander_init(AES_XOF_struct *ctx,
                  unsigned char *seed,
                  unsigned char *diversifier,
                  unsigned long maxlen)
{
  if ( maxlen >= 0x100000000 )
    return RNG_BAD_MAXLEN;

  ctx->length_remaining = maxlen;

  memcpy(ctx->key, seed, 32);

  memcpy(ctx->ctr, diversifier, 8);
  ctx->ctr[11] = maxlen % 256;
  maxlen >>= 8;
  ctx->ctr[10] = maxlen % 256;
  maxlen >>= 8;
  ctx->ctr[9] = maxlen % 256;
  maxlen >>= 8;
  ctx->ctr[8] = maxlen % 256;
  memset(ctx->ctr+12, 0x00, 4);

  ctx->buffer_pos = 16;
  memset(ctx->buffer, 0x00, 16);

  return RNG_SUCCESS;
}

/*
 seedexpander()
  ctx  - stores the current state of an instance of the seed expander
  x    - returns the XOF data
  xlen - number of bytes to return
 */
int
seedexpander(AES_XOF_struct *ctx, unsigned char *x, unsigned long xlen)
{
  unsigned long   offset;
  int i;

  if ( x == NULL )
    return RNG_BAD_OUTBUF;
  if ( xlen >= ctx->length_remaining )
    return RNG_BAD_REQ_LEN;

  ctx->length_remaining -= xlen;

  offset = 0;
  while ( xlen > 0 ) {
    if ( xlen <= (16-ctx->buffer_pos) ) { /* buffer has what we need */
      memcpy(x+offset, ctx->buffer+ctx->buffer_pos, xlen);
      ctx->buffer_pos += xlen;

      return RNG_SUCCESS;
    }

    /* take what's in the buffer */
    memcpy(x+offset, ctx->buffer+ctx->buffer_pos, 16-ctx->buffer_pos);
    xlen -= 16-ctx->buffer_pos;
    offset += 16-ctx->buffer_pos;

    AES256_ECB(ctx->key, ctx->ctr, ctx->buffer);
    ctx->buffer_pos = 0;

    /* increment the counter */
    for (i=15; i>=12; i--) {
      if ( ctx->ctr[i] == 0xff )
        ctx->ctr[i] = 0x00;
      else {
        ctx->ctr[i]++;
        break;
      }
    }
  }

  return RNG_SUCCESS;
}


void handleErrors(void)
{
  ERR_print_errors_fp(stderr);
  abort();
}

/*
 Use whatever AES implementation you have. This uses AES from openSSL library
    key - 256-bit AES key
    ctr - a 128-bit plaintext value
    buffer - a 128-bit ciphertext value
*/
void
AES256_ECB(unsigned char *key, unsigned char *ctr, unsigned char *buffer)
{
  EVP_CIPHER_CTX *ctx;

  int len;

  /* Create and initialise the context */
  if(!(ctx = EVP_CIPHER_CTX_new())) handleErrors();

  if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_ecb(), NULL, key, NULL))
    handleErrors();

  if(1 != EVP_EncryptUpdate(ctx, buffer, &len, ctr, 16))
    handleErrors();

  /* Clean up */
  EVP_CIPHER_CTX_free(ctx);
}

void
randombytes_init(unsigned char *entropy_input,
                 unsigned char *personalization_string,
                 int security_strength)
{
  unsigned char   seed_material[48];
  int i;

  memcpy(seed_material, entropy_input, 48);
  if (personalization_string)
    for (i=0; i<48; i++)
      seed_material[i] ^= personalization_string[i];
  memset(DRBG_ctx.Key, 0x00, 32);
  memset(DRBG_ctx.V, 0x00, 16);
  AES256_CTR_DRBG_Update(seed_material, DRBG_ctx.Key, DRBG_ctx.V);
  DRBG_ctx.reseed_counter = 1;
}

int
randombytes(unsigned char *x, unsigned long long xlen)
{
  unsigned char   block[16];
  int             i = 0;
  int j;

  while ( xlen > 0 ) {
    /* increment V */
    for (j=15; j>=0; j--) {
      if ( DRBG_ctx.V[j] == 0xff )
        DRBG_ctx.V[j] = 0x00;
      else {
        DRBG_ctx.V[j]++;
        break;
      }
    }
    AES256_ECB(DRBG_ctx.Key, DRBG_ctx.V, block);
    if ( xlen > 15 ) {
      memcpy(x+i, block, 16);
      i += 16;
      xlen -= 16;
    }
    else {
      memcpy(x+i, block, xlen);
      xlen = 0;
    }
  }
  AES256_CTR_DRBG_Update(NULL, DRBG_ctx.Key, DRBG_ctx.V);
  DRBG_ctx.reseed_counter++;

  return RNG_SUCCESS;
}

void
AES256_CTR_DRBG_Update(unsigned char *provided_data,
                       unsigned char *Key,
                       unsigned char *V)
{
  unsigned char   temp[48];
  int i;
  int j;

  for (i=0; i<3; i++) {
    /* increment V */
    for (j=15; j>=0; j--) {
      if ( V[j] == 0xff )
        V[j] = 0x00;
      else {
        V[j]++;
        break;
      }
    }

    AES256_ECB(Key, V, temp+16*i);
  }
  if ( provided_data != NULL )
    for (i=0; i<48; i++)
      temp[i] ^= provided_data[i];
  memcpy(Key, temp, 32);
  memcpy(V, temp+32, 16);
}

My main routine calls the init-function once and randombytes two times:

#include <stdio.h>

void
fprintBstr(FILE *fp, char *S, unsigned char *A, unsigned long long L)
{
  unsigned long long  i;

  fprintf(fp, "%s", S);

  for ( i=0; i<L; i++ )
    fprintf(fp, "%02X", A[i]);

  if ( L == 0 )
    fprintf(fp, "00");

  fprintf(fp, "\n");
}

int main() {
  unsigned char entropy_input[48];
  unsigned char personalization_string[48];
  unsigned char data[256];

  for (int i=0; i<48; i++) {
    entropy_input[i] = i;
    personalization_string[i] = 0;
  }

  randombytes_init(entropy_input, personalization_string, 256);

  printf("randombytes returned %u\n", randombytes(data, sizeof(data)));
  fprintBstr(stdout, "data = ", data, sizeof(data));
  printf("randombytes returned %u\n", randombytes(data, sizeof(data)));
  fprintBstr(stdout, "data = ", data, sizeof(data));

  return 0;
}

With this main routine, we get the following output:

randombytes returned 0
data = 061550234D158C5EC95595FE04EF7A25767F2E24CC2BC479D09D86DC9ABCFDE7056A8C266F9EF97ED08541DBD2E1FFA19810F5392D076276EF41277C3AB6E94A4E3B7DCC104A05BB089D338BF55C72CAB375389A94BB920BD5D6DC9E7F2EC6FDE028B6F5724BB039F3652AD98DF8CE6C97013210B84BBE81388C3D141D61957C73BCDC5E5CD92525F46A2B757B03CAB5C337004A2DA35324A325713564DAE28F57ACC6DBE32A0726190BAA6B8A0A255AA1AD01E8DD569AA36D096256C420718A69D46D8DB1C6DD40606A0BE3C235BEFE623A90593F82D6A8F9F924E44E36BE87F7D26B8445966F9EE329C426C12521E85F6FD4ECD5D566BA0A3487125D79CC64
randombytes returned 0
data = C17E034061ED5EA817C41D61636281E816F817DCF753A91D97C018FF82FBC9B1728FC66AF114B57978FB6082B70D285140B26725AA5F7BB4409820F67E2D656EDACA30B5BB12EB5249CC3809B188CF0CC95B5AE0EFE8FC5887152CB6601B4CCF9FC411894FA0C0264EB51A481D4D7074FDF065053030C8A92BFCDD06BF18C8489C38D03784FD63001830E5A385A4A37866693F5BDAB8A8A25B519DDBF2D28268601D95BEED647E430484A227C023B0297A282F06C91376433BDE5EC3ABBA8C06B830C26452EA2FA7EDEA8DCFE20EAFCF8980B3D5AECEF89DD861ACEC1F5F7CD2AE6B3CDE3C1D80A2830DD0B9E8468AFAD161981074BEB33DF1CDFF9A5214F9F0

A compatible rust implementation

Now, we would like to write an equivalent rust implementation. Our implementation should focus on compatibility with the C implementation. First, we need to define the dependencies. The hex crate is only used for representation in the main routine below.

[dependencies]
lazy_static = "1.4.0"
hex = "0.4.3"
aes = "0.7.5"

Like the C implementation, we use 3 arguments for the init-function and 2 arguments for randombytes. The RNG state is stored in a global variable utilized through the lazy_static crate (in general, a bad idea due to concurrent usecases):

use hex;
use std::error;
use aes::NewBlockCipher;
use aes::BlockEncrypt;
use std::sync;
use lazy_static::lazy_static;


pub struct Aes256CtrDrbgStruct {
  pub key: [u8; 32],
  pub v: [u8; 16],
  pub reseed_counter: i32,
}

lazy_static! {
  static ref DRBG_CTX: sync::Mutex<Aes256CtrDrbgStruct> = sync::Mutex::new({
    Aes256CtrDrbgStruct {
      key: [0u8; 32],
      v: [0u8; 16],
      reseed_counter: 0,
    }
  });
}

fn aes256_ecb(
  key: &[u8; 32],
  ctr: &[u8; 16],
  buffer: &mut [u8; 16]
) {
  let cipher = aes::Aes256::new(key.into());
  buffer.copy_from_slice(ctr);
  cipher.encrypt_block(buffer.into());
}

fn aes256_ctr_drbg_update(
  provided_data: &mut Option<[u8; 48]>,
  key: &mut [u8; 32],
  v: &mut [u8; 16]
) {
  let mut temp = [[0u8; 16]; 3];

  for i in 0..3 {
    let count = u128::from_be_bytes(*v);
    v.copy_from_slice(&(count + 1).to_be_bytes());

    aes256_ecb(key, v, &mut temp[i]);
  }

  if let Some(d) = provided_data {
    for j in 0..3 {
      for i in 0..16 {
        temp[j][i] ^= d[16 * j + i];
      }
    }
  }

  key[0..16].copy_from_slice(&temp[0]);
  key[16..32].copy_from_slice(&temp[1]);
  v.copy_from_slice(&temp[2]);
}

fn randombytes_init(
  entropy_input: &[u8; 48],
  personalization_string: &[u8; 48],
  _security_strength: u32,
) -> Result<(), Box<dyn error::Error>> {
  let mut drbg_ctx = DRBG_CTX.lock()?;

  // reset ctx
  drbg_ctx.key = [0u8; 32];
  drbg_ctx.v = [0u8; 16];
  drbg_ctx.reseed_counter = 1i32;

  // get seed ready
  let mut seed = [0u8; 48];
  for i in 0..48 {
    seed[i] = entropy_input[i] ^ personalization_string[i];
  }

  let mut key = drbg_ctx.key;
  aes256_ctr_drbg_update(&mut Some(seed), &mut key, &mut drbg_ctx.v);
  drbg_ctx.key.copy_from_slice(&key);

  Ok(())
}

fn randombytes(
  x: &mut [u8],
  xlen: usize,
) -> Result<(), Box<dyn error::Error>> {
  assert_eq!(x.len(), xlen);
  let mut drbg_ctx = DRBG_CTX.lock()?;

  for chunk in x.chunks_mut(16) {
    let count = u128::from_be_bytes(drbg_ctx.v);
    drbg_ctx.v.copy_from_slice(&(count + 1).to_be_bytes());

    let mut block = [0u8; 16];
    aes256_ecb(&drbg_ctx.key, &drbg_ctx.v, &mut block);

    (*chunk).copy_from_slice(&mut block[..chunk.len()]);
  }

  let mut key = drbg_ctx.key;
  aes256_ctr_drbg_update(&mut None, &mut key, &mut drbg_ctx.v);
  drbg_ctx.key.copy_from_slice(&key);

  drbg_ctx.reseed_counter += 1;

  Ok(())
}

The main routine similarly calls the init-function and randombytes two times. It also checks against the output of the C implementation:

fn main() -> Result<(), Box<dyn error::Error>> {
  let mut data = [0u8; 256];
  let mut entropy_input = [0u8; 48];
  let mut personalization_string = [0u8; 48];

  for i in 0..48 {
    entropy_input[i] = i as u8;
    personalization_string[i] = 0 as u8;
  }

  {
    randombytes_init(&entropy_input, &personalization_string, 256)?;
  }

  {
    randombytes(&mut data, 256)?;
    println!("randombytes returned 0");
    println!("data = {}", hex::encode_upper(&data));
  }
  {
    let mut ref_bytes = [0u8; 256];
    hex::decode_to_slice("061550234D158C5EC95595FE04EF7A25767F2E24CC2BC479D09D86DC9ABCFDE7056A8C266F9EF97ED08541DBD2E1FFA19810F5392D076276EF41277C3AB6E94A4E3B7DCC104A05BB089D338BF55C72CAB375389A94BB920BD5D6DC9E7F2EC6FDE028B6F5724BB039F3652AD98DF8CE6C97013210B84BBE81388C3D141D61957C73BCDC5E5CD92525F46A2B757B03CAB5C337004A2DA35324A325713564DAE28F57ACC6DBE32A0726190BAA6B8A0A255AA1AD01E8DD569AA36D096256C420718A69D46D8DB1C6DD40606A0BE3C235BEFE623A90593F82D6A8F9F924E44E36BE87F7D26B8445966F9EE329C426C12521E85F6FD4ECD5D566BA0A3487125D79CC64", &mut ref_bytes as &mut [u8])?;
    assert_eq!(data, ref_bytes);
  }

  {
    randombytes(&mut data, 256)?;
    println!("randombytes returned 0");
    println!("data = {}", hex::encode_upper(&data));
  }
  {
    let mut ref_bytes = [0u8; 256];
    hex::decode_to_slice("C17E034061ED5EA817C41D61636281E816F817DCF753A91D97C018FF82FBC9B1728FC66AF114B57978FB6082B70D285140B26725AA5F7BB4409820F67E2D656EDACA30B5BB12EB5249CC3809B188CF0CC95B5AE0EFE8FC5887152CB6601B4CCF9FC411894FA0C0264EB51A481D4D7074FDF065053030C8A92BFCDD06BF18C8489C38D03784FD63001830E5A385A4A37866693F5BDAB8A8A25B519DDBF2D28268601D95BEED647E430484A227C023B0297A282F06C91376433BDE5EC3ABBA8C06B830C26452EA2FA7EDEA8DCFE20EAFCF8980B3D5AECEF89DD861ACEC1F5F7CD2AE6B3CDE3C1D80A2830DD0B9E8468AFAD161981074BEB33DF1CDFF9A5214F9F0", &mut ref_bytes as &mut [u8])?;
    assert_eq!(data, ref_bytes);
  }

  Ok(())
}

An idiomatic rust implementation

In this implementation, we want to skip implementation aspects resulting from C conventions and focus on idiomatic rust implementations:

  • I removed the unused _security_strength parameter.

  • the testcases are provided as actual unittests.

  • RNG state is now an object passed as argument not stored as global variable.

First, we reduce dependencies to the aes crate:

[dependencies]
aes = "0.7.5"

Then the implementation has the same set of functions (and a new method for Aes256CtrDrbgStruct) but sometimes different arguments:

use std::error;
use aes::NewBlockCipher;
use aes::BlockEncrypt;


pub struct Aes256CtrDrbgStruct {
  pub key: [u8; 32],
  pub v: [u8; 16],
  pub reseed_counter: i32,
}

impl Aes256CtrDrbgStruct {
  pub fn new() -> Aes256CtrDrbgStruct {
    Aes256CtrDrbgStruct {
      key: [0; 32],
      v: [0; 16],
      reseed_counter: 0
    }
  }
}

fn aes256_ecb(
  key: &[u8; 32],
  ctr: &[u8; 16],
  buffer: &mut [u8; 16]
) {
  let cipher = aes::Aes256::new(key.into());
  buffer.copy_from_slice(ctr);
  cipher.encrypt_block(buffer.into());
}

fn aes256_ctr_drbg_update(
  provided_data: &mut Option<[u8; 48]>,
  key: &mut [u8; 32],
  v: &mut [u8; 16]
) {
  let mut temp = [[0u8; 16]; 3];

  for i in 0..3 {
    let count = u128::from_be_bytes(*v);
    v.copy_from_slice(&(count + 1).to_be_bytes());

    aes256_ecb(key, v, &mut temp[i]);
  }

  if let Some(d) = provided_data {
    for j in 0..3 {
      for i in 0..16 {
        temp[j][i] ^= d[16 * j + i];
      }
    }
  }

  key[0..16].copy_from_slice(&temp[0]);
  key[16..32].copy_from_slice(&temp[1]);
  v.copy_from_slice(&temp[2]);
}

fn randombytes_init(
  entropy_input: &[u8; 48],
  personalization_string: &[u8; 48],
  drbg_ctx: &mut Aes256CtrDrbgStruct
) {
  // reset ctx
  drbg_ctx.key = [0u8; 32];
  drbg_ctx.v = [0u8; 16];
  drbg_ctx.reseed_counter = 1i32;

  // get seed ready
  let mut seed = [0u8; 48];
  for i in 0..48 {
    seed[i] = entropy_input[i] ^ personalization_string[i];
  }

  aes256_ctr_drbg_update(&mut Some(seed), &mut drbg_ctx.key, &mut drbg_ctx.v);
}

fn randombytes(
  x: &mut [u8],
  drbg_ctx: &mut Aes256CtrDrbgStruct
) -> Result<(), Box<dyn error::Error>> {
  for chunk in x.chunks_mut(16) {
    let count = u128::from_be_bytes(drbg_ctx.v);
    drbg_ctx.v.copy_from_slice(&(count + 1).to_be_bytes());

    let mut block = [0u8; 16];
    aes256_ecb(&drbg_ctx.key, &drbg_ctx.v, &mut block);

    (*chunk).copy_from_slice(&mut block[..chunk.len()]);
  }

  aes256_ctr_drbg_update(&mut None, &mut drbg_ctx.key, &mut drbg_ctx.v);
  drbg_ctx.reseed_counter += 1;

  Ok(())
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_rng() -> Result<(), Box<dyn error::Error>> {
    let mut data = [0u8; 256];
    let mut entropy_input = [0u8; 48];
    let mut personalization_string = [0u8; 48];
    let mut rng_state = Aes256CtrDrbgStruct::new();

    for i in 0..48 {
      entropy_input[i] = i as u8;
      personalization_string[i] = 0 as u8;
    }

    randombytes_init(&entropy_input, &personalization_string, &mut rng_state);

    randombytes(&mut data, &mut rng_state)?;
    let ref1 = [0x06u8, 0x15, 0x50, 0x23, 0x4D, 0x15, 0x8C, 0x5E, 0xC9, 0x55, 0x95, 0xFE, 0x04, 0xEF, 0x7A, 0x25, 0x76, 0x7F, 0x2E, 0x24, 0xCC, 0x2B, 0xC4, 0x79, 0xD0, 0x9D, 0x86, 0xDC, 0x9A, 0xBC, 0xFD, 0xE7, 0x05, 0x6A, 0x8C, 0x26, 0x6F, 0x9E, 0xF9, 0x7E, 0xD0, 0x85, 0x41, 0xDB, 0xD2, 0xE1, 0xFF, 0xA1, 0x98, 0x10, 0xF5, 0x39, 0x2D, 0x07, 0x62, 0x76, 0xEF, 0x41, 0x27, 0x7C, 0x3A, 0xB6, 0xE9, 0x4A, 0x4E, 0x3B, 0x7D, 0xCC, 0x10, 0x4A, 0x05, 0xBB, 0x08, 0x9D, 0x33, 0x8B, 0xF5, 0x5C, 0x72, 0xCA, 0xB3, 0x75, 0x38, 0x9A, 0x94, 0xBB, 0x92, 0x0B, 0xD5, 0xD6, 0xDC, 0x9E, 0x7F, 0x2E, 0xC6, 0xFD, 0xE0, 0x28, 0xB6, 0xF5, 0x72, 0x4B, 0xB0, 0x39, 0xF3, 0x65, 0x2A, 0xD9, 0x8D, 0xF8, 0xCE, 0x6C, 0x97, 0x01, 0x32, 0x10, 0xB8, 0x4B, 0xBE, 0x81, 0x38, 0x8C, 0x3D, 0x14, 0x1D, 0x61, 0x95, 0x7C, 0x73, 0xBC, 0xDC, 0x5E, 0x5C, 0xD9, 0x25, 0x25, 0xF4, 0x6A, 0x2B, 0x75, 0x7B, 0x03, 0xCA, 0xB5, 0xC3, 0x37, 0x00, 0x4A, 0x2D, 0xA3, 0x53, 0x24, 0xA3, 0x25, 0x71, 0x35, 0x64, 0xDA, 0xE2, 0x8F, 0x57, 0xAC, 0xC6, 0xDB, 0xE3, 0x2A, 0x07, 0x26, 0x19, 0x0B, 0xAA, 0x6B, 0x8A, 0x0A, 0x25, 0x5A, 0xA1, 0xAD, 0x01, 0xE8, 0xDD, 0x56, 0x9A, 0xA3, 0x6D, 0x09, 0x62, 0x56, 0xC4, 0x20, 0x71, 0x8A, 0x69, 0xD4, 0x6D, 0x8D, 0xB1, 0xC6, 0xDD, 0x40, 0x60, 0x6A, 0x0B, 0xE3, 0xC2, 0x35, 0xBE, 0xFE, 0x62, 0x3A, 0x90, 0x59, 0x3F, 0x82, 0xD6, 0xA8, 0xF9, 0xF9, 0x24, 0xE4, 0x4E, 0x36, 0xBE, 0x87, 0xF7, 0xD2, 0x6B, 0x84, 0x45, 0x96, 0x6F, 0x9E, 0xE3, 0x29, 0xC4, 0x26, 0xC1, 0x25, 0x21, 0xE8, 0x5F, 0x6F, 0xD4, 0xEC, 0xD5, 0xD5, 0x66, 0xBA, 0x0A, 0x34, 0x87, 0x12, 0x5D, 0x79, 0xCC, 0x64];
    assert_eq!(data, ref1);

    randombytes(&mut data, &mut rng_state)?;
    let ref2 = [0xC1u8, 0x7E, 0x03, 0x40, 0x61, 0xED, 0x5E, 0xA8, 0x17, 0xC4, 0x1D, 0x61, 0x63, 0x62, 0x81, 0xE8, 0x16, 0xF8, 0x17, 0xDC, 0xF7, 0x53, 0xA9, 0x1D, 0x97, 0xC0, 0x18, 0xFF, 0x82, 0xFB, 0xC9, 0xB1, 0x72, 0x8F, 0xC6, 0x6A, 0xF1, 0x14, 0xB5, 0x79, 0x78, 0xFB, 0x60, 0x82, 0xB7, 0x0D, 0x28, 0x51, 0x40, 0xB2, 0x67, 0x25, 0xAA, 0x5F, 0x7B, 0xB4, 0x40, 0x98, 0x20, 0xF6, 0x7E, 0x2D, 0x65, 0x6E, 0xDA, 0xCA, 0x30, 0xB5, 0xBB, 0x12, 0xEB, 0x52, 0x49, 0xCC, 0x38, 0x09, 0xB1, 0x88, 0xCF, 0x0C, 0xC9, 0x5B, 0x5A, 0xE0, 0xEF, 0xE8, 0xFC, 0x58, 0x87, 0x15, 0x2C, 0xB6, 0x60, 0x1B, 0x4C, 0xCF, 0x9F, 0xC4, 0x11, 0x89, 0x4F, 0xA0, 0xC0, 0x26, 0x4E, 0xB5, 0x1A, 0x48, 0x1D, 0x4D, 0x70, 0x74, 0xFD, 0xF0, 0x65, 0x05, 0x30, 0x30, 0xC8, 0xA9, 0x2B, 0xFC, 0xDD, 0x06, 0xBF, 0x18, 0xC8, 0x48, 0x9C, 0x38, 0xD0, 0x37, 0x84, 0xFD, 0x63, 0x00, 0x18, 0x30, 0xE5, 0xA3, 0x85, 0xA4, 0xA3, 0x78, 0x66, 0x69, 0x3F, 0x5B, 0xDA, 0xB8, 0xA8, 0xA2, 0x5B, 0x51, 0x9D, 0xDB, 0xF2, 0xD2, 0x82, 0x68, 0x60, 0x1D, 0x95, 0xBE, 0xED, 0x64, 0x7E, 0x43, 0x04, 0x84, 0xA2, 0x27, 0xC0, 0x23, 0xB0, 0x29, 0x7A, 0x28, 0x2F, 0x06, 0xC9, 0x13, 0x76, 0x43, 0x3B, 0xDE, 0x5E, 0xC3, 0xAB, 0xBA, 0x8C, 0x06, 0xB8, 0x30, 0xC2, 0x64, 0x52, 0xEA, 0x2F, 0xA7, 0xED, 0xEA, 0x8D, 0xCF, 0xE2, 0x0E, 0xAF, 0xCF, 0x89, 0x80, 0xB3, 0xD5, 0xAE, 0xCE, 0xF8, 0x9D, 0xD8, 0x61, 0xAC, 0xEC, 0x1F, 0x5F, 0x7C, 0xD2, 0xAE, 0x6B, 0x3C, 0xDE, 0x3C, 0x1D, 0x80, 0xA2, 0x83, 0x0D, 0xD0, 0xB9, 0xE8, 0x46, 0x8A, 0xFA, 0xD1, 0x61, 0x98, 0x10, 0x74, 0xBE, 0xB3, 0x3D, 0xF1, 0xCD, 0xFF, 0x9A, 0x52, 0x14, 0xF9, 0xF0];
    assert_eq!(data, ref2);

    Ok(())
  }
}

Conclusion

We provided two implementations. One focuses on compatibility with C, one focuses on idiomatic rust. Both yield the same results like the C implementation and the latter implementation uses only the aes crate as dependency.