dig_message/
transcript.rs1use chia_protocol::Bytes32;
17use sha2::{Digest, Sha256};
18
19use crate::constants::SIG_DOMAIN;
20use crate::envelope::StreamHeader;
21use dig_identity::bls::SecretKey;
22use dig_identity::{sign_message, verify_signature};
23
24const TRANSCRIPT_LEN: usize = 1 + 4 + 1 + 32 + 32 + 32 + 4 + 8 + 8 + 8 + 1 + 8 + 48 + 1 + 4 + 32;
26
27#[derive(Debug, Clone)]
30pub struct TranscriptFields<'a> {
31 pub version: u8,
32 pub message_type: u32,
33 pub flags: u8,
34 pub correlation_id: Bytes32,
35 pub sender: Bytes32,
36 pub recipient: Bytes32,
37 pub sender_epoch: u32,
38 pub counter: u64,
39 pub timestamp_ms: u64,
40 pub expires_at: u64,
41 pub stream: Option<StreamHeader>,
43 pub kem_enc: &'a [u8; 48],
45 pub compression: u8,
46 pub uncompressed_len: u32,
47 pub compressed_payload: &'a [u8],
49}
50
51impl TranscriptFields<'_> {
52 #[must_use]
54 pub fn signing_bytes(&self) -> Vec<u8> {
55 let mut out = Vec::with_capacity(SIG_DOMAIN.len() + TRANSCRIPT_LEN);
56 out.extend_from_slice(SIG_DOMAIN);
57 out.extend_from_slice(&self.version.to_be_bytes());
58 out.extend_from_slice(&self.message_type.to_be_bytes());
59 out.extend_from_slice(&self.flags.to_be_bytes());
60 out.extend_from_slice(self.correlation_id.as_ref());
61 out.extend_from_slice(self.sender.as_ref());
62 out.extend_from_slice(self.recipient.as_ref());
63 out.extend_from_slice(&self.sender_epoch.to_be_bytes());
64 out.extend_from_slice(&self.counter.to_be_bytes());
65 out.extend_from_slice(&self.timestamp_ms.to_be_bytes());
66 out.extend_from_slice(&self.expires_at.to_be_bytes());
67 let (frame, seq) = self.stream.map_or((0u8, 0u64), |s| (s.frame, s.seq));
68 out.extend_from_slice(&frame.to_be_bytes());
69 out.extend_from_slice(&seq.to_be_bytes());
70 out.extend_from_slice(self.kem_enc);
71 out.extend_from_slice(&self.compression.to_be_bytes());
72 out.extend_from_slice(&self.uncompressed_len.to_be_bytes());
73 let hash: [u8; 32] = Sha256::digest(self.compressed_payload).into();
74 out.extend_from_slice(&hash);
75 out
76 }
77
78 #[must_use]
81 pub fn sign(&self, sender_sk: &SecretKey) -> [u8; 96] {
82 sign_message(sender_sk, &self.signing_bytes())
83 }
84
85 #[must_use]
88 pub fn verify(&self, sender_pub: &[u8; 48], sig: &[u8; 96]) -> bool {
89 verify_signature(sender_pub, &self.signing_bytes(), sig)
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use dig_identity::{derive_identity_sk, master_secret_key_from_seed, public_key_bytes};
97
98 fn sk(label: &str) -> SecretKey {
99 let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
100 derive_identity_sk(&master_secret_key_from_seed(&seed))
101 }
102
103 fn fields<'a>(kem_enc: &'a [u8; 48], payload: &'a [u8]) -> TranscriptFields<'a> {
104 TranscriptFields {
105 version: 1,
106 message_type: 0x0000_0201,
107 flags: 0b0000_0100,
108 correlation_id: Bytes32::new([1u8; 32]),
109 sender: Bytes32::new([2u8; 32]),
110 recipient: Bytes32::new([3u8; 32]),
111 sender_epoch: 7,
112 counter: 42,
113 timestamp_ms: 1_700_000_000_000,
114 expires_at: 0,
115 stream: None,
116 kem_enc,
117 compression: 0,
118 uncompressed_len: payload.len() as u32,
119 compressed_payload: payload,
120 }
121 }
122
123 #[test]
124 fn signing_bytes_are_fixed_width_and_domain_prefixed() {
125 let kem = [4u8; 48];
126 let f = fields(&kem, b"hello");
127 let bytes = f.signing_bytes();
128 assert_eq!(bytes.len(), SIG_DOMAIN.len() + TRANSCRIPT_LEN);
129 assert!(bytes.starts_with(SIG_DOMAIN));
130 }
131
132 #[test]
133 fn signing_bytes_are_deterministic() {
134 let kem = [4u8; 48];
135 assert_eq!(
136 fields(&kem, b"abc").signing_bytes(),
137 fields(&kem, b"abc").signing_bytes()
138 );
139 }
140
141 #[test]
142 fn sign_then_verify_round_trips() {
143 let sk = sk("transcript/sign");
144 let pk = public_key_bytes(&sk);
145 let kem = [9u8; 48];
146 let f = fields(&kem, b"payload-bytes");
147 let sig = f.sign(&sk);
148 assert!(f.verify(&pk, &sig));
149 }
150
151 #[test]
152 fn any_field_change_breaks_the_signature() {
153 let sk = sk("transcript/tamper");
154 let pk = public_key_bytes(&sk);
155 let kem = [9u8; 48];
156 let f = fields(&kem, b"payload-bytes");
157 let sig = f.sign(&sk);
158
159 let mut tampered = fields(&kem, b"payload-bytes");
160 tampered.counter = 43;
161 assert!(
162 !tampered.verify(&pk, &sig),
163 "changing counter must break the sig"
164 );
165
166 let mut tampered_exp = fields(&kem, b"payload-bytes");
167 tampered_exp.expires_at = 999;
168 assert!(
169 !tampered_exp.verify(&pk, &sig),
170 "changing expires_at must break the sig"
171 );
172
173 assert!(f.verify(&pk, &sig));
175 }
176
177 #[test]
178 fn wrong_key_does_not_verify() {
179 let signer = sk("transcript/wrong-a");
180 let kem = [9u8; 48];
181 let f = fields(&kem, b"x");
182 let sig = f.sign(&signer);
183 let other = public_key_bytes(&sk("transcript/wrong-b"));
184 assert!(!f.verify(&other, &sig));
185 }
186
187 #[test]
188 fn payload_hash_is_bound_not_the_bytes() {
189 let kem = [9u8; 48];
191 assert_ne!(
192 fields(&kem, b"one").signing_bytes(),
193 fields(&kem, b"two").signing_bytes()
194 );
195 }
196
197 #[test]
198 fn stream_fields_are_bound() {
199 let kem = [9u8; 48];
200 let mut a = fields(&kem, b"x");
201 a.stream = Some(StreamHeader {
202 frame: 2,
203 seq: 5,
204 window: 0,
205 });
206 let mut b = fields(&kem, b"x");
207 b.stream = Some(StreamHeader {
208 frame: 2,
209 seq: 6,
210 window: 0,
211 });
212 assert_ne!(a.signing_bytes(), b.signing_bytes(), "stream_seq is bound");
213 }
214}