use ciborium::value::Value;
use crate::cbor::{
MAJOR_ARRAY, MAJOR_MAP, MAJOR_TAG, MAJOR_UNSIGNED, write_bytes, write_head, write_i64,
write_text,
};
use crate::error::{TrustError, TrustResult};
pub(crate) const CONTENT_TYPE: &str = "application/vnd.lys.attestation.v2+cbor";
pub(crate) const MAX_ARTIFACT_LEN: usize = 1024;
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 ALG_EDDSA: i128 = -8;
const CLAIM_KEY_PAYLOAD_HASH: u64 = 1;
const CLAIM_KEY_TIMESTAMP: u64 = 2;
pub(crate) fn protected_bytes(signer_public_key: &[u8; 32]) -> Vec<u8> {
let mut out = Vec::with_capacity(80);
write_head(&mut out, MAJOR_MAP, 3);
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_ALG);
write_i64(&mut out, -8);
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, signer_public_key);
out
}
pub(crate) fn claims_bytes(payload_hash: &[u8; 32], timestamp: i64) -> Vec<u8> {
let mut out = Vec::with_capacity(46);
write_head(&mut out, MAJOR_MAP, 2);
write_head(&mut out, MAJOR_UNSIGNED, CLAIM_KEY_PAYLOAD_HASH);
write_bytes(&mut out, payload_hash);
write_head(&mut out, MAJOR_UNSIGNED, CLAIM_KEY_TIMESTAMP);
write_i64(&mut out, timestamp);
out
}
pub(crate) fn artifact_bytes(
signer_public_key: &[u8; 32],
payload_hash: &[u8; 32],
timestamp: i64,
signature: &[u8; 64],
) -> Vec<u8> {
let protected = protected_bytes(signer_public_key);
let claims = claims_bytes(payload_hash, timestamp);
let mut out = Vec::with_capacity(199);
write_head(&mut out, MAJOR_TAG, COSE_SIGN1_TAG);
write_head(&mut out, MAJOR_ARRAY, 4);
write_bytes(&mut out, &protected);
write_head(&mut out, MAJOR_MAP, 0);
write_bytes(&mut out, &claims);
write_bytes(&mut out, signature);
out
}
pub(crate) struct DecodedFields {
pub(crate) signer_public_key: [u8; 32],
pub(crate) payload_hash: [u8; 32],
pub(crate) timestamp: i64,
pub(crate) signature: [u8; 64],
}
fn reject() -> TrustError {
TrustError::InvalidSignature
}
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())
}
}
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,
claims_item,
signature_item,
] = items.as_slice()
else {
return Err(reject());
};
let Value::Map(unprotected) = unprotected_item else {
return Err(reject());
};
if !unprotected.is_empty() {
return Err(reject());
}
let signature: [u8; 64] = fixed_bytes(signature_item)?;
let Value::Bytes(protected_raw) = protected_item else {
return Err(reject());
};
let Value::Map(protected) = parse_value(protected_raw)? else {
return Err(reject());
};
let [(alg_key, alg), (ct_key, ct), (kid_key, kid)] = protected.as_slice() else {
return Err(reject());
};
require_integer(alg_key, i128::from(HEADER_LABEL_ALG))?;
require_integer(alg, 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 signer_public_key: [u8; 32] = fixed_bytes(kid)?;
let Value::Bytes(claims_raw) = claims_item else {
return Err(reject());
};
let Value::Map(claims) = parse_value(claims_raw)? else {
return Err(reject());
};
let [(hash_key, hash), (ts_key, ts)] = claims.as_slice() else {
return Err(reject());
};
require_integer(hash_key, i128::from(CLAIM_KEY_PAYLOAD_HASH))?;
let payload_hash: [u8; 32] = fixed_bytes(hash)?;
require_integer(ts_key, i128::from(CLAIM_KEY_TIMESTAMP))?;
let Value::Integer(ts_int) = ts else {
return Err(reject());
};
let timestamp: i64 = (*ts_int).try_into().map_err(|_err| reject())?;
Ok(DecodedFields {
signer_public_key,
payload_hash,
timestamp,
signature,
})
}
#[cfg(test)]
#[path = "encoding_tests.rs"]
mod tests;