Slow And Steady Wins The Light-weight Race: Meet ASCON

Since 2016, NIST has been assessing light-weight encryption methods, and, in 2022, NIST published the final 10: ASCON, Elephant, GIFT-COFB…

Photo by Chris Ried on Unsplash

Slow And Steady Wins The Light-weight Race: Meet ASCON

Since 2016, NIST has been assessing light-weight encryption methods, and, in 2022, NIST published the final 10: ASCON, Elephant, GIFT-COFB, Grain128-AEAD, ISAP, Photon-Beetle, Romulus, Sparkle, TinyJambu, and Xoodyak (Table 1). A particular focus is on the security of the methods, along with their performance on low-cost FPGAs/embedded processes and their robustness against side-channel attacks. And, so, in Feb 2023, NIST annouced the ASCON was the winner, and would move to standardization.

Table 1: Specifications of the NIST LWC finalist algorithms [3]

Generally, ASCON does well in most tests and is a good all-rounder. Itwas designed by Christoph Dobraunig, Maria Eichlseder, Florian Mendel and Martin Schläffer from Graz University of Technology, Infineon Technologies, and Radboud University. It is both a lightweight hashing and encryption method.

ASCON uses a single lightweight permutation with Sponge-based modes of operation and an SPN (substitution–permutation network) permutation. Overall it has an easy method of implementing within hardware (2.6 gate equivalents) and software. A 5-bit S-box (as used in Keccak’s S-box core) is used to enable a lightweight approach and it has no known side-channel attacks. It can also achieve high throughputs such as throughputs of between 4.9 and 7.3 Gbps. It stores its current state with 320 bits.

You can learn more about Sponge functions here:

Evaluation (Performance)

The current set of benchmarks includes running on an Arduino Uno R3 (AVR ARmega 328P — Figure 1), Arduino Nano Every (AVR ARmega 4809), Arduino MKR Zero (ARM Cortex M10+) and Arduino Nano 33 BLE (ARM Cortex M4F). These are just 8-bit processors and fit into an Arduino board. Along with their processing limitations, they are also limited in their memory footprint (to run code and also store it). The lightweight cryptography method must thus overcome these limitations and still, be secure and provide a good performance level. Running AES in block modes on these devices is often not possible, as there are insufficient resources. Overall we use a benchmark for encryption — with AEAD (Authenticated Encryption with Additional Data) and for hashing. With AEAD we add extra information — such as the session ID — into the encryption process. This type of method can bind the encryption to a specific stream.

ARM performance

In Table 2 [5], we see a sample run using an Arduino Due with an ARM Cortex M3 running at 84MHz. The tests are taken in comparison with the ChaCha20 stream cipher and defined for AEAD, and where the higher the value, the better the performance. We can see that Sparkle, Xoodyak and ASCON are the fastest of all. Sparkle has a 100% improvement, and Xoodyak gives a 60% increase in speed over ChaCha20. Elephant, ISAP and PHOTON-Beetle have the worst performance for encryption (with around 1/20th of the speed of ChaCha20).

Table 2: Arduino Due with an ARM Cortex M3 running at 84MHz for encryption against ChaCha20 [5]

Not all of the finalists can do hash functions. Table 3 outlines these.

Table 3: Arduino Due with an ARM Cortex M3 running at 84MHz for hashing against BLAKE2s [5]

Again, we see Sparkle and Xoodyak in the lead, with Sparkle actually faster in the test than BLAKE2s, and Xoodyak just a little bit slower. ASCON has a weaker performance, and PHOTON-Beetle is relatively slow. For all the tests, the ranking for authenticated encryption is (and where the higher the rank, the better):

and for hashing Sparkle and Xoodyak are ranked the same:

Uno Nano performance

For AEAD on Uno Nano Every [6], the benchmark is against AES GCM. We can see in Table 4, that Sparkle is 4.7 times faster than AES GCM for 128-bit data sizes, and Xoodyak comes in second with a 3.3 times improvement over AES GCM. When it comes to 8-bit data sizes, TinyJambu is actually the fastest, but where Sparkle and Xoodyak still perform well. PHOTON-Beetle, Grain128 and ISAP do not do well and only slightly improve on AES GCM. In fact, Grain128 and ISAP are actually slower than AES GCM.

Table 4: Uno Nano for AEAD against AES GCM and showing cycles [6] (showing the fastest of the methods)

And so for AEAD (performance):

1. Sparkle.
2. Xoodyak.
3. ASCON.
4. GIFT-COFB.
5. Elephant.
6. Romulus.
7. Tiny Jambu.
8. PHOTON-Beetle.
9. Grain128.
10. ISAP.

For hashing on an Uno Nano Every [6], Table 5 shows a similar performance level as the ARM Cortex M3 assessment. In this case, the benchmark hash is SHA-256, and we can see that it takes Sparkle twice as many cycles for a 128-bit hash and 2.9 times for Xoodyak. PHOTON-Beetle is way behind with a 128-bit hash and which is 17.4 times slower than SHA-256. That said, though, PHOTON-Beetle could be more focused on reducing power consumption rather than speed. GIMLI and SKINNY are included to show a comparison with well-designed methods in lightweight hashing. It can be seen that every method beats SKINNY, but only Sparkle and Xoodyak beat GIMLI.

Table 5: Uno Nano for hashing against SHA-256 and showing cycles [6] (showing fastest of the method for hashing)

And so for hashing (performance):

1. Sparkle.
2. Xoodyak.
3. ASCON.
4. PHOTON-Beetle.

Evaluation (Energy)

The key evaluators for the best method include their overall security; their general performance (as measured by the number of cycles taken to conduct an operation); the gate count; the chip size; and energy consumption. When it comes to energy consumption and hardware requirements, Elsadek et al [4] performed a review of the 10 contenders.

In terms of the size of each implementation, we see that TinyJambu and Grain128 have the smallest footprint, while Sparkle has by far the largest footprint. In terms of gate equivalent (GE), TinyJambu requires 3,600 GEs, while Sparkle needs 39,500:

The energy efficiency is then defined as:

Energy efficiency(bit/J)=Throughput(bits/sec)/Power(J/sec)

and where we measure the bits per Joule. Figure 2 outlines the average energy efficiency, and where TinyJambu, Xoodyak and ASCON do well for energy, while ISAP, Elephant and Grain128-AEAD were poorest.

Figure 2: Average energy efficiency [4]

Methods

ASCON can be used to create AEAD (Authenticated Encryption with Additional Data), a MAC (Message Authentication Code), a PRF (Pseudo-Random Function), and a fixed or variable length hash. The main variants for symmetric keys using AEAD are:

  • Ascon-128. This provides 128-bit security and uses a 128-bit and a 128-bit nonce (16 bytes).
  • Ascon-128a. This is the “a” variant, which provides 160-bit (20 bytes) security and uses a 128-bit and a 128-bit nonce (16 bytes).
  • Ascon-80pq. This is a variant that has a different rate for the sponge function.

ASCON uses a sponge function to produce both AEAD and hashes. The basic parameters of the sponge function are:

  • rate: block size in bytes (8 for Ascon-128 and Ascon-80pq; 16 for Ascon-128a).
  • a: number of initialization/finalization rounds for permutation (by default 12).
  • b: number of intermediate rounds for permutation (6 for Ascon-128 and Ascon-80pq, and 8 for Ascon-128a).

The main variants we have for hashing are:

  • Ascon-Hash. This defines the Ascon hashing method for a fixed-length output hash (32 bytes — 256 bits). We also get “Ascon-Hasha”, which has the “a” modification.
  • Ascon-Xof. This defines the Ascon hashing method for a variable length output hash. We also have the “a” modification (“Ascon-Xofa”).

For MAC (Message Authentication Code)/PRF (Pseudo-Random Function), we create a hash that is signed with a secret key (for MAC) or a random value based on a defined salt value and key. For this, we get:

  • Ascon-Mac. This creates a tag that is 16 bytes long. We also have “Ascon-Maca”, which is the “a” modification.
  • Ascon-Prf. This creates a tag which has an arbitrary length. We also have “Ascon-Prfa”, which is the “a” modification.
  • Ascon-PrfShort. This is used for a small number of bit inputs.

Coding

In the following, we will implement the AEAD function of Ascon-128, Ascon-128a and Ascon-80pq. An outline of the Python code is [1][here]:

#!/usr/bin/env python3

import sys

# Code from: https://github.com/meichlseder/pyascon/blob/master/ascon.py

"""
Implementation of Ascon v1.2, an authenticated cipher and hash function
http://ascon.iaik.tugraz.at/
"""


debug = False
debugpermutation = False

# === Ascon AEAD encryption and decryption ===

def ascon_encrypt(key, nonce, associateddata, plaintext, variant="Ascon-128"):
"""
Ascon encryption.
key: a bytes object of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; 128-bit security)
nonce: a bytes object of size 16 (must not repeat for the same key!)
associateddata: a bytes object of arbitrary length
plaintext: a bytes object of arbitrary length
variant: "Ascon-128", "Ascon-128a", or "Ascon-80pq" (specifies key size, rate and number of rounds)
returns a bytes object of length len(plaintext)+16 containing the ciphertext and tag
"""

assert variant in ["Ascon-128", "Ascon-128a", "Ascon-80pq"]
if variant in ["Ascon-128", "Ascon-128a"]: assert(len(key) == 16 and len(nonce) == 16)
if variant == "Ascon-80pq": assert(len(key) == 20 and len(nonce) == 16)
S = [0, 0, 0, 0, 0]
k = len(key) * 8 # bits
a = 12 # rounds
b = 8 if variant == "Ascon-128a" else 6 # rounds
rate = 16 if variant == "Ascon-128a" else 8 # bytes

ascon_initialize(S, k, rate, a, b, key, nonce)
ascon_process_associated_data(S, b, rate, associateddata)
ciphertext = ascon_process_plaintext(S, b, rate, plaintext)
tag = ascon_finalize(S, rate, a, key)
return ciphertext + tag


def ascon_decrypt(key, nonce, associateddata, ciphertext, variant="Ascon-128"):
"""
Ascon decryption.
key: a bytes object of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; 128-bit security)
nonce: a bytes object of size 16 (must not repeat for the same key!)
associateddata: a bytes object of arbitrary length
ciphertext: a bytes object of arbitrary length (also contains tag)
variant: "Ascon-128", "Ascon-128a", or "Ascon-80pq" (specifies key size, rate and number of rounds)
returns a bytes object containing the plaintext or None if verification fails
"""

assert variant in ["Ascon-128", "Ascon-128a", "Ascon-80pq"]
if variant in ["Ascon-128", "Ascon-128a"]: assert(len(key) == 16 and len(nonce) == 16 and len(ciphertext) >= 16)
if variant == "Ascon-80pq": assert(len(key) == 20 and len(nonce) == 16 and len(ciphertext) >= 16)
S = [0, 0, 0, 0, 0]
k = len(key) * 8 # bits
a = 12 # rounds
b = 8 if variant == "Ascon-128a" else 6 # rounds
rate = 16 if variant == "Ascon-128a" else 8 # bytes

ascon_initialize(S, k, rate, a, b, key, nonce)
ascon_process_associated_data(S, b, rate, associateddata)
plaintext = ascon_process_ciphertext(S, b, rate, ciphertext[:-16])
tag = ascon_finalize(S, rate, a, key)
if tag == ciphertext[-16:]:
return plaintext
else:
return None


# === Ascon AEAD building blocks ===

def ascon_initialize(S, k, rate, a, b, key, nonce):
"""
Ascon initialization phase - internal helper function.
S: Ascon state, a list of 5 64-bit integers
k: key size in bits
rate: block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a)
a: number of initialization/finalization rounds for permutation
b: number of intermediate rounds for permutation
key: a bytes object of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; 128-bit security)
nonce: a bytes object of size 16
returns nothing, updates S
"""

iv_zero_key_nonce = to_bytes([k, rate * 8, a, b] + (20-len(key))*[0]) + key + nonce
S[0], S[1], S[2], S[3], S[4] = bytes_to_state(iv_zero_key_nonce)
if debug: printstate(S, "initial value:")

ascon_permutation(S, a)

zero_key = bytes_to_state(zero_bytes(40-len(key)) + key)
S[0] ^= zero_key[0]
S[1] ^= zero_key[1]
S[2] ^= zero_key[2]
S[3] ^= zero_key[3]
S[4] ^= zero_key[4]
if debug: printstate(S, "initialization:")


def ascon_process_associated_data(S, b, rate, associateddata):
"""
Ascon associated data processing phase - internal helper function.
S: Ascon state, a list of 5 64-bit integers
b: number of intermediate rounds for permutation
rate: block size in bytes (8 for Ascon-128, 16 for Ascon-128a)
associateddata: a bytes object of arbitrary length
returns nothing, updates S
"""

if len(associateddata) > 0:
a_zeros = rate - (len(associateddata) % rate) - 1
a_padding = to_bytes([0x80] + [0 for i in range(a_zeros)])
a_padded = associateddata + a_padding

for block in range(0, len(a_padded), rate):
S[0] ^= bytes_to_int(a_padded[block:block+8])
if rate == 16:
S[1] ^= bytes_to_int(a_padded[block+8:block+16])

ascon_permutation(S, b)

S[4] ^= 1
if debug: printstate(S, "process associated data:")


def ascon_process_plaintext(S, b, rate, plaintext):
"""
Ascon plaintext processing phase (during encryption) - internal helper function.
S: Ascon state, a list of 5 64-bit integers
b: number of intermediate rounds for permutation
rate: block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a)
plaintext: a bytes object of arbitrary length
returns the ciphertext (without tag), updates S
"""

p_lastlen = len(plaintext) % rate
p_padding = to_bytes([0x80] + (rate-p_lastlen-1)*[0x00])
p_padded = plaintext + p_padding

# first t-1 blocks
ciphertext = to_bytes([])
for block in range(0, len(p_padded) - rate, rate):
if rate == 8:
S[0] ^= bytes_to_int(p_padded[block:block+8])
ciphertext += int_to_bytes(S[0], 8)
elif rate == 16:
S[0] ^= bytes_to_int(p_padded[block:block+8])
S[1] ^= bytes_to_int(p_padded[block+8:block+16])
ciphertext += (int_to_bytes(S[0], 8) + int_to_bytes(S[1], 8))

ascon_permutation(S, b)

# last block t
block = len(p_padded) - rate
if rate == 8:
S[0] ^= bytes_to_int(p_padded[block:block+8])
ciphertext += int_to_bytes(S[0], 8)[:p_lastlen]
elif rate == 16:
S[0] ^= bytes_to_int(p_padded[block:block+8])
S[1] ^= bytes_to_int(p_padded[block+8:block+16])
ciphertext += (int_to_bytes(S[0], 8)[:min(8,p_lastlen)] + int_to_bytes(S[1], 8)[:max(0,p_lastlen-8)])
if debug: printstate(S, "process plaintext:")
return ciphertext


def ascon_process_ciphertext(S, b, rate, ciphertext):
"""
Ascon ciphertext processing phase (during decryption) - internal helper function.
S: Ascon state, a list of 5 64-bit integers
b: number of intermediate rounds for permutation
rate: block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a)
ciphertext: a bytes object of arbitrary length
returns the plaintext, updates S
"""

c_lastlen = len(ciphertext) % rate
c_padded = ciphertext + zero_bytes(rate - c_lastlen)

# first t-1 blocks
plaintext = to_bytes([])
for block in range(0, len(c_padded) - rate, rate):
if rate == 8:
Ci = bytes_to_int(c_padded[block:block+8])
plaintext += int_to_bytes(S[0] ^ Ci, 8)
S[0] = Ci
elif rate == 16:
Ci = (bytes_to_int(c_padded[block:block+8]), bytes_to_int(c_padded[block+8:block+16]))
plaintext += (int_to_bytes(S[0] ^ Ci[0], 8) + int_to_bytes(S[1] ^ Ci[1], 8))
S[0] = Ci[0]
S[1] = Ci[1]

ascon_permutation(S, b)

# last block t
block = len(c_padded) - rate
if rate == 8:
c_padding1 = (0x80 << (rate-c_lastlen-1)*8)
c_mask = (0xFFFFFFFFFFFFFFFF >> (c_lastlen*8))
Ci = bytes_to_int(c_padded[block:block+8])
plaintext += int_to_bytes(Ci ^ S[0], 8)[:c_lastlen]
S[0] = Ci ^ (S[0] & c_mask) ^ c_padding1
elif rate == 16:
c_lastlen_word = c_lastlen % 8
c_padding1 = (0x80 << (8-c_lastlen_word-1)*8)
c_mask = (0xFFFFFFFFFFFFFFFF >> (c_lastlen_word*8))
Ci = (bytes_to_int(c_padded[block:block+8]), bytes_to_int(c_padded[block+8:block+16]))
plaintext += (int_to_bytes(S[0] ^ Ci[0], 8) + int_to_bytes(S[1] ^ Ci[1], 8))[:c_lastlen]
if c_lastlen < 8:
S[0] = Ci[0] ^ (S[0] & c_mask) ^ c_padding1
else:
S[0] = Ci[0]
S[1] = Ci[1] ^ (S[1] & c_mask) ^ c_padding1
if debug: printstate(S, "process ciphertext:")
return plaintext


def ascon_finalize(S, rate, a, key):
"""
Ascon finalization phase - internal helper function.
S: Ascon state, a list of 5 64-bit integers
rate: block size in bytes (8 for Ascon-128, Ascon-80pq; 16 for Ascon-128a)
a: number of initialization/finalization rounds for permutation
key: a bytes object of size 16 (for Ascon-128, Ascon-128a; 128-bit security) or 20 (for Ascon-80pq; 128-bit security)
returns the tag, updates S
"""

assert(len(key) in [16,20])
S[rate//8+0] ^= bytes_to_int(key[0:8])
S[rate//8+1] ^= bytes_to_int(key[8:16])
S[rate//8+2] ^= bytes_to_int(key[16:] + zero_bytes(24-len(key)))

ascon_permutation(S, a)

S[3] ^= bytes_to_int(key[-16:-8])
S[4] ^= bytes_to_int(key[-8:])
tag = int_to_bytes(S[3], 8) + int_to_bytes(S[4], 8)
if debug: printstate(S, "finalization:")
return tag


# === Ascon permutation ===

def ascon_permutation(S, rounds=1):
"""
Ascon core permutation for the sponge construction - internal helper function.
S: Ascon state, a list of 5 64-bit integers
rounds: number of rounds to perform
returns nothing, updates S
"""

assert(rounds <= 12)
if debugpermutation: printwords(S, "permutation input:")
for r in range(12-rounds, 12):
# --- add round constants ---
S[2] ^= (0xf0 - r*0x10 + r*0x1)
if debugpermutation: printwords(S, "round constant addition:")
# --- substitution layer ---
S[0] ^= S[4]
S[4] ^= S[3]
S[2] ^= S[1]
T = [(S[i] ^ 0xFFFFFFFFFFFFFFFF) & S[(i+1)%5] for i in range(5)]
for i in range(5):
S[i] ^= T[(i+1)%5]
S[1] ^= S[0]
S[0] ^= S[4]
S[3] ^= S[2]
S[2] ^= 0XFFFFFFFFFFFFFFFF
if debugpermutation: printwords(S, "substitution layer:")
# --- linear diffusion layer ---
S[0] ^= rotr(S[0], 19) ^ rotr(S[0], 28)
S[1] ^= rotr(S[1], 61) ^ rotr(S[1], 39)
S[2] ^= rotr(S[2], 1) ^ rotr(S[2], 6)
S[3] ^= rotr(S[3], 10) ^ rotr(S[3], 17)
S[4] ^= rotr(S[4], 7) ^ rotr(S[4], 41)
if debugpermutation: printwords(S, "linear diffusion layer:")


# === helper functions ===

def get_random_bytes(num):
import os
return to_bytes(os.urandom(num))

def zero_bytes(n):
return n * b"\x00"

def to_bytes(l): # where l is a list or bytearray or bytes
return bytes(bytearray(l))

def bytes_to_int(bytes):
return sum([bi << ((len(bytes) - 1 - i)*8) for i, bi in enumerate(to_bytes(bytes))])

def bytes_to_state(bytes):
return [bytes_to_int(bytes[8*w:8*(w+1)]) for w in range(5)]

def int_to_bytes(integer, nbytes):
return to_bytes([(integer >> ((nbytes - 1 - i) * 8)) % 256 for i in range(nbytes)])

def rotr(val, r):
return (val >> r) | ((val & (1<<r)-1) << (64-r))




ad = b"ASCON"
plaintext = b"ascon"


if (len(sys.argv)>1):
plaintext=sys.argv[1].encode()

if (len(sys.argv)>2):
ad=(sys.argv[2].encode())

# ["Ascon-128", "Ascon-128a", "Ascon-80pq"]
variant = "Ascon-128"

if (len(sys.argv)>3):
variant=(sys.argv[3])

keysize = 20 if variant == "Ascon-80pq" else 16

key = get_random_bytes(keysize) # zero_bytes(keysize)
nonce = get_random_bytes(16) # zero_bytes(16)

ct = ascon_encrypt(key, nonce, ad, plaintext, variant)
pt= ascon_decrypt(key, nonce, ad, ct, variant)

print ("== ASCON AEAD ==")
print ("Plaintext: ",plaintext.decode())
print ("Additional data: ",ad.decode())
print ("Type: ",variant)

print ("\nKey: ",bytes(key).hex())
print ("Nonce: ",bytes(nonce).hex())
print ("Key size: ",keysize)

print ("\nCipher text: ",bytes(ct).hex())
print ("Plain text: ",pt.decode())

A sample run is:

== ASCON AEAD ==
Plaintext: abc
Additional data: abc
Type: Ascon-128

Key: 2f8b71dedf7979765233fa9a2fc64a6f
Nonce: 22f88d186806d9bbdee0537a8e07606a
Key size: 16

Cipher text: e48f418be0c9d779016b10f02d0992e42bdb37
Plain text: abc

The code is here:

https://asecuritysite.com/light/ascon01

References

[1] ASCON GitHub [here].

[2] Dobraunig, C., Eichlseder, M., Mendel, F., & Schläffer, M. (2016). Ascon v1. 2. Submission to the CAESAR Competition.

[3] Madushan, H., Salam, I., & Alawatugoda, J. (2022). A Review of the NIST Lightweight Cryptography Finalists and Their Fault Analyses. Electronics, 11(24), 4199.

[4] Elsadek, I., Aftabjahani, S., Gardner, D., MacLean, E., Wallrabenstein, J. R., & Tawfik, E. Y. (2022, May). Hardware and Energy Efficiency Evaluation of NIST Lightweight Cryptography Standardization Finalists. In 2022 IEEE International Symposium on Circuits and Systems (ISCAS) (pp. 133–137). IEEE.

[5] https://rweather.github.io/lightweight-crypto/performance.html

[6] https://github.com/usnistgov/Lightweight-Cryptography-Benchmarking/blob/main/benchmarks/results_nano_every_hash_all.csv