1use bamboo_rs_core_ed25519_yasmf::entry::{is_lipmaa_required, MAX_ENTRY_SIZE};
16use bamboo_rs_core_ed25519_yasmf::{Entry as BambooEntry, Signature as BambooSignature};
17
18use crate::entry::error::EncodeEntryError;
19use crate::entry::traits::AsEntry;
20use crate::entry::validate::validate_links;
21use crate::entry::{EncodedEntry, Entry, LogId, SeqNum};
22use crate::hash::Hash;
23use crate::identity::KeyPair;
24use crate::operation::EncodedOperation;
25
26pub fn sign_entry(
36 log_id: &LogId,
37 seq_num: &SeqNum,
38 skiplink_hash: Option<&Hash>,
39 backlink_hash: Option<&Hash>,
40 payload: &EncodedOperation,
41 key_pair: &KeyPair,
42) -> Result<Entry, EncodeEntryError> {
43 let payload_hash = payload.hash();
45 let payload_size = payload.size();
46
47 let backlink = backlink_hash.map(|link| link.into());
49 let lipmaa_link = if is_lipmaa_required(seq_num.as_u64()) {
50 skiplink_hash.map(|link| link.into())
51 } else {
52 None
54 };
55
56 let entry: BambooEntry<_, &[u8]> = BambooEntry {
61 is_end_of_feed: false,
62 author: key_pair.public_key().into(),
63 log_id: log_id.as_u64(),
64 seq_num: seq_num.as_u64(),
65 lipmaa_link,
66 backlink,
67 payload_size,
68 payload_hash: (&payload_hash).into(),
69 sig: None,
70 };
71
72 let mut entry_bytes = [0u8; MAX_ENTRY_SIZE];
73
74 let entry_size = entry.encode(&mut entry_bytes)?;
76
77 let signature = key_pair.sign(&entry_bytes[..entry_size]);
79
80 let signed_entry = Entry {
81 public_key: key_pair.public_key(),
82 log_id: log_id.to_owned(),
83 seq_num: seq_num.to_owned(),
84 skiplink: skiplink_hash.cloned(),
85 backlink: backlink_hash.cloned(),
86 payload_size,
87 payload_hash,
88 signature: signature.into(),
89 };
90
91 validate_links(&signed_entry)?;
93
94 Ok(signed_entry)
95}
96
97pub fn encode_entry(entry: &Entry) -> Result<EncodedEntry, EncodeEntryError> {
103 let signature_bytes = entry.signature().into_bytes();
104
105 let entry: BambooEntry<_, &[u8]> = BambooEntry {
106 is_end_of_feed: false,
107 author: entry.public_key().into(),
108 log_id: entry.log_id().as_u64(),
109 seq_num: entry.seq_num().as_u64(),
110 lipmaa_link: entry.skiplink().map(|link| link.into()),
111 backlink: entry.backlink().map(|link| link.into()),
112 payload_size: entry.payload_size(),
113 payload_hash: entry.payload_hash().into(),
114 sig: Some(BambooSignature(&signature_bytes[..])),
115 };
116
117 let mut entry_bytes = [0u8; MAX_ENTRY_SIZE];
118
119 let signed_entry_size = entry.encode(&mut entry_bytes)?;
125
126 Ok(EncodedEntry::from_bytes(&entry_bytes[..signed_entry_size]))
127}
128
129pub fn sign_and_encode_entry(
134 log_id: &LogId,
135 seq_num: &SeqNum,
136 skiplink_hash: Option<&Hash>,
137 backlink_hash: Option<&Hash>,
138 payload: &EncodedOperation,
139 key_pair: &KeyPair,
140) -> Result<EncodedEntry, EncodeEntryError> {
141 let entry = sign_entry(
142 log_id,
143 seq_num,
144 skiplink_hash,
145 backlink_hash,
146 payload,
147 key_pair,
148 )?;
149
150 let encoded_entry = encode_entry(&entry)?;
151
152 Ok(encoded_entry)
153}
154
155#[cfg(test)]
156mod tests {
157 use std::collections::HashMap;
158 use std::convert::TryInto;
159
160 use rstest::rstest;
161 use rstest_reuse::apply;
162
163 use crate::entry::traits::AsEncodedEntry;
164 use crate::entry::{EncodedEntry, Entry, LogId, SeqNum};
165 use crate::hash::Hash;
166 use crate::identity::KeyPair;
167 use crate::operation::EncodedOperation;
168 use crate::test_utils::fixtures::{
169 encoded_entry, encoded_operation, entry, key_pair, random_hash, Fixture,
170 };
171 use crate::test_utils::templates::{many_valid_entries, version_fixtures};
172
173 use super::{encode_entry, sign_and_encode_entry, sign_entry};
174
175 #[rstest]
176 #[case(1, false, false)]
177 #[case(2, true, false)]
178 #[case(3, true, false)]
179 #[case(4, true, true)]
180 #[case(5, true, false)]
181 #[case(6, true, false)]
182 #[case(7, true, false)]
183 #[case(8, true, true)]
184 #[case(9, true, false)]
185 #[should_panic]
186 #[case::backlink_missing(2, false, false)]
187 #[should_panic]
188 #[case::skiplink_missing(4, true, false)]
189 fn signing_entry_validation(
190 #[case] seq_num: u64,
191 #[case] backlink: bool,
192 #[case] skiplink: bool,
193 #[from(random_hash)] entry_hash_1: Hash,
194 #[from(random_hash)] entry_hash_2: Hash,
195 #[from(encoded_operation)] operation: EncodedOperation,
196 #[from(key_pair)] key_pair: KeyPair,
197 ) {
198 sign_entry(
199 &LogId::default(),
200 &seq_num.try_into().unwrap(),
201 skiplink.then_some(&entry_hash_1),
202 backlink.then_some(&entry_hash_2),
203 &operation,
204 &key_pair,
205 )
206 .unwrap();
207
208 sign_and_encode_entry(
209 &LogId::default(),
210 &seq_num.try_into().unwrap(),
211 skiplink.then_some(&entry_hash_1),
212 backlink.then_some(&entry_hash_2),
213 &operation,
214 &key_pair,
215 )
216 .unwrap();
217 }
218
219 #[rstest]
220 fn encode_entry_to_hex(#[from(entry)] entry: Entry) {
221 assert_eq!(
222 encode_entry(&entry).unwrap().to_string(),
223 concat!(
224 "002f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc",
225 "960001f901b200205610cb28a37deed208bd52980f54132a062a5f8e3eac7fb9",
226 "e6d404f3b1b76b32e6897d47a56691d0d2ea2ba14c676a4154d7226d678c6fbe",
227 "b0a2ffb70ad245c942b0194e7ac73f38902c08d19a4a44cfa73083e296c256f3",
228 "c7be49843e52a402"
229 )
230 )
231 }
232
233 #[rstest]
234 fn invalid_sign_entry_links(
235 #[from(random_hash)] entry_hash: Hash,
236 #[from(encoded_operation)] operation: EncodedOperation,
237 #[from(key_pair)] key_pair: KeyPair,
238 ) {
239 assert_eq!(
240 sign_entry(
241 &LogId::new(9),
242 &SeqNum::new(4).unwrap(),
243 Some(&entry_hash),
244 None,
245 &operation,
246 &key_pair
247 )
248 .unwrap_err()
249 .to_string(),
250 "backlink and skiplink not valid for this sequence number"
251 );
252
253 assert_eq!(
254 sign_and_encode_entry(
255 &LogId::new(9),
256 &SeqNum::new(4).unwrap(),
257 Some(&entry_hash),
258 None,
259 &operation,
260 &key_pair
261 )
262 .unwrap_err()
263 .to_string(),
264 "backlink and skiplink not valid for this sequence number"
265 );
266 }
267
268 #[rstest]
269 fn it_hashes(encoded_entry: EncodedEntry) {
270 let mut hash_map = HashMap::new();
272 let key_value = "Value identified by a hash".to_string();
273 hash_map.insert(&encoded_entry, key_value.clone());
274
275 let key_value_retrieved = hash_map.get(&encoded_entry).unwrap().to_owned();
277 assert_eq!(key_value, key_value_retrieved)
278 }
279
280 #[apply(version_fixtures)]
281 fn fixture_encode(#[case] fixture: Fixture) {
282 let entry_encoded = encode_entry(&fixture.entry).unwrap();
284
285 assert_eq!(fixture.entry_encoded.hash(), entry_encoded.hash(),);
287 }
288
289 #[apply(many_valid_entries)]
290 fn fixture_encode_valid_entries(#[case] entry: Entry) {
291 assert!(encode_entry(&entry).is_ok());
292 }
293}