newton-core 0.4.16

newton protocol core sdk
//! AWS Nitro Enclave attestation document parsing.
//!
//! Parses CBOR-encoded COSE Sign1 attestation documents produced by the
//! Nitro Secure Module (NSM). Vendored from Automata Network's
//! aws-nitro-enclave-attestation (Apache-2.0) and adapted for Newton's types.
//!
//! Attestation document format:
//!   COSE_Sign1 (CBOR tag 18) → {
//!     protected: { alg: ES384 },
//!     payload: CBOR-encoded AttestationDocument,
//!     signature: P-384 ECDSA signature
//!   }

use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use serde_cbor::tags::Tagged;
use std::collections::BTreeMap;

/// CBOR map type used in COSE headers.
#[derive(Debug, Deserialize)]
pub struct HeaderMap(pub BTreeMap<serde_cbor::Value, serde_cbor::Value>);

/// COSE_Sign1 structure (RFC 8152 Section 4.2).
///
/// The attestation document is the `payload` field. The `signature` is an
/// ECDSA-P384 signature over the Sig_structure built from `protected` + `payload`.
#[derive(Debug, Deserialize)]
pub struct CoseSign1 {
    /// CBOR-encoded protected header (contains algorithm identifier)
    pub protected: ByteBuf,
    /// Unprotected header map (typically empty for Nitro)
    pub unprotected: HeaderMap,
    /// CBOR-encoded AttestationDocument
    pub payload: ByteBuf,
    /// P-384 ECDSA signature bytes
    pub signature: ByteBuf,
}

impl CoseSign1 {
    /// Parse a COSE_Sign1 structure from raw CBOR bytes.
    ///
    /// Accepts CBOR tag 18 (COSE_Sign1) or untagged.
    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)
    }

    /// Build the COSE Sig_structure for signature verification (RFC 8152 Section 4.4).
    ///
    /// Sig_structure = ["Signature1", protected, external_aad="", payload]
    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}"))
    }
}

/// AWS Nitro Enclave attestation document.
///
/// Deserialized from the COSE_Sign1 payload. Contains PCR measurements,
/// the certificate chain (cabundle), and optional fields (user_data, nonce, public_key).
#[derive(Debug, Deserialize, Serialize)]
pub struct AttestationDocument {
    /// Issuing enclave module ID
    pub module_id: String,
    /// UTC timestamp (milliseconds since epoch)
    pub timestamp: u64,
    /// Digest algorithm used (always "SHA384" for Nitro)
    pub digest: String,
    /// Platform Configuration Registers — PCR0 is the enclave image measurement
    pub pcrs: BTreeMap<u64, ByteBuf>,
    /// DER-encoded leaf certificate (enclave's attestation cert)
    pub certificate: ByteBuf,
    /// DER-encoded CA bundle (root → intermediate chain, excluding leaf)
    pub cabundle: Vec<ByteBuf>,
    /// Optional application-defined public key
    pub public_key: Option<ByteBuf>,
    /// Optional application-defined data (Newton uses keccak256(task_id || response_digest))
    pub user_data: Option<ByteBuf>,
    /// Optional nonce for replay protection
    pub nonce: Option<ByteBuf>,
}

impl AttestationDocument {
    /// Extract PCR0 (enclave image measurement) as raw 48-byte SHA-384 hash.
    pub fn pcr0(&self) -> Option<&[u8]> {
        self.pcrs.get(&0).map(|b| b.as_ref())
    }

    /// Extract user_data field (Newton's task binding).
    pub fn user_data_bytes(&self) -> Option<&[u8]> {
        self.user_data.as_ref().map(|b| b.as_ref())
    }
}

/// Parse an attestation document from raw CBOR bytes.
///
/// Returns the COSE_Sign1 wrapper (for signature verification) and the
/// parsed attestation document (for field extraction).
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)");
        // Note: test fixtures from debug enclaves may have all-zero PCR0.
        // Production enclaves always have non-zero PCR0 (SHA-384 of the EIF).
    }

    #[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");

        // Nitro attestation chains typically have 2-3 intermediates
        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() {
        // Verify the root CA DER can be parsed as X.509
        assert_eq!(ROOT_CA_DER.len(), 533);
        // DER certificates start with ASN.1 SEQUENCE tag (0x30)
        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());

        // Sig_structure is CBOR array: ["Signature1", protected, "", payload]
        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");
        }
    }
}