SHA-3 Derived Functions

This section describes the API for the functionality defined in NIST Special publication 800-185 SHA-3 Derived Functions: cSHAKE, KMAC, TupleHash and ParallelHash.

cSHAKE

cSHAKE is an extension of SHAKE that allows domain separation with a "function name" and a "customization string". The API provided by cshake_128 and cshake_256 is similar to that of SHAKE, except for accepting two additional optional parameters:

julia> msg = "a message to compute the digest of";

julia> cshake_128(msg, Val(10)) # equal to shake_128
(0xd4, 0xa7, 0x25, 0x77, 0xde, 0x29, 0x05, 0x20, 0x9b, 0x35)

julia> cshake_128(msg, Val(10), "example") # with a function name
(0x13, 0x74, 0xed, 0x93, 0xc6, 0x7d, 0x22, 0x55, 0xad, 0x6d)

julia> cshake_128(msg, Val(10), "example", "customized") # ... and customization
(0x25, 0x21, 0x2b, 0x95, 0xee, 0x09, 0x86, 0xea, 0x8a, 0x90)

As for SHAKE, giving the output length as a plain number (instead of Val) causes a Vector{UInt8} to be returned and omitting it entirely creates a sponge ready for squeezing:

julia> sponge = cshake_128(msg, "example"); # could take customization string a third parameter

julia> squeeze(sponge, Val(10))[2]
(0x13, 0x74, 0xed, 0x93, 0xc6, 0x7d, 0x22, 0x55, 0xad, 0x6d)

It is also possible to create a sponge for (chunk-wise) absorption of input. However, the function name and customization string (if any) have to be given upon sponge construction:

julia> sponge = cshake_128_sponge("example", "customized");

julia> sponge = absorb(sponge, msg);

julia> sponge = pad(sponge);

julia> squeeze(sponge, Val(10))[2]
(0x25, 0x21, 0x2b, 0x95, 0xee, 0x09, 0x86, 0xea, 0x8a, 0x90)

KMAC

The Keccak Message Authentication Code (KMAC) extends cSHAKE to a keyed hash function. It offers the same security strengths, 128 bits and 256 bits, available as kmac_128 and kmac_256, respectively.

Computing a KMAC is straight-forward:

julia> kmac_128("secret_key", "message to authenticate", Val(16))
(0x65, 0x65, 0xf2, 0x69, 0xe5, 0x9e, 0x79, 0x12, 0xcf, 0x10, 0xc0, 0x38, 0x1f, 0x97, 0xd8, 0x35)

julia> kmac_128("secret_key", "message to authenticate", Val(16), "optional customization")
(0xc5, 0xfc, 0xa4, 0x68, 0xad, 0x49, 0x44, 0xf8, 0x74, 0x2b, 0x5c, 0x16, 0x31, 0x90, 0x84, 0x57)

julia> kmac_128("secret_key", "message to authenticate", Val(10)) # output length is included in hash
(0x58, 0x83, 0xac, 0x0e, 0xf4, 0x6b, 0x3a, 0x31, 0x65, 0xcc)

Chunked input (and output) is possible by creating sponge with kmac_128_sponge or kmac_256_sponge:

julia> sponge = kmac_128_sponge("secret_key", 16, "optional customization");

julia> sponge = absorb(sponge, "message");

julia> sponge = absorb(sponge, " to authenticate");

julia> sponge = pad(sponge);

julia> squeeze(sponge, Val(16))[2] # repeat output length
(0xc5, 0xfc, 0xa4, 0x68, 0xad, 0x49, 0x44, 0xf8, 0x74, 0x2b, 0x5c, 0x16, 0x31, 0x90, 0x84, 0x57)

While it is possible to obtain an output of a different length than was specified when initializing the sponge, the resulting KMAC will not be standard-compliant. However, there is an arbitrary-length output variant (extensible output function, XOF) for this purpose. Calling kmac_xof_128 (or kmac_xof_256) instead of kmac_128 (kmac_256) computes a KMAC where the output length is not included in the hash:

julia> kmac_xof_128("secret_key", "message to authenticate", Val(16))
(0x7b, 0xca, 0x90, 0xfd, 0xf6, 0x1e, 0x9d, 0x1e, 0x96, 0x3f, 0xf0, 0xf6, 0x7e, 0x37, 0x2c, 0x21)

julia> kmac_xof_128("secret_key", "message to authenticate", Val(10)) # prefix of the above
(0x7b, 0xca, 0x90, 0xfd, 0xf6, 0x1e, 0x9d, 0x1e, 0x96, 0x3f)

Omitting the length results in a sponge ready to be squeezed from:

julia> sponge = kmac_xof_128("secret_key", "message to authenticate");

julia> sponge, out1 = squeeze(sponge, Val(10));

julia> out1
(0x7b, 0xca, 0x90, 0xfd, 0xf6, 0x1e, 0x9d, 0x1e, 0x96, 0x3f)

julia> squeeze(sponge, Val(6))[2] # next six bytes
(0xf0, 0xf6, 0x7e, 0x37, 0x2c, 0x21)

To enable chunk-wise input, the length argument can be omitted from kmac_128_sponge and kmac_256_sponge to obtain a sponge for KMAC XOF:

julia> sponge = kmac_128_sponge("secret_key");

julia> sponge = absorb(sponge, "message to authenticate");

julia> sponge = pad(sponge);

julia> squeeze(sponge, Val(10))[2]
(0x7b, 0xca, 0x90, 0xfd, 0xf6, 0x1e, 0x9d, 0x1e, 0x96, 0x3f)

TupleHash

TupleHash is a SHA-3-derived hash function for hashing a collection of input byte-strings. It is available as tuplehash_128 and tuplehash_256 for security lengths 128 bits and 256 bits, respectively. The desired output length has to be specified and is included in the hash:

julia> tuplehash_128(["abcd", "ef"], Val(16))
(0x9c, 0x68, 0x5d, 0x62, 0x70, 0xab, 0x35, 0x59, 0xcb, 0x34, 0x60, 0x07, 0x8b, 0xb2, 0x30, 0x2b)

julia> tuplehash_128(["abc", "def"], Val(16)) # different partitioning of same data -> different hash
(0xe9, 0xee, 0x78, 0x85, 0x9a, 0x46, 0x37, 0x69, 0xc3, 0xaa, 0x05, 0xcb, 0x3c, 0x41, 0x81, 0xa3)

julia> tuplehash_128(["abc", "def"], Val(12)) # different output length -> different hash
(0x20, 0x7e, 0x63, 0xbb, 0x5c, 0xae, 0xb1, 0x5e, 0x6e, 0xd9, 0x67, 0xf8)

Note that, contrary to what the name suggests, the collection of input byte-strings does not have to be passed as a tuple. Any iterator (that produces Strings, Vector{UInt8}s, or Tuple{Vararg{UInt8}}s) will do, including a tuple.

There is also an arbitrary-length output variant available as tuplehash_xof_128 and tuplehash_xof_256:

julia> tuplehash_xof_128(["abc", "def"], Val(16))
(0xf0, 0xde, 0xf4, 0x88, 0x9a, 0xed, 0x8e, 0x8f, 0x77, 0xc0, 0xef, 0xf1, 0x60, 0xa3, 0x80, 0x54)

julia> tuplehash_xof_128(["abc", "def"], Val(12)) # prefix of the above
(0xf0, 0xde, 0xf4, 0x88, 0x9a, 0xed, 0x8e, 0x8f, 0x77, 0xc0, 0xef, 0xf1)

Omitting the output length gives a sponge that can be squeezed from:

julia> sponge = tuplehash_xof_128(["abc", "def"]);

julia> squeeze(sponge, Val(16))[2]
(0xf0, 0xde, 0xf4, 0x88, 0x9a, 0xed, 0x8e, 0x8f, 0x77, 0xc0, 0xef, 0xf1, 0x60, 0xa3, 0x80, 0x54)

Chunked input is not supported, i.e. there is no function to create a sponge ready for absorption to produce a TupleHash.

ParallelHash

ParallelHash is a SHA-3-derived hash function designed to allow exploiting parallelism. This is achieved by processing the input in independent blocks. The implementation in parallelhash_128 and parallelhash_256 (for security lengths of 128 and 256 bits, respectively) utilizes 4-fold SIMD and multithreading to maximize throughput.

Note

Multi-threading requires julia to be started with multi-threading enabled, e.g. by passing -t auto. The multi-threaded implementation allocates memory. If avoiding allocations is a priority, multi-threading can be disabled by passing the keyword argument threaded=false.

Usage is straight-forward:

julia> data = rand(UInt8, 1_000_000);

julia> parallelhash_128(data, 1024, Val(16)) # blocks of size 1024
(0x44, 0xd8, 0x5d, 0x94, 0xdc, 0x7e, 0xba, 0xe8, 0xee, 0x2f, 0xb4, 0x0a, 0xc8, 0x11, 0x1d, 0x4d)
Note

The choice of the block size has an effect on the run-time, but also on the result. It is therefore not only a tuning parameter, but will usually be determined by the context.

Another aspect to note is that the input data has to be an AbstractArray{UInt8} or a String. Contrary to all other hashing functions, Tuple{Vararg{UInt8}} is not supported. Given that ParallelHash is typically applied to long input data, this should not be a restriction in practice.

Similar to KMAC and TupleHash, a special XOF mode is supported where the output length is not hashed. This is provided by parallelhash_xof_128 and parallelhash_xof_256 which allow directly calculating the hash or creating a squeeze-ready sponge:

julia> data = rand(UInt8, 1_000_000);

julia> parallelhash_xof_128(data, 1024, Val(8))
(0x7a, 0xb2, 0x0d, 0xa2, 0xea, 0x3f, 0x33, 0xb9)

julia> parallelhash_xof_128(data, 1024, Val(16))
(0x7a, 0xb2, 0x0d, 0xa2, 0xea, 0x3f, 0x33, 0xb9, 0xe8, 0xd6, 0xf8, 0xfc, 0x41, 0x39, 0x89, 0x67)

julia> sponge = parallelhash_xof_128(data, 1024);

julia> squeeze(sponge, Val(8))[2]
(0x7a, 0xb2, 0x0d, 0xa2, 0xea, 0x3f, 0x33, 0xb9)