use alloc::{vec, vec::Vec};
use coset::cbor::Value;
use sha2::{Digest, Sha256};
use crate::{Error, Result};
pub const RECEIPTS_LABEL: i64 = 394;
pub const VDS_LABEL: i64 = 395;
pub const VDP_LABEL: i64 = 396;
pub const INCLUSION_PROOF_LABEL: i64 = -1;
pub const RFC9162_SHA256: i64 = 1;
const LEAF_PREFIX: u8 = 0x00;
const NODE_PREFIX: u8 = 0x01;
#[must_use]
pub fn leaf_hash(entry: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([LEAF_PREFIX]);
hasher.update(entry);
hasher.finalize().into()
}
fn node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([NODE_PREFIX]);
hasher.update(left);
hasher.update(right);
hasher.finalize().into()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InclusionProof {
pub tree_size: u64,
pub leaf_index: u64,
pub inclusion_path: Vec<[u8; 32]>,
}
impl InclusionProof {
pub fn from_wrapped_cbor(wrapped: &[u8]) -> Result<Self> {
let mut cursor = wrapped;
let value: Value = coset::cbor::de::from_reader(&mut cursor)
.map_err(|_| Error::Receipt("inclusion proof is not valid CBOR"))?;
if !cursor.is_empty() {
return Err(Error::Receipt("trailing bytes after inclusion proof"));
}
let Value::Array(items) = value else {
return Err(Error::Receipt("inclusion proof must be a CBOR array"));
};
let [tree_size, leaf_index, path] = items.as_slice() else {
return Err(Error::Receipt(
"inclusion proof must carry exactly three elements",
));
};
let tree_size = unsigned(tree_size)
.ok_or(Error::Receipt("inclusion proof tree_size must be a uint"))?;
let leaf_index = unsigned(leaf_index)
.ok_or(Error::Receipt("inclusion proof leaf_index must be a uint"))?;
let Value::Array(path) = path else {
return Err(Error::Receipt(
"inclusion proof inclusion_path must be an array",
));
};
if path.is_empty() {
return Err(Error::Receipt(
"inclusion proof inclusion_path must not be empty",
));
}
let mut inclusion_path = Vec::with_capacity(path.len());
for element in path {
let Value::Bytes(bytes) = element else {
return Err(Error::Receipt(
"inclusion proof inclusion_path elements must be byte strings",
));
};
let hash: [u8; 32] = bytes.as_slice().try_into().map_err(|_| {
Error::Receipt("inclusion proof inclusion_path elements must be 32 bytes")
})?;
inclusion_path.push(hash);
}
Ok(Self {
tree_size,
leaf_index,
inclusion_path,
})
}
pub fn reconstruct_root(&self, leaf: [u8; 32]) -> Result<[u8; 32]> {
if self.leaf_index >= self.tree_size {
return Err(Error::Receipt(
"inclusion proof leaf_index is not less than tree_size",
));
}
let mut node_index = self.leaf_index;
let mut last_index = self.tree_size - 1;
let mut root = leaf;
for sibling in &self.inclusion_path {
if last_index == 0 {
return Err(Error::Receipt(
"inclusion proof path is longer than the tree permits",
));
}
if node_index & 1 == 1 || node_index == last_index {
root = node_hash(sibling, &root);
if node_index & 1 == 0 {
loop {
node_index >>= 1;
last_index >>= 1;
if node_index & 1 == 1 || node_index == 0 {
break;
}
}
}
} else {
root = node_hash(&root, sibling);
}
node_index >>= 1;
last_index >>= 1;
}
if last_index != 0 {
return Err(Error::Receipt(
"inclusion proof path ended before the root was reached",
));
}
Ok(root)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttachedReceipts {
Absent,
Malformed(&'static str),
Present(Vec<Vec<u8>>),
}
impl AttachedReceipts {
#[must_use]
pub fn as_slice(&self) -> &[Vec<u8>] {
match self {
Self::Present(receipts) => receipts,
Self::Absent | Self::Malformed(_) => &[],
}
}
}
pub fn attached_receipts(statement: &[u8]) -> Result<AttachedReceipts> {
let mut cursor = statement;
let value: Value = coset::cbor::de::from_reader(&mut cursor)
.map_err(|_| Error::Cose("failed to parse COSE_Sign1 CBOR"))?;
if !cursor.is_empty() {
return Err(Error::Cose("trailing bytes after COSE_Sign1"));
}
let items = cose_sign1_items(&value).ok_or(Error::Cose("COSE_Sign1 must be an array"))?;
let [
Value::Bytes(protected),
Value::Map(unprotected),
Value::Bytes(_),
Value::Bytes(_),
] = items
else {
return Err(Error::Cose(
"COSE_Sign1 must carry protected bytes, unprotected map, payload bytes, signature bytes",
));
};
let header = if protected.is_empty() {
Value::Map(Vec::new())
} else {
let mut cursor = protected.as_slice();
let header: Value = coset::cbor::de::from_reader(&mut cursor)
.map_err(|_| Error::Cose("protected header is not valid CBOR"))?;
if !cursor.is_empty() {
return Err(Error::Cose("trailing bytes in protected header"));
}
header
};
let Value::Map(protected) = &header else {
return Err(Error::Cose("protected header must encode a map"));
};
for map in [protected, unprotected] {
for (index, (key, _)) in map.iter().enumerate() {
if !matches!(key, Value::Integer(_) | Value::Text(_)) {
return Err(Error::Cose("COSE header labels must be integers or text"));
}
if map[..index].iter().any(|(other, _)| key == other) {
return Err(Error::Cose("duplicate COSE header label"));
}
}
}
if protected
.iter()
.any(|(key, _)| unprotected.iter().any(|(other, _)| key == other))
{
return Err(Error::Cose(
"header label occurs in both protected and unprotected maps",
));
}
if let Some((_, found)) = unprotected
.iter()
.chain(protected)
.find(|(key, _)| signed(key) == Some(RECEIPTS_LABEL))
{
return Ok(read_receipts_array(found));
}
Ok(AttachedReceipts::Absent)
}
fn read_receipts_array(value: &Value) -> AttachedReceipts {
let Value::Array(items) = value else {
return AttachedReceipts::Malformed("receipts header is not an array");
};
if items.is_empty() {
return AttachedReceipts::Malformed("receipts header is an empty array");
}
let mut receipts = Vec::with_capacity(items.len());
for item in items {
let encoded = match item {
Value::Bytes(b) => b.clone(),
Value::Array(_) | Value::Tag(18, _) => {
let Some(
[
Value::Bytes(_),
Value::Map(_),
Value::Bytes(_) | Value::Null,
Value::Bytes(_),
],
) = cose_sign1_items(item)
else {
return AttachedReceipts::Malformed(
"a legacy receipts element is not a COSE_Sign1 container",
);
};
let mut buf = Vec::new();
if coset::cbor::ser::into_writer(item, &mut buf).is_err() {
return AttachedReceipts::Malformed(
"a receipts element could not be re-encoded",
);
}
buf
}
_ => {
return AttachedReceipts::Malformed(
"a receipts element is not a byte string, array, or tag-18 value",
);
}
};
receipts.push(encoded);
}
AttachedReceipts::Present(receipts)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Receipt {
pub vds: i64,
pub inclusion_proofs: Vec<InclusionProof>,
pub payload: Option<Vec<u8>>,
protected_raw: Vec<u8>,
signature: Vec<u8>,
}
impl Receipt {
pub fn from_cose_sign1(receipt: &[u8]) -> Result<Self> {
let mut cursor = receipt;
let value: Value = coset::cbor::de::from_reader(&mut cursor)
.map_err(|_| Error::Receipt("receipt is not valid CBOR"))?;
if !cursor.is_empty() {
return Err(Error::Receipt("trailing bytes after receipt"));
}
crate::receipt_inspection::check_unique_maps(&value).map_err(Error::Receipt)?;
let items =
cose_sign1_items(&value).ok_or(Error::Receipt("receipt must be a COSE_Sign1 array"))?;
let [protected, unprotected, payload, signature] = items else {
return Err(Error::Receipt("receipt must carry exactly four elements"));
};
let Value::Bytes(protected_raw) = protected else {
return Err(Error::Receipt("receipt protected header must be bytes"));
};
let Value::Bytes(signature) = signature else {
return Err(Error::Receipt("receipt signature must be bytes"));
};
let mut cursor = protected_raw.as_slice();
let header: Value = coset::cbor::de::from_reader(&mut cursor)
.map_err(|_| Error::Receipt("receipt protected header is not valid CBOR"))?;
if !cursor.is_empty() {
return Err(Error::Receipt("trailing bytes in receipt protected header"));
}
crate::receipt_inspection::check_unique_maps(&header).map_err(Error::Receipt)?;
let Value::Map(_) = &header else {
return Err(Error::Receipt("receipt protected header must encode a map"));
};
let Value::Map(_) = unprotected else {
return Err(Error::Receipt("receipt unprotected header must be a map"));
};
let vds = map_entry(&header, VDS_LABEL)
.and_then(signed)
.ok_or(Error::Receipt("receipt protected header must carry vds"))?;
let vdp = map_entry(unprotected, VDP_LABEL)
.ok_or(Error::Receipt("receipt unprotected header must carry vdp"))?;
let proofs = map_entry(vdp, INCLUSION_PROOF_LABEL)
.ok_or(Error::Receipt("vdp map must carry an inclusion proof"))?;
let Value::Array(proofs) = proofs else {
return Err(Error::Receipt("inclusion proofs must be an array"));
};
if proofs.is_empty() {
return Err(Error::Receipt("inclusion proofs must not be empty"));
}
let mut inclusion_proofs = Vec::with_capacity(proofs.len());
for proof in proofs {
let Value::Bytes(wrapped) = proof else {
return Err(Error::Receipt("each inclusion proof must be a byte string"));
};
inclusion_proofs.push(InclusionProof::from_wrapped_cbor(wrapped)?);
}
let payload = match payload {
Value::Bytes(bytes) => Some(bytes.clone()),
Value::Null => None,
_ => return Err(Error::Receipt("receipt payload must be bytes or null")),
};
Ok(Self {
vds,
inclusion_proofs,
payload,
protected_raw: protected_raw.clone(),
signature: signature.clone(),
})
}
fn signed_bytes(&self, root: &[u8]) -> Result<Vec<u8>> {
let structure = Value::Array(vec![
Value::Text("Signature1".into()),
Value::Bytes(self.protected_raw.clone()),
Value::Bytes(Vec::new()),
Value::Bytes(root.to_vec()),
]);
let mut encoded = Vec::new();
coset::cbor::ser::into_writer(&structure, &mut encoded)
.map_err(|_| Error::Receipt("failed to encode Sig_structure"))?;
Ok(encoded)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedInclusion {
pub root: [u8; 32],
pub tree_size: u64,
pub leaf_index: u64,
}
pub fn verify_inclusion(
receipt: &[u8],
entry: &[u8],
transparency_service_key: &ed25519_dalek::VerifyingKey,
) -> Result<VerifiedInclusion> {
use ed25519_dalek::Verifier;
let receipt = Receipt::from_cose_sign1(receipt)?;
if receipt.vds != RFC9162_SHA256 {
return Err(Error::Receipt(
"unsupported verifiable data structure; only RFC9162_SHA256 is implemented",
));
}
let signature =
ed25519_dalek::Signature::from_slice(&receipt.signature).map_err(|_| Error::Signature)?;
let leaf = leaf_hash(entry);
let mut last = Error::Receipt("receipt carried no usable inclusion proof");
for proof in &receipt.inclusion_proofs {
let root = match proof.reconstruct_root(leaf) {
Ok(root) => root,
Err(error) => {
last = error;
continue;
}
};
if let Some(attached) = &receipt.payload
&& attached.as_slice() != root.as_slice()
{
last =
Error::Receipt("reconstructed root does not match the receipt's attached payload");
continue;
}
let signed = receipt.signed_bytes(&root)?;
if transparency_service_key
.verify(&signed, &signature)
.is_err()
{
last = Error::Signature;
continue;
}
return Ok(VerifiedInclusion {
root,
tree_size: proof.tree_size,
leaf_index: proof.leaf_index,
});
}
Err(last)
}
fn cose_sign1_items(value: &Value) -> Option<&[Value]> {
let value = match value {
Value::Tag(18, inner) => inner.as_ref(),
other => other,
};
match value {
Value::Array(items) => Some(items.as_slice()),
_ => None,
}
}
fn map_entry(value: &Value, label: i64) -> Option<&Value> {
let Value::Map(entries) = value else {
return None;
};
entries
.iter()
.find(|(key, _)| signed(key) == Some(label))
.map(|(_, found)| found)
}
fn signed(value: &Value) -> Option<i64> {
match value {
Value::Integer(integer) => i128::from(*integer).try_into().ok(),
_ => None,
}
}
fn unsigned(value: &Value) -> Option<u64> {
match value {
Value::Integer(integer) => i128::from(*integer).try_into().ok(),
_ => None,
}
}