use bamboo_rs_core_ed25519_yasmf::entry::{is_lipmaa_required, MAX_ENTRY_SIZE};
use bamboo_rs_core_ed25519_yasmf::{Entry as BambooEntry, Signature as BambooSignature};
use crate::entry::error::EncodeEntryError;
use crate::entry::traits::AsEntry;
use crate::entry::validate::validate_links;
use crate::entry::{EncodedEntry, Entry, LogId, SeqNum};
use crate::hash::Hash;
use crate::identity::KeyPair;
use crate::operation::EncodedOperation;
pub fn sign_entry(
log_id: &LogId,
seq_num: &SeqNum,
skiplink_hash: Option<&Hash>,
backlink_hash: Option<&Hash>,
payload: &EncodedOperation,
key_pair: &KeyPair,
) -> Result<Entry, EncodeEntryError> {
let payload_hash = payload.hash();
let payload_size = payload.size();
let backlink = backlink_hash.map(|link| link.into());
let lipmaa_link = if is_lipmaa_required(seq_num.as_u64()) {
skiplink_hash.map(|link| link.into())
} else {
None
};
let entry: BambooEntry<_, &[u8]> = BambooEntry {
is_end_of_feed: false,
author: key_pair.public_key().into(),
log_id: log_id.as_u64(),
seq_num: seq_num.as_u64(),
lipmaa_link,
backlink,
payload_size,
payload_hash: (&payload_hash).into(),
sig: None,
};
let mut entry_bytes = [0u8; MAX_ENTRY_SIZE];
let entry_size = entry.encode(&mut entry_bytes)?;
let signature = key_pair.sign(&entry_bytes[..entry_size]);
let signed_entry = Entry {
public_key: key_pair.public_key(),
log_id: log_id.to_owned(),
seq_num: seq_num.to_owned(),
skiplink: skiplink_hash.cloned(),
backlink: backlink_hash.cloned(),
payload_size,
payload_hash,
signature: signature.into(),
};
validate_links(&signed_entry)?;
Ok(signed_entry)
}
pub fn encode_entry(entry: &Entry) -> Result<EncodedEntry, EncodeEntryError> {
let signature_bytes = entry.signature().into_bytes();
let entry: BambooEntry<_, &[u8]> = BambooEntry {
is_end_of_feed: false,
author: entry.public_key().into(),
log_id: entry.log_id().as_u64(),
seq_num: entry.seq_num().as_u64(),
lipmaa_link: entry.skiplink().map(|link| link.into()),
backlink: entry.backlink().map(|link| link.into()),
payload_size: entry.payload_size(),
payload_hash: entry.payload_hash().into(),
sig: Some(BambooSignature(&signature_bytes[..])),
};
let mut entry_bytes = [0u8; MAX_ENTRY_SIZE];
let signed_entry_size = entry.encode(&mut entry_bytes)?;
Ok(EncodedEntry::from_bytes(&entry_bytes[..signed_entry_size]))
}
pub fn sign_and_encode_entry(
log_id: &LogId,
seq_num: &SeqNum,
skiplink_hash: Option<&Hash>,
backlink_hash: Option<&Hash>,
payload: &EncodedOperation,
key_pair: &KeyPair,
) -> Result<EncodedEntry, EncodeEntryError> {
let entry = sign_entry(
log_id,
seq_num,
skiplink_hash,
backlink_hash,
payload,
key_pair,
)?;
let encoded_entry = encode_entry(&entry)?;
Ok(encoded_entry)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::convert::TryInto;
use rstest::rstest;
use rstest_reuse::apply;
use crate::entry::traits::AsEncodedEntry;
use crate::entry::{EncodedEntry, Entry, LogId, SeqNum};
use crate::hash::Hash;
use crate::identity::KeyPair;
use crate::operation::EncodedOperation;
use crate::test_utils::fixtures::{
encoded_entry, encoded_operation, entry, key_pair, random_hash, Fixture,
};
use crate::test_utils::templates::{many_valid_entries, version_fixtures};
use super::{encode_entry, sign_and_encode_entry, sign_entry};
#[rstest]
#[case(1, false, false)]
#[case(2, true, false)]
#[case(3, true, false)]
#[case(4, true, true)]
#[case(5, true, false)]
#[case(6, true, false)]
#[case(7, true, false)]
#[case(8, true, true)]
#[case(9, true, false)]
#[should_panic]
#[case::backlink_missing(2, false, false)]
#[should_panic]
#[case::skiplink_missing(4, true, false)]
fn signing_entry_validation(
#[case] seq_num: u64,
#[case] backlink: bool,
#[case] skiplink: bool,
#[from(random_hash)] entry_hash_1: Hash,
#[from(random_hash)] entry_hash_2: Hash,
#[from(encoded_operation)] operation: EncodedOperation,
#[from(key_pair)] key_pair: KeyPair,
) {
sign_entry(
&LogId::default(),
&seq_num.try_into().unwrap(),
skiplink.then_some(&entry_hash_1),
backlink.then_some(&entry_hash_2),
&operation,
&key_pair,
)
.unwrap();
sign_and_encode_entry(
&LogId::default(),
&seq_num.try_into().unwrap(),
skiplink.then_some(&entry_hash_1),
backlink.then_some(&entry_hash_2),
&operation,
&key_pair,
)
.unwrap();
}
#[rstest]
fn encode_entry_to_hex(#[from(entry)] entry: Entry) {
assert_eq!(
encode_entry(&entry).unwrap().to_string(),
concat!(
"002f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc",
"960001f902960020c62c1dca517ac87334919852758bee5865715533462d97cd",
"b51d1c57eb0a9d1c26cba7eda41294acabf7a8f2afa2e0d64ce7e6ff6a7bc1bd",
"29a0ab353d01c1a925711ca69c84aa71e75b0f4a0cc51b2fabdb0e25ecbc067e",
"43faad7d25bb110e"
)
)
}
#[rstest]
fn invalid_sign_entry_links(
#[from(random_hash)] entry_hash: Hash,
#[from(encoded_operation)] operation: EncodedOperation,
#[from(key_pair)] key_pair: KeyPair,
) {
assert_eq!(
sign_entry(
&LogId::new(9),
&SeqNum::new(4).unwrap(),
Some(&entry_hash),
None,
&operation,
&key_pair
)
.unwrap_err()
.to_string(),
"backlink and skiplink not valid for this sequence number"
);
assert_eq!(
sign_and_encode_entry(
&LogId::new(9),
&SeqNum::new(4).unwrap(),
Some(&entry_hash),
None,
&operation,
&key_pair
)
.unwrap_err()
.to_string(),
"backlink and skiplink not valid for this sequence number"
);
}
#[rstest]
fn it_hashes(encoded_entry: EncodedEntry) {
let mut hash_map = HashMap::new();
let key_value = "Value identified by a hash".to_string();
hash_map.insert(&encoded_entry, key_value.clone());
let key_value_retrieved = hash_map.get(&encoded_entry).unwrap().to_owned();
assert_eq!(key_value, key_value_retrieved)
}
#[apply(version_fixtures)]
fn fixture_encode(#[case] fixture: Fixture) {
let entry_encoded = encode_entry(&fixture.entry).unwrap();
assert_eq!(fixture.entry_encoded.hash(), entry_encoded.hash(),);
}
#[apply(many_valid_entries)]
fn fixture_encode_valid_entries(#[case] entry: Entry) {
assert!(encode_entry(&entry).is_ok());
}
}