Skip to main content

runmat_execution_artifact/program/
native_object.rs

1use minicbor::{Decoder, Encoder};
2use runmat_execution::Digest;
3use serde::{Deserialize, Serialize};
4
5use crate::{ArtifactError, ArtifactResult};
6
7pub const NATIVE_OBJECT_PAYLOAD_SCHEMA_VERSION: u16 = 1;
8const PREFIX: &[u8] = b"runmat-native-object-payload-v1\0";
9const MAX_METADATA_BYTES: usize = 8 * 1024 * 1024;
10const MAX_OBJECT_BYTES: usize = 512 * 1024 * 1024;
11
12#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct NativeObjectPayload {
15    pub schema_version: u16,
16    pub object_format: String,
17    pub metadata_digest: Digest,
18    pub object_digest: Digest,
19    pub metadata: Vec<u8>,
20    pub object: Vec<u8>,
21}
22
23impl NativeObjectPayload {
24    pub fn new(
25        object_format: impl Into<String>,
26        metadata: Vec<u8>,
27        object: Vec<u8>,
28    ) -> ArtifactResult<Self> {
29        let payload = Self {
30            schema_version: NATIVE_OBJECT_PAYLOAD_SCHEMA_VERSION,
31            object_format: object_format.into(),
32            metadata_digest: Digest::sha256(&metadata),
33            object_digest: Digest::sha256(&object),
34            metadata,
35            object,
36        };
37        payload.validate()?;
38        Ok(payload)
39    }
40
41    pub fn validate(&self) -> ArtifactResult<()> {
42        if self.schema_version != NATIVE_OBJECT_PAYLOAD_SCHEMA_VERSION
43            || self.object_format.is_empty()
44            || self.object_format.len() > 32
45            || !self.object_format.is_ascii()
46            || self.object_format.chars().any(char::is_control)
47            || self.metadata.is_empty()
48            || self.metadata.len() > MAX_METADATA_BYTES
49            || self.object.is_empty()
50            || self.object.len() > MAX_OBJECT_BYTES
51            || self.metadata_digest != Digest::sha256(&self.metadata)
52            || self.object_digest != Digest::sha256(&self.object)
53        {
54            return Err(ArtifactError::Invalid(
55                "native object payload is invalid or exceeds its bounds".into(),
56            ));
57        }
58        Ok(())
59    }
60
61    pub fn to_canonical_bytes(&self) -> ArtifactResult<Vec<u8>> {
62        self.validate()?;
63        let mut bytes = PREFIX.to_vec();
64        let mut encoder = Encoder::new(&mut bytes);
65        encoder
66            .array(6)
67            .and_then(|encoder| encoder.u16(self.schema_version))
68            .and_then(|encoder| encoder.str(&self.object_format))
69            .and_then(|encoder| encoder.bytes(self.metadata_digest.bytes()))
70            .and_then(|encoder| encoder.bytes(self.object_digest.bytes()))
71            .and_then(|encoder| encoder.bytes(&self.metadata))
72            .and_then(|encoder| encoder.bytes(&self.object))
73            .map_err(|error| ArtifactError::Encoding(error.to_string()))?;
74        Ok(bytes)
75    }
76
77    pub fn from_canonical_bytes(bytes: &[u8]) -> ArtifactResult<Self> {
78        let encoded = bytes.strip_prefix(PREFIX).ok_or_else(|| {
79            ArtifactError::Invalid("native object payload prefix is invalid".into())
80        })?;
81        let mut decoder = Decoder::new(encoded);
82        if decoder.array().map_err(decoding)? != Some(6) {
83            return Err(ArtifactError::Invalid(
84                "native object payload field count is invalid".into(),
85            ));
86        }
87        let schema_version = decoder.u16().map_err(decoding)?;
88        let object_format = decoder.str().map_err(decoding)?.to_string();
89        let metadata_digest = decode_digest(&mut decoder)?;
90        let object_digest = decode_digest(&mut decoder)?;
91        let metadata = decode_bounded_bytes(&mut decoder, MAX_METADATA_BYTES, "metadata")?;
92        let object = decode_bounded_bytes(&mut decoder, MAX_OBJECT_BYTES, "object")?;
93        if decoder.position() != encoded.len() {
94            return Err(ArtifactError::Invalid(
95                "native object payload has trailing data".into(),
96            ));
97        }
98        let payload = Self {
99            schema_version,
100            object_format,
101            metadata_digest,
102            object_digest,
103            metadata,
104            object,
105        };
106        payload.validate()?;
107        Ok(payload)
108    }
109}
110
111fn decode_digest(decoder: &mut Decoder<'_>) -> ArtifactResult<Digest> {
112    let bytes = decoder.bytes().map_err(decoding)?;
113    let bytes: [u8; 32] = bytes.try_into().map_err(|_| {
114        ArtifactError::Invalid("native object payload digest length is invalid".into())
115    })?;
116    Ok(Digest::from_bytes(bytes))
117}
118
119fn decode_bounded_bytes(
120    decoder: &mut Decoder<'_>,
121    maximum: usize,
122    label: &str,
123) -> ArtifactResult<Vec<u8>> {
124    let bytes = decoder.bytes().map_err(decoding)?;
125    if bytes.is_empty() || bytes.len() > maximum {
126        return Err(ArtifactError::Invalid(format!(
127            "native object {label} exceeds its payload bound"
128        )));
129    }
130    Ok(bytes.to_vec())
131}
132
133fn decoding(error: minicbor::decode::Error) -> ArtifactError {
134    ArtifactError::Encoding(error.to_string())
135}
136
137#[cfg(test)]
138mod tests {
139    use super::NativeObjectPayload;
140
141    #[test]
142    fn canonical_payload_round_trips_and_rejects_tampering() {
143        let payload =
144            NativeObjectPayload::new("mach-o", b"metadata".to_vec(), b"object".to_vec()).unwrap();
145        let bytes = payload.to_canonical_bytes().unwrap();
146        assert_eq!(
147            NativeObjectPayload::from_canonical_bytes(&bytes).unwrap(),
148            payload
149        );
150
151        let mut tampered = payload;
152        tampered.object.push(0);
153        assert!(tampered.validate().is_err());
154    }
155}