use std::io::Cursor;
use sha2::{Digest, Sha256};
use super::error::ScittError;
pub const MAX_COSE_INPUT_SIZE: usize = 1024 * 1024;
#[derive(Debug, Clone)]
pub struct ParsedCoseSign1 {
pub protected_bytes: Vec<u8>,
pub protected: ProtectedHeader,
pub unprotected: ciborium::Value,
pub payload: Vec<u8>,
pub signature: Vec<u8>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct ProtectedHeader {
pub alg: i64,
pub kid: [u8; 4],
pub vds: Option<i64>,
pub content_type: Option<String>,
pub cwt_iss: Option<String>,
pub cwt_iat: Option<i64>,
}
pub fn parse_cose_sign1(bytes: &[u8]) -> Result<ParsedCoseSign1, ScittError> {
if bytes.len() > MAX_COSE_INPUT_SIZE {
return Err(ScittError::OversizedInput {
max_bytes: MAX_COSE_INPUT_SIZE,
});
}
let mut cursor = Cursor::new(bytes);
let value: ciborium::Value = ciborium::de::from_reader(&mut cursor)
.map_err(|e| ScittError::CborDecodeError(e.to_string()))?;
if cursor.position() != bytes.len() as u64 {
return Err(ScittError::CborDecodeError(
"trailing bytes after COSE_Sign1".to_string(),
));
}
let inner = match value {
ciborium::Value::Tag(18, boxed) => *boxed,
ciborium::Value::Array(_) => value,
_ => return Err(ScittError::NotACoseSign1),
};
let ciborium::Value::Array(array) = inner else {
return Err(ScittError::NotACoseSign1);
};
if array.len() != 4 {
return Err(ScittError::InvalidArrayLength { found: array.len() });
}
let mut iter = array.into_iter();
let Some(ciborium::Value::Bytes(protected_bytes)) = iter.next() else {
return Err(ScittError::InvalidProtectedHeader(
"protected header must be a bstr".to_string(),
));
};
let unprotected = iter.next().ok_or_else(|| {
ScittError::InvalidProtectedHeader("missing unprotected header".to_string())
})?;
let payload = match iter.next() {
Some(ciborium::Value::Bytes(b)) => b,
Some(ciborium::Value::Null) => {
return Err(ScittError::InvalidProtectedHeader(
"detached payload (null) is not supported".to_string(),
));
}
_ => {
return Err(ScittError::InvalidProtectedHeader(
"payload must be a bstr".to_string(),
));
}
};
let Some(ciborium::Value::Bytes(signature)) = iter.next() else {
return Err(ScittError::InvalidProtectedHeader(
"signature must be a bstr".to_string(),
));
};
if signature.len() != 64 {
return Err(ScittError::InvalidSignatureLength {
actual: signature.len(),
});
}
let protected = decode_protected_header(&protected_bytes)?;
Ok(ParsedCoseSign1 {
protected_bytes,
protected,
unprotected,
payload,
signature,
})
}
fn decode_protected_header(bytes: &[u8]) -> Result<ProtectedHeader, ScittError> {
if bytes.is_empty() {
return Err(ScittError::InvalidProtectedHeader(
"protected header bytes are empty".to_string(),
));
}
let mut cursor = Cursor::new(bytes);
let value: ciborium::Value = ciborium::de::from_reader(&mut cursor)
.map_err(|e| ScittError::InvalidProtectedHeader(format!("CBOR decode: {e}")))?;
if cursor.position() != bytes.len() as u64 {
return Err(ScittError::InvalidProtectedHeader(
"trailing bytes after protected header".to_string(),
));
}
let ciborium::Value::Map(map) = value else {
return Err(ScittError::InvalidProtectedHeader(
"protected header must be a CBOR map".to_string(),
));
};
let mut alg: Option<i64> = None;
let mut kid: Option<Vec<u8>> = None;
let mut vds: Option<i64> = None;
let mut content_type: Option<String> = None;
let mut cwt_iss: Option<String> = None;
let mut cwt_iat: Option<i64> = None;
for (k, v) in map {
let label = cbor_value_to_i64(&k);
match label {
Some(1) => {
alg = cbor_value_to_i64(&v);
}
Some(3) => {
if let ciborium::Value::Text(s) = v {
content_type = Some(s);
}
}
Some(4) => {
if let ciborium::Value::Bytes(b) = v {
kid = Some(b);
}
}
Some(15) => {
if let ciborium::Value::Map(cwt_map) = v {
for (ck, cv) in cwt_map {
match cbor_value_to_i64(&ck) {
Some(1) => {
if let ciborium::Value::Text(s) = cv {
cwt_iss = Some(s);
}
}
Some(6) => {
cwt_iat = cbor_value_to_i64(&cv);
}
_ => {}
}
}
}
}
Some(395) => {
vds = cbor_value_to_i64(&v);
}
_ => {}
}
}
let alg = match alg {
None => {
return Err(ScittError::InvalidProtectedHeader(
"missing alg field (label 1)".to_string(),
));
}
Some(v) if v == -7 => v,
Some(other) => {
return Err(ScittError::UnsupportedAlgorithm(format!(
"{other} (only ES256/-7 is supported)"
)));
}
};
let kid_vec = kid.ok_or(ScittError::MissingKid)?;
if kid_vec.len() != 4 {
return Err(ScittError::InvalidProtectedHeader(format!(
"kid must be 4 bytes, got {}",
kid_vec.len()
)));
}
let kid: [u8; 4] = [kid_vec[0], kid_vec[1], kid_vec[2], kid_vec[3]];
Ok(ProtectedHeader {
alg,
kid,
vds,
content_type,
cwt_iss,
cwt_iat,
})
}
fn cbor_value_to_i64(v: &ciborium::Value) -> Option<i64> {
match v {
ciborium::Value::Integer(i) => i128::from(*i).try_into().ok(),
_ => None,
}
}
pub fn build_sig_structure(protected_bytes: &[u8], payload: &[u8]) -> Result<Vec<u8>, ScittError> {
let sig_structure = ciborium::Value::Array(vec![
ciborium::Value::Text("Signature1".to_string()),
ciborium::Value::Bytes(protected_bytes.to_vec()),
ciborium::Value::Bytes(vec![]), ciborium::Value::Bytes(payload.to_vec()),
]);
let mut out = Vec::new();
ciborium::ser::into_writer(&sig_structure, &mut out)
.map_err(|e| ScittError::CborDecodeError(format!("Sig_structure serialization: {e}")))?;
Ok(out)
}
pub fn compute_sig_structure_digest(
protected_bytes: &[u8],
payload: &[u8],
) -> Result<[u8; 32], ScittError> {
let sig_structure_bytes = build_sig_structure(protected_bytes, payload)?;
let digest = Sha256::digest(&sig_structure_bytes);
Ok(digest.into())
}
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
fn make_protected_bytes(alg: i64, kid: &[u8], vds: Option<i64>, ct: Option<&str>) -> Vec<u8> {
let mut pairs: Vec<(ciborium::Value, ciborium::Value)> = vec![
(
ciborium::Value::Integer(1.into()),
ciborium::Value::Integer(alg.into()),
),
(
ciborium::Value::Integer(4.into()),
ciborium::Value::Bytes(kid.to_vec()),
),
];
if let Some(v) = vds {
pairs.push((
ciborium::Value::Integer(395.into()),
ciborium::Value::Integer(v.into()),
));
}
if let Some(s) = ct {
pairs.push((
ciborium::Value::Integer(3.into()),
ciborium::Value::Text(s.to_string()),
));
}
let map = ciborium::Value::Map(pairs);
let mut buf = Vec::new();
ciborium::ser::into_writer(&map, &mut buf).unwrap();
buf
}
fn make_cose_sign1_bytes(
protected_bytes: Vec<u8>,
payload: Vec<u8>,
signature: Vec<u8>,
tagged: bool,
) -> Vec<u8> {
let array = ciborium::Value::Array(vec![
ciborium::Value::Bytes(protected_bytes),
ciborium::Value::Map(vec![]),
ciborium::Value::Bytes(payload),
ciborium::Value::Bytes(signature),
]);
let value = if tagged {
ciborium::Value::Tag(18, Box::new(array))
} else {
array
};
let mut buf = Vec::new();
ciborium::ser::into_writer(&value, &mut buf).unwrap();
buf
}
fn valid_kid() -> Vec<u8> {
vec![0xDE, 0xAD, 0xBE, 0xEF]
}
fn valid_signature() -> Vec<u8> {
vec![0u8; 64]
}
fn valid_payload() -> Vec<u8> {
b"test payload".to_vec()
}
#[test]
fn valid_receipt_parsing() {
let protected_bytes = make_protected_bytes(-7, &valid_kid(), Some(1), None);
let bytes = make_cose_sign1_bytes(
protected_bytes.clone(),
valid_payload(),
valid_signature(),
false,
);
let parsed = parse_cose_sign1(&bytes).unwrap();
assert_eq!(parsed.protected_bytes, protected_bytes);
assert_eq!(parsed.protected.alg, -7);
assert_eq!(parsed.protected.kid, [0xDE, 0xAD, 0xBE, 0xEF]);
assert_eq!(parsed.protected.vds, Some(1));
assert!(parsed.protected.content_type.is_none());
assert_eq!(parsed.payload, valid_payload());
assert_eq!(parsed.signature.len(), 64);
}
#[test]
fn valid_status_token_parsing() {
let protected_bytes =
make_protected_bytes(-7, &valid_kid(), None, Some("application/json"));
let bytes = make_cose_sign1_bytes(
protected_bytes.clone(),
valid_payload(),
valid_signature(),
false,
);
let parsed = parse_cose_sign1(&bytes).unwrap();
assert_eq!(parsed.protected.alg, -7);
assert_eq!(parsed.protected.vds, None);
assert_eq!(
parsed.protected.content_type.as_deref(),
Some("application/json")
);
}
#[test]
fn cwt_claims_parsed_from_label_15() {
let cwt_claims = ciborium::Value::Map(vec![
(
ciborium::Value::Integer(1.into()),
ciborium::Value::Text("tl.example.com".to_string()),
),
(
ciborium::Value::Integer(6.into()),
ciborium::Value::Integer(1_700_000_000_i64.into()),
),
]);
let pairs: Vec<(ciborium::Value, ciborium::Value)> = vec![
(
ciborium::Value::Integer(1.into()),
ciborium::Value::Integer((-7_i64).into()),
),
(
ciborium::Value::Integer(4.into()),
ciborium::Value::Bytes(valid_kid()),
),
(
ciborium::Value::Integer(395.into()),
ciborium::Value::Integer(1.into()),
),
(ciborium::Value::Integer(15.into()), cwt_claims),
];
let map = ciborium::Value::Map(pairs);
let mut protected_bytes = Vec::new();
ciborium::ser::into_writer(&map, &mut protected_bytes).unwrap();
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let parsed = parse_cose_sign1(&bytes).unwrap();
assert_eq!(parsed.protected.cwt_iss.as_deref(), Some("tl.example.com"));
assert_eq!(parsed.protected.cwt_iat, Some(1_700_000_000));
}
#[test]
fn cwt_claims_absent_returns_none() {
let protected_bytes = make_protected_bytes(-7, &valid_kid(), Some(1), None);
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let parsed = parse_cose_sign1(&bytes).unwrap();
assert!(parsed.protected.cwt_iss.is_none());
assert!(parsed.protected.cwt_iat.is_none());
}
#[test]
fn tag_18_and_untagged_both_work() {
let protected_bytes = make_protected_bytes(-7, &valid_kid(), None, None);
let payload = valid_payload();
let sig = valid_signature();
let untagged =
make_cose_sign1_bytes(protected_bytes.clone(), payload.clone(), sig.clone(), false);
let r1 = parse_cose_sign1(&untagged).unwrap();
assert_eq!(r1.protected.alg, -7);
let tagged =
make_cose_sign1_bytes(protected_bytes.clone(), payload.clone(), sig.clone(), true);
let r2 = parse_cose_sign1(&tagged).unwrap();
assert_eq!(r2.protected.alg, -7);
}
#[test]
fn error_input_too_large() {
let big = vec![0u8; MAX_COSE_INPUT_SIZE + 1];
let err = parse_cose_sign1(&big).unwrap_err();
assert!(matches!(
err,
ScittError::OversizedInput {
max_bytes: MAX_COSE_INPUT_SIZE
}
));
}
#[test]
fn error_not_cbor() {
let bytes = &[0xFF, 0xFF, 0xFF, 0xFF];
let err = parse_cose_sign1(bytes).unwrap_err();
assert!(matches!(err, ScittError::CborDecodeError(_)));
}
#[test]
fn error_valid_cbor_not_array_or_tag() {
let mut buf = Vec::new();
ciborium::ser::into_writer(&ciborium::Value::Integer(42.into()), &mut buf).unwrap();
let err = parse_cose_sign1(&buf).unwrap_err();
assert!(matches!(err, ScittError::NotACoseSign1));
}
#[test]
fn error_array_3_elements() {
let arr = ciborium::Value::Array(vec![
ciborium::Value::Bytes(vec![]),
ciborium::Value::Map(vec![]),
ciborium::Value::Bytes(vec![]),
]);
let mut buf = Vec::new();
ciborium::ser::into_writer(&arr, &mut buf).unwrap();
let err = parse_cose_sign1(&buf).unwrap_err();
assert!(matches!(err, ScittError::InvalidArrayLength { found: 3 }));
}
#[test]
fn error_array_5_elements() {
let arr = ciborium::Value::Array(vec![
ciborium::Value::Bytes(vec![]),
ciborium::Value::Map(vec![]),
ciborium::Value::Bytes(vec![]),
ciborium::Value::Bytes(vec![]),
ciborium::Value::Bytes(vec![]),
]);
let mut buf = Vec::new();
ciborium::ser::into_writer(&arr, &mut buf).unwrap();
let err = parse_cose_sign1(&buf).unwrap_err();
assert!(matches!(err, ScittError::InvalidArrayLength { found: 5 }));
}
#[test]
fn error_detached_payload_null() {
let protected_bytes = make_protected_bytes(-7, &valid_kid(), None, None);
let arr = ciborium::Value::Array(vec![
ciborium::Value::Bytes(protected_bytes),
ciborium::Value::Map(vec![]),
ciborium::Value::Null,
ciborium::Value::Bytes(valid_signature()),
]);
let mut buf = Vec::new();
ciborium::ser::into_writer(&arr, &mut buf).unwrap();
let err = parse_cose_sign1(&buf).unwrap_err();
assert!(matches!(err, ScittError::InvalidProtectedHeader(_)));
assert!(err.to_string().contains("detached"));
}
#[test]
fn error_signature_32_bytes() {
let protected_bytes = make_protected_bytes(-7, &valid_kid(), None, None);
let bytes = make_cose_sign1_bytes(protected_bytes, valid_payload(), vec![0u8; 32], false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(
err,
ScittError::InvalidSignatureLength { actual: 32 }
));
}
#[test]
fn error_signature_128_bytes() {
let protected_bytes = make_protected_bytes(-7, &valid_kid(), None, None);
let bytes = make_cose_sign1_bytes(protected_bytes, valid_payload(), vec![0u8; 128], false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(
err,
ScittError::InvalidSignatureLength { actual: 128 }
));
}
#[test]
fn error_protected_header_missing_alg() {
let map = ciborium::Value::Map(vec![(
ciborium::Value::Integer(4.into()),
ciborium::Value::Bytes(valid_kid()),
)]);
let mut protected_bytes = Vec::new();
ciborium::ser::into_writer(&map, &mut protected_bytes).unwrap();
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(err, ScittError::InvalidProtectedHeader(_)));
assert!(err.to_string().contains("alg"));
}
#[test]
fn error_wrong_alg() {
let protected_bytes = make_protected_bytes(-35, &valid_kid(), None, None);
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(err, ScittError::UnsupportedAlgorithm(_)));
assert!(err.to_string().contains("-35"));
}
#[test]
fn error_missing_kid() {
let map = ciborium::Value::Map(vec![(
ciborium::Value::Integer(1.into()),
ciborium::Value::Integer((-7_i64).into()),
)]);
let mut protected_bytes = Vec::new();
ciborium::ser::into_writer(&map, &mut protected_bytes).unwrap();
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(err, ScittError::MissingKid));
}
#[test]
fn error_kid_3_bytes() {
let protected_bytes = make_protected_bytes(-7, &[0x01, 0x02, 0x03], None, None);
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(err, ScittError::InvalidProtectedHeader(_)));
assert!(err.to_string().contains("4 bytes"));
}
#[test]
fn error_kid_5_bytes() {
let protected_bytes = make_protected_bytes(-7, &[0x01, 0x02, 0x03, 0x04, 0x05], None, None);
let bytes =
make_cose_sign1_bytes(protected_bytes, valid_payload(), valid_signature(), false);
let err = parse_cose_sign1(&bytes).unwrap_err();
assert!(matches!(err, ScittError::InvalidProtectedHeader(_)));
assert!(err.to_string().contains("4 bytes"));
}
#[test]
fn sig_structure_matches_expected_cbor() {
let protected_bytes = b"\xa1\x01\x26"; let payload = b"hello";
let sig_structure = build_sig_structure(protected_bytes, payload).unwrap();
let decoded: ciborium::Value = ciborium::de::from_reader(sig_structure.as_slice()).unwrap();
match decoded {
ciborium::Value::Array(arr) => {
assert_eq!(arr.len(), 4);
assert_eq!(arr[0], ciborium::Value::Text("Signature1".to_string()));
assert_eq!(arr[1], ciborium::Value::Bytes(protected_bytes.to_vec()));
assert_eq!(arr[2], ciborium::Value::Bytes(vec![]));
assert_eq!(arr[3], ciborium::Value::Bytes(payload.to_vec()));
}
other => panic!("Expected Array, got: {other:?}"),
}
}
#[test]
fn sig_structure_uses_protected_bytes_verbatim() {
let raw_protected = b"\xff\xfe\xfd";
let payload = b"payload";
let sig_structure = build_sig_structure(raw_protected, payload).unwrap();
let decoded: ciborium::Value = ciborium::de::from_reader(sig_structure.as_slice()).unwrap();
match decoded {
ciborium::Value::Array(arr) => {
assert_eq!(arr[1], ciborium::Value::Bytes(raw_protected.to_vec()));
}
other => panic!("Expected Array, got: {other:?}"),
}
}
#[test]
fn digest_is_sha256_of_sig_structure() {
let protected_bytes = b"\xa1\x01\x26";
let payload = b"hello";
let sig_structure = build_sig_structure(protected_bytes, payload).unwrap();
let expected: [u8; 32] = Sha256::digest(&sig_structure).into();
let actual = compute_sig_structure_digest(protected_bytes, payload).unwrap();
assert_eq!(actual, expected);
}
}