Skip to main content

hashsigs_reference/
lib.rs

1//! Auditable software oracle for the two `HashSigs` RHDL cryptographic profiles.
2//!
3//! The implementation follows pinned `HashSigsRS` commit
4//! `2d315dd4168804b7cbc51c51a1bf7ca27bf74140`. It intentionally models only
5//! the non-Solana WOTS+ primitive. It is suitable for test-vector generation,
6//! differential tests, and checking hardware behavior; it has not been audited
7//! as a production cryptographic library.
8//!
9//! # Security boundary
10//!
11//! - A private WOTS key may sign **one message only**. Key reuse can enable
12//!   forgeries. Signing APIs consume private-key values, but durable one-time
13//!   use remains the caller's responsibility.
14//! - Inputs are already-hashed 32-byte messages. Applications must define their
15//!   own collision-resistant, domain-separated prehash.
16//! - `HashSigsRS` uses shared masks and a simplified address construction. It is
17//!   not RFC 8391 XMSS, FIPS 205 SLH-DSA, or a many-time signature scheme.
18//! - [`LegacyKeccak`] uses legacy Keccak-256 padding and is byte-compatible with
19//!   upstream. [`HashSigsSha256GenericV1`] is a distinct, incompatible profile.
20#![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
32/// A 32-byte hash value used internally by the WOTS construction.
33pub type HashValue = [u8; HASH_BYTES];
34
35/// Hash behavior associated with one supported reference profile.
36///
37/// This trait extends the sealed [`Profile`] trait, so only profiles defined by
38/// `hashsigs-types` can implement it.
39pub trait ReferenceProfile: Profile {
40    /// Hashes one complete software-oracle input according to this profile.
41    ///
42    /// Hardware designs may split this operation into compression or
43    /// permutation jobs, but their final digest must match this function.
44    #[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/// A generated public/private key pair for one WOTS signing operation.
65///
66/// The private key is non-cloneable and consumed by [`Wots::sign`] or a fused
67/// operation. Persisting or reconstructing its raw bytes requires explicit
68/// action and moves one-time-use enforcement outside this API.
69#[derive(Debug)]
70pub struct KeyPair<P: ReferenceProfile> {
71    /// Public verification key.
72    pub public_key: PublicKey<P>,
73    /// One-time private signing key.
74    pub private_key: PrivateKey<P>,
75}
76
77/// Output produced by a constant-work fused sign-and-public-key operation.
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct FusedOutput<P: ReferenceProfile> {
80    /// Public key derived from the same private key used for this signature.
81    pub public_key: PublicKey<P>,
82    /// Signature captured while every chain is advanced to its endpoint.
83    pub signature: Signature<P>,
84}
85
86/// Stateless `HashSigsRS` WOTS+ reference implementation for profile `P`.
87///
88/// `Wots<LegacyKeccak>` and `Wots<HashSigsSha256GenericV1>` have different key
89/// and signature types. The following accidental cross-profile verification is
90/// rejected at compile time:
91///
92/// ```compile_fail
93/// use hashsigs_reference::{
94///     HashSigsSha256GenericV1, LegacyKeccak, MessageDigest, PublicKey, Signature,
95///     Wots, CHAIN_COUNT,
96/// };
97///
98/// let key = PublicKey::<LegacyKeccak>::new([0; 32], [0; 32]);
99/// let signature = Signature::<HashSigsSha256GenericV1>::new([[0; 32]; CHAIN_COUNT]);
100/// let message = MessageDigest::new([0; 32]);
101/// Wots::<LegacyKeccak>::new().verify(&key, &message, &signature);
102/// ```
103///
104/// Serialized keys and signatures have no in-band profile tag. A protocol that
105/// handles bytes must authenticate [`Profile::ID`] separately.
106#[derive(Clone, Copy, Debug)]
107pub struct Wots<P: ReferenceProfile> {
108    profile: PhantomData<fn() -> P>,
109}
110
111/// Byte-compatible oracle for the pinned `HashSigsRS` Keccak construction.
112pub type LegacyKeccakWots = Wots<LegacyKeccak>;
113
114/// Oracle for the incompatible SHA-256 generic performance profile.
115pub 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    /// Constructs a stateless software oracle.
125    #[must_use]
126    pub const fn new() -> Self {
127        Self {
128            profile: PhantomData,
129        }
130    }
131
132    /// Returns the cryptographic profile implemented by this oracle.
133    #[must_use]
134    pub const fn profile_id(&self) -> ProfileId {
135        P::ID
136    }
137
138    /// Applies the `HashSigsRS` domain-separated PRF.
139    ///
140    /// The exact preimage is `0x03 || seed || u16_be(index)`, totaling 35
141    /// bytes. The two-byte index is big-endian, matching pinned upstream code.
142    #[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    /// Derives all randomization masks that can affect a result.
152    ///
153    /// Pinned upstream allocates 67 PRF results. Its function key is mask zero;
154    /// signing and key generation start every chain at index zero and perform at
155    /// most 15 transitions; verification starts at a message digit and ends at
156    /// index 15. Consequently every observable access is in `0..=15`, and
157    /// upstream masks 16 through 66 are provably unreachable. Computing exactly
158    /// these 16 entries preserves every key, signature, and verification result.
159    #[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    /// Converts a 32-byte message digest to 64 base-16 digits plus checksum.
166    ///
167    /// Each byte contributes its high nibble followed by its low nibble. The
168    /// checksum is `sum(15 - digit)` and is appended as three big-endian base-16
169    /// digits, including leading zeroes.
170    #[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    /// Derives a one-time private key by hashing a caller-supplied seed once.
193    ///
194    /// The seed is consumed to discourage accidental derivation of the same key
195    /// for multiple signing calls.
196    #[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    /// Derives a public key from a one-time private key.
202    ///
203    /// This computes all 67 complete, 15-transition chains and hashes their
204    /// concatenated endpoints. The private key is borrowed so callers can derive
205    /// the public key before consuming it in exactly one signing operation.
206    #[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    /// Generates a public/private WOTS key pair from a one-time seed.
213    #[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    /// Signs one 32-byte message digest and consumes the one-time private key.
224    ///
225    /// This compatibility-oriented path stops each chain at its message digit,
226    /// so its amount of hash work depends on the public message. Use
227    /// [`Self::sign_and_public_key_from_private_key`] for the fixed-work fused
228    /// hardware oracle.
229    #[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    /// Verifies a typed signature against a typed public key and message digest.
235    ///
236    /// Profile mismatches are compile-time type errors. The final public-key hash
237    /// comparison examines every byte before returning.
238    #[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    /// Parses and verifies untrusted serialized inputs.
266    ///
267    /// Wrong-length message, public-key, or signature values return a
268    /// [`LengthError`]. Correctly sized but corrupted values return `Ok(false)`.
269    ///
270    /// # Errors
271    ///
272    /// Returns [`LengthError`] when any input does not have its protocol-defined
273    /// fixed length.
274    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    /// Signs and derives the public key in one fixed-work traversal.
287    ///
288    /// Every one of the 67 chains is advanced through all 15 transitions. The
289    /// signature segment is copied when the public message digit is reached,
290    /// and the final value becomes the public-key endpoint. No message digit can
291    /// skip a hash operation. The private key is consumed to discourage reuse.
292    #[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    /// Derives a private key, signs, and derives its public key at fixed work.
302    ///
303    /// The seed is hashed once and then every chain performs all 15 transitions.
304    /// For `LEGACY_KECCAK` this costs exactly 1,173 Keccak-f permutations. For
305    /// `HASHSIGS_SHA256_GENERIC_V1` it costs exactly 1,258 SHA-256 compression
306    /// blocks. See the corresponding constants in `hashsigs-types` for the full
307    /// derivations.
308    #[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            // Fixed work: the loop always executes all 15 chain transitions.
380            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}