1- # Copyright (c) 2023 - 2023 , Oracle and/or its affiliates. All rights reserved.
1+ # Copyright (c) 2023 - 2024 , Oracle and/or its affiliates. All rights reserved.
22# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
33
44"""This module handles in-toto version 1 attestations."""
55
6- from typing import TypedDict
6+ from __future__ import annotations
7+
8+ from collections .abc import Callable
9+ from typing import TypedDict , TypeGuard
10+
11+ from macaron .slsa_analyzer .provenance .intoto .errors import ValidateInTotoPayloadError
12+ from macaron .util import JsonType
13+
14+ # The full list of cryptographic algorithms supported in SLSA v1 provenance. These are used as keys within the digest
15+ # set of the resource descriptors within the subject.
16+ # See: https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md
17+ VALID_ALGORITHMS = [
18+ "sha256" ,
19+ "sha224" ,
20+ "sha384" ,
21+ "sha512" ,
22+ "sha512_224" ,
23+ "sha512_256" ,
24+ "sha3_224" ,
25+ "sha3_256" ,
26+ "sha3_384" ,
27+ "sha3_512" ,
28+ "shake128" ,
29+ "shake256" ,
30+ "blake2b" ,
31+ "blake2s" ,
32+ "ripemd160" ,
33+ "sm3" ,
34+ "gost" ,
35+ "sha1" ,
36+ "md5" ,
37+ ]
738
839
940class InTotoV1Statement (TypedDict ):
@@ -12,3 +43,171 @@ class InTotoV1Statement(TypedDict):
1243 This is the type of the payload in a version 1 in-toto attestation.
1344 Specification: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md.
1445 """
46+
47+ _type : str
48+ subject : list [InTotoV1ResourceDescriptor ]
49+ predicateType : str # noqa: N815
50+ predicate : dict [str , JsonType ] | None
51+
52+
53+ class InTotoV1ResourceDescriptor (TypedDict ):
54+ """An in-toto resource descriptor.
55+
56+ Specification: https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md
57+ """
58+
59+ name : str | None
60+ uri : str | None
61+ digest : dict [str , str ] | None
62+ content : str | None
63+ download_location : str | None
64+ media_type : str | None
65+ annotations : dict [str , JsonType ] | None
66+
67+
68+ def validate_intoto_statement (payload : dict [str , JsonType ]) -> TypeGuard [InTotoV1Statement ]:
69+ """Validate the statement of an in-toto attestation.
70+
71+ Specification: https://github.com/in-toto/attestation/tree/main/spec/v1/statement.md.
72+
73+ Parameters
74+ ----------
75+ payload : dict[str, JsonType]
76+ The JSON statement after being base64-decoded.
77+
78+ Returns
79+ -------
80+ TypeGuard[InTotoStatement]
81+ ``True`` if the attestation statement is valid, in which case its type is narrowed to an
82+ ``InTotoStatement``; ``False`` otherwise.
83+
84+ Raises
85+ ------
86+ ValidateInTotoPayloadError
87+ When the payload does not follow the expected schema.
88+ """
89+ type_ = payload .get ("_type" )
90+ if type_ is None :
91+ raise ValidateInTotoPayloadError (
92+ "The attribute '_type' of the in-toto statement is missing." ,
93+ )
94+ if not isinstance (type_ , str ) or not type_ == "https://in-toto.io/Statement/v1" :
95+ raise ValidateInTotoPayloadError (
96+ "The value of attribute '_type' in the in-toto statement must be: 'https://in-toto.io/Statement/v1'" ,
97+ )
98+
99+ subjects_payload = payload .get ("subject" )
100+ if subjects_payload is None :
101+ raise ValidateInTotoPayloadError (
102+ "The attribute 'subject' of the in-toto statement is missing." ,
103+ )
104+ if not isinstance (subjects_payload , list ):
105+ raise ValidateInTotoPayloadError (
106+ "The value of attribute 'subject' in the in-toto statement is invalid: expecting a list." ,
107+ )
108+
109+ for subject_json in subjects_payload :
110+ validate_intoto_subject (subject_json )
111+
112+ predicate_type = payload .get ("predicateType" )
113+ if predicate_type is None :
114+ raise ValidateInTotoPayloadError (
115+ "The attribute 'predicateType' of the in-toto statement is missing." ,
116+ )
117+
118+ if not isinstance (predicate_type , str ):
119+ raise ValidateInTotoPayloadError (
120+ "The value of attribute 'predicateType' in the in-toto statement is invalid: expecting a string."
121+ )
122+
123+ predicate = payload .get ("predicate" )
124+ if predicate is not None and not isinstance (predicate , dict ):
125+ raise ValidateInTotoPayloadError (
126+ "The value attribute 'predicate' in the in-toto statement is invalid: expecting an object." ,
127+ )
128+
129+ return True
130+
131+
132+ def validate_intoto_subject (subject : JsonType ) -> TypeGuard [InTotoV1ResourceDescriptor ]:
133+ """Validate a single subject in the in-toto statement.
134+
135+ See specification: https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md
136+
137+ Parameters
138+ ----------
139+ subject : JsonType
140+ The JSON element representing a single subject.
141+
142+ Returns
143+ -------
144+ TypeGuard[InTotoV1ResourceDescriptor]
145+ ``True`` if the subject element is valid, in which case its type is narrowed to an
146+ ``InTotoV1ResourceDescriptor``; ``False`` otherwise.
147+
148+ Raises
149+ ------
150+ ValidateInTotoPayloadError
151+ When the payload does not follow the expecting schema.
152+ """
153+ if not isinstance (subject , dict ):
154+ raise ValidateInTotoPayloadError (
155+ "A subject in the in-toto statement is invalid: expecting an object." ,
156+ )
157+
158+ # At least one of 'uri', 'digest', and 'content' must be valid and present.
159+ uri = _validate_property (subject , "uri" , lambda x : isinstance (x , str ))
160+ content = _validate_property (subject , "content" , lambda x : isinstance (x , str ))
161+ digest = _validate_property (subject , "digest" , is_valid_digest_set )
162+ if not any ([uri , content , digest ]):
163+ raise ValidateInTotoPayloadError (
164+ "One of 'uri', 'digest', or 'content' must be present and valid within 'subject'."
165+ )
166+
167+ _validate_property (subject , "name" , lambda x : isinstance (x , str ))
168+ _validate_property (subject , "downloadLocation" , lambda x : isinstance (x , str ))
169+ _validate_property (subject , "mediaType" , lambda x : isinstance (x , str ))
170+ _validate_property (subject , "annotations" , lambda x : isinstance (x , dict ))
171+
172+ return True
173+
174+
175+ def is_valid_digest_set (digest : JsonType ) -> bool :
176+ """Validate the digest set.
177+
178+ Specification for the digest set: https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md.
179+
180+ Parameters
181+ ----------
182+ digest : JsonType
183+ The digest set.
184+
185+ Returns
186+ -------
187+ bool:
188+ ``True`` if the digest is valid according to the spec.
189+ """
190+ if not isinstance (digest , dict ):
191+ return False
192+ for key in digest :
193+ if key not in VALID_ALGORITHMS :
194+ return False
195+ if not isinstance (digest [key ], str ):
196+ return False
197+ return True
198+
199+
200+ def _validate_property (
201+ object_ : dict [str , JsonType ],
202+ key : str ,
203+ validator : Callable [[JsonType ], bool ],
204+ ) -> JsonType :
205+ """Validate the existence and type of target within the passed Json object."""
206+ value = object_ .get (key )
207+ if not value :
208+ return None
209+
210+ if not validator (value ):
211+ raise ValidateInTotoPayloadError (f"The attribute { key } of the in-toto subject is invalid." )
212+
213+ return value
0 commit comments