Skip to content

Commit 07758a9

Browse files
authored
Merge pull request #58 from chain/oleg/keccak-duplex
ProofTranscript: use stock `tiny-keccak` crate with duplex construction
2 parents a473b6e + 2aeebef commit 07758a9

File tree

2 files changed

+159
-86
lines changed

2 files changed

+159
-86
lines changed

Cargo.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ rand = "^0.4"
1919
byteorder = "1.2.1"
2020
serde = "1"
2121
serde_derive = "1"
22-
23-
[dependencies.tiny-keccak]
24-
git = 'https://github.com/chain/tiny-keccak.git'
25-
rev = '5925f81b3c351440283c3328e2345d982aac0f6e'
22+
tiny-keccak = "1.4.1"
2623

2724
[dev-dependencies]
2825
hex = "^0.3"

src/proof_transcript.rs

Lines changed: 158 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
77
use curve25519_dalek::scalar::Scalar;
88

9-
// XXX This uses experiment fork of tiny_keccak with half-duplex
10-
// support that we require in this implementation.
11-
// Review this after this PR is merged or updated:
12-
// https://github.com/debris/tiny-keccak/pull/24
139
use tiny_keccak::Keccak;
1410

1511
use byteorder::{ByteOrder, LittleEndian};
@@ -31,12 +27,13 @@ use byteorder::{ByteOrder, LittleEndian};
3127
/// ensure that their challenge values are bound to the *entire* proof
3228
/// transcript, not just the sub-protocol.
3329
///
34-
/// Internally, the `ProofTranscript` is supposed to use Keccak to
35-
/// absorb incoming messages and to squeeze challenges. The
36-
/// construction currently used is ad-hoc, has no security analysis,
37-
/// and is **only suitable for testing**.
30+
/// ## Warning
3831
///
39-
/// # Example
32+
/// Internally, the `ProofTranscript` uses ad-hoc duplex construction
33+
/// using Keccak that absorbs incoming messages and squeezes challenges.
34+
/// There is no security analysis yet, so it is **only suitable for testing**.
35+
///
36+
/// ## Example
4037
///
4138
/// ```
4239
/// # extern crate curve25519_dalek;
@@ -63,41 +60,49 @@ use byteorder::{ByteOrder, LittleEndian};
6360
#[derive(Clone)]
6461
pub struct ProofTranscript {
6562
hash: Keccak,
63+
rate: usize,
64+
write_offset: usize, // index within a block where the message must be absorbed
6665
}
6766

6867
impl ProofTranscript {
68+
// Implementation notes
69+
//
70+
// The implementation has 3 layers:
71+
// 1. commit/challenge - take input/output buffers <64K, responsible for disambiguation (length prefixing)
72+
// 2. write/read - take arbitrary buffers, responsible for splitting data over Keccak-f invocations and padding
73+
// 3. absorb/squeeze - actual sponge operations, outer layers ensure that absorb/squeeze do not perform unnecessary permutation
74+
//
75+
6976
/// Begin a new, empty proof transcript, using the given `label`
7077
/// for domain separation.
7178
pub fn new(label: &[u8]) -> Self {
7279
let mut ro = ProofTranscript {
80+
// NOTE: if you change the security parameter, also change the rate below
7381
hash: Keccak::new_shake128(),
82+
rate: 1600/8 - (2*128/8), // 168 bytes
83+
write_offset: 0,
7484
};
85+
// We will bump the version prefix each time we
86+
// make a breaking change in order to disambiguate
87+
// from the previous versions of this implementation.
88+
ro.commit(b"ProofTranscript v2");
7589
ro.commit(label);
76-
// makes sure the label is disambiguated from the rest of the messages.
77-
ro.pad();
7890
ro
7991
}
8092

81-
/// Commit a `message` to the proof transcript.
93+
/// Commit a `input` to the proof transcript.
8294
///
8395
/// # Note
8496
///
85-
/// Each message must be shorter than 64Kb (65536 bytes).
86-
pub fn commit(&mut self, message: &[u8]) {
87-
let len = message.len();
97+
/// Each input must be ≤ than the number of bytes
98+
/// returned by `max_commit_size()`.
99+
pub fn commit(&mut self, input: &[u8]) {
100+
let len = input.len();
88101
if len > (u16::max_value() as usize) {
89-
panic!("Committed message must be less than 64Kb!");
102+
panic!("Committed input must be less than 64Kb!");
90103
}
91-
92-
let mut len_prefix = [0u8; 2];
93-
LittleEndian::write_u16(&mut len_prefix, len as u16);
94-
95-
// XXX we rely on tiny_keccak experimental support for half-duplex mode and
96-
// correct switching from absorbing to squeezing and back.
97-
// Review this after this PR is merged or updated:
98-
// https://github.com/debris/tiny-keccak/pull/24
99-
self.hash.absorb(&len_prefix);
100-
self.hash.absorb(message);
104+
self.write_u16(len as u16);
105+
self.write(input);
101106
}
102107

103108
/// Commit a `u64` to the proof transcript.
@@ -112,30 +117,79 @@ impl ProofTranscript {
112117
}
113118

114119
/// Extracts an arbitrary-sized challenge byte slice.
115-
pub fn challenge_bytes(&mut self, mut output: &mut [u8]) {
116-
// XXX we rely on tiny_keccak experimental support for half-duplex mode and
117-
// correct switching from absorbing to squeezing and back.
118-
// Review this after this PR is merged or updated:
119-
// https://github.com/debris/tiny-keccak/pull/24
120-
self.hash.squeeze(&mut output);
120+
///
121+
/// Note: each call performs at least one Keccak permutation,
122+
/// so if you need to read multiple logical challenges at once,
123+
/// you should read a bigger slice in one call for minimal overhead.
124+
pub fn challenge_bytes(&mut self, output: &mut [u8]) {
125+
let len = output.len();
126+
if output.len() > (u16::max_value() as usize) {
127+
panic!("Challenge output must be less than 64Kb!");
128+
}
129+
// Note: when reading, length prefix N is followed by keccak padding 10*1
130+
// as if empty message was written; when writing, length prefix N is followed
131+
// by N bytes followed by keccak padding 10*1.
132+
// This creates ambiguity only for case N=0 (empty write or empty read),
133+
// which is safe as no information is actually transmitted in either direction.
134+
self.write_u16(len as u16);
135+
self.read(output);
121136
}
122137

123138
/// Extracts a challenge scalar.
124139
///
125140
/// This is a convenience method that extracts 64 bytes and
126141
/// reduces modulo the group order.
142+
///
143+
/// Note: each call performs at least one Keccak permutation,
144+
/// so if you need to read multiple challenge scalars,
145+
/// for the minimal overhead you should read `n*64` bytes
146+
/// using the `challenge_bytes` method and reduce each
147+
/// 64-byte window into a scalar yourself.
127148
pub fn challenge_scalar(&mut self) -> Scalar {
128149
let mut buf = [0u8; 64];
129150
self.challenge_bytes(&mut buf);
130151
Scalar::from_bytes_mod_order_wide(&buf)
131152
}
132153

133-
/// Pad separates the prior operations by padding
134-
/// the rest of the block with zeroes and applying a permutation.
135-
/// Each incoming message is length-prefixed anyway, but padding
136-
/// enables pre-computing and re-using the oracle state.
137-
fn pad(&mut self) {
154+
/// Internal API: writes 2-byte length prefix.
155+
fn write_u16(&mut self, integer: u16) {
156+
let mut intbuf = [0u8; 2];
157+
LittleEndian::write_u16(&mut intbuf, integer);
158+
self.write(&intbuf);
159+
}
160+
161+
/// Internal API: writes arbitrary byte slice
162+
/// splitting it over multiple duplex calls if needed.
163+
fn write(&mut self, mut input: &[u8]) {
164+
// `write` can be called multiple times.
165+
// If we overflow the available room (`rate-1` at most)
166+
// we absorb what we can, add Keccak padding, permute and continue.
167+
let mut room = self.rate - 1 - self.write_offset; // 1 byte is reserved for keccak padding 10*1.
168+
while input.len() > room {
169+
self.hash.absorb(&input[..room]);
170+
self.hash.pad();
171+
self.hash.fill_block();
172+
self.write_offset = 0;
173+
room = self.rate - 1;
174+
input = &input[room..];
175+
}
176+
self.hash.absorb(input);
177+
self.write_offset += input.len(); // could end up == (rate-1)
178+
}
179+
180+
/// Internal API: reads arbitrary byte slice
181+
/// splitting it over multiple duplex calls if needed.
182+
fn read(&mut self, output: &mut [u8]) {
183+
// Note 1: `read` is called only once after `write`, so we do
184+
// not need to support multiple reads from some offset.
185+
// We only need to complete the pending duplex call by padding and permuting.
186+
// Note 2: Since we hash in the total output buffer length,
187+
// we can use default squeeze behaviour w/o simulating blank inputs:
188+
// the resulting byte-stream will be disambiguated by that length prefix and keccak padding.
189+
self.hash.pad();
138190
self.hash.fill_block();
191+
self.write_offset = 0;
192+
self.hash.squeeze(output);
139193
}
140194
}
141195

@@ -149,61 +203,56 @@ mod tests {
149203
{
150204
let mut ro = ProofTranscript::new(b"TestProtocol");
151205
ro.commit(b"test");
152-
{
153-
let mut ch = [0u8; 32];
154-
ro.challenge_bytes(&mut ch);
155-
assert_eq!(
156-
hex::encode(ch),
157-
"9ba30a0e71e8632b55fbae92495440b6afb5d2646ba6b1bb419933d97e06b810"
158-
);
159-
ro.challenge_bytes(&mut ch);
160-
assert_eq!(
161-
hex::encode(ch),
162-
"add523844517c2320fc23ca72423b0ee072c6d076b05a6a7b6f46d8d2e322f94"
163-
);
164-
ro.challenge_bytes(&mut ch);
165-
assert_eq!(
166-
hex::encode(ch),
167-
"ac279a11cac0b1271d210592c552d719d82d67c82d7f86772ed7bc6618b0927c"
168-
);
169-
}
170-
171-
let mut ro = ProofTranscript::new(b"TestProtocol");
172-
ro.commit(b"test");
173-
{
174-
let mut ch = [0u8; 16];
175-
ro.challenge_bytes(&mut ch);
176-
assert_eq!(hex::encode(ch), "9ba30a0e71e8632b55fbae92495440b6");
177-
ro.challenge_bytes(&mut ch);
178-
assert_eq!(hex::encode(ch), "afb5d2646ba6b1bb419933d97e06b810");
179-
ro.challenge_bytes(&mut ch);
180-
assert_eq!(hex::encode(ch), "add523844517c2320fc23ca72423b0ee");
181-
}
206+
let mut ch = [0u8; 32];
207+
ro.challenge_bytes(&mut ch);
208+
assert_eq!(
209+
hex::encode(ch),
210+
"dec44a90f423c15874f7c0afaf62cc6cc0987bf428202cb3508fc7d7c9b5b30a"
211+
);
212+
ro.challenge_bytes(&mut ch);
213+
assert_eq!(
214+
hex::encode(ch),
215+
"f83256ef4964d71ec6f2dd2f79db70820c781bd8c3d1fceec7cbfa4965d4e530"
216+
);
217+
ro.challenge_bytes(&mut ch);
218+
assert_eq!(
219+
hex::encode(ch),
220+
"962f9ef161604c5dcbe3387773b293a0e27a6e6ee14ec5d9f6c78a45c36fc0e1"
221+
);
222+
}
182223

224+
{
183225
let mut ro = ProofTranscript::new(b"TestProtocol");
184226
ro.commit(b"test");
185-
{
186-
let mut ch = [0u8; 16];
187-
ro.challenge_bytes(&mut ch);
188-
assert_eq!(hex::encode(ch), "9ba30a0e71e8632b55fbae92495440b6");
189-
ro.commit(b"extra commitment");
190-
ro.challenge_bytes(&mut ch);
191-
assert_eq!(hex::encode(ch), "11536e09cedbb6b302d8c7cd96471be5");
192-
ro.challenge_bytes(&mut ch);
193-
assert_eq!(hex::encode(ch), "058c383da5f2e193a381aaa420b505b2");
194-
}
227+
let mut ch = [0u8; 32];
228+
ro.challenge_bytes(&mut ch);
229+
assert_eq!(
230+
hex::encode(ch),
231+
"dec44a90f423c15874f7c0afaf62cc6cc0987bf428202cb3508fc7d7c9b5b30a"
232+
);
233+
ro.commit(b"extra commitment");
234+
ro.challenge_bytes(&mut ch);
235+
assert_eq!(
236+
hex::encode(ch),
237+
"edf99afca6c21e4240f33826d60cb1b7c5d59d3dd363d2928bab7b8f94d24eaa"
238+
);
239+
ro.challenge_bytes(&mut ch);
240+
assert_eq!(
241+
hex::encode(ch),
242+
"a42eabb9d1c9c73dc8c33c0933cee8d5fabd48fcab686d9fcb8f1680841e4369"
243+
);
195244
}
196245
}
197246

198247
#[test]
199-
fn messages_are_disambiguated_by_length_prefix() {
248+
fn inputs_are_disambiguated_by_length_prefix() {
200249
{
201250
let mut ro = ProofTranscript::new(b"TestProtocol");
202251
ro.commit(b"msg1msg2");
203252
{
204253
let mut ch = [0u8; 8];
205254
ro.challenge_bytes(&mut ch);
206-
assert_eq!(hex::encode(ch), "1ad843ea2bf7f8b6");
255+
assert_eq!(hex::encode(ch), "3a941266af4275d5");
207256
}
208257
}
209258
{
@@ -213,7 +262,7 @@ mod tests {
213262
{
214263
let mut ch = [0u8; 8];
215264
ro.challenge_bytes(&mut ch);
216-
assert_eq!(hex::encode(ch), "79abbe29d8c33bb0");
265+
assert_eq!(hex::encode(ch), "644d94299bcc5590");
217266
}
218267
}
219268
{
@@ -223,7 +272,7 @@ mod tests {
223272
{
224273
let mut ch = [0u8; 8];
225274
ro.challenge_bytes(&mut ch);
226-
assert_eq!(hex::encode(ch), "f88d0f790cde50d5");
275+
assert_eq!(hex::encode(ch), "14f18d260e679f9a");
227276
}
228277
}
229278
{
@@ -234,8 +283,35 @@ mod tests {
234283
{
235284
let mut ch = [0u8; 8];
236285
ro.challenge_bytes(&mut ch);
237-
assert_eq!(hex::encode(ch), "90ca22b443fb78a1");
286+
assert_eq!(hex::encode(ch), "09dccc9d7dfa6f37");
238287
}
239288
}
240289
}
290+
291+
292+
#[test]
293+
fn outputs_are_disambiguated_by_length_prefix() {
294+
let mut ro = ProofTranscript::new(b"TestProtocol");
295+
{
296+
let mut ch = [0u8; 16];
297+
ro.challenge_bytes(&mut ch);
298+
assert_eq!(hex::encode(ch), "60890c8d774932db1aba587941cbffca");
299+
ro.challenge_bytes(&mut ch);
300+
assert_eq!(hex::encode(ch), "bb9308c7d34769ae2a3c040394efb2ab");
301+
}
302+
303+
let mut ro = ProofTranscript::new(b"TestProtocol");
304+
{
305+
let mut ch = [0u8; 8];
306+
ro.challenge_bytes(&mut ch);
307+
assert_eq!(hex::encode(ch), "cc76fac64922bc58");
308+
ro.challenge_bytes(&mut ch);
309+
assert_eq!(hex::encode(ch), "d259804aae5c3246");
310+
ro.challenge_bytes(&mut ch);
311+
assert_eq!(hex::encode(ch), "6d3a732156286895");
312+
ro.challenge_bytes(&mut ch);
313+
assert_eq!(hex::encode(ch), "2165dcd38764b5ae");
314+
}
315+
}
316+
241317
}

0 commit comments

Comments
 (0)