use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use serde_cbor::tags::Tagged;
use std::collections::BTreeMap;
#[derive(Debug, Deserialize)]
pub struct HeaderMap(pub BTreeMap<serde_cbor::Value, serde_cbor::Value>);
#[derive(Debug, Deserialize)]
pub struct CoseSign1 {
pub protected: ByteBuf,
pub unprotected: HeaderMap,
pub payload: ByteBuf,
pub signature: ByteBuf,
}
impl CoseSign1 {
pub fn from_bytes(bytes: &[u8]) -> eyre::Result<Self> {
let tagged: Tagged<Self> =
serde_cbor::from_slice(bytes).map_err(|e| eyre::eyre!("COSE_Sign1 CBOR decode failed: {e}"))?;
match tagged.tag {
None | Some(18) => {}
Some(tag) => {
return Err(eyre::eyre!("invalid COSE tag: expected 18 (COSE_Sign1), got {tag}"));
}
}
let _: HeaderMap = serde_cbor::from_slice(tagged.value.protected.as_slice())
.map_err(|e| eyre::eyre!("protected header decode failed: {e}"))?;
Ok(tagged.value)
}
pub fn sig_structure_bytes(&self) -> eyre::Result<Vec<u8>> {
let sig_structure = serde_cbor::Value::Array(vec![
serde_cbor::Value::Text("Signature1".to_string()),
serde_cbor::Value::Bytes(self.protected.to_vec()),
serde_cbor::Value::Bytes(vec![]),
serde_cbor::Value::Bytes(self.payload.to_vec()),
]);
serde_cbor::to_vec(&sig_structure).map_err(|e| eyre::eyre!("Sig_structure CBOR encode failed: {e}"))
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AttestationDocument {
pub module_id: String,
pub timestamp: u64,
pub digest: String,
pub pcrs: BTreeMap<u64, ByteBuf>,
pub certificate: ByteBuf,
pub cabundle: Vec<ByteBuf>,
pub public_key: Option<ByteBuf>,
pub user_data: Option<ByteBuf>,
pub nonce: Option<ByteBuf>,
}
impl AttestationDocument {
pub fn pcr0(&self) -> Option<&[u8]> {
self.pcrs.get(&0).map(|b| b.as_ref())
}
pub fn user_data_bytes(&self) -> Option<&[u8]> {
self.user_data.as_ref().map(|b| b.as_ref())
}
}
pub fn parse_attestation(bytes: &[u8]) -> eyre::Result<(CoseSign1, AttestationDocument)> {
let cose = CoseSign1::from_bytes(bytes)?;
let doc: AttestationDocument = serde_cbor::from_slice(&cose.payload)
.map_err(|e| eyre::eyre!("attestation document CBOR decode failed: {e}"))?;
Ok((cose, doc))
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &[u8] = include_bytes!("../../../../circuits/sp1-attestation/tests/fixtures/attestation_1.report");
const ROOT_CA_DER: &[u8] = include_bytes!("../../../../circuits/sp1-attestation/tests/fixtures/aws_root.der");
#[test]
fn test_parse_attestation_document() {
let (cose, doc) = parse_attestation(FIXTURE).unwrap();
assert!(!cose.protected.is_empty());
assert!(!cose.payload.is_empty());
assert!(!cose.signature.is_empty());
assert!(!doc.module_id.is_empty());
assert_eq!(doc.digest, "SHA384");
assert!(doc.timestamp > 0);
}
#[test]
fn test_pcr0_extraction() {
let (_cose, doc) = parse_attestation(FIXTURE).unwrap();
let pcr0 = doc.pcr0().expect("PCR0 should be present");
assert_eq!(pcr0.len(), 48, "PCR0 must be 48 bytes (SHA-384)");
}
#[test]
fn test_certificate_chain() {
let (_cose, doc) = parse_attestation(FIXTURE).unwrap();
assert!(!doc.certificate.is_empty(), "leaf cert must be present");
assert!(!doc.cabundle.is_empty(), "CA bundle must not be empty");
assert!(
doc.cabundle.len() >= 2,
"expected at least 2 certs in CA bundle, got {}",
doc.cabundle.len()
);
}
#[test]
fn test_root_ca_fixture_is_valid_der() {
assert_eq!(ROOT_CA_DER.len(), 533);
assert_eq!(
ROOT_CA_DER[0], 0x30,
"root CA should be DER-encoded (starts with SEQUENCE tag)"
);
}
#[test]
fn test_sig_structure_construction() {
let (cose, _doc) = parse_attestation(FIXTURE).unwrap();
let sig_bytes = cose.sig_structure_bytes().unwrap();
assert!(!sig_bytes.is_empty());
let parsed: serde_cbor::Value = serde_cbor::from_slice(&sig_bytes).unwrap();
if let serde_cbor::Value::Array(arr) = parsed {
assert_eq!(arr.len(), 4);
assert_eq!(arr[0], serde_cbor::Value::Text("Signature1".to_string()));
} else {
panic!("Sig_structure should be a CBOR array");
}
}
}