commonware_stream/public_key/
handshake.rs

1//! Operations over handshake messages.
2
3use super::{x25519, AUTHENTICATION_TAG_LENGTH};
4use crate::Error;
5use bytes::{Buf, BufMut};
6use chacha20poly1305::{
7    aead::{AeadMutInPlace, Tag},
8    ChaCha20Poly1305, Nonce,
9};
10use commonware_codec::{
11    varint::UInt, Encode, EncodeSize, Error as CodecError, FixedSize, Read, ReadExt, Write,
12};
13use commonware_cryptography::{Hasher, PublicKey, Sha256, Signer};
14use commonware_runtime::Clock;
15use commonware_utils::SystemTimeExt;
16use std::time::Duration;
17
18/// Handshake information that is signed over by some peer.
19pub struct Info<P: PublicKey> {
20    /// The public key of the recipient.
21    recipient: P,
22
23    /// The ephemeral public key of the sender.
24    ///
25    /// This is used to derive the shared secret for the encrypted connection.
26    ephemeral_public_key: x25519::PublicKey,
27
28    /// Timestamp of the handshake (in epoch milliseconds).
29    timestamp: u64,
30
31    /// c.f. [`Self::new_with_tag`]
32    continuation_tag: Option<<Sha256 as Hasher>::Digest>,
33}
34
35impl<P: PublicKey> Info<P> {
36    /// Create a new info.
37    pub fn new(recipient: P, ephemeral_public_key: x25519::PublicKey, timestamp: u64) -> Self {
38        Self {
39            recipient,
40            ephemeral_public_key,
41            timestamp,
42            continuation_tag: None,
43        }
44    }
45
46    /// Create a new info with a tag linking to previous data.
47    ///
48    /// For example, to allow for the info to also be linked to the hello message sent by the other peer,
49    /// or having this information attest to a MAC using a previous shared session key.
50    pub fn new_with_tag(
51        recipient: P,
52        ephemeral_public_key: x25519::PublicKey,
53        timestamp: u64,
54        data: &[u8],
55    ) -> Self {
56        Self {
57            recipient,
58            ephemeral_public_key,
59            timestamp,
60            continuation_tag: Some(Sha256::hash(data)),
61        }
62    }
63
64    pub fn check_tag(&self, data: &[u8]) -> Result<(), Error> {
65        if self
66            .continuation_tag
67            .is_some_and(|tag| tag == Sha256::hash(data))
68        {
69            Ok(())
70        } else {
71            Err(Error::InvalidInfoContinuationTag)
72        }
73    }
74}
75
76impl<P: PublicKey> Write for Info<P> {
77    fn write(&self, buf: &mut impl BufMut) {
78        self.recipient.write(buf);
79        self.ephemeral_public_key.write(buf);
80        UInt(self.timestamp).write(buf);
81        self.continuation_tag.write(buf);
82    }
83}
84
85impl<P: PublicKey> Read for Info<P> {
86    type Cfg = ();
87
88    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
89        let recipient = P::read(buf)?;
90        let ephemeral_public_key = x25519::PublicKey::read(buf)?;
91        let timestamp = UInt::read(buf)?.into();
92        let continuation_tag = Option::read(buf)?;
93        Ok(Self {
94            recipient,
95            ephemeral_public_key,
96            timestamp,
97            continuation_tag,
98        })
99    }
100}
101
102impl<P: PublicKey> EncodeSize for Info<P> {
103    fn encode_size(&self) -> usize {
104        self.recipient.encode_size()
105            + self.ephemeral_public_key.encode_size()
106            + UInt(self.timestamp).encode_size()
107            + self.continuation_tag.encode_size()
108    }
109}
110
111/// A signed hello message.
112///
113/// Allows recipient to verify that the sender has the private key
114/// of public key before sending any data.
115///
116/// By requiring the server to have their public key signed, they prevent
117/// a malicious peer from forwarding a handshake message from a previous
118/// connection with public key (which could be used to convince the server
119/// to start a useless handshake). Alternatively, we could require the
120/// dialer to sign some random bytes provided by the server but this would
121/// require the server to send a message to a peer before authorizing that
122/// it should connect to them.
123pub struct Hello<P: PublicKey> {
124    // The handshake info that was signed over
125    info: Info<P>,
126
127    // The public key of the sender
128    signer: P,
129
130    // The signature of the sender
131    signature: P::Signature,
132}
133
134impl<P: PublicKey> Hello<P> {
135    /// Sign a hello message.
136    pub fn sign<Sk: Signer<PublicKey = P, Signature = P::Signature>>(
137        crypto: &mut Sk,
138        namespace: &[u8],
139        info: Info<P>,
140    ) -> Self {
141        let signature = crypto.sign(Some(namespace), &info.encode());
142        Self {
143            info,
144            signer: crypto.public_key(),
145            signature,
146        }
147    }
148
149    /// Get the public key of the signer.
150    pub fn signer(&self) -> P {
151        self.signer.clone()
152    }
153
154    /// Get the ephemeral public key of the signer.
155    pub fn ephemeral(&self) -> x25519::PublicKey {
156        self.info.ephemeral_public_key
157    }
158
159    /// Verify a signed hello message.
160    pub fn verify<E: Clock>(
161        &self,
162        context: &E,
163        crypto: &P,
164        namespace: &[u8],
165        synchrony_bound: Duration,
166        max_handshake_age: Duration,
167        tag_data: Option<&[u8]>,
168    ) -> Result<(), Error> {
169        // Verify that the signature is for us
170        //
171        // If we didn't verify this, it would be trivial for any peer to impersonate another peer (even though
172        // they would not be able to decrypt any messages from the shared secret). This would prevent us
173        // from making a legitimate connection to the intended peer.
174        if *crypto != self.info.recipient {
175            return Err(Error::HelloNotForUs);
176        }
177
178        // Verify that the hello is not signed by us
179        //
180        // This could indicate a self-connection attempt, which is not allowed.
181        // It could also indicate a replay attack or a malformed message.
182        // Either way, fail early to avoid any potential issues.
183        if *crypto == self.signer {
184            return Err(Error::HelloUsesOurKey);
185        }
186
187        // Verify that the timestamp in the hello is recent
188        //
189        // This prevents an adversary from reopening an encrypted connection
190        // if a peer's ephemeral key is compromised (which would be stored in-memory
191        // unlike the peer identity) and/or from blocking a peer from connecting
192        // to others (if an adversary recovered a handshake message could open a
193        // connection to a peer first, peers only maintain one connection per peer).
194        let current_timestamp = context.current().epoch();
195        let hello_timestamp = Duration::from_millis(self.info.timestamp);
196        if hello_timestamp + max_handshake_age < current_timestamp {
197            return Err(Error::InvalidTimestampOld(self.info.timestamp));
198        }
199        if hello_timestamp > current_timestamp + synchrony_bound {
200            return Err(Error::InvalidTimestampFuture(self.info.timestamp));
201        }
202
203        // Verify signature
204        if !self
205            .signer
206            .verify(Some(namespace), &self.info.encode(), &self.signature)
207        {
208            return Err(Error::InvalidSignature);
209        }
210
211        // Verify the continuation tag (if provided)
212        if let Some(d) = tag_data {
213            self.info.check_tag(d)?;
214        }
215        Ok(())
216    }
217}
218
219impl<P: PublicKey> Write for Hello<P> {
220    fn write(&self, buf: &mut impl BufMut) {
221        self.info.write(buf);
222        self.signer.write(buf);
223        self.signature.write(buf);
224    }
225}
226
227impl<P: PublicKey> Read for Hello<P> {
228    type Cfg = ();
229
230    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
231        let info = Info::read(buf)?;
232        let signer = P::read(buf)?;
233        let signature = P::Signature::read(buf)?;
234        Ok(Self {
235            info,
236            signer,
237            signature,
238        })
239    }
240}
241
242impl<P: PublicKey> EncodeSize for Hello<P> {
243    fn encode_size(&self) -> usize {
244        self.info.encode_size() + self.signer.encode_size() + self.signature.encode_size()
245    }
246}
247
248/// Key confirmation message used during the handshake.
249///
250/// This struct contains cryptographic proof that a party can correctly derive
251/// the shared secret from the Diffie-Hellman exchange. It prevents attacks where
252/// an adversary might forward [Hello] messages without actually knowing the
253/// corresponding private keys.
254pub struct Confirmation {
255    /// AEAD tag of the encrypted proof demonstrating knowledge of the shared secret.
256    tag: Tag<ChaCha20Poly1305>,
257}
258
259impl Confirmation {
260    /// Create a new [Confirmation] using the provided cipher and [Hello] transcript.
261    ///
262    /// The confirmation encrypts the hello transcript to demonstrate knowledge of the shared
263    /// secret and bind the confirmation to the entire hello exchange.
264    ///
265    /// # Security
266    ///
267    /// To prevent nonce-reuse, the cipher should **not** be the same cipher used for future
268    /// encrypted messages, but should be generated from the same shared secret. The function takes
269    /// ownership of the cipher to help prevent this.
270    pub fn create(mut cipher: ChaCha20Poly1305, transcript: &[u8]) -> Result<Self, Error> {
271        // Encrypt the confirmation using the transcript as associated data
272        let tag = cipher
273            .encrypt_in_place_detached(&Nonce::default(), transcript, &mut [])
274            .map_err(|_| Error::ConfirmationFailed)?;
275
276        Ok(Self { tag })
277    }
278
279    /// Verify the [Confirmation] using the provided cipher and [Hello] transcript.
280    ///
281    /// Returns Ok(()) if the confirmation is valid, otherwise returns an error.
282    pub fn verify(&self, mut cipher: ChaCha20Poly1305, transcript: &[u8]) -> Result<(), Error> {
283        // Decrypt the confirmation using the transcript as associated data
284        cipher
285            .decrypt_in_place_detached(&Nonce::default(), transcript, &mut [], &self.tag)
286            .map_err(|_| Error::InvalidConfirmation)?;
287        Ok(())
288    }
289}
290
291impl Write for Confirmation {
292    fn write(&self, buf: &mut impl BufMut) {
293        let tag_bytes: [u8; Self::SIZE] = self.tag.into();
294        tag_bytes.write(buf);
295    }
296}
297
298impl Read for Confirmation {
299    type Cfg = ();
300
301    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
302        let tag = <[u8; Self::SIZE]>::read_cfg(buf, &())?;
303        Ok(Self { tag: tag.into() })
304    }
305}
306
307impl FixedSize for Confirmation {
308    const SIZE: usize = AUTHENTICATION_TAG_LENGTH;
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::{
315        public_key::{Config, IncomingConnection},
316        utils::codec::send_frame,
317    };
318    use commonware_codec::DecodeExt;
319    use commonware_cryptography::{
320        ed25519::{PrivateKey, PublicKey as edPublicKey},
321        PrivateKeyExt as _, Verifier as _,
322    };
323    use commonware_runtime::{deterministic, mocks, Metrics, Runner, Spawner};
324    use x25519::PublicKey;
325
326    const TEST_NAMESPACE: &[u8] = b"test_namespace";
327    const ONE_MEGABYTE: usize = 1024 * 1024;
328
329    #[test]
330    fn test_hello_create_verify() {
331        // Initialize context
332        let executor = deterministic::Runner::default();
333        executor.start(|context| async move {
334            // Create participants
335            let mut sender = PrivateKey::from_seed(0);
336            let recipient = PrivateKey::from_seed(1).public_key();
337            let ephemeral_public_key = PublicKey::from_bytes([3u8; 32]);
338
339            // Create hello message
340            let timestamp = context.current().epoch_millis();
341            let hello = Hello::sign(
342                &mut sender,
343                TEST_NAMESPACE,
344                Info::new(recipient.clone(), ephemeral_public_key, timestamp),
345            );
346
347            // Decode the hello message
348            let hello =
349                Hello::<edPublicKey>::decode(hello.encode()).expect("failed to decode hello");
350
351            // Verify the timestamp
352            let synchrony_bound = Duration::from_secs(5);
353            let max_handshake_age = Duration::from_secs(5);
354            let hello_timestamp = Duration::from_millis(hello.info.timestamp);
355            let current_timestamp = Duration::from_millis(timestamp);
356            assert!(hello_timestamp <= current_timestamp + synchrony_bound);
357            assert!(hello_timestamp + max_handshake_age >= current_timestamp);
358
359            // Verify the signature
360            assert_eq!(hello.info.recipient, recipient);
361            assert_eq!(hello.info.ephemeral_public_key, ephemeral_public_key,);
362
363            // Verify signature
364            assert!(sender.public_key().verify(
365                Some(TEST_NAMESPACE),
366                &hello.info.encode(),
367                &hello.signature,
368            ));
369
370            // Verify using the hello struct
371            hello
372                .verify(
373                    &context,
374                    &recipient,
375                    TEST_NAMESPACE,
376                    synchrony_bound,
377                    max_handshake_age,
378                    None,
379                )
380                .unwrap();
381            assert_eq!(hello.signer, sender.public_key());
382            assert_eq!(hello.info.ephemeral_public_key, ephemeral_public_key);
383        });
384    }
385
386    #[test]
387    fn test_hello() {
388        // Initialize context
389        let executor = deterministic::Runner::default();
390        executor.start(|context| async move {
391            // Create participants
392            let mut sender = PrivateKey::from_seed(0);
393            let recipient = PrivateKey::from_seed(1);
394            let ephemeral_public_key = PublicKey::from_bytes([3u8; 32]);
395
396            // Create hello message
397            let hello = Hello::sign(
398                &mut sender,
399                TEST_NAMESPACE,
400                Info::new(recipient.public_key(), ephemeral_public_key, 0),
401            );
402
403            // Setup a mock sink and stream
404            let (sink, _) = mocks::Channel::init();
405            let (mut stream_sender, stream) = mocks::Channel::init();
406
407            // Send message over stream
408            context.with_label("stream_sender").spawn(|_| async move {
409                send_frame(&mut stream_sender, &hello.encode(), ONE_MEGABYTE)
410                    .await
411                    .unwrap();
412            });
413
414            // Call the verify function
415            let config = Config {
416                crypto: recipient.clone(),
417                namespace: TEST_NAMESPACE.to_vec(),
418                max_message_size: ONE_MEGABYTE,
419                synchrony_bound: Duration::from_secs(5),
420                max_handshake_age: Duration::from_secs(5),
421                handshake_timeout: Duration::from_secs(5),
422            };
423            let result = IncomingConnection::verify(&context, config, sink, stream)
424                .await
425                .unwrap();
426
427            // Assert that the result is expected
428            assert_eq!(result.peer(), sender.public_key());
429            assert_eq!(result.ephemeral(), ephemeral_public_key);
430        });
431    }
432
433    #[test]
434    fn test_hello_not_for_us() {
435        // Initialize context
436        let executor = deterministic::Runner::default();
437        executor.start(|context| async move {
438            // Create participants
439            let mut sender = PrivateKey::from_seed(0);
440            let ephemeral_public_key = PublicKey::from_bytes([3u8; 32]);
441
442            // Create hello message
443            let hello = Hello::sign(
444                &mut sender,
445                TEST_NAMESPACE,
446                Info::new(
447                    PrivateKey::from_seed(1).public_key(),
448                    ephemeral_public_key,
449                    0,
450                ),
451            );
452
453            // Setup a mock sink and stream
454            let (sink, _) = mocks::Channel::init();
455            let (mut stream_sender, stream) = mocks::Channel::init();
456
457            // Send message over stream
458            context.with_label("stream_sender").spawn(|_| async move {
459                send_frame(&mut stream_sender, &hello.encode(), ONE_MEGABYTE)
460                    .await
461                    .unwrap();
462            });
463
464            // Call the verify function
465            let config = Config {
466                crypto: PrivateKey::from_seed(2),
467                namespace: TEST_NAMESPACE.to_vec(),
468                max_message_size: ONE_MEGABYTE,
469                synchrony_bound: Duration::from_secs(5),
470                max_handshake_age: Duration::from_secs(5),
471                handshake_timeout: Duration::from_secs(5),
472            };
473            let result = IncomingConnection::verify(&context, config, sink, stream).await;
474
475            // Assert that the result is an error
476            assert!(matches!(result, Err(Error::HelloNotForUs)));
477        });
478    }
479
480    #[test]
481    fn test_incoming_hello_invalid_data() {
482        // Initialize context
483        let executor = deterministic::Runner::default();
484        executor.start(|context| async move {
485            // Setup a mock sink and stream
486            let (sink, _) = mocks::Channel::init();
487            let (mut stream_sender, stream) = mocks::Channel::init();
488
489            // Send invalid data over stream
490            context.with_label("stream_sender").spawn(|_| async move {
491                send_frame(&mut stream_sender, b"mock data", ONE_MEGABYTE)
492                    .await
493                    .unwrap();
494            });
495
496            // Call the verify function
497            let config = Config {
498                crypto: PrivateKey::from_seed(0),
499                namespace: TEST_NAMESPACE.to_vec(),
500                max_message_size: ONE_MEGABYTE,
501                synchrony_bound: Duration::from_secs(1),
502                max_handshake_age: Duration::from_secs(1),
503                handshake_timeout: Duration::from_secs(1),
504            };
505            let result = IncomingConnection::verify(&context, config, sink, stream).await;
506
507            // Assert that the result is an error
508            assert!(matches!(result, Err(Error::UnableToDecode(_))));
509        });
510    }
511
512    #[test]
513    fn test_incoming_hello_verify_timeout() {
514        let executor = deterministic::Runner::default();
515        executor.start(|context| async move {
516            // Setup a mock sink and stream
517            let (sink, _stream) = mocks::Channel::init();
518            let (_sink, stream) = mocks::Channel::init();
519
520            // Call the verify function for one peer, but never send the handshake from the other
521            let config = Config {
522                crypto: PrivateKey::from_seed(1),
523                namespace: TEST_NAMESPACE.to_vec(),
524                max_message_size: ONE_MEGABYTE,
525                synchrony_bound: Duration::from_secs(1),
526                max_handshake_age: Duration::from_secs(1),
527                handshake_timeout: Duration::from_secs(1),
528            };
529            let result = IncomingConnection::verify(&context, config, sink, stream).await;
530
531            // Assert that the result is an Err of type Error::HandshakeTimeout
532            assert!(matches!(result, Err(Error::HandshakeTimeout)));
533        });
534    }
535
536    #[test]
537    fn test_hello_verify_invalid_signature() {
538        let executor = deterministic::Runner::default();
539        executor.start(|context| async move {
540            let mut sender = PrivateKey::from_seed(0);
541            let recipient = PrivateKey::from_seed(1);
542            let ephemeral_public_key = x25519::PublicKey::from_bytes([0u8; 32]);
543
544            // The peer creates a valid hello intended for us
545            let hello = Hello::sign(
546                &mut sender,
547                TEST_NAMESPACE,
548                Info::new(recipient.public_key(), ephemeral_public_key, 0),
549            );
550
551            // Tamper with the hello to make the signature invalid
552            let mut hello =
553                Hello::<edPublicKey>::decode(hello.encode()).expect("failed to decode hello");
554            hello.info.timestamp += 1;
555
556            // Verify the hello
557            let result = hello.verify(
558                &context,
559                &recipient.public_key(),
560                TEST_NAMESPACE,
561                Duration::from_secs(5),
562                Duration::from_secs(5),
563                None,
564            );
565            assert!(matches!(result, Err(Error::InvalidSignature)));
566        });
567    }
568
569    #[test]
570    fn test_hello_verify_invalid_timestamp_old() {
571        let executor = deterministic::Runner::default();
572        executor.start(|context| async move {
573            let mut signer = PrivateKey::from_seed(0);
574            let recipient = PrivateKey::from_seed(1).public_key();
575            let ephemeral_public_key = x25519::PublicKey::from_bytes([0u8; 32]);
576
577            let timeout_duration = Duration::from_secs(5);
578            let synchrony_bound = Duration::from_secs(0);
579
580            // The peer creates a hello, setting the timestamp to 0.
581            let hello = Hello::sign(
582                &mut signer,
583                TEST_NAMESPACE,
584                Info::new(recipient.clone(), ephemeral_public_key, 0),
585            );
586
587            // Time starts at 0 in deterministic executor.
588            // Sleep for the exact timeout duration.
589            context.sleep(timeout_duration).await;
590
591            // Verify the hello, it should be fine still.
592            hello
593                .verify(
594                    &context,
595                    &recipient,
596                    TEST_NAMESPACE,
597                    synchrony_bound,
598                    timeout_duration,
599                    None,
600                )
601                .unwrap();
602
603            // Timeout by waiting 1 more millisecond.
604            context.sleep(Duration::from_millis(1)).await;
605
606            // Verify that a timeout error is returned.
607            let result = hello.verify(
608                &context,
609                &recipient,
610                TEST_NAMESPACE,
611                synchrony_bound,
612                timeout_duration,
613                None,
614            );
615            assert!(matches!(result, Err(Error::InvalidTimestampOld(t)) if t == 0));
616        });
617    }
618
619    #[test]
620    fn test_confirmation_create_and_verify() {
621        use chacha20poly1305::KeyInit;
622
623        let key = [1u8; 32];
624        let cipher = ChaCha20Poly1305::new(&key.into());
625        let transcript = b"test_transcript_data";
626
627        // Create confirmation
628        let confirmation = Confirmation::create(cipher, transcript).unwrap();
629
630        // Verify the confirmation with the same parameters
631        let cipher = ChaCha20Poly1305::new(&key.into());
632        confirmation.verify(cipher, transcript).unwrap();
633
634        // Verify that confirmation fails with different transcript
635        let different_transcript = b"different_transcript_data";
636        let cipher = ChaCha20Poly1305::new(&key.into());
637        let result = confirmation.verify(cipher, different_transcript);
638        assert!(matches!(result, Err(Error::InvalidConfirmation)));
639
640        // Verify that confirmation fails with different cipher
641        let different_key = [2u8; 32];
642        let different_cipher = ChaCha20Poly1305::new(&different_key.into());
643        let result = confirmation.verify(different_cipher, transcript);
644        assert!(matches!(result, Err(Error::InvalidConfirmation)));
645    }
646
647    #[test]
648    fn test_confirmation_encoding() {
649        use chacha20poly1305::KeyInit;
650        use commonware_codec::{DecodeExt, Encode};
651
652        let key = [1u8; 32];
653        let cipher = ChaCha20Poly1305::new(&key.into());
654        let transcript = b"test_transcript_for_encoding";
655
656        // Create and encode confirmation
657        let original_confirmation = Confirmation::create(cipher, transcript).unwrap();
658        let encoded = original_confirmation.encode();
659
660        // Decode and verify it matches
661        let decoded_confirmation = Confirmation::decode(encoded).unwrap();
662        let cipher = ChaCha20Poly1305::new(&key.into());
663        decoded_confirmation.verify(cipher, transcript).unwrap();
664    }
665
666    #[test]
667    fn test_hello_verify_invalid_timestamp_future() {
668        let executor = deterministic::Runner::default();
669        executor.start(|context| async move {
670            let mut signer = PrivateKey::from_seed(0);
671            let recipient = PrivateKey::from_seed(1).public_key();
672            let ephemeral_public_key = x25519::PublicKey::from_bytes([0u8; 32]);
673
674            let timeout_duration = Duration::from_secs(0);
675            const SYNCHRONY_BOUND_MILLIS: u64 = 5_000;
676            let synchrony_bound = Duration::from_millis(SYNCHRONY_BOUND_MILLIS);
677
678            // The peer creates a hello at the synchrony bound.
679            let hello_ok = Hello::sign(
680                &mut signer,
681                TEST_NAMESPACE,
682                Info::new(recipient.clone(), ephemeral_public_key, SYNCHRONY_BOUND_MILLIS)
683            );
684
685            // Create a hello 1ms too far into the future.
686            let hello_late = Hello::sign(
687                &mut signer,
688                TEST_NAMESPACE,
689                Info::new(recipient.clone(), ephemeral_public_key, SYNCHRONY_BOUND_MILLIS + 1)
690            );
691
692            // Verify the okay hello.
693            hello_ok.verify(
694                &context,
695                &recipient,
696                TEST_NAMESPACE,
697                synchrony_bound,
698                timeout_duration,
699                None,
700            ).unwrap(); // no error
701
702            // Hello too far into the future fails.
703            let result = hello_late.verify(
704                &context,
705                &recipient,
706                TEST_NAMESPACE,
707                synchrony_bound,
708                timeout_duration,
709                None
710            );
711            assert!(matches!(result, Err(Error::InvalidTimestampFuture(t)) if t == SYNCHRONY_BOUND_MILLIS + 1));
712        });
713    }
714
715    #[test]
716    fn test_info_tag_confirmation_happy_path() {
717        let info = Info::new_with_tag(
718            PrivateKey::from_seed(1).public_key(),
719            PublicKey::from_bytes([1u8; 32]),
720            0,
721            b"ABC",
722        );
723        assert!(info.check_tag(b"ABC").is_ok())
724    }
725
726    #[test]
727    fn test_info_no_tag_but_check_with_data_fails() {
728        let info = Info::new(
729            PrivateKey::from_seed(1).public_key(),
730            PublicKey::from_bytes([1u8; 32]),
731            0,
732        );
733        assert!(info.check_tag(b"ABC").is_err())
734    }
735
736    #[test]
737    fn test_info_tag_but_check_with_different_data_fails() {
738        let info = Info::new_with_tag(
739            PrivateKey::from_seed(1).public_key(),
740            PublicKey::from_bytes([1u8; 32]),
741            0,
742            b"ABC",
743        );
744        assert!(info.check_tag(b"not ABC").is_err())
745    }
746}