Skip to content

Commit d6ffe44

Browse files
committed
verifier: implemented rules-based OCI spec validation
Signed-off-by: Simon Ott <[email protected]>
1 parent 28c9a27 commit d6ffe44

File tree

3 files changed

+623
-15
lines changed

3 files changed

+623
-15
lines changed

Diff for: verifier/ocivalidation.go

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Copyright (c) 2025 Fraunhofer AISEC
2+
// Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package verifier
17+
18+
import (
19+
"fmt"
20+
"reflect"
21+
"strings"
22+
23+
ar "github.com/Fraunhofer-AISEC/cmc/attestationreport"
24+
"github.com/opencontainers/runtime-spec/specs-go"
25+
)
26+
27+
func ConvertSpec(s ar.Serializer, spec *specs.Spec) (map[string]interface{}, error) {
28+
29+
data, err := s.Marshal(spec)
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to marshal config: %w", err)
32+
}
33+
34+
conv := make(map[string]interface{})
35+
err = s.Unmarshal(data, &conv)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to unmarshal config to map: %w", err)
38+
}
39+
40+
return conv, nil
41+
}
42+
43+
func ValidateConfig(reference, measurement, rules map[string]interface{}) error {
44+
45+
log.Tracef("Running recursive config validation..")
46+
47+
// Ensure all fields in the reference config have a corresponding measurement value
48+
// according to the rules defined in the rules config
49+
for key, refValue := range reference {
50+
51+
// Get rules and measurement property for reference property
52+
rule, ruleExists := rules[key]
53+
measureValue, measurementExists := measurement[key]
54+
55+
if !ruleExists {
56+
// If no rule is defined, treat the value as mandatory, i.e., enforce exact match
57+
log.Tracef(" No rule defined for field '%v'", key)
58+
if !reflect.DeepEqual(refValue, measureValue) {
59+
return fmt.Errorf(" field '%v' is implicitly mandatory and must match reference", key)
60+
} else {
61+
log.Tracef(" Field '%v' matches reference", key)
62+
}
63+
} else {
64+
65+
// Else validate based on the rule
66+
switch ruleType := rule.(type) {
67+
case string: // Rule specifies a JSON scalar or array of scalars
68+
69+
log.Debugf(" Rule %v defined for scalar field '%v'", rule, key)
70+
71+
if rule != "mandatory" && rule != "optional" && rule != "modifiable" && rule != "subset" {
72+
return fmt.Errorf(" unsupported rule type: %v", rule)
73+
}
74+
75+
if reflect.DeepEqual(refValue, measureValue) {
76+
log.Tracef(" Field '%v' is %v and matches reference", key, rule)
77+
continue
78+
}
79+
if ruleType == "optional" {
80+
if !measurementExists {
81+
log.Tracef(" Field '%v' is optional and missing in target config", key)
82+
continue
83+
} else {
84+
return fmt.Errorf(" field '%v' is optional, present, and has different value %v (vs. %v)",
85+
key, measureValue, refValue)
86+
}
87+
} else if ruleType == "modifiable" {
88+
log.Tracef(" Field '%v' is %v and was modified (%v to %v)",
89+
key, rule, refValue, measureValue)
90+
continue
91+
} else if ruleType == "subset" {
92+
refArr, refOk := refValue.([]interface{})
93+
measurementArr, measurementOk := measureValue.([]interface{})
94+
if !refOk || !measurementOk {
95+
return fmt.Errorf(" fields '%v' must be JSON array type: ref: %T, meas: %T",
96+
key, refValue, measureValue)
97+
}
98+
if isSubset(measurementArr, refArr) {
99+
log.Tracef(" Field '%v' is subset of reference", key)
100+
continue
101+
} else {
102+
return fmt.Errorf(" field '%v' is not a subset (%v vs. %v)",
103+
key, refValue, measureValue)
104+
}
105+
}
106+
107+
case []interface{}: // Rule specifies a JSON array of nested objects
108+
log.Debugf(" Rules defined for array %v", key)
109+
110+
refArr, refOk := refValue.([]interface{})
111+
measurementArr, measurementOk := measureValue.([]interface{})
112+
if !refOk || !measurementOk {
113+
return fmt.Errorf(" nested array field '%v' must be a JSON object: ref: %v, meas: %v",
114+
key, refOk, measurementOk)
115+
}
116+
if len(ruleType) > len(refArr) || len(ruleType) > len(measurementArr) {
117+
return fmt.Errorf(" field '%v' has %v elements, but rule has %v elements",
118+
key, len(refArr), len(ruleType))
119+
}
120+
if len(measurementArr) != len(refArr) {
121+
return fmt.Errorf(" field '%v' has %v elements, but reference has %v elements",
122+
key, len(measurementArr), len(refArr))
123+
}
124+
125+
for i := range refArr {
126+
127+
if i >= len(ruleType) {
128+
log.Tracef(" No rule defined for array element %v: %v, implicit mandatory",
129+
key, i)
130+
if reflect.DeepEqual(refArr[i], measurementArr[i]) {
131+
log.Tracef(" Array element %v matches reference", i)
132+
continue
133+
} else {
134+
return fmt.Errorf(" array element %v must match reference (%v vs. %v)",
135+
i, measurementArr[i], refArr[i])
136+
}
137+
}
138+
139+
switch innerRuleType := ruleType[i].(type) {
140+
case map[string]interface{}:
141+
refElem, ok := refArr[i].(map[string]interface{})
142+
if !ok {
143+
return fmt.Errorf(" nested ref array field elements in '%v' must be JSON objects",
144+
key)
145+
}
146+
measElem, ok := measurementArr[i].(map[string]interface{})
147+
if !ok {
148+
return fmt.Errorf(" nested array field elements in %v must be JSON objects",
149+
key)
150+
}
151+
err := ValidateConfig(refElem, measElem, innerRuleType)
152+
if err != nil {
153+
return err
154+
}
155+
default:
156+
log.Errorf(" unknown ruletype: %v", innerRuleType)
157+
}
158+
}
159+
continue
160+
161+
case map[string]interface{}: // Rule specifies a nested JSON object
162+
log.Debugf(" Rules defined for nested object %v", key)
163+
refMap, refOk := refValue.(map[string]interface{})
164+
measurementMap, measurementOk := measureValue.(map[string]interface{})
165+
if !refOk || !measurementOk {
166+
return fmt.Errorf(" nested field '%v' must be a JSON object", key)
167+
}
168+
err := ValidateConfig(refMap, measurementMap, ruleType)
169+
if err != nil {
170+
return err
171+
}
172+
continue
173+
174+
default:
175+
log.Warnf(" Unsupported rule type: %v", ruleType)
176+
177+
}
178+
179+
return fmt.Errorf(" field '%v' is %v and does not match reference (%v vs. %v)",
180+
key, rule, measureValue, refValue)
181+
}
182+
}
183+
184+
// Ensure all fields in the measurement config are in the reference config or marked as additional
185+
for key := range measurement {
186+
// If the measurement key exists in the reference, it has already been validated
187+
err := checkAdditionalKey(key, reference, measurement, rules)
188+
if err != nil {
189+
return fmt.Errorf(" %w", err)
190+
}
191+
}
192+
193+
log.Tracef("Returning from recursive validation")
194+
195+
return nil
196+
}
197+
198+
func checkAdditionalKey(key string, reference, measurement, rules map[string]interface{}) error {
199+
if _, exists := reference[key]; !exists {
200+
// If it does not exist, there must be an additional rule defined for the key
201+
rule, ruleExists := rules[key]
202+
if !ruleExists {
203+
return fmt.Errorf("unexpected field '%v' found in measurement config", key)
204+
}
205+
switch ruleType := rule.(type) {
206+
case string:
207+
// Check if an "additional" rule was defined
208+
ruleParts := strings.SplitN(ruleType, ":", 2)
209+
if ruleParts[0] != "additional" {
210+
return fmt.Errorf("measurement-only key %v is not defined as additional (%v)", key, ruleType)
211+
}
212+
// Check if specific additional values have been specified
213+
if len(ruleParts) == 2 {
214+
if ruleParts[1] != measurement[key] {
215+
return fmt.Errorf("additional measurement-only key %v does not have value %v (%v)",
216+
key, measurement[key], ruleParts[1])
217+
}
218+
log.Tracef("Allow additional measurement-only key %v with specific value %v", key, ruleParts[1])
219+
} else {
220+
log.Tracef("Allow additional measurement-only key %v with arbitrary value", key)
221+
}
222+
default:
223+
return fmt.Errorf("unsupported rule type for measurement-only key %v: %v", key, ruleType)
224+
}
225+
}
226+
return nil
227+
}
228+
229+
// isSubset checks if a slice is a subset of another slice
230+
func isSubset(subset, superset []interface{}) bool {
231+
for _, subElem := range subset {
232+
found := false
233+
for _, superElem := range superset {
234+
if reflect.DeepEqual(subElem, superElem) {
235+
found = true
236+
break
237+
}
238+
}
239+
if !found {
240+
return false
241+
}
242+
}
243+
return true
244+
}

0 commit comments

Comments
 (0)