use ciborium::value::Value;
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 MAJOR_UNSIGNED: u8 = 0;
const MAJOR_NEGATIVE: u8 = 1;
const MAJOR_BYTES: u8 = 2;
const MAJOR_TEXT: u8 = 3;
const MAJOR_ARRAY: u8 = 4;
const MAJOR_MAP: u8 = 5;
const MAJOR_TAG: u8 = 6;
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;
fn write_head(out: &mut Vec<u8>, major: u8, value: u64) {
let major_bits = major << 5;
if let Ok(small) = u8::try_from(value) {
if small < 24 {
out.push(major_bits | small);
} else {
out.push(major_bits | 0x18);
out.push(small);
}
} else if let Ok(v) = u16::try_from(value) {
out.push(major_bits | 0x19);
out.extend_from_slice(&v.to_be_bytes());
} else if let Ok(v) = u32::try_from(value) {
out.push(major_bits | 0x1a);
out.extend_from_slice(&v.to_be_bytes());
} else {
out.push(major_bits | 0x1b);
out.extend_from_slice(&value.to_be_bytes());
}
}
fn write_i64(out: &mut Vec<u8>, value: i64) {
let (major, magnitude) = if value >= 0 {
(MAJOR_UNSIGNED, value.unsigned_abs())
} else {
(MAJOR_NEGATIVE, value.unsigned_abs() - 1)
};
write_head(out, major, magnitude);
}
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_head(&mut out, MAJOR_TEXT, CONTENT_TYPE.len() as u64);
out.extend_from_slice(CONTENT_TYPE.as_bytes());
write_head(&mut out, MAJOR_UNSIGNED, HEADER_LABEL_KID);
write_head(&mut out, MAJOR_BYTES, signer_public_key.len() as u64);
out.extend_from_slice(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_head(&mut out, MAJOR_BYTES, payload_hash.len() as u64);
out.extend_from_slice(payload_hash);
write_head(&mut out, MAJOR_UNSIGNED, CLAIM_KEY_TIMESTAMP);
write_i64(&mut out, timestamp);
out
}
pub(crate) fn sig_structure_bytes(protected: &[u8], claims: &[u8]) -> Vec<u8> {
const CONTEXT: &str = "Signature1";
let mut out = Vec::with_capacity(143);
write_head(&mut out, MAJOR_ARRAY, 4);
write_head(&mut out, MAJOR_TEXT, CONTEXT.len() as u64);
out.extend_from_slice(CONTEXT.as_bytes());
write_head(&mut out, MAJOR_BYTES, protected.len() as u64);
out.extend_from_slice(protected);
write_head(&mut out, MAJOR_BYTES, 0);
write_head(&mut out, MAJOR_BYTES, claims.len() as u64);
out.extend_from_slice(claims);
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_head(&mut out, MAJOR_BYTES, protected.len() as u64);
out.extend_from_slice(&protected);
write_head(&mut out, MAJOR_MAP, 0);
write_head(&mut out, MAJOR_BYTES, claims.len() as u64);
out.extend_from_slice(&claims);
write_head(&mut out, MAJOR_BYTES, signature.len() as u64);
out.extend_from_slice(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;