use crate::coordinate::Coordinate;
use crate::event::EventKind;
use crate::id::{EntityIdType, EventId};
use crate::store::{EncodedBytes, ExtensionKey, ReceiptVerification, ReceiptVerificationError};
use ed25519_compact::{PublicKey, Signature};
use std::collections::BTreeMap;
const COVER_VERSION_V1: u8 = 0x01;
#[derive(Clone, Debug, Default)]
pub struct ReceiptVerifyingKeys {
keys: BTreeMap<[u8; 32], [u8; 32]>,
}
impl ReceiptVerifyingKeys {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn from_public_keys<I>(public_keys: I) -> Self
where
I: IntoIterator<Item = [u8; 32]>,
{
let keys = public_keys
.into_iter()
.map(|public_key| (key_id_for_public_key(&public_key), public_key))
.collect();
Self { keys }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
fn lookup(&self, key_id: &[u8; 32]) -> Option<&[u8; 32]> {
self.keys.get(key_id)
}
}
#[derive(Clone, Debug)]
pub struct ReceiptClaim {
pub event_id: EventId,
pub global_sequence: u64,
pub content_hash: [u8; 32],
pub key_id: [u8; 32],
pub signature: Option<[u8; 64]>,
pub extensions: BTreeMap<ExtensionKey, EncodedBytes>,
}
#[must_use]
pub fn verify_receipt_claim(
claim: &ReceiptClaim,
coord: &Coordinate,
kind: EventKind,
prev_hash: [u8; 32],
keys: &ReceiptVerifyingKeys,
) -> ReceiptVerification {
verify_claim_parts(
ClaimParts {
event_id: claim.event_id.as_u128(),
global_sequence: claim.global_sequence,
content_hash: claim.content_hash,
key_id: claim.key_id,
signature: claim.signature,
extensions: &claim.extensions,
coord,
kind,
prev_hash,
},
keys,
)
}
#[derive(Clone, Copy)]
pub(crate) struct ClaimParts<'a> {
pub(crate) event_id: u128,
pub(crate) global_sequence: u64,
pub(crate) content_hash: [u8; 32],
pub(crate) key_id: [u8; 32],
pub(crate) signature: Option<[u8; 64]>,
pub(crate) extensions: &'a BTreeMap<ExtensionKey, EncodedBytes>,
pub(crate) coord: &'a Coordinate,
pub(crate) kind: EventKind,
pub(crate) prev_hash: [u8; 32],
}
pub(crate) fn verify_claim_parts(
parts: ClaimParts<'_>,
keys: &ReceiptVerifyingKeys,
) -> ReceiptVerification {
if parts.signature.is_none() && parts.key_id == [0; 32] {
return if keys.is_empty() {
ReceiptVerification::UnsignedAccepted
} else {
ReceiptVerification::Invalid(ReceiptVerificationError::UnsignedReceiptRejected)
};
}
let cover = match cover_bytes(
parts.event_id,
parts.global_sequence,
parts.coord,
parts.kind,
parts.prev_hash,
parts.content_hash,
parts.extensions,
) {
Ok(cover) => cover,
Err(error) => {
tracing::error!(error = %error, "failed to rebuild receipt signature cover");
return ReceiptVerification::Invalid(ReceiptVerificationError::CoverBuildFailed {
reason: error.to_string(),
});
}
};
verify_signature_with_keys(keys, parts.key_id, parts.signature, cover)
}
pub(crate) fn verify_signature_with_keys(
keys: &ReceiptVerifyingKeys,
key_id: [u8; 32],
signature: Option<[u8; 64]>,
cover: [u8; 32],
) -> ReceiptVerification {
let Some(signature_bytes) = signature else {
return if key_id == [0; 32] && keys.is_empty() {
ReceiptVerification::UnsignedAccepted
} else if key_id == [0; 32] {
ReceiptVerification::Invalid(ReceiptVerificationError::UnsignedReceiptRejected)
} else {
ReceiptVerification::Invalid(ReceiptVerificationError::MissingSignature)
};
};
if key_id == [0; 32] {
return ReceiptVerification::Invalid(ReceiptVerificationError::ZeroKeyWithSignature);
};
let Some(public_key_bytes) = keys.lookup(&key_id) else {
return ReceiptVerification::Invalid(ReceiptVerificationError::UnknownSigningKey);
};
let signature = Signature::new(signature_bytes);
if PublicKey::new(*public_key_bytes)
.verify(cover, &signature)
.is_ok()
{
ReceiptVerification::Signed
} else {
ReceiptVerification::Invalid(ReceiptVerificationError::InvalidSignature)
}
}
pub(crate) fn key_id_for_public_key(public_key: &[u8; 32]) -> [u8; 32] {
crate::event::hash::compute_hash(public_key)
}
#[derive(Debug)]
pub(crate) enum CoverBuildError {
CoordinateEncoding(rmp_serde::encode::Error),
ExtensionsEncoding(rmp_serde::encode::Error),
}
impl std::fmt::Display for CoverBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CoordinateEncoding(error) => {
write!(
f,
"coordinate encoding failed while building receipt cover: {error}"
)
}
Self::ExtensionsEncoding(error) => {
write!(
f,
"extension encoding failed while building receipt cover: {error}"
)
}
}
}
}
impl std::error::Error for CoverBuildError {}
pub(crate) fn cover_bytes(
event_id: u128,
sequence: u64,
coord: &Coordinate,
kind: EventKind,
prev_hash: [u8; 32],
content_hash: [u8; 32],
extensions: &BTreeMap<ExtensionKey, EncodedBytes>,
) -> Result<[u8; 32], CoverBuildError> {
let mut cover = Vec::new();
cover.push(COVER_VERSION_V1);
cover.extend_from_slice(&event_id.to_le_bytes());
cover.extend_from_slice(&sequence.to_le_bytes());
let coord_bytes =
crate::canonical::to_bytes(coord).map_err(CoverBuildError::CoordinateEncoding)?;
cover.extend_from_slice(&coord_bytes);
let raw_kind = kind.as_raw_u16();
cover.extend_from_slice(&raw_kind.to_le_bytes());
cover.extend_from_slice(&prev_hash);
cover.extend_from_slice(&content_hash);
let extension_bytes =
crate::canonical::to_bytes(extensions).map_err(CoverBuildError::ExtensionsEncoding)?;
cover.extend_from_slice(&extension_bytes);
Ok(crate::event::hash::compute_hash(&cover))
}