x.crypto.ascon: improve single-shot functions of ascon hashing variant; add benchmark (#25282)

This commit is contained in:
blackshirt 2025-09-11 15:04:21 +07:00 committed by GitHub
parent f16452d3a6
commit a10c59704b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 366 additions and 27 deletions

View file

@ -7,7 +7,7 @@ thats provides Authenticated Encryption, Hash, and Extendable Output Functions.
See the [NIST.SP.800-232 Standard](https://doi.org/10.6028/NIST.SP.800-232) for more detail. See the [NIST.SP.800-232 Standard](https://doi.org/10.6028/NIST.SP.800-232) for more detail.
This module mostly implements all the features availables on the document. This module mostly implements all the features availables on the document.
Its currently implements: It currently implements:
- `Ascon-Hash256`, Ascon-based hashing implementation that produces 256-bits output. - `Ascon-Hash256`, Ascon-based hashing implementation that produces 256-bits output.
- `Ascon-XOF128`, Ascon-based eXtendable Output Function (XOF) where the output size of - `Ascon-XOF128`, Ascon-based eXtendable Output Function (XOF) where the output size of
the hash of the message can be selected by the user. the hash of the message can be selected by the user.

View file

@ -2,8 +2,9 @@
// Use of this source code is governed by an MIT license // Use of this source code is governed by an MIT license
// that can be found in the LICENSE file. // that can be found in the LICENSE file.
// //
module ascon
import encoding.hex import encoding.hex
import x.crypto.ascon
// This test materials was taken and adapted into v from references implementation of Ascon-aead128 // This test materials was taken and adapted into v from references implementation of Ascon-aead128
// especially for the known answer test data, but, its not all fully-taken, just randomly choosen item. // especially for the known answer test data, but, its not all fully-taken, just randomly choosen item.
@ -26,14 +27,14 @@ fn test_ascon_aead128_enc_dec() ! {
ad := hex.decode(item.ad)! ad := hex.decode(item.ad)!
ct := hex.decode(item.ct)! ct := hex.decode(item.ct)!
out := ascon.encrypt(key, nonce, ad, pt)! out := encrypt(key, nonce, ad, pt)!
assert out == ct assert out == ct
msg := ascon.decrypt(key, nonce, ad, ct)! msg := decrypt(key, nonce, ad, ct)!
assert msg == pt assert msg == pt
// Work with object-based Cipher // Work with object-based Cipher
mut c := ascon.new_aead128(key)! mut c := new_aead128(key)!
// Lets encrypt the message // Lets encrypt the message
exp_ct := c.encrypt(msg, nonce, ad)! exp_ct := c.encrypt(msg, nonce, ad)!
assert exp_ct == ct assert exp_ct == ct

View file

@ -0,0 +1,75 @@
// This benchmark is for Ascon-AEAD128 in `x.crypto.ascon` compared to
// already stocked `x.crypto.cacha20poly1305 for AEAD functionalities.
//
// Here is output in my tests, first item was `x.crypto.ascon` and the later
// for `x.crypto.chacha20poly1305` on encryption or decryption part.
//
// Encryption..
// -----------
// Iterations: 10000 Total Duration: 26.008ms ns/op: 2600 B/op: 16 allocs/op: 17
// Iterations: 10000 Total Duration: 158.865ms ns/op: 15886 B/op: 16 allocs/op: 16
//
// Decryption..
// -----------
// Iterations: 10000 Total Duration: 29.091ms ns/op: 2909 B/op: 6 allocs/op: 8
// Iterations: 10000 Total Duration: 158.373ms ns/op: 15837 B/op: 8 allocs/op: 12
//
import encoding.hex
import x.benchmark
import x.crypto.ascon
import x.crypto.chacha20poly1305
// randomly generated key and nonce, 16-bytes of ascon key and 32-bytes of chacha20poly1305 key.
const key_ascon = hex.decode('7857bfb462c654d1d1b02971be021235')!
const key_cpoly = hex.decode('9d9603f4fc460e273b80795ea50eab5873c04f589226c7d591b5336feb32fcba')!
// 16-bytes ascon-nonce
const ascon_nonce = hex.decode('8b521028fb54591472d8d8ee14430835')!
// 12-bytes chacha20poly1305 nonce
const cpoly_nonce = hex.decode('9a3c83e4236ea9a2c4e482da')!
const ad = 'Ascon-AEAD128 additional data'.bytes()
const msg = 'Ascon-AEAD128 benchmarking message'.bytes()
// expected ciphertext for aead128 := 4b21a18cbca65b11aaf73dc74241c89bfcec96a4c8973ae696a938e0a591e846c4eb7b2906664f2318c0fd6ec1c56424aa9b
const ciphertext_aead128 = hex.decode('4b21a18cbca65b11aaf73dc74241c89bfcec96a4c8973ae696a938e0a591e846c4eb7b2906664f2318c0fd6ec1c56424aa9b')!
fn bench_ascon_aead128_encrypt() ! {
_ := ascon.encrypt(key_ascon, ascon_nonce, ad, msg)!
}
fn bench_ascon_aead128_decrypt() ! {
_ := ascon.decrypt(key_ascon, ascon_nonce, ad, ciphertext_aead128)!
}
// expected ciphertext for chacha20poly1305
const ciphertext_chachapoly1305 = hex.decode('67dea3c65f0f326bcf587f024140a85d9535790d9b16129210a2289eda43bb9b62746450026fc1baf466bcb8a181843cd424')!
fn bench_chacha20poly1305_encrypt() ! {
_ := chacha20poly1305.encrypt(msg, key_cpoly, cpoly_nonce, ad)!
}
fn bench_chacha20poly1305_decrypt() ! {
_ := chacha20poly1305.decrypt(ciphertext_chachapoly1305, key_cpoly, cpoly_nonce, ad)!
}
fn main() {
cf := benchmark.BenchmarkDefaults{
n: 10000
}
println('Encryption..')
println('-----------')
mut b0 := benchmark.setup(bench_ascon_aead128_encrypt, cf)!
b0.run()
mut b1 := benchmark.setup(bench_chacha20poly1305_encrypt, cf)!
b1.run()
println('')
println('Decryption..')
println('-----------')
mut b2 := benchmark.setup(bench_ascon_aead128_decrypt, cf)!
b2.run()
mut b3 := benchmark.setup(bench_chacha20poly1305_decrypt, cf)!
b3.run()
}

View file

@ -0,0 +1,197 @@
// Ascon-Hash256 (and Ascon-XOF128) benchmark compared to builtin
// crypto.sha256 (for sum256) and sha3.shake256 (for xof outputing 256-bits)
//
// This benchmark code was adapted from argon2 benchmark by @fleximus, the creator argon2 module.
// Credit tributed to @fleximus
// See https://gist.github.com/fleximus/db5b867a9a37da46340db61bdac6e696
//
// Output
// ======
// Sum and Xof 256-bits output performance comparison
// ============================================================
// Iterations per test: 10000
// --------------------------------------------------------------------------------------------------
// Data Size | Ascon256 | Sha256 | Ratio 256 || AsconXof128 | Shake256 | Ratio (Xof) |
// --------------------------------------------------------------------------------------------------
// 4 B | 24.00ms | 33.00ms | 0.73x || 24.00ms | 208.00ms | 0.12x |
// 6 B | 23.00ms | 53.00ms | 0.45x || 25.00ms | 287.00ms | 0.08x |
// 8 B | 35.00ms | 37.00ms | 0.95x || 26.00ms | 202.00ms | 0.18x |
// 16 B | 30.00ms | 37.00ms | 0.83x || 30.00ms | 205.00ms | 0.15x |
// 64 B | 55.00ms | 61.00ms | 0.89x || 53.00ms | 241.00ms | 0.23x |
// 75 B | 61.00ms | 57.00ms | 1.07x || 58.00ms | 182.00ms | 0.34x |
// 256 B | 154.00ms | 123.00ms | 1.25x || 144.00ms | 398.00ms | 0.39x |
// 512 B | 273.00ms | 216.00ms | 1.26x || 265.00ms | 779.00ms | 0.35x |
// 1025 B | 610.00ms | 401.00ms | 1.52x || 509.00ms | 1.37s | 0.45x |
// --------------------------------------------------------------------------------------------------
// Total | 1.27s | 1.02s | 1.24x || 1.14s | 3.87s | 0.294x|
// --------------------------------------------------------------------------------------------------
//
// Per-operation averages:
// Ascon256: 14108 ns per hash
// Sha256: 11360 ns per hash
// AsconXof128: 12648 ns per hash
// Shake256: 43036 ns per hash
//
module main
import time
import crypto.sha3
import crypto.sha256
import x.crypto.ascon
const benchmark_iterations = 10000
// We include more small size because, Ascon-Hash256 working with more smaller block size.
const test_data_sizes = [
4, // below Ascon-Hash256 block size
6, // Still below Ascon-Hash256 block size
8, // align with Ascon-Hash256 block size
16, // Small data
64, // Medium data
75, // above 64-bytes block
256, // Large data
512,
1025,
]
fn generate_test_data(size int) []u8 {
mut data := []u8{len: size}
for i in 0 .. size {
data[i] = u8(i % 256)
}
return data
}
fn benchmark_ascon_sha256(data []u8, iterations int) time.Duration {
start := time.now()
for _ in 0 .. iterations {
_ := ascon.sum256(data)
}
return time.since(start)
}
fn benchmark_sha256_sum256(data []u8, iterations int) time.Duration {
start := time.now()
for _ in 0 .. iterations {
_ := sha256.sum256(data)
}
return time.since(start)
}
// for eXtendable output functions (XOF)
fn benchmark_ascon_xof128_32(data []u8, iterations int) time.Duration {
start := time.now()
for _ in 0 .. iterations {
_ := ascon.xof128(data, 32) or { panic(err) }
}
return time.since(start)
}
fn benchmark_sha3_shake256(data []u8, iterations int) time.Duration {
start := time.now()
for _ in 0 .. iterations {
_ := sha3.shake256(data, 32)
}
return time.since(start)
}
fn format_duration(d time.Duration) string {
if d.microseconds() < 1000 {
return '${d.microseconds():6}μs'
} else if d.milliseconds() < 1000 {
return '${f64(d.milliseconds()):6.2f}ms'
} else {
return '${f64(d.seconds()):6.2f}s'
}
}
const data_title = 'Data Size'
const ascon_sum256_title = 'Ascon256'
const sha256_title = 'Sha256'
const ascon_xof128_title = 'AsconXof128'
const sha3_shake256_title = 'Shake256'
const ratio_ascon256_w_sha256 = 'Ratio 256'
const ratio_asconxof128_w_shake256 = 'Ratio (Xof)'
fn main() {
println('')
println('Sum and Xof 256-bits output performance comparison')
println('============================================================')
println('Iterations per test: ${benchmark_iterations}')
println('${'-'.repeat(98)}')
println('${data_title:12} | ${ascon_sum256_title:10} | ${sha256_title:10} | ${ratio_ascon256_w_sha256:12} || ${ascon_xof128_title:10} | ${sha3_shake256_title:10} | ${ratio_asconxof128_w_shake256:12} |')
println('${'-'.repeat(98)}')
mut total_ascon256 := time.Duration(0)
mut total_sha256 := time.Duration(0)
mut total_shake256 := time.Duration(0)
mut total_asconxof128 := time.Duration(0)
for size in test_data_sizes {
test_data := generate_test_data(size)
// Warm up
_ := ascon.sum256(test_data)
_ := sha256.sum256(test_data)
_ := ascon.xof128(test_data, 32)!
_ := sha3.shake256(test_data, 32)
// Benchmark Ascon-HASH256
ascon256_time := benchmark_ascon_sha256(test_data, benchmark_iterations)
// Benchmark Sha256 implementation
sha256_time := benchmark_sha256_sum256(test_data, benchmark_iterations)
// Benchmark Sha3 shake256 implementation
shake256_time := benchmark_sha3_shake256(test_data, benchmark_iterations)
// Benchmark AsconXof128 256-bits output
asconxof128_time := benchmark_ascon_xof128_32(test_data, benchmark_iterations)
// Calculate ratio ascon256 / sha256
ratio_ascon256_sha256 := f64(ascon256_time.nanoseconds()) / f64(sha256_time.nanoseconds())
// Calculate ratio asconxof128 / shake256
ratio_asconxof128_shake256 := f64(asconxof128_time.nanoseconds()) / f64(shake256_time.nanoseconds())
ascon256_str := format_duration(ascon256_time)
sha256_str := format_duration(sha256_time)
asconxof128_str := format_duration(asconxof128_time)
shake256_str := format_duration(shake256_time)
ratio_ascon256_sha256_str := '${ratio_ascon256_sha256:6.2f}x'
ratio_asconxof128_shake256_str := '${ratio_asconxof128_shake256:6.2f}x'
println('${size:10} B | ${ascon256_str:10} | ${sha256_str:10} | ${ratio_ascon256_sha256_str:12} || ${asconxof128_str:11} | ${shake256_str:10} | ${ratio_asconxof128_shake256_str:12} |')
total_ascon256 += ascon256_time
total_sha256 += sha256_time
total_asconxof128 += asconxof128_time
total_shake256 += shake256_time
}
println('${'-'.repeat(98)}')
// Overall performance comparison
overall_ascon256_w_sha256_ratio := f64(total_ascon256.nanoseconds()) / f64(total_sha256.nanoseconds())
overall_asconxof128_w_shake256_ratio := f64(total_asconxof128.nanoseconds()) / f64(total_shake256.nanoseconds())
total_title := 'Total'
println('${total_title:12} | ${format_duration(total_ascon256):10} | ${format_duration(total_sha256):10} | ${overall_ascon256_w_sha256_ratio:11.2f}x || ${format_duration(total_asconxof128):11} | ${format_duration(total_shake256):10} | ${overall_asconxof128_w_shake256_ratio:12.2f}x|')
println('${'-'.repeat(98)}')
println('')
println('Per-operation averages:')
avg_ascon256 := total_ascon256.nanoseconds() / (benchmark_iterations * test_data_sizes.len)
avg_sha256 := total_sha256.nanoseconds() / (benchmark_iterations * test_data_sizes.len)
avg_shake256 := total_shake256.nanoseconds() / (benchmark_iterations * test_data_sizes.len)
avg_asconxof128 := total_asconxof128.nanoseconds() / (benchmark_iterations * test_data_sizes.len)
println(' Ascon256:\t ${avg_ascon256:8} ns per hash')
println(' Sha256:\t ${avg_sha256:8} ns per hash')
println(' AsconXof128:\t ${avg_asconxof128:8} ns per hash')
println(' Shake256:\t ${avg_shake256:8} ns per hash')
println('')
}

View file

@ -0,0 +1,36 @@
import time
import x.crypto.ascon
// Before:
// Benchmarking ascon.sum256 ...
// Average ascon.sum256 time: 8 µs
// Benchmarking ascon.sum256 ...
// Average ascon.sum256 time: 6 µs
// For xof128 (32 bytes)
// Benchmarking ascon.xof128 ...
// Average ascon.xof128 time: 7 µs
// Benchmarking ascon.xof128 ...
// Average ascon.xof128 time: 6 µs
// For cxof128 32 bytes
// Benchmarking ascon.cxof128 ...
// Average ascon.cxof128 time: 9 µs
// Benchmarking ascon.sum256 ...
// Average ascon.cxof128 time: 7 µs
//
fn main() {
iterations := 1000
msg := [u8(0xff)].repeat(100)
println('Benchmarking ascon.sum256 ...')
mut total_sum_time := i64(0)
for _ in 0 .. iterations {
sw := time.new_stopwatch()
_ := ascon.sum256(msg)
elapsed := sw.elapsed().microseconds()
total_sum_time += elapsed
}
avg_sum_time := total_sum_time / iterations
println('Average ascon.sum256 time: ${avg_sum_time} µs')
}

View file

@ -133,3 +133,39 @@ fn (mut d Digest) squeeze(mut dst []u8) int {
return pos return pos
} }
@[direct_array_access; inline]
fn ascon_generic_hash(mut s State, msg_ []u8, size int) []u8 {
// Assumed state was correctly initialized
// Absorbing the message
mut msg := msg_.clone()
for msg.len >= block_size {
s.e0 ^= binary.little_endian_u64(msg[0..block_size])
unsafe {
msg = msg[block_size..]
}
ascon_pnr(mut s, ascon_prnd_12)
}
// Absorb the last partial message block
s.e0 ^= load_bytes(msg, msg.len)
s.e0 ^= pad(msg.len)
// Squeezing phase
//
// The squeezing phase begins after msg is absorbed with an
// permutation 𝐴𝑠𝑐𝑜𝑛-𝑝[12] to the state:
ascon_pnr(mut s, ascon_prnd_12)
mut out := []u8{len: size}
mut pos := 0
mut clen := out.len
for clen >= block_size {
binary.little_endian_put_u64(mut out[pos..pos + 8], s.e0)
ascon_pnr(mut s, ascon_prnd_12)
pos += block_size
clen -= block_size
}
// final output, the resulting 256-bit digest is the concatenation of hash blocks
store_bytes(mut out[pos..], s.e0, clen)
return out
}

View file

@ -42,13 +42,13 @@ const hash256_initial_state = State{
} }
// sum256 creates an Ascon-Hash256 checksum for bytes on msg and produces a 256-bit hash. // sum256 creates an Ascon-Hash256 checksum for bytes on msg and produces a 256-bit hash.
pub fn sum256(msg []u8) []u8 { pub fn sum256(msg_ []u8) []u8 {
mut h := new_hash256() // This is single-shot function, so, no need to use Hash256 opaque that process
_ := h.write(msg) or { panic(err) } // message in streaming way. To reduce this overhead, use raw processing instead.
h.Digest.finish() //
mut dst := []u8{len: hash256_size} // Initialize state
_ := h.Digest.squeeze(mut dst) mut s := hash256_initial_state
return dst return ascon_generic_hash(mut s, msg_, hash256_size)
} }
// Hash256 is an opaque provides an implementation of Ascon-Hash256 from NIST.SP.800-232 standard. // Hash256 is an opaque provides an implementation of Ascon-Hash256 from NIST.SP.800-232 standard.

View file

@ -5,7 +5,6 @@
// Utility helpers used across the module // Utility helpers used across the module
module ascon module ascon
import math.bits
import encoding.binary import encoding.binary
// clear_bytes clears the bytes of x in n byte // clear_bytes clears the bytes of x in n byte

View file

@ -30,13 +30,11 @@ const xof128_initial_state = State{
// xof128 creates an Ascon-XOF128 checksum of msg with specified desired size of output. // xof128 creates an Ascon-XOF128 checksum of msg with specified desired size of output.
pub fn xof128(msg []u8, size int) ![]u8 { pub fn xof128(msg []u8, size int) ![]u8 {
mut x := new_xof128(size) if size > max_hash_size {
_ := x.write(msg)! return error('xof128: invalid size')
x.Digest.finish() }
mut out := []u8{len: size} mut s := xof128_initial_state
_ := x.Digest.squeeze(mut out) return ascon_generic_hash(mut s, msg, size)
x.reset()
return out
} }
// xof128_64 creates a 64-bytes of Ascon-XOF128 checksum of msg. // xof128_64 creates a 64-bytes of Ascon-XOF128 checksum of msg.
@ -170,13 +168,10 @@ const cxof128_initial_state = State{
// cxof128 creates an Ascon-CXOF128 checksum of msg with supplied size and custom string cs. // cxof128 creates an Ascon-CXOF128 checksum of msg with supplied size and custom string cs.
pub fn cxof128(msg []u8, size int, cs []u8) ![]u8 { pub fn cxof128(msg []u8, size int, cs []u8) ![]u8 {
mut cx := new_cxof128(size, cs)! // Initialize CXof128 state with precomputed-value and absorb the customization string
_ := cx.write(msg)! mut s := cxof128_initial_state
cx.Digest.finish() cxof128_absorb_custom_string(mut s, cs)
mut out := []u8{len: size} return ascon_generic_hash(mut s, msg, size)
_ := cx.Digest.squeeze(mut out)
cx.reset()
return out
} }
// cxof128_64 creates a 64-bytes of Ascon-CXOF128 checksum of msg with supplied custom string in cs. // cxof128_64 creates a 64-bytes of Ascon-CXOF128 checksum of msg with supplied custom string in cs.