uor_addr/schema/
codemodule_signed.rs1use prism::pipeline::{ShapeViolation, ViolationKind};
36
37use crate::json::{JsonValue, JsonValueRef};
38
39const SCHEMA_VIOLATION: ShapeViolation = ShapeViolation {
40 shape_iri: "https://in-toto.io/Statement/v1",
41 constraint_iri: "https://in-toto.io/Statement/v1/schemaConformance",
42 property_iri: "https://in-toto.io/Statement/v1",
43 expected_range: "https://in-toto.io/Statement/v1",
44 min_count: 0,
45 max_count: 1,
46 kind: ViolationKind::ValueCheck,
47};
48
49pub const STATEMENT_TYPE_IRI: &[u8] = b"https://in-toto.io/Statement/v1";
51
52pub const SHA256_HEX_BYTES: usize = 64;
54
55pub const REQUIRED_PROPERTIES: &[&[u8]] = &[b"_type", b"subject", b"predicateType", b"predicate"];
56
57#[derive(Clone)]
58pub struct SignedCodeModuleValue {
59 inner: JsonValue,
60}
61
62impl core::fmt::Debug for SignedCodeModuleValue {
63 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64 f.debug_struct("SignedCodeModuleValue")
65 .finish_non_exhaustive()
66 }
67}
68
69impl SignedCodeModuleValue {
70 pub fn parse(raw: &[u8]) -> Result<Self, ShapeViolation> {
71 let inner = JsonValue::parse(raw).map_err(|_| SCHEMA_VIOLATION)?;
72 let root = JsonValueRef::root(&inner);
73 if !root.is_object() {
74 return Err(SCHEMA_VIOLATION);
75 }
76 let ty = root
78 .get(b"_type")
79 .and_then(|v| v.as_str())
80 .ok_or(SCHEMA_VIOLATION)?;
81 if ty != STATEMENT_TYPE_IRI {
82 return Err(SCHEMA_VIOLATION);
83 }
84 let subject = root.get(b"subject").ok_or(SCHEMA_VIOLATION)?;
86 let subjects = subject.iter_array().ok_or(SCHEMA_VIOLATION)?;
87 let mut subject_count = 0;
88 for s in subjects {
89 if !s.is_object() {
90 return Err(SCHEMA_VIOLATION);
91 }
92 let name = s
93 .get(b"name")
94 .and_then(|v| v.as_str())
95 .ok_or(SCHEMA_VIOLATION)?;
96 if name.is_empty() {
97 return Err(SCHEMA_VIOLATION);
98 }
99 let digest = s.get(b"digest").ok_or(SCHEMA_VIOLATION)?;
100 if !digest.is_object() {
101 return Err(SCHEMA_VIOLATION);
102 }
103 let digest_iter = digest.iter_object().ok_or(SCHEMA_VIOLATION)?;
106 let mut digest_count = 0;
107 for (_k, _v) in digest_iter {
108 digest_count += 1;
109 }
110 if digest_count == 0 {
111 return Err(SCHEMA_VIOLATION);
112 }
113 let sha256 = digest
114 .get(b"sha256")
115 .and_then(|v| v.as_str())
116 .ok_or(SCHEMA_VIOLATION)?;
117 if sha256.len() != SHA256_HEX_BYTES
118 || !sha256
119 .iter()
120 .all(|&b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
121 {
122 return Err(SCHEMA_VIOLATION);
123 }
124 subject_count += 1;
125 }
126 if subject_count == 0 {
127 return Err(SCHEMA_VIOLATION);
128 }
129 let pt = root
131 .get(b"predicateType")
132 .and_then(|v| v.as_str())
133 .ok_or(SCHEMA_VIOLATION)?;
134 if pt.is_empty() {
135 return Err(SCHEMA_VIOLATION);
136 }
137 let predicate = root.get(b"predicate").ok_or(SCHEMA_VIOLATION)?;
139 if !predicate.is_object() {
140 return Err(SCHEMA_VIOLATION);
141 }
142 Ok(Self { inner })
143 }
144
145 #[must_use]
146 pub fn tagged_bytes(&self) -> &[u8] {
147 self.inner.tagged_bytes()
148 }
149}
150
151pub fn address(raw: &[u8]) -> Result<crate::AddressOutcome<71>, AddressFailure> {
153 SignedCodeModuleValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
154 crate::json::address(raw).map_err(|e| match e {
155 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
156 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
157 })
158}
159
160pub fn address_blake3(raw: &[u8]) -> Result<crate::AddressOutcome<71>, AddressFailure> {
167 SignedCodeModuleValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
168 crate::json::address_blake3(raw).map_err(|e| match e {
169 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
170 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
171 })
172}
173
174pub fn address_sha3_256(raw: &[u8]) -> Result<crate::AddressOutcome<73>, AddressFailure> {
181 SignedCodeModuleValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
182 crate::json::address_sha3_256(raw).map_err(|e| match e {
183 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
184 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
185 })
186}
187
188pub fn address_keccak256(raw: &[u8]) -> Result<crate::AddressOutcome<74>, AddressFailure> {
195 SignedCodeModuleValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
196 crate::json::address_keccak256(raw).map_err(|e| match e {
197 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
198 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
199 })
200}
201
202pub fn address_sha512(raw: &[u8]) -> Result<crate::AddressOutcome<135, 64>, AddressFailure> {
208 SignedCodeModuleValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
209 crate::json::address_sha512(raw).map_err(|e| match e {
210 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
211 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
212 })
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum AddressFailure {
217 SchemaViolation,
218 PipelineFailure,
219}
220
221#[cfg(feature = "alloc")]
223pub fn canonicalize(raw: &[u8]) -> Result<alloc::vec::Vec<u8>, AddressFailure> {
224 extern crate alloc;
225 SignedCodeModuleValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
226 crate::json::canonicalize(raw).map_err(|_| AddressFailure::PipelineFailure)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 const VALID_STATEMENT: &[u8] = br#"{
234 "_type": "https://in-toto.io/Statement/v1",
235 "subject": [
236 {
237 "name": "uor-addr-v0.1.0",
238 "digest": {
239 "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
240 }
241 }
242 ],
243 "predicateType": "https://slsa.dev/provenance/v1",
244 "predicate": {
245 "buildDefinition": {"buildType": "uor:test"},
246 "runDetails": {"builder": {"id": "uor:test-builder"}}
247 }
248 }"#;
249
250 #[test]
251 fn admits_valid_in_toto_statement() {
252 let s = SignedCodeModuleValue::parse(VALID_STATEMENT).expect("valid");
253 assert!(!s.tagged_bytes().is_empty());
254 }
255
256 #[test]
257 fn admits_multiple_subjects() {
258 let raw = br#"{
259 "_type": "https://in-toto.io/Statement/v1",
260 "subject": [
261 {"name": "a", "digest": {"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}},
262 {"name": "b", "digest": {"sha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"}}
263 ],
264 "predicateType": "https://slsa.dev/provenance/v1",
265 "predicate": {}
266 }"#;
267 SignedCodeModuleValue::parse(raw).expect("valid");
268 }
269
270 #[test]
271 fn rejects_wrong_statement_type_iri() {
272 let raw = br#"{
273 "_type": "https://example.org/CustomStatement",
274 "subject": [{"name": "x", "digest": {"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}],
275 "predicateType": "x",
276 "predicate": {}
277 }"#;
278 let err = SignedCodeModuleValue::parse(raw).expect_err("wrong _type");
279 assert_eq!(err.constraint_iri, SCHEMA_VIOLATION.constraint_iri);
280 }
281
282 #[test]
283 fn rejects_empty_subject() {
284 let raw = br#"{
285 "_type": "https://in-toto.io/Statement/v1",
286 "subject": [],
287 "predicateType": "x",
288 "predicate": {}
289 }"#;
290 let err = SignedCodeModuleValue::parse(raw).expect_err("empty subject");
291 assert_eq!(err.constraint_iri, SCHEMA_VIOLATION.constraint_iri);
292 }
293
294 #[test]
295 fn rejects_subject_without_sha256_digest() {
296 let raw = br#"{
297 "_type": "https://in-toto.io/Statement/v1",
298 "subject": [{"name": "x", "digest": {"md5": "deadbeef"}}],
299 "predicateType": "x",
300 "predicate": {}
301 }"#;
302 let err = SignedCodeModuleValue::parse(raw).expect_err("no sha256");
303 assert_eq!(err.constraint_iri, SCHEMA_VIOLATION.constraint_iri);
304 }
305
306 #[test]
307 fn rejects_sha256_with_wrong_length() {
308 let raw = br#"{
309 "_type": "https://in-toto.io/Statement/v1",
310 "subject": [{"name": "x", "digest": {"sha256": "tooshort"}}],
311 "predicateType": "x",
312 "predicate": {}
313 }"#;
314 let err = SignedCodeModuleValue::parse(raw).expect_err("short hex");
315 assert_eq!(err.constraint_iri, SCHEMA_VIOLATION.constraint_iri);
316 }
317
318 #[test]
319 fn rejects_missing_predicate_type() {
320 let raw = br#"{
321 "_type": "https://in-toto.io/Statement/v1",
322 "subject": [{"name": "x", "digest": {"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}],
323 "predicate": {}
324 }"#;
325 let err = SignedCodeModuleValue::parse(raw).expect_err("missing predicateType");
326 assert_eq!(err.constraint_iri, SCHEMA_VIOLATION.constraint_iri);
327 }
328
329 #[test]
330 fn address_matches_json_realization() {
331 let from_signed = address(VALID_STATEMENT).expect("κ-label").address;
332 let from_json = crate::json::address(VALID_STATEMENT)
333 .expect("κ-label")
334 .address;
335 assert_eq!(from_signed, from_json);
336 }
337}