use ciborium::value::Value;
use crate::cbor::{
MAJOR_ARRAY, MAJOR_MAP, MAJOR_TAG, MAJOR_UNSIGNED, NULL, write_bytes, write_head, write_i64,
write_text,
};
use crate::error::{TrustError, TrustResult};
pub(crate) const CONTENT_TYPE: &str = "application/vnd.lys.receipt.v1+cbor";
pub(crate) const DIGEST_LEN: usize = 32;
pub(crate) const MAX_PATH_ELEMENTS: usize = 64;
pub(crate) const MAX_ARTIFACT_LEN: usize = 4096;
const COSE_SIGN1_TAG: u64 = 18;
const HEADER_LABEL_ALG: u64 = 1;
const HEADER_LABEL_CONTENT_TYPE: u64 = 3;
const HEADER_LABEL_KID: u64 = 4;
const HEADER_LABEL_VDS: u64 = 395;
const HEADER_LABEL_VDP: u64 = 396;
const ALG_EDDSA: i64 = -8;
const VDS_RFC9162_SHA256: u64 = 1;
const PROOF_TYPE_INCLUSION: i64 = -1;
pub(crate) fn protected_bytes(anchor_public_key: &[u8; DIGEST_LEN]) -> Vec<u8> {
let mut out = Vec::with_capacity(80);
write_head(&mut out, MAJOR_MAP, 4);
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_ALG);
write_i64(&mut out, ALG_EDDSA);
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_CONTENT_TYPE);
write_text(&mut out, CONTENT_TYPE);
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_KID);
write_bytes(&mut out, anchor_public_key);
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_VDS);
write_head(&mut out, MAJOR_UNSIGNED, VDS_RFC9162_SHA256);
out
}
fn inclusion_proof_bytes(
tree_size: u64,
leaf_index: u64,
inclusion_path: &[[u8; DIGEST_LEN]],
) -> Vec<u8> {
let mut out = Vec::with_capacity(24 + inclusion_path.len() * (DIGEST_LEN + 2));
write_head(&mut out, MAJOR_ARRAY, 3);
write_head(&mut out, MAJOR_UNSIGNED, tree_size);
write_head(&mut out, MAJOR_UNSIGNED, leaf_index);
write_head(&mut out, MAJOR_ARRAY, inclusion_path.len() as u64);
for node in inclusion_path {
write_bytes(&mut out, node);
}
out
}
fn unprotected_bytes(
tree_size: u64,
leaf_index: u64,
inclusion_path: &[[u8; DIGEST_LEN]],
) -> Vec<u8> {
let proof = inclusion_proof_bytes(tree_size, leaf_index, inclusion_path);
let mut out = Vec::with_capacity(proof.len() + 16);
write_head(&mut out, MAJOR_MAP, 1);
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_VDP);
write_head(&mut out, MAJOR_MAP, 1);
write_i64(&mut out, PROOF_TYPE_INCLUSION);
write_head(&mut out, MAJOR_ARRAY, 1);
write_bytes(&mut out, &proof);
out
}
pub(crate) fn artifact_bytes(
anchor_public_key: &[u8; DIGEST_LEN],
tree_size: u64,
leaf_index: u64,
inclusion_path: &[[u8; DIGEST_LEN]],
signature: &[u8; 64],
) -> Vec<u8> {
let protected = protected_bytes(anchor_public_key);
let unprotected = unprotected_bytes(tree_size, leaf_index, inclusion_path);
let mut out = Vec::with_capacity(protected.len() + unprotected.len() + 96);
write_head(&mut out, MAJOR_TAG, COSE_SIGN1_TAG);
write_head(&mut out, MAJOR_ARRAY, 4);
write_bytes(&mut out, &protected);
out.extend_from_slice(&unprotected);
out.push(NULL);
write_bytes(&mut out, signature);
out
}
pub(crate) struct DecodedFields {
pub(crate) anchor_public_key: [u8; DIGEST_LEN],
pub(crate) tree_size: u64,
pub(crate) leaf_index: u64,
pub(crate) inclusion_path: Vec<[u8; DIGEST_LEN]>,
pub(crate) signature: [u8; 64],
}
fn reject() -> TrustError {
TrustError::ReceiptVerification
}
fn parse_value(bytes: &[u8]) -> TrustResult<Value> {
ciborium::de::from_reader(bytes).map_err(|_err| reject())
}
fn fixed_bytes<const N: usize>(value: &Value) -> TrustResult<[u8; N]> {
let Value::Bytes(bytes) = value else {
return Err(reject());
};
bytes.as_slice().try_into().map_err(|_err| reject())
}
fn require_integer(value: &Value, expected: i128) -> TrustResult<()> {
let Value::Integer(int) = value else {
return Err(reject());
};
if i128::from(*int) == expected {
Ok(())
} else {
Err(reject())
}
}
fn unsigned(value: &Value) -> TrustResult<u64> {
let Value::Integer(int) = value else {
return Err(reject());
};
u64::try_from(i128::from(*int)).map_err(|_err| reject())
}
fn decode_protected(protected_raw: &[u8]) -> TrustResult<[u8; DIGEST_LEN]> {
let Value::Map(protected) = parse_value(protected_raw)? else {
return Err(reject());
};
let [(alg_key, alg), (ct_key, ct), (kid_key, kid), (vds_key, vds)] = protected.as_slice()
else {
return Err(reject());
};
require_integer(alg_key, i128::from(HEADER_LABEL_ALG))?;
require_integer(alg, i128::from(ALG_EDDSA))?;
require_integer(ct_key, i128::from(HEADER_LABEL_CONTENT_TYPE))?;
let Value::Text(content_type) = ct else {
return Err(reject());
};
if content_type != CONTENT_TYPE {
return Err(reject());
}
require_integer(kid_key, i128::from(HEADER_LABEL_KID))?;
let anchor_public_key: [u8; DIGEST_LEN] = fixed_bytes(kid)?;
require_integer(vds_key, i128::from(HEADER_LABEL_VDS))?;
require_integer(vds, i128::from(VDS_RFC9162_SHA256))?;
Ok(anchor_public_key)
}
struct DecodedProof {
tree_size: u64,
leaf_index: u64,
inclusion_path: Vec<[u8; DIGEST_LEN]>,
}
fn decode_unprotected(unprotected_item: &Value) -> TrustResult<DecodedProof> {
let Value::Map(unprotected) = unprotected_item else {
return Err(reject());
};
let [(vdp_key, vdp)] = unprotected.as_slice() else {
return Err(reject());
};
require_integer(vdp_key, i128::from(HEADER_LABEL_VDP))?;
let Value::Map(proofs_by_type) = vdp else {
return Err(reject());
};
let [(proof_type, proofs)] = proofs_by_type.as_slice() else {
return Err(reject());
};
require_integer(proof_type, i128::from(PROOF_TYPE_INCLUSION))?;
let Value::Array(proofs) = proofs else {
return Err(reject());
};
let [proof_item] = proofs.as_slice() else {
return Err(reject());
};
let Value::Bytes(proof_raw) = proof_item else {
return Err(reject());
};
let Value::Array(proof) = parse_value(proof_raw)? else {
return Err(reject());
};
let [size_item, index_item, path_item] = proof.as_slice() else {
return Err(reject());
};
let tree_size = unsigned(size_item)?;
let leaf_index = unsigned(index_item)?;
let Value::Array(path) = path_item else {
return Err(reject());
};
if path.len() > MAX_PATH_ELEMENTS {
return Err(reject());
}
let mut inclusion_path = Vec::with_capacity(path.len());
for node in path {
inclusion_path.push(fixed_bytes::<DIGEST_LEN>(node)?);
}
Ok(DecodedProof {
tree_size,
leaf_index,
inclusion_path,
})
}
pub(crate) fn decode_fields(bytes: &[u8]) -> TrustResult<DecodedFields> {
if bytes.len() > MAX_ARTIFACT_LEN {
return Err(reject());
}
let Value::Tag(COSE_SIGN1_TAG, boxed) = parse_value(bytes)? else {
return Err(reject());
};
let Value::Array(items) = *boxed else {
return Err(reject());
};
let [
protected_item,
unprotected_item,
payload_item,
signature_item,
] = items.as_slice()
else {
return Err(reject());
};
if !matches!(payload_item, Value::Null) {
return Err(reject());
}
let signature: [u8; 64] = fixed_bytes(signature_item)?;
let Value::Bytes(protected_raw) = protected_item else {
return Err(reject());
};
let anchor_public_key = decode_protected(protected_raw)?;
let proof = decode_unprotected(unprotected_item)?;
Ok(DecodedFields {
anchor_public_key,
tree_size: proof.tree_size,
leaf_index: proof.leaf_index,
inclusion_path: proof.inclusion_path,
signature,
})
}
#[cfg(test)]
#[path = "encoding_tests.rs"]
mod tests;