Overview

Table of contents
  1. NIST
  2. API Overview
    1. PQC_init_context_hash
    2. PQC_add_data
    3. PQC_hash_size
    4. PQC_get_hash
    5. PQC_close_context
  3. Example
    1. Const size SHA-3
    2. Shake SHA-3 example

SHA-3 (Secure Hash Algorithm 3) is the latest member of the Secure Hash Algorithm family, released by the National Institute of Standards and Technology (NIST) on August 5, 2015. While belonging to the same series of standards that includes SHA-0, SHA-1, and SHA-2, SHA-3 is internally different and does not share the MD5-like structure that SHA-1 and SHA-2 have.

The SHA-3 family of hashing algorithms was developed through a public competition and is based on the Keccak algorithm, which was designed by a team led by cryptographer Guido Bertoni and includes Joan Daemen, Michaël Peeters, and Gilles Van Assche. Keccak won the NIST competition to become the SHA-3 standard.

SHA-3 functions as a cryptographic hash function that takes an input with unfixed size (or ‘message’) and produces a fixed-size string of bytes, which is typically a ‘digest’. Hash functions are deterministic in nature, meaning the same input will always result in the same hash value. They are also designed to be one-way functions, making it computationally infeasible to reverse or to find two different inputs that produce the same hash value (resistance to collisions).

The SHA-3 standard as defined in FIPS 202 includes a variety of functions catering to different applications:

  • SHA-3-224

  • SHA-3-256

  • SHA-3-384

  • SHA-3-512

  • SHAKE128 and SHAKE256, which are extendable-output functions (XOFs)

Cryptographic hash functions like SHA-3 are vital in the realm of information security. They are used for data integrity checks, digital signatures, proof-of-work systems in cryptocurrencies, and in many other scenarios where a secure and reliable method of hashing is required.

NIST

SHA-3 was formally standardized by NIST in FIPS PUB 202 in August 2015. It is an official member of the Secure Hash Algorithm (SHA) family, which is widely recognized and used for cryptographic applications to ensure data integrity and authentication.

API Overview

Include: pqc/sha3.h

PQC_init_context_hash

Function Signature:

    CIPHER_HANDLE PQC_init_context_hash(uint32_t algorithm, uint32_t mode);

Purpose:

  • The function initializes a hashing context for a specified cryptographic hash algorithm and mode.

Parameters:

  • algorithm: This is a constant that determines which hashing algorithm to use. In this case, the option provided is PQC_CIPHER_SHA3, which indicates SHA-3, a secure hash algorithm.

  • mode: parameter defines the operational mode of the algorithm. It must be assigned one of the following values to specify the desired cryptographic strength and function: PQC_SHA3_224, PQC_SHA3_256, PQC_SHA3_384, PQC_SHA3_512, with alternatives including PQC_SHAKE_128 or PQC_SHAKE_256 for variable output lengths.

Return Values:

  • PQC_BAD_CIPHER: This is an error code returned if an unknown, unsupported cipher is specified, or if there is another issue such as an incorrect size of a private key.

  • Otherwise: The function returns a “handle” to the created encryption context.

PQC_add_data

Function Signature:

    int PQC_add_data(CIPHER_HANDLE ctx, uint8_t* buffer, size_t length);

Purpose:

  • The function’s purpose is to process data for hashing, using the initialized cryptographic context referred to by ctx.

Parameters:

  • ctx: This is the cryptographic context handle which would have been initialized by a previous call to a setup function (like PQC_init_context_hash).

  • buffer (input): This is a pointer to a block of data to be processed. The data type uint8_t* implies that it is a pointer to an array of bytes, since uint8_t is typically defined as an unsigned 8-bit integer, representing a byte.

  • length: This is the size of the buffer array, indicating how much data (in bytes) from the buffer should be processed by this call to PQC_add_data.

Return Values:

  • PQC_OK: The operation was successful.

  • PQC_BAD_CIPHER: An error code indicating an issue with the cryptographic context, such as an unknown or unsupported cipher algorithm. This likely means that the context referred to by ctx was not properly initialized with a supported cipher.

The caller should check the return value after each call to this function to ensure the data was processed correctly. PQC_add_data function is being used for hashing data with SHA-3, the typical sequence would involve initializing a context with PQC_init_context_hash, calling PQC_add_data one or multiple times to process the data chunks, and subsequently finalizing the hash computation.

PQC_hash_size

Function Signature:

    unsigned int PQC_hash_size(CIPHER_HANDLE ctx);

Purpose:

  • The function is used to query the size of the hash that the cryptographic context, identified by ctx, is configured to produce. This is useful for determining the amount of space required to store the hash or for validating that the context is set up correctly.

Parameters:

  • ctx: This is a handle to the initialized encryption context. This context should have been previously set up for a hash operation, likely by a call to a function like PQC_init_context_hash.

Return Values:

  • 0: This indicates an error condition. There are two scenarios where this could be returned: if the hashing context has not been initialized (meaning ctx does not reference a valid context), or if the ctx refers to an incorrect type that doesn’t support the hash size retrieval operation. Additionally, in the case where the context is initialized with one of the SHAKE modes (SHAKE128 or SHAKE256), the function also returns 0 to indicate that the hash size is not fixed and can be chosen by the user during the final hash output generation.

  • Otherwise: The function returns the size of the expected hash output in bytes if the context is set up with one of the fixed-size SHA-3 hash modes (SHA3-224, SHA3-256, SHA3-384, or SHA3-512). These figures correspond to the hash output sizes of 224 bits (28 bytes), 256 bits (32 bytes), 384 bits (48 bytes), and 512 bits (64 bytes), respectively.

Understanding the size of the output is crucial, especially when allocating memory to hold the hash result or when interfacing with other systems that expect a hash of a specific size. In the case of SHAKE modes, since the output size is variable, the size value returned by this function is not useful other than to indicate that the context has been correctly set up for a SHAKE mode (since it returns 0 for these modes).

PQC_get_hash

Function Signature:

    int PQC_get_hash(CIPHER_HANDLE ctx, uint8_t* hash, size_t hash_length);

Purpose:

  • The function finalizes the hashing process and stores the resulting hash value into a provided buffer.

Parameters:

  • ctx: This is the handle to an encryption context that has been previously initialized, presumably for performing hash operations.

  • hash (output): This is a pointer to a buffer where the hash value will be stored. The buffer must be allocated by the caller before this function is called.

  • hash_length: This parameter specifies the length of the hash buffer provided. If ctx is configured for one of the fixed-size SHA-3 hash modes, this length should match the size returned by PQC_hash_size. If ctx is configured for one of the SHAKE modes, the length can be any positive value, as these modes support variable output lengths.

Return Values:

  • PQC_OK: Indicates that the operation was successful and the hash value has been stored in the provided buffer.

  • PQC_BAD_CIPHER: The error code returned if the cipher related to the context is unknown or unsupported.

  • PQC_BAD_LEN: This error is returned if the hash buffer length (hash_length) does not match the expected size of the hash, as determined by the context’s configuration.

The description clarifies that you can interleave calls to PQC_add_data (which adds data to be hashed) with PQC_get_hash. No matter how many times PQC_add_data has been called, each invocation of PQC_get_hash will produce a hash for all the data added to the context since its creation.

Usage Notes:

  • Before calling PQC_get_hash, data should have been added to the context using PQC_add_data.

  • The buffer pointed to by hash should be of appropriate size to store the hash. This means you should either:
    • Use PQC_hash_size to obtain the fixed hash size for SHA-3 variants and allocate the buffer accordingly or,
    • Choose an arbitrary positive hash_length for SHAKE variants, depending on how many bytes of the hash you require.
  • You should handle the returned value by checking for errors (PQC_BAD_CIPHER, PQC_BAD_LEN) and ensuring successful operation (PQC_OK).

Each call to PQC_get_hash effectively gives a snapshot of the cumulative hash of the data processed by the context up to that point. The operation’s correctness relies on the proper sequence of calls, correct buffer sizes, and the monitoring of return values for error handling.

PQC_close_context

Function Signature:

    int PQC_close_context(CIPHER_HANDLE ctx);

Purpose:

  • The purpose of this function is to deallocate the encryption context referred to by ctx. Once a cryptographic operation is completed, it is important to properly release any dynamically allocated memory or other resources to prevent memory leaks and ensure that sensitive information is not left in memory longer than necessary.

Parameters:

  • ctx: This is a handle to an initialized encryption context that has been previously created and used for cryptographic operations such as hashing or encryption.

Return Values:

  • PQC_OK: This indicates that the operation was successful, signaling that the context has been closed and all associated resources have been freed.

By calling PQC_close_context, users ensure that their program behaves responsibly with system resources. It is a standard best practice for C, which don’t have automatic garbage collection, that every resource allocated should be paired with a corresponding deallocation.

Usage Notes:

  • You should only call PQC_close_context once for each initialized context. Attempting to free an already freed context can lead to undefined behavior, including crashes.

  • After PQC_close_context has been called with a particular context handle, that handle should not be used again unless it is reassigned by re-initializing a new context.

Example

Const size SHA-3

// SHA3 is a hash-function. Input data - message of any size.
// Hash of fixed size is saved into hash array.
// Look shake_sha3_example.cpp to use arbitary size of hash.
#include <cstring>
#include <iostream>

#include <pqc/sha3.h>

int main(void)
{
    const int sha_len =
        PQC_SHA3_512; // fixed size of hash. It can be one of PQC_SHA3_224, PQC_SHA3_256, PQC_SHA3_384, PQC_SHA3_512
                      // Please note that expected value of SHA3 hash is valid only for PQC_SHA3_512.
    const int message_len = 200;

    // Message is a data to get hash from.
    uint8_t message[message_len] = {
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3,
        0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3, 0xA3};

    uint8_t expected[] = {0xE7, 0x6D, 0xFA, 0xD2, 0x20, 0x84, 0xA8, 0xB1, 0x46, 0x7F, 0xCF, 0x2F,
                          0xFA, 0x58, 0x36, 0x1B, // Expected 512-bit (PQC_SHA3_512) SHA3 hash for message above
                          0xEC, 0x76, 0x28, 0xED, 0xF5, 0xF3, 0xFD, 0xC0, 0xE4, 0x80, 0x5D, 0xC4,
                          0x8C, 0xAE, 0xEC, 0xA8, 0x1B, 0x7C, 0x13, 0xC3, 0x0A, 0xDF, 0x52, 0xA3,
                          0x65, 0x95, 0x84, 0x73, 0x9A, 0x2D, 0xF4, 0x6B, 0xE5, 0x89, 0xC5, 0x1C,
                          0xA1, 0xA4, 0xA8, 0x41, 0x6D, 0xF6, 0x54, 0x5A, 0x1C, 0xE8, 0xBA, 0x00};


    CIPHER_HANDLE sha3 = PQC_init_context_hash(PQC_CIPHER_SHA3, sha_len);


    // Add message content to the hash function
    size_t pqc_add_data_return = PQC_add_data(sha3, message, message_len);
    if (pqc_add_data_return != PQC_OK)
        std::cout << "\nERROR!!! Returned value must be PQC_OK if operation was done successfully";

    // check hash size
    size_t hash_size = PQC_hash_size(sha3);
    if (hash_size != sha_len / 8)
        std::cout << "\nERROR!!! Returned value must be equal to hash size of this mode!" << std::endl;

    // create memory space for hash result
    uint8_t * hash = new uint8_t[hash_size];

    size_t pqc_get_hash_return = PQC_get_hash(
        sha3, hash, hash_size
    ); // PQC_get_hash gets hash from message. pqc_get_hash_return should be equal to PQC_OK
    if (pqc_get_hash_return != PQC_OK)
        std::cout << "\nERROR!!! Returned value must be PQC_OK if operation was done successfully";

    // close context
    size_t pqClose = PQC_close_context(sha3);
    if (pqClose != PQC_OK)
        std::cout << "\nERROR!!! Returned value must be PQC_OK if operation was done successfully" << std::endl;


    // Verification. Hash should be similar with constant digest message for this initial message
    if (memcmp(hash, expected, sha_len / 8) == 0)
    {
        std::cout << "Verification successfull" << std::endl;
    }
    else
    {
        std::cout << "Verification failed" << std::endl;
    }

    // Free memory
    delete[] hash;

    return 0;
}

Shake SHA-3 example

// SHA3 is a hash-function. Input data - message of any size.
// Hash of arbitary size is saved into hash array.
#include <cstring>
#include <iostream>

#include <pqc/sha3.h>

/*
The hash taking mode SHAKE is the mode of the sha 3 hash function, but with a variable output size.
Simply put, you can take hash with any hash size.
The SHAKE function exists in two instances: SHAKE 128 and SHAKE 256. They differ somewhat in their internal
structure. In particular, SHAKE 256 will do more permutations with larger output hash sizes, which means it
will have higher reliability. However, this does not significantly affect the quality of hash functions. And
it has absolutely no effect on the interface to use.

In this example, we will create a message with data, take a hash from it and compare it with the default cache
that should be received.
*/

const int hash_size = 500; // Output hash size we want to have

int main(void)
{
    // message compare. We should get hash equal to this message
    uint8_t defaultHash[hash_size] = {
        205, 101, 164, 229, 83,  64,  91,  80,  194, 243, 112, 1,   234, 129, 144, 95,  54,  214, 80,  204, 119, 95,
        218, 216, 152, 178, 227, 67,  100, 76,  179, 219, 37,  107, 192, 233, 179, 1,   247, 242, 26,  219, 170, 250,
        217, 121, 49,  191, 41,  11,  51,  214, 57,  22,  134, 186, 17,  63,  163, 254, 152, 167, 170, 245, 125, 216,
        158, 51,  74,  128, 19,  212, 117, 46,  101, 219, 156, 53,  12,  128, 52,  29,  51,  57,  57,  96,  79,  98,
        221, 237, 241, 250, 62,  201, 189, 236, 185, 4,   73,  2,   44,  138, 79,  111, 255, 199, 122, 220, 122, 164,
        35,  176, 114, 94,  42,  219, 172, 130, 171, 7,   41,  181, 146, 68,  16,  25,  36,  255, 73,  170, 245, 103,
        42,  5,   208, 16,  252, 164, 211, 196, 173, 180, 84,  216, 137, 16,  214, 178, 217, 254, 162, 133, 153, 55,
        74,  172, 161, 157, 139, 201, 100, 79,  128, 68,  170, 148, 8,   148, 190, 5,   146, 176, 115, 174, 213, 75,
        189, 108, 192, 166, 197, 155, 182, 190, 208, 29,  241, 143, 211, 232, 116, 230, 33,  161, 199, 22,  21,  51,
        242, 240, 175, 166, 238, 189, 193, 123, 186, 185, 177, 176, 110, 66,  10,  31,  114, 54,  84,  52,  196, 229,
        135, 213, 67,  250, 246, 94,  207, 135, 138, 174, 81,  93,  70,  202, 64,  16,  106, 123, 160, 135, 75,  135,
        221, 90,  74,  180, 220, 71,  32,  185, 136, 9,   205, 215, 86,  110, 102, 69,  192, 196, 171, 221, 61,  110,
        51,  201, 200, 200, 7,   188, 162, 29,  152, 62,  183, 35,  161, 85,  20,  158, 172, 70,  100, 113, 39,  192,
        107, 173, 3,   186, 160, 29,  70,  79,  13,  8,   168, 118, 53,  215, 63,  133, 191, 71,  181, 58,  131, 86,
        2,   229, 50,  78,  71,  174, 120, 167, 124, 69,  100, 203, 38,  4,   126, 120, 55,  190, 152, 121, 217, 204,
        185, 128, 68,  146, 196, 168, 62,  193, 194, 66,  61,  200, 179, 62,  13,  185, 117, 23,  91,  66,  106, 181,
        181, 210, 190, 102, 10,  211, 94,  190, 73,  27,  252, 81,  253, 150, 121, 51,  198, 176, 59,  148, 198, 209,
        64,  114, 10,  130, 97,  133, 240, 133, 99,  48,  148, 178, 88,  17,  144, 111, 220, 219, 213, 232, 24,  242,
        212, 109, 238, 252, 167, 250, 123, 114, 65,  253, 118, 160, 219, 168, 100, 0,   126, 162, 214, 208, 227, 130,
        43,  255, 247, 215, 230, 226, 148, 55,  204, 136, 59,  221, 121, 210, 19,  64,  200, 232, 214, 52,  104, 141,
        198, 222, 244, 239, 105, 236, 194, 127, 214, 206, 79,  39,  41,  242, 96,  74,  210, 81,  118, 54,  93,  130,
        80,  184, 83,  37,  212, 55,  4,   10,  233, 196, 253, 51,  34,  158, 65,  10,  162, 7,   239, 147, 115, 43,
        253, 35,  106, 183, 160, 83,  104, 63,  120, 78,  242, 0,   72,  91,  22,  48};

    // Let's init memmory space for our output hash
    uint8_t out[hash_size];

    // Init context of sha3 SHAKE hash function using library API
    CIPHER_HANDLE sha3 = PQC_init_context_hash(PQC_CIPHER_SHA3, PQC_SHAKE_256);

    /*
    In detail. There is a function PQC_add_data(). It allows you to add data to the buffer from which the hash is taken.
    It is important to understand that this function can be applied to one hash function object many times. That is, if
    you need to take a hash from data of this type "1234567890", then you can add "1234" first, and then additionally
    add "567890" and take the hash. And it won't be any different from taking the hash from "1234567890". Moreover, you
    can first add "1234", take the hash from this data, and then add "567890" and again take the hash from the added
    data. And the resulting hash will be equivalent to the hash from "1234567890".

    In the example, we will first add "1234", then we will take a hash from this data, show that it is NOT equal to our
    default message. Then add "567890", take the hash again. And show that it is equal to our default message. After
    that, we will create a new hash function object and take the hash from "1234567890". And let's show that it is also
    equal to our default message.
    */
    PQC_add_data(sha3, (uint8_t *)"1234", 4);
    PQC_get_hash(sha3, out, hash_size);

    // So, now in out is hash of SHAKE256 fron "1234" data

    if (memcmp(out, defaultHash, hash_size) == 0)
        std::cout << "ERROR!!! The shouldn't be equal!!!";

    PQC_add_data(sha3, (uint8_t *)"567890", 6);
    PQC_get_hash(sha3, out, hash_size);
    if (memcmp(out, defaultHash, hash_size) != 0)
        std::cout << "ERROR!!! The should be equal!!!";

    // So, now in out is hash of SHAKE256 fron "1234567890" data

    PQC_close_context(sha3);


    // Let's create new context
    CIPHER_HANDLE sha3_new = PQC_init_context_hash(PQC_CIPHER_SHA3, PQC_SHAKE_256);
    PQC_add_data(sha3_new, (uint8_t *)"1234567890", 10);
    PQC_get_hash(sha3_new, out, hash_size);
    if (memcmp(out, defaultHash, hash_size) != 0)
        std::cout << "ERROR!!! The should be equal!!!";

    // So, now in out is hash of SHAKE256 fron "1234567890" data

    PQC_close_context(sha3_new);

    std::cout << "end of shake example";


    /*
    To use SHAKE128 intead of SHAKE256 it is nessary only to change PQC_SHAKE_256 to PQC_SHAKE_128 in initialization
    context
    */

    return 0;
}


© Copyright 2024, Terra Quantum AG.