use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContextLabel {
label: String,
namespace_len: usize,
record_type_len: usize,
version: u64,
}
impl ContextLabel {
pub fn parse(label: &str) -> Result<Self> {
let mut parts = label.split('/');
let namespace = parts.next().unwrap_or("");
let record_type = parts.next().unwrap_or("");
let version_seg = parts.next().unwrap_or("");
if parts.next().is_some() {
return Err(Error::MalformedLeaf(format!(
"context label has too many '/'-segments: {label:?}"
)));
}
let valid_segment =
|s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_graphic() && b != b'/');
if !valid_segment(namespace) || !valid_segment(record_type) {
return Err(Error::MalformedLeaf(format!(
"context label segments must be non-empty printable ASCII: {label:?}"
)));
}
let digits = version_seg.strip_prefix('v').ok_or_else(|| {
Error::MalformedLeaf(format!(
"context label version must start with 'v': {label:?}"
))
})?;
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(Error::MalformedLeaf(format!(
"context label version must be 'v' followed by digits: {label:?}"
)));
}
if digits.len() > 1 && digits.starts_with('0') {
return Err(Error::MalformedLeaf(format!(
"context label version must not have leading zeros: {label:?}"
)));
}
let version: u64 = digits.parse().map_err(|_| {
Error::MalformedLeaf(format!("context label version overflow: {label:?}"))
})?;
Ok(Self {
label: label.to_string(),
namespace_len: namespace.len(),
record_type_len: record_type.len(),
version,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.label
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.label[..self.namespace_len]
}
#[must_use]
pub fn record_type(&self) -> &str {
let start = self.namespace_len + 1;
&self.label[start..start + self.record_type_len]
}
#[must_use]
pub fn version(&self) -> u64 {
self.version
}
}
#[must_use]
pub fn content_hash(label: &ContextLabel, content: &[u8]) -> [u8; 64] {
metamorphic_crypto::hash::sha3_512_with_context(label.as_str(), content)
}
fn push_lp(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
out.extend_from_slice(bytes);
}
pub mod key_history_v1 {
use super::{ContextLabel, Error, Result, content_hash, push_lp};
use crate::merkle::{Hash, hash_leaf};
pub const CONTEXT: &str = "mosslet/key-history/v1";
pub const VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub seq: u64,
pub ts_ms: u64,
pub enc_x25519: Vec<u8>,
pub enc_pq: Vec<u8>,
pub signing_pub: Vec<u8>,
pub prev_entry_hash: Option<Vec<u8>>,
}
impl Entry {
pub fn canonical_bytes(&self) -> Result<Vec<u8>> {
if matches!(self.prev_entry_hash.as_deref(), Some([])) {
return Err(Error::MalformedLeaf(
"prev_entry_hash present but empty; genesis must use None".into(),
));
}
let prev: &[u8] = self.prev_entry_hash.as_deref().unwrap_or(&[]);
let mut out = Vec::new();
out.extend_from_slice(&VERSION.to_be_bytes());
out.extend_from_slice(&self.seq.to_be_bytes());
out.extend_from_slice(&self.ts_ms.to_be_bytes());
push_lp(&mut out, &self.enc_x25519);
push_lp(&mut out, &self.enc_pq);
push_lp(&mut out, &self.signing_pub);
push_lp(&mut out, prev);
Ok(out)
}
pub fn entry_hash(&self) -> Result<[u8; 64]> {
let canonical = self.canonical_bytes()?;
let label = ContextLabel::parse(CONTEXT)?;
Ok(content_hash(&label, &canonical))
}
pub fn rfc6962_leaf_hash(&self) -> Result<Hash> {
Ok(hash_leaf(&self.canonical_bytes()?))
}
}
}