newton-core 0.7.2

newton protocol core sdk
//! CID content addressing, classification, and integrity verification.
//!
//! This module provides:
//! - **CID integrity verification**: SHA2-256 multihash validation against downloaded bytes
//! - **CIDv1 addressing**: content-address bytes the same way IPFS does (raw codec + SHA2-256)
//!
//! Content is immutable by CID, making it safe for aggressive caching.

use cid::{multihash::Multihash, Cid};
use sha2::{Digest, Sha256};

/// Multihash code for SHA2-256, per the multiformats table.
pub const SHA2_256_CODE: u64 = 0x12;
/// Multicodec for the `raw` codec (opaque bytes), per the multiformats table.
pub const RAW_CODEC: u64 = 0x55;
/// Multicodec for DAG-PB/UnixFS objects.
pub const DAG_PB_CODEC: u64 = 0x70;

/// Parse a CID used by persisted policy artifact storage.
///
/// The returned [`Cid`] retains the version, codec, and multihash so callers do
/// not need to reparse or mirror that information in another type. All CID
/// versions represented by the `cid` crate are accepted; persisted artifacts
/// currently use raw or DAG-PB.
pub fn parse_persisted_cid(cid_str: &str) -> Result<Cid, String> {
    let parsed = Cid::try_from(cid_str).map_err(|e| format!("invalid CID '{cid_str}': {e}"))?;
    if !matches!(parsed.codec(), RAW_CODEC | DAG_PB_CODEC) {
        return Err(format!("unsupported CID codec for {cid_str}: 0x{:x}", parsed.codec()));
    }
    Ok(parsed)
}

/// Validate the multihash algorithm accepted at the public object boundary.
pub fn validate_persisted_multihash(cid: &Cid) -> Result<(), String> {
    if cid.hash().code() == SHA2_256_CODE && cid.hash().digest().len() == 32 {
        Ok(())
    } else {
        Err(format!(
            "unsupported multihash for CID {cid}; only SHA2-256 is supported"
        ))
    }
}

/// Hash bytes into the multihash format used for new objects and migration sidecars.
pub fn sha2_256_multihash(data: &[u8]) -> Multihash<64> {
    let digest = Sha256::digest(data);
    Multihash::<64>::wrap(SHA2_256_CODE, digest.as_slice()).expect("sha256 fits in multihash")
}

/// Content-address `data` as an IPFS-compatible CIDv1 (raw codec + SHA2-256, base32).
///
/// This matches `ipfs add --cid-version=1 --raw-leaves` for content that fits in a
/// single block: the digest is a plain SHA2-256 of the bytes, wrapped as a v1 CID over
/// the `raw` codec. Policies referencing such CIDs keep resolving against our backend.
pub fn cid_v1_raw(data: &[u8]) -> String {
    cid_v1_raw_cid(data).to_string()
}

/// Content-address `data` as a parsed CIDv1 using the raw codec and SHA2-256.
pub fn cid_v1_raw_cid(data: &[u8]) -> Cid {
    Cid::new_v1(RAW_CODEC, sha2_256_multihash(data))
}

/// Verify that downloaded bytes match the SHA2-256 multihash embedded in a CID.
///
/// IPFS CIDs embed a content hash. This function re-hashes the downloaded data
/// and compares it against the CID's digest, catching malicious or buggy storage
/// backends that serve the wrong content.
///
/// Only SHA2-256 (multihash code 0x12) is supported — this covers all standard
/// IPFS content (CIDv0 always uses SHA2-256; CIDv1 typically does).
pub fn verify_cid_multihash(cid: &Cid, data: &[u8]) -> Result<(), String> {
    validate_persisted_multihash(cid)?;
    if cid.hash() != &sha2_256_multihash(data) {
        return Err(format!(
            "CID multihash mismatch: downloaded data hash does not match CID '{cid}'"
        ));
    }

    Ok(())
}

/// Verify that `cid` uses the raw codec and its multihash matches `data`.
pub fn verify_raw_cid(cid: &Cid, data: &[u8]) -> Result<(), String> {
    if cid.codec() != RAW_CODEC {
        return Err(format!("CID {cid} does not use the raw codec"));
    }
    verify_cid_multihash(cid, data)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cid_v1_raw_roundtrips_through_verify() {
        let data = b"newton policy wasm bytes";
        let cid = cid_v1_raw(data);
        // v1 raw CIDs are base32, lowercase, prefixed 'b'.
        assert!(cid.starts_with('b'), "expected base32 CIDv1, got {cid}");
        // The digest embedded in the CID must match the content.
        verify_raw_cid(&Cid::try_from(cid.as_str()).unwrap(), data).unwrap();
    }

    #[test]
    fn cid_v1_raw_is_deterministic() {
        assert_eq!(cid_v1_raw(b"abc"), cid_v1_raw(b"abc"));
        assert_ne!(cid_v1_raw(b"abc"), cid_v1_raw(b"abd"));
    }

    #[test]
    fn cid_v1_raw_rejects_mismatched_data() {
        let cid = cid_v1_raw(b"expected");
        assert!(verify_raw_cid(&Cid::try_from(cid.as_str()).unwrap(), b"tampered").is_err());
    }

    #[test]
    fn raw_verification_rejects_cid_v0() {
        let cid = "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4";
        assert!(verify_raw_cid(&Cid::try_from(cid).unwrap(), b"hello world").is_err());
    }

    #[test]
    fn persisted_cid_parser_returns_codec_information() {
        let cidv0 = "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4";
        assert_eq!(parse_persisted_cid(cidv0).unwrap().codec(), DAG_PB_CODEC);

        let digest = Sha256::digest(b"dag-pb block");
        let mh = Multihash::<64>::wrap(SHA2_256_CODE, digest.as_slice()).unwrap();
        let cidv1 = Cid::new_v1(DAG_PB_CODEC, mh).to_string();
        assert_eq!(parse_persisted_cid(&cidv1).unwrap().codec(), DAG_PB_CODEC);
    }

    #[test]
    fn persisted_cid_parser_rejects_invalid_cid() {
        let result = parse_persisted_cid("not-a-valid-cid!!!");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("invalid CID"));
    }
}