1#![forbid(unsafe_code)]
21
22use core::marker::PhantomData;
23
24pub use hashsigs_types::{
25 CHAIN_COUNT, CHAIN_STEPS, CHECKSUM_DIGITS, FUSED_OUTPUT_BYTES, HASH_BYTES,
26 HashSigsSha256GenericV1, LegacyKeccak, LengthError, MASK_COUNT, MESSAGE_BYTES, MESSAGE_DIGITS,
27 MessageDigest, PRF_INPUT_BYTES, ParseProfileIdError, PrivateKey, PrivateSeed, Profile,
28 ProfileId, PublicKey, SIGNATURE_BYTES, Signature, WINTERNITZ_W,
29};
30use sha2::Digest as _;
31
32pub type HashValue = [u8; HASH_BYTES];
34
35pub trait ReferenceProfile: Profile {
40 #[doc(hidden)]
45 fn hash(input: &[u8]) -> HashValue;
46}
47
48impl ReferenceProfile for LegacyKeccak {
49 fn hash(input: &[u8]) -> HashValue {
50 let mut hasher = sha3::Keccak256::new();
51 hasher.update(input);
52 hasher.finalize().into()
53 }
54}
55
56impl ReferenceProfile for HashSigsSha256GenericV1 {
57 fn hash(input: &[u8]) -> HashValue {
58 let mut hasher = sha2::Sha256::new();
59 hasher.update(input);
60 hasher.finalize().into()
61 }
62}
63
64#[derive(Debug)]
70pub struct KeyPair<P: ReferenceProfile> {
71 pub public_key: PublicKey<P>,
73 pub private_key: PrivateKey<P>,
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct FusedOutput<P: ReferenceProfile> {
80 pub public_key: PublicKey<P>,
82 pub signature: Signature<P>,
84}
85
86#[derive(Clone, Copy, Debug)]
107pub struct Wots<P: ReferenceProfile> {
108 profile: PhantomData<fn() -> P>,
109}
110
111pub type LegacyKeccakWots = Wots<LegacyKeccak>;
113
114pub type Sha256GenericWots = Wots<HashSigsSha256GenericV1>;
116
117impl<P: ReferenceProfile> Default for Wots<P> {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl<P: ReferenceProfile> Wots<P> {
124 #[must_use]
126 pub const fn new() -> Self {
127 Self {
128 profile: PhantomData,
129 }
130 }
131
132 #[must_use]
134 pub const fn profile_id(&self) -> ProfileId {
135 P::ID
136 }
137
138 #[must_use]
143 pub fn prf(&self, seed: &HashValue, index: u16) -> HashValue {
144 let mut input = [0_u8; PRF_INPUT_BYTES];
145 input[0] = 0x03;
146 input[1..=HASH_BYTES].copy_from_slice(seed);
147 input[1 + HASH_BYTES..].copy_from_slice(&index.to_be_bytes());
148 hash::<P>(&input)
149 }
150
151 #[must_use]
160 pub fn randomization_masks(&self, public_seed: &HashValue) -> [HashValue; MASK_COUNT] {
161 #[allow(clippy::cast_possible_truncation)]
162 core::array::from_fn(|index| self.prf(public_seed, index as u16))
163 }
164
165 #[must_use]
171 pub fn message_chain_indexes(&self, message: &MessageDigest) -> [u8; CHAIN_COUNT] {
172 let mut indexes = [0_u8; CHAIN_COUNT];
173
174 for (index, byte) in message.as_bytes().iter().copied().enumerate() {
175 indexes[index * 2] = byte >> 4;
176 indexes[index * 2 + 1] = byte & 0x0F;
177 }
178
179 let checksum: u16 = indexes[..MESSAGE_DIGITS]
180 .iter()
181 .map(|digit| u16::from(15 - digit))
182 .sum();
183 for (output, shift) in indexes[MESSAGE_DIGITS..].iter_mut().zip([8_u32, 4, 0]) {
184 #[allow(clippy::cast_possible_truncation)]
185 let digit = ((checksum >> shift) & 0x0F) as u8;
186 *output = digit;
187 }
188
189 indexes
190 }
191
192 #[must_use]
197 pub fn private_key_from_seed(&self, private_seed: PrivateSeed<P>) -> PrivateKey<P> {
198 PrivateKey::new(hash::<P>(&private_seed.into_secret()))
199 }
200
201 #[must_use]
207 pub fn public_key_from_private_key(&self, private_key: &PrivateKey<P>) -> PublicKey<P> {
208 let public_seed = self.prf(private_key.expose_secret(), 0);
209 self.public_key_from_private_key_and_seed(private_key.expose_secret(), public_seed)
210 }
211
212 #[must_use]
214 pub fn generate_key_pair(&self, private_seed: PrivateSeed<P>) -> KeyPair<P> {
215 let private_key = self.private_key_from_seed(private_seed);
216 let public_key = self.public_key_from_private_key(&private_key);
217 KeyPair {
218 public_key,
219 private_key,
220 }
221 }
222
223 #[must_use]
230 pub fn sign(&self, private_key: PrivateKey<P>, message: &MessageDigest) -> Signature<P> {
231 self.sign_from_private_key_bytes(&private_key.into_secret(), message)
232 }
233
234 #[must_use]
239 pub fn verify(
240 &self,
241 public_key: &PublicKey<P>,
242 message: &MessageDigest,
243 signature: &Signature<P>,
244 ) -> bool {
245 let masks = self.randomization_masks(public_key.public_seed());
246 let indexes = self.message_chain_indexes(message);
247 let mut endpoints = [0_u8; SIGNATURE_BYTES];
248
249 for (chain_index, ((signature_segment, digit), endpoint)) in signature
250 .segments()
251 .iter()
252 .zip(indexes.iter().copied())
253 .zip(endpoints.chunks_exact_mut(HASH_BYTES))
254 .enumerate()
255 {
256 debug_assert!(chain_index < CHAIN_COUNT);
257 let value = advance_chain::<P>(signature_segment, &masks, digit, 15 - digit);
258 endpoint.copy_from_slice(&value);
259 }
260
261 let computed = hash::<P>(&endpoints);
262 constant_time_eq(&computed, public_key.public_key_hash())
263 }
264
265 pub fn verify_bytes(
275 &self,
276 public_key: &[u8],
277 message: &[u8],
278 signature: &[u8],
279 ) -> Result<bool, LengthError> {
280 let message = MessageDigest::from_slice(message)?;
281 let public_key = PublicKey::<P>::from_slice(public_key)?;
282 let signature = Signature::<P>::from_slice(signature)?;
283 Ok(self.verify(&public_key, &message, &signature))
284 }
285
286 #[must_use]
293 pub fn sign_and_public_key_from_private_key(
294 &self,
295 private_key: PrivateKey<P>,
296 message: &MessageDigest,
297 ) -> FusedOutput<P> {
298 self.fused_from_private_key_bytes(&private_key.into_secret(), message)
299 }
300
301 #[must_use]
309 pub fn sign_and_public_key_from_private_seed(
310 &self,
311 private_seed: PrivateSeed<P>,
312 message: &MessageDigest,
313 ) -> FusedOutput<P> {
314 let private_key = hash::<P>(&private_seed.into_secret());
315 self.fused_from_private_key_bytes(&private_key, message)
316 }
317
318 fn sign_from_private_key_bytes(
319 &self,
320 private_key: &HashValue,
321 message: &MessageDigest,
322 ) -> Signature<P> {
323 let public_seed = self.prf(private_key, 0);
324 let masks = self.randomization_masks(&public_seed);
325 let indexes = self.message_chain_indexes(message);
326 let mut signature = [[0_u8; HASH_BYTES]; CHAIN_COUNT];
327
328 for (index, (output, digit)) in signature
329 .iter_mut()
330 .zip(indexes.iter().copied())
331 .enumerate()
332 {
333 let secret = self.secret_segment(private_key, &masks[0], index);
334 *output = advance_chain::<P>(&secret, &masks, 0, digit);
335 }
336
337 Signature::new(signature)
338 }
339
340 fn public_key_from_private_key_and_seed(
341 &self,
342 private_key: &HashValue,
343 public_seed: HashValue,
344 ) -> PublicKey<P> {
345 let masks = self.randomization_masks(&public_seed);
346 let mut endpoints = [0_u8; SIGNATURE_BYTES];
347
348 for (index, endpoint) in endpoints.chunks_exact_mut(HASH_BYTES).enumerate() {
349 let secret = self.secret_segment(private_key, &masks[0], index);
350 let value = advance_chain::<P>(&secret, &masks, 0, 15);
351 endpoint.copy_from_slice(&value);
352 }
353
354 PublicKey::new(public_seed, hash::<P>(&endpoints))
355 }
356
357 fn fused_from_private_key_bytes(
358 &self,
359 private_key: &HashValue,
360 message: &MessageDigest,
361 ) -> FusedOutput<P> {
362 let public_seed = self.prf(private_key, 0);
363 let masks = self.randomization_masks(&public_seed);
364 let indexes = self.message_chain_indexes(message);
365 let mut signature = [[0_u8; HASH_BYTES]; CHAIN_COUNT];
366 let mut endpoints = [0_u8; SIGNATURE_BYTES];
367
368 for (index, ((signature_segment, digit), endpoint)) in signature
369 .iter_mut()
370 .zip(indexes.iter().copied())
371 .zip(endpoints.chunks_exact_mut(HASH_BYTES))
372 .enumerate()
373 {
374 let mut state = self.secret_segment(private_key, &masks[0], index);
375 if digit == 0 {
376 *signature_segment = state;
377 }
378
379 for step in 1_u8..=15 {
381 state = hash::<P>(&xor(&state, &masks[usize::from(step)]));
382 if step == digit {
383 *signature_segment = state;
384 }
385 }
386
387 endpoint.copy_from_slice(&state);
388 }
389
390 FusedOutput {
391 public_key: PublicKey::new(public_seed, hash::<P>(&endpoints)),
392 signature: Signature::new(signature),
393 }
394 }
395
396 fn secret_segment(
397 &self,
398 private_key: &HashValue,
399 function_key: &HashValue,
400 chain_index: usize,
401 ) -> HashValue {
402 let secret_prf = self.prf(
403 private_key,
404 u16::try_from(chain_index + 1).expect("chain index fits u16"),
405 );
406 let mut input = [0_u8; HASH_BYTES * 2];
407 input[..HASH_BYTES].copy_from_slice(function_key);
408 input[HASH_BYTES..].copy_from_slice(&secret_prf);
409 hash::<P>(&input)
410 }
411}
412
413fn hash<P: ReferenceProfile>(input: &[u8]) -> HashValue {
414 #[cfg(test)]
415 work_audit::record(input.len());
416 P::hash(input)
417}
418
419fn advance_chain<P: ReferenceProfile>(
420 initial: &HashValue,
421 masks: &[HashValue; MASK_COUNT],
422 start: u8,
423 steps: u8,
424) -> HashValue {
425 debug_assert!(usize::from(start) + usize::from(steps) <= CHAIN_STEPS);
426 let mut value = *initial;
427 for offset in 1..=steps {
428 let mask_index = usize::from(start + offset);
429 value = hash::<P>(&xor(&value, &masks[mask_index]));
430 }
431 value
432}
433
434fn xor(left: &HashValue, right: &HashValue) -> HashValue {
435 let mut output = [0_u8; HASH_BYTES];
436 for ((byte, left), right) in output.iter_mut().zip(left).zip(right) {
437 *byte = left ^ right;
438 }
439 output
440}
441
442fn constant_time_eq(left: &HashValue, right: &HashValue) -> bool {
443 left.iter()
444 .zip(right)
445 .fold(0_u8, |difference, (left, right)| {
446 difference | (left ^ right)
447 })
448 == 0
449}
450
451#[cfg(test)]
452mod work_audit {
453 use std::cell::RefCell;
454
455 thread_local! {
456 static INPUT_LENGTHS: RefCell<Option<Vec<usize>>> = const { RefCell::new(None) };
457 }
458
459 pub fn record(length: usize) {
460 INPUT_LENGTHS.with(|lengths| {
461 if let Some(lengths) = lengths.borrow_mut().as_mut() {
462 lengths.push(length);
463 }
464 });
465 }
466
467 pub fn capture<T>(operation: impl FnOnce() -> T) -> (T, Vec<usize>) {
468 INPUT_LENGTHS.with(|lengths| {
469 assert!(lengths.borrow().is_none(), "work audit cannot be nested");
470 *lengths.borrow_mut() = Some(Vec::new());
471 });
472 let output = operation();
473 let lengths = INPUT_LENGTHS.with(|lengths| {
474 lengths
475 .borrow_mut()
476 .take()
477 .expect("work audit must be active")
478 });
479 (output, lengths)
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use hashsigs_types::{
487 LEGACY_FUSED_PRIVATE_KEY_KECCAK_PERMUTATIONS,
488 LEGACY_FUSED_PRIVATE_SEED_KECCAK_PERMUTATIONS, SHA256_FUSED_PRIVATE_KEY_COMPRESSION_BLOCKS,
489 SHA256_FUSED_PRIVATE_SEED_COMPRESSION_BLOCKS,
490 };
491
492 #[test]
493 fn message_digits_match_upstream_order_and_checksum() {
494 let oracle = LegacyKeccakWots::new();
495 let zeros = oracle.message_chain_indexes(&MessageDigest::new([0; MESSAGE_BYTES]));
496 assert_eq!(&zeros[..MESSAGE_DIGITS], &[0; MESSAGE_DIGITS]);
497 assert_eq!(&zeros[MESSAGE_DIGITS..], &[3, 12, 0]);
498
499 let ones = oracle.message_chain_indexes(&MessageDigest::new([0xFF; MESSAGE_BYTES]));
500 assert_eq!(&ones[..MESSAGE_DIGITS], &[15; MESSAGE_DIGITS]);
501 assert_eq!(&ones[MESSAGE_DIGITS..], &[0, 0, 0]);
502
503 let mut ordered = [0_u8; MESSAGE_BYTES];
504 for (index, byte) in ordered.iter_mut().enumerate() {
505 *byte = u8::try_from(index).expect("index fits u8");
506 }
507 let digits = oracle.message_chain_indexes(&MessageDigest::new(ordered));
508 assert_eq!(&digits[..8], &[0, 0, 0, 1, 0, 2, 0, 3]);
509 }
510
511 #[test]
512 fn fused_and_independent_paths_match_for_both_profiles() {
513 assert_fused_matches::<LegacyKeccak>();
514 assert_fused_matches::<HashSigsSha256GenericV1>();
515 }
516
517 fn assert_fused_matches<P: ReferenceProfile>() {
518 let oracle = Wots::<P>::new();
519 let key_bytes = [0xA7; HASH_BYTES];
520 let message = MessageDigest::new(core::array::from_fn(|index| {
521 u8::try_from(index * 7).expect("test value fits u8")
522 }));
523 let public_key = oracle.public_key_from_private_key(&PrivateKey::new(key_bytes));
524 let signature = oracle.sign(PrivateKey::new(key_bytes), &message);
525 let fused =
526 oracle.sign_and_public_key_from_private_key(PrivateKey::new(key_bytes), &message);
527 assert_eq!(fused.public_key.to_bytes(), public_key.to_bytes());
528 assert_eq!(fused.signature.to_bytes(), signature.to_bytes());
529 assert!(oracle.verify(&fused.public_key, &message, &fused.signature));
530 }
531
532 #[test]
533 fn fused_work_counts_match_architecture_contract() {
534 let message = MessageDigest::new([0x39; MESSAGE_BYTES]);
535
536 let keccak = LegacyKeccakWots::new();
537 let (_, lengths) = work_audit::capture(|| {
538 keccak
539 .sign_and_public_key_from_private_key(PrivateKey::new([0x11; HASH_BYTES]), &message)
540 });
541 let permutations: usize = lengths.iter().map(|length| length / 136 + 1).sum();
542 assert_eq!(permutations, LEGACY_FUSED_PRIVATE_KEY_KECCAK_PERMUTATIONS);
543
544 let (_, lengths) = work_audit::capture(|| {
545 keccak.sign_and_public_key_from_private_seed(
546 PrivateSeed::new([0x11; HASH_BYTES]),
547 &message,
548 )
549 });
550 let permutations: usize = lengths.iter().map(|length| length / 136 + 1).sum();
551 assert_eq!(permutations, LEGACY_FUSED_PRIVATE_SEED_KECCAK_PERMUTATIONS);
552
553 let sha = Sha256GenericWots::new();
554 let (_, lengths) = work_audit::capture(|| {
555 sha.sign_and_public_key_from_private_key(PrivateKey::new([0x11; HASH_BYTES]), &message)
556 });
557 let compression_blocks: usize =
558 lengths.iter().map(|length| (length + 9).div_ceil(64)).sum();
559 assert_eq!(
560 compression_blocks,
561 SHA256_FUSED_PRIVATE_KEY_COMPRESSION_BLOCKS
562 );
563
564 let (_, lengths) = work_audit::capture(|| {
565 sha.sign_and_public_key_from_private_seed(
566 PrivateSeed::new([0x11; HASH_BYTES]),
567 &message,
568 )
569 });
570 let compression_blocks: usize =
571 lengths.iter().map(|length| (length + 9).div_ceil(64)).sum();
572 assert_eq!(
573 compression_blocks,
574 SHA256_FUSED_PRIVATE_SEED_COMPRESSION_BLOCKS
575 );
576 }
577
578 #[test]
579 fn malformed_and_corrupted_serialized_inputs_are_distinct() {
580 let oracle = LegacyKeccakWots::new();
581 let message = MessageDigest::new([0x42; MESSAGE_BYTES]);
582 let fused = oracle
583 .sign_and_public_key_from_private_key(PrivateKey::new([0x17; HASH_BYTES]), &message);
584 let public_key = fused.public_key.to_bytes();
585 let signature = fused.signature.to_bytes();
586
587 assert!(matches!(
588 oracle.verify_bytes(&public_key[..63], message.as_bytes(), &signature),
589 Err(error) if error.expected() == hashsigs_types::PUBLIC_KEY_BYTES
590 ));
591 assert!(
592 oracle
593 .verify_bytes(&public_key, &message.as_bytes()[..31], &signature)
594 .is_err()
595 );
596 assert!(
597 oracle
598 .verify_bytes(&public_key, message.as_bytes(), &signature[..2_143])
599 .is_err()
600 );
601
602 let mut corrupted_signature = signature;
603 corrupted_signature[777] ^= 0x80;
604 assert_eq!(
605 oracle.verify_bytes(&public_key, message.as_bytes(), &corrupted_signature),
606 Ok(false)
607 );
608
609 let mut corrupted_key = public_key;
610 corrupted_key[63] ^= 1;
611 assert_eq!(
612 oracle.verify_bytes(&corrupted_key, message.as_bytes(), &signature),
613 Ok(false)
614 );
615
616 let mut corrupted_message = message.into_bytes();
617 corrupted_message[0] ^= 1;
618 assert_eq!(
619 oracle.verify_bytes(&public_key, &corrupted_message, &signature),
620 Ok(false)
621 );
622 }
623
624 #[test]
625 fn profiles_are_cryptographically_distinct() {
626 let message = MessageDigest::new([0xCC; MESSAGE_BYTES]);
627 let legacy = LegacyKeccakWots::new()
628 .sign_and_public_key_from_private_key(PrivateKey::new([0x55; HASH_BYTES]), &message);
629 let sha = Sha256GenericWots::new()
630 .sign_and_public_key_from_private_key(PrivateKey::new([0x55; HASH_BYTES]), &message);
631 assert_ne!(legacy.public_key.to_bytes(), sha.public_key.to_bytes());
632 assert_ne!(legacy.signature.to_bytes(), sha.signature.to_bytes());
633 }
634}