4 releases
0.2.1 | May 8, 2024 |
---|---|
0.2.0 | Mar 27, 2024 |
0.2.0-rc.0 | Mar 21, 2024 |
0.1.0 | Mar 6, 2024 |
#1143 in Cryptography
1,264 downloads per month
Used in 7 crates
(via dusk-poseidon)
36KB
471 lines
SAFE - Sponge API for Field Elements
A generic API for sponge functions.
This is a minimal, no_std
, pure Rust implementation of a sponge function, based on SAFE (Sponge API for Field Elements), to be used in permutation-based symmetric primitives' design, such as hash functions, MACs, authenticated encryption schemes, PRNGs, and other.
The sponge is designed to be usable in zero-knowledge proving systems (ZKPs) as well as natively, operating on any type implementing the Default
and Copy
trait with a size of minimal 32 bytes.
Introduction
Sponge functions are the basis of permutation-based symmetric primitives’ design. They can be seen as a stateful object that can ingest input (“absorb”) and produce output (“squeeze”) at any time and in arbitrary order.
As its main features, this sponge API:
- Does not use any padding, thus not wasting an extra call to the sponge permutation in any circumstances
- Is independent of an underlying permutation and thus can be used with almost every design on the market (including Poseidon’s).
- Eliminates a number of misuse patterns by limiting the set of operations callable at sponge and by binding a protocol designer to a specific order of these operations.
- Is provably secure in the random permutation model in a number of settings, including the overlooked but frequently required cross-protocol security.
- Is among the first constructions to store the protocol’s metadata in the sponge inner part, provably losing no security
This sponge construction in itself does not support variable-length hashing, i.e. hashing where the length of data hashed is unknown in advance.
However, this behavior can be achieved by wrapping the sponge in a hasher, that only starts the sponge upon finalizing the hash, thus at a time when the length of the input is known (example implementation of this wrapper can be found in dusk-poseidon
).
Construction
The sponge constructed in this library is defined by:
- a state
[T; W]
with typeT: Default + Copy
and widthW
- a permutation function that permutes the state
- a capacity of
1
- a rate
R
withR = W - 1
- an input-output (IO) pattern that defines the sequence to ingest
len
items of input (absorb(len)
) and pruduce output (squeeze(len)
) (eg.[absorb(4), absorb(1), squeeze(3)]
) - a domain separator to distinguish between equivalent sponges with different usecases.
Note: With the capacity beeing one element of type T
we need to restrict T
to be at least 256 bits. It is the responsibility of the user to properly serialize input of different sizes into a type with at least 256 bits.
Abstract API
start
- Verify IO pattern:
- IO pattern has at least two calls.
- First call is to
absorb
. - Last call is to
squeeze
. - No call has a
len == 0
.
- Compute the tag using the IO pattern and a domain separator.
- Encode the IO pattern as a list of 32-bit words whose MSB is set to 1 for
absorb
and to 0 forsqueeze
, and the length is added to the lower bits. Any contiguous calls toabsorb
andsqueeze
will be aggregated, e.g. the above example of an IO pattern of[absorb(4), absorb(1), squeeze(3)]
will have the same encoding as[absorb(5), squeeze(3)]
:[0x8000_0005, 0x0000_0001]
. - Serialize the list of words into a byte string and append to it the domain separator: e.g. if the domain separator encoding is set to the two-byte sequence
0x4142
, then the example above would yield the string (with big-endian convention):0x80000005000000014142
. - Hash the byte string into the tag, an element of type
T
.
- Encode the IO pattern as a list of 32-bit words whose MSB is set to 1 for
- Initialize first element of the state to the tag and set the remaining elements to the default value of
T
. - Set both absorb and squeeze positions to zero.
- Set the IO count to zero.
finish
- If the IO count is equal to the length of the IO pattern, return the output vector, if not return an error.
- Erase the state and its variables
absorb(len, input)
- Check that the call to absorb matches the entry of in the IO pattern at the IO count, and check that the input yields sufficient elements (erase state and return error if not).
- For the first
len
elements ofinput
:- Call the permutation function if
pos_absorb == rate
and setpos_absorb = 0
. - Add the element to the state at
pos_absort + 1
(we skip the first element which is the capacity). - Increment
pos_absorb
by one.
- Call the permutation function if
- Increment the IO count.
- Set the
pos_squeeze
to the rate to force a call to the permutation function at the start of the next call tosqueeze
.
squeeze(len)
- Check that the call to absorb matches the entry of in the IO pattern at the IO count (erase state and return error if not).
len
times:- Call the permutation function if
pos_squeeze == rate
and setpos_squeeze = 0
- Append the element of the state at position
pos_squeeze + 1
(also here we skip the first element due to the capacity) to the output vector.
- Call the permutation function if
- Increment the IO count.
Note that we do not set the pos_absorb
to the rate as we do with the pos_squeeze
in the call to absorb
, this is because we may want the state to absorb at the same positions that have been squeezed.
Example
use dusk_bls12_381::BlsScalar;
use ff::Field;
use rand::rngs::StdRng;
use rand::SeedableRng;
use dusk_safe::{Error, Call, Safe, Sponge};
const W: usize = 7;
#[derive(Default, Debug, Clone, Copy, PartialEq)]
struct Rotate();
impl Safe<BlsScalar, W> for Rotate {
// Rotate every item one item to the left, first item becomes last.
// Note: This permutation is just an example and *should not* be used for a
// sponge construction for cryptographically safe hash functions.
fn permute(&mut self, state: &mut [BlsScalar; W]) {
let tmp = state[0];
for i in 1..W {
state[i - 1] = state[i];
}
state[W - 1] = tmp;
}
// Define the hasher used to generate the tag from the encoding of the
// io-pattern and domain-separator.
fn tag(&mut self, input: &[u8]) -> BlsScalar {
BlsScalar::hash_to_scalar(input)
}
fn add(&mut self, right: &BlsScalar, left: &BlsScalar) -> BlsScalar {
right + left
}
}
impl Rotate {
pub fn new() -> Self {
Self()
}
}
// pick a domain-separator
let domain_sep = 0;
// generate random input
let mut rng = StdRng::seed_from_u64(0x42424242);
let mut input = [BlsScalar::zero(); 8];
input.iter_mut().for_each(|s| *s = BlsScalar::random(&mut rng));
// build the io-pattern
let iopattern = vec![
Call::Absorb(6),
Call::Absorb(2),
Call::Squeeze(1),
Call::Squeeze(2),
];
// start the sponge
let mut sponge = Sponge::start(
Rotate::new(),
iopattern,
domain_sep,
)
.expect("io-pattern should be valid");
// absorb 6 elements
sponge.absorb(6, &input).expect("absorbing should not fail");
// absorb 2 elements
sponge.absorb(2, &input[6..]).expect("absorbing should not fail");
// squeeze 1 element
sponge.squeeze(1).expect("squeezing should not fail");
// squeeze 2 elements
sponge.squeeze(2).expect("squeezing should not fail");
// generate the hash output
let output1 = sponge.finish().expect("Finishing should not fail");
// Generate another hash output from the same input and aggregated IO pattern:
// build the io-pattern
let iopattern = vec![
Call::Absorb(8),
Call::Squeeze(3),
];
// start the sponge
let mut sponge = Sponge::start(
Rotate::new(),
iopattern,
domain_sep,
)
.expect("io-pattern should be valid");
// absorb 8 elements
sponge.absorb(8, &input).expect("absorbing should not fail");
// squeeze 3 elements
sponge.squeeze(3).expect("squeezing should not fail");
// generate the hash output
let output2 = sponge.finish().expect("Finishing should not fail");
assert_eq!(output1, output2);
Dependencies
~32KB