• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

in-toto / in-toto-golang / 6488915271

11 Oct 2023 10:59PM UTC coverage: 90.351% (-0.3%) from 90.602%
6488915271

Pull #274

github

web-flow
chore(deps): bump golang.org/x/net from 0.12.0 to 0.17.0

Bumps [golang.org/x/net](https://github.com/golang/net) from 0.12.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.12.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #274: chore(deps): bump golang.org/x/net from 0.12.0 to 0.17.0

2163 of 2394 relevant lines covered (90.35%)

244.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.74
/in_toto/verifylib.go
1
/*
2
Package in_toto implements types and routines to verify a software supply chain
3
according to the in-toto specification.
4
See https://github.com/in-toto/docs/blob/master/in-toto-spec.md
5
*/
6
package in_toto
7

8
import (
9
        "crypto/x509"
10
        "errors"
11
        "fmt"
12
        "io"
13
        "os"
14
        "path"
15
        "path/filepath"
16
        "reflect"
17
        "regexp"
18
        "strings"
19
        "time"
20
)
21

22
// ErrInspectionRunDirIsSymlink gets thrown if the runDir is a symlink
23
var ErrInspectionRunDirIsSymlink = errors.New("runDir is a symlink. This is a security risk")
24

25
var ErrNotLayout = errors.New("verification workflow passed a non-layout")
26

27
/*
28
RunInspections iteratively executes the command in the Run field of all
29
inspections of the passed layout, creating unsigned link metadata that records
30
all files found in the current working directory as materials (before command
31
execution) and products (after command execution).  A map with inspection names
32
as keys and Metablocks containing the generated link metadata as values is
33
returned.  The format is:
34

35
        {
36
                <inspection name> : Metablock,
37
                <inspection name> : Metablock,
38
                ...
39
        }
40

41
If executing the inspection command fails, or if the executed command has a
42
non-zero exit code, the first return value is an empty Metablock map and the
43
second return value is the error.
44
*/
45
func RunInspections(layout Layout, runDir string, lineNormalization bool, useDSSE bool) (map[string]Metadata, error) {
18✔
46
        inspectionMetadata := make(map[string]Metadata)
18✔
47

18✔
48
        for _, inspection := range layout.Inspect {
36✔
49

18✔
50
                paths := []string{"."}
18✔
51
                if runDir != "" {
20✔
52
                        paths = []string{runDir}
2✔
53
                }
2✔
54

55
                linkEnv, err := InTotoRun(inspection.Name, runDir, paths, paths,
18✔
56
                        inspection.Run, Key{}, []string{"sha256"}, nil, nil, lineNormalization, false, useDSSE)
18✔
57

18✔
58
                if err != nil {
20✔
59
                        return nil, err
2✔
60
                }
2✔
61

62
                retVal := linkEnv.GetPayload().(Link).ByProducts["return-value"]
16✔
63
                if retVal != float64(0) {
18✔
64
                        return nil, fmt.Errorf("inspection command '%s' of inspection '%s'"+
2✔
65
                                " returned a non-zero value: %d", inspection.Run, inspection.Name,
2✔
66
                                retVal)
2✔
67
                }
2✔
68

69
                // Dump inspection link to cwd using the short link name format
70
                linkName := fmt.Sprintf(LinkNameFormatShort, inspection.Name)
14✔
71
                if err := linkEnv.Dump(linkName); err != nil {
14✔
72
                        fmt.Printf("JSON serialization or writing failed: %s", err)
×
73
                }
×
74

75
                inspectionMetadata[inspection.Name] = linkEnv
14✔
76
        }
77
        return inspectionMetadata, nil
14✔
78
}
79

80
// verifyMatchRule is a helper function to process artifact rules of
81
// type MATCH. See VerifyArtifacts for more details.
82
func verifyMatchRule(ruleData map[string]string,
83
        srcArtifacts map[string]HashObj, srcArtifactQueue Set,
84
        itemsMetadata map[string]Metadata) Set {
58✔
85
        consumed := NewSet()
58✔
86
        // Get destination link metadata
58✔
87
        dstLinkEnv, exists := itemsMetadata[ruleData["dstName"]]
58✔
88
        if !exists {
62✔
89
                // Destination link does not exist, rule can't consume any
4✔
90
                // artifacts
4✔
91
                return consumed
4✔
92
        }
4✔
93

94
        // Get artifacts from destination link metadata
95
        var dstArtifacts map[string]HashObj
54✔
96
        switch ruleData["dstType"] {
54✔
97
        case "materials":
16✔
98
                dstArtifacts = dstLinkEnv.GetPayload().(Link).Materials
16✔
99
        case "products":
38✔
100
                dstArtifacts = dstLinkEnv.GetPayload().(Link).Products
38✔
101
        }
102

103
        // cleanup paths in pattern and artifact maps
104
        if ruleData["pattern"] != "" {
108✔
105
                ruleData["pattern"] = path.Clean(ruleData["pattern"])
54✔
106
        }
54✔
107
        for k := range srcArtifacts {
1,027✔
108
                if path.Clean(k) != k {
975✔
109
                        srcArtifacts[path.Clean(k)] = srcArtifacts[k]
2✔
110
                        delete(srcArtifacts, k)
2✔
111
                }
2✔
112
        }
113
        for k := range dstArtifacts {
126✔
114
                if path.Clean(k) != k {
74✔
115
                        dstArtifacts[path.Clean(k)] = dstArtifacts[k]
2✔
116
                        delete(dstArtifacts, k)
2✔
117
                }
2✔
118
        }
119

120
        // Normalize optional source and destination prefixes, i.e. if
121
        // there is a prefix, then add a trailing slash if not there yet
122
        for _, prefix := range []string{"srcPrefix", "dstPrefix"} {
162✔
123
                if ruleData[prefix] != "" {
112✔
124
                        ruleData[prefix] = path.Clean(ruleData[prefix])
4✔
125
                        if !strings.HasSuffix(ruleData[prefix], "/") {
8✔
126
                                ruleData[prefix] += "/"
4✔
127
                        }
4✔
128
                }
129
        }
130
        // Iterate over queue and mark consumed artifacts
131
        for srcPath := range srcArtifactQueue {
1,018✔
132
                // Remove optional source prefix from source artifact path
964✔
133
                // Noop if prefix is empty, or artifact does not have it
964✔
134
                srcBasePath := strings.TrimPrefix(srcPath, ruleData["srcPrefix"])
964✔
135

964✔
136
                // Ignore artifacts not matched by rule pattern
964✔
137
                matched, err := match(ruleData["pattern"], srcBasePath)
964✔
138
                if err != nil || !matched {
1,870✔
139
                        continue
906✔
140
                }
141

142
                // Construct corresponding destination artifact path, i.e.
143
                // an optional destination prefix plus the source base path
144
                dstPath := path.Clean(path.Join(ruleData["dstPrefix"], srcBasePath))
58✔
145

58✔
146
                // Try to find the corresponding destination artifact
58✔
147
                dstArtifact, exists := dstArtifacts[dstPath]
58✔
148
                // Ignore artifacts without corresponding destination artifact
58✔
149
                if !exists {
60✔
150
                        continue
2✔
151
                }
152

153
                // Ignore artifact pairs with no matching hashes
154
                if !reflect.DeepEqual(srcArtifacts[srcPath], dstArtifact) {
60✔
155
                        continue
4✔
156
                }
157

158
                // Only if a source and destination artifact pair was found and
159
                // their hashes are equal, will we mark the source artifact as
160
                // successfully consumed, i.e. it will be removed from the queue
161
                consumed.Add(srcPath)
52✔
162
        }
163
        return consumed
54✔
164
}
165

166
/*
167
VerifyArtifacts iteratively applies the material and product rules of the
168
passed items (step or inspection) to enforce and authorize artifacts (materials
169
or products) reported by the corresponding link and to guarantee that
170
artifacts are linked together across links.  In the beginning all artifacts are
171
placed in a queue according to their type.  If an artifact gets consumed by a
172
rule it is removed from the queue.  An artifact can only be consumed once in
173
the course of processing the set of rules in ExpectedMaterials or
174
ExpectedProducts.
175

176
Rules of type MATCH, ALLOW, CREATE, DELETE, MODIFY and DISALLOW are supported.
177

178
All rules except for DISALLOW consume queued artifacts on success, and
179
leave the queue unchanged on failure.  Hence, it is left to a terminal
180
DISALLOW rule to fail overall verification, if artifacts are left in the queue
181
that should have been consumed by preceding rules.
182
*/
183
func VerifyArtifacts(items []interface{},
184
        itemsMetadata map[string]Metadata) error {
76✔
185
        // Verify artifact rules for each item in the layout
76✔
186
        for _, itemI := range items {
164✔
187
                // The layout item (interface) must be a Link or an Inspection we are only
88✔
188
                // interested in the name and the expected materials and products
88✔
189
                var itemName string
88✔
190
                var expectedMaterials [][]string
88✔
191
                var expectedProducts [][]string
88✔
192

88✔
193
                switch item := itemI.(type) {
88✔
194
                case Step:
54✔
195
                        itemName = item.Name
54✔
196
                        expectedMaterials = item.ExpectedMaterials
54✔
197
                        expectedProducts = item.ExpectedProducts
54✔
198

199
                case Inspection:
32✔
200
                        itemName = item.Name
32✔
201
                        expectedMaterials = item.ExpectedMaterials
32✔
202
                        expectedProducts = item.ExpectedProducts
32✔
203

204
                default: // Something wrong
2✔
205
                        return fmt.Errorf("VerifyArtifacts received an item of invalid type,"+
2✔
206
                                " elements of passed slice 'items' must be one of 'Step' or"+
2✔
207
                                " 'Inspection', got: '%s'", reflect.TypeOf(item))
2✔
208
                }
209

210
                // Use the item's name to extract the corresponding link
211
                srcLinkEnv, exists := itemsMetadata[itemName]
86✔
212
                if !exists {
90✔
213
                        return fmt.Errorf("VerifyArtifacts could not find metadata"+
4✔
214
                                " for item '%s', got: '%s'", itemName, itemsMetadata)
4✔
215
                }
4✔
216

217
                // Create shortcuts to materials and products (including hashes) reported
218
                // by the item's link, required to verify "match" rules
219
                materials := srcLinkEnv.GetPayload().(Link).Materials
82✔
220
                products := srcLinkEnv.GetPayload().(Link).Products
82✔
221

82✔
222
                // All other rules only require the material or product paths (without
82✔
223
                // hashes). We extract them from the corresponding maps and store them as
82✔
224
                // sets for convenience in further processing
82✔
225
                materialPaths := NewSet()
82✔
226
                for _, p := range artifactsDictKeyStrings(materials) {
588✔
227
                        materialPaths.Add(path.Clean(p))
506✔
228
                }
506✔
229
                productPaths := NewSet()
82✔
230
                for _, p := range artifactsDictKeyStrings(products) {
594✔
231
                        productPaths.Add(path.Clean(p))
512✔
232
                }
512✔
233

234
                // For `create`, `delete` and `modify` rules we prepare sets of artifacts
235
                // (without hashes) that were created, deleted or modified in the current
236
                // step or inspection
237
                created := productPaths.Difference(materialPaths)
82✔
238
                deleted := materialPaths.Difference(productPaths)
82✔
239
                remained := materialPaths.Intersection(productPaths)
82✔
240
                modified := NewSet()
82✔
241
                for name := range remained {
550✔
242
                        if !reflect.DeepEqual(materials[name], products[name]) {
472✔
243
                                modified.Add(name)
4✔
244
                        }
4✔
245
                }
246

247
                // For each item we have to run rule verification, once per artifact type.
248
                // Here we prepare the corresponding data for each round.
249
                verificationDataList := []map[string]interface{}{
82✔
250
                        {
82✔
251
                                "srcType":       "materials",
82✔
252
                                "rules":         expectedMaterials,
82✔
253
                                "artifacts":     materials,
82✔
254
                                "artifactPaths": materialPaths,
82✔
255
                        },
82✔
256
                        {
82✔
257
                                "srcType":       "products",
82✔
258
                                "rules":         expectedProducts,
82✔
259
                                "artifacts":     products,
82✔
260
                                "artifactPaths": productPaths,
82✔
261
                        },
82✔
262
                }
82✔
263
                // TODO: Add logging library (see in-toto/in-toto-golang#4)
82✔
264
                // fmt.Printf("Verifying %s '%s' ", reflect.TypeOf(itemI), itemName)
82✔
265

82✔
266
                // Process all material rules using the corresponding materials and all
82✔
267
                // product rules using the corresponding products
82✔
268
                for _, verificationData := range verificationDataList {
228✔
269
                        // TODO: Add logging library (see in-toto/in-toto-golang#4)
146✔
270
                        // fmt.Printf("%s...\n", verificationData["srcType"])
146✔
271

146✔
272
                        rules := verificationData["rules"].([][]string)
146✔
273
                        artifacts := verificationData["artifacts"].(map[string]HashObj)
146✔
274

146✔
275
                        // Use artifacts (without hashes) as base queue. Each rule only operates
146✔
276
                        // on artifacts in that queue.  If a rule consumes an artifact (i.e. can
146✔
277
                        // be applied successfully), the artifact is removed from the queue. By
146✔
278
                        // applying a DISALLOW rule eventually, verification may return an error,
146✔
279
                        // if the rule matches any artifacts in the queue that should have been
146✔
280
                        // consumed earlier.
146✔
281
                        queue := verificationData["artifactPaths"].(Set)
146✔
282

146✔
283
                        // TODO: Add logging library (see in-toto/in-toto-golang#4)
146✔
284
                        // fmt.Printf("Initial state\nMaterials: %s\nProducts: %s\nQueue: %s\n\n",
146✔
285
                        //         materialPaths.Slice(), productPaths.Slice(), queue.Slice())
146✔
286

146✔
287
                        // Verify rules sequentially
146✔
288
                        for _, rule := range rules {
334✔
289
                                // Parse rule and error out if it is malformed
188✔
290
                                // NOTE: the rule format should have been validated before
188✔
291
                                ruleData, err := UnpackRule(rule)
188✔
292
                                if err != nil {
196✔
293
                                        return err
8✔
294
                                }
8✔
295

296
                                // Apply rule pattern to filter queued artifacts that are up for rule
297
                                // specific consumption
298
                                filtered := queue.Filter(path.Clean(ruleData["pattern"]))
180✔
299

180✔
300
                                var consumed Set
180✔
301
                                switch ruleData["type"] {
180✔
302
                                case "match":
42✔
303
                                        // Note: here we need to perform more elaborate filtering
42✔
304
                                        consumed = verifyMatchRule(ruleData, artifacts, queue, itemsMetadata)
42✔
305

306
                                case "allow":
42✔
307
                                        // Consumes all filtered artifacts
42✔
308
                                        consumed = filtered
42✔
309

310
                                case "create":
6✔
311
                                        // Consumes filtered artifacts that were created
6✔
312
                                        consumed = filtered.Intersection(created)
6✔
313

314
                                case "delete":
2✔
315
                                        // Consumes filtered artifacts that were deleted
2✔
316
                                        consumed = filtered.Intersection(deleted)
2✔
317

318
                                case "modify":
6✔
319
                                        // Consumes filtered artifacts that were modified
6✔
320
                                        consumed = filtered.Intersection(modified)
6✔
321

322
                                case "disallow":
72✔
323
                                        // Does not consume but errors out if artifacts were filtered
72✔
324
                                        if len(filtered) > 0 {
90✔
325
                                                return fmt.Errorf("artifact verification failed for %s '%s',"+
18✔
326
                                                        " %s %s disallowed by rule %s",
18✔
327
                                                        reflect.TypeOf(itemI).Name(), itemName,
18✔
328
                                                        verificationData["srcType"], filtered.Slice(), rule)
18✔
329
                                        }
18✔
330
                                case "require":
10✔
331
                                        // REQUIRE is somewhat of a weird animal that does not use
10✔
332
                                        // patterns bur rather single filenames (for now).
10✔
333
                                        if !queue.Has(ruleData["pattern"]) {
18✔
334
                                                return fmt.Errorf("artifact verification failed for %s in REQUIRE '%s',"+
8✔
335
                                                        " because %s is not in %s", verificationData["srcType"],
8✔
336
                                                        ruleData["pattern"], ruleData["pattern"], queue.Slice())
8✔
337
                                        }
8✔
338
                                }
339
                                // Update queue by removing consumed artifacts
340
                                queue = queue.Difference(consumed)
154✔
341
                                // TODO: Add logging library (see in-toto/in-toto-golang#4)
342
                                // fmt.Printf("Rule: %s\nQueue: %s\n\n", rule, queue.Slice())
343
                        }
344
                }
345
        }
346
        return nil
36✔
347
}
348

349
/*
350
ReduceStepsMetadata merges for each step of the passed Layout all the passed
351
per-functionary links into a single link, asserting that the reported Materials
352
and Products are equal across links for a given step.  This function may be
353
used at a time during the overall verification, where link threshold's have
354
been verified and subsequent verification only needs one exemplary link per
355
step.  The function returns a map with one Metablock (link) per step:
356

357
        {
358
                <step name> : Metablock,
359
                <step name> : Metablock,
360
                ...
361
        }
362

363
If links corresponding to the same step report different Materials or different
364
Products, the first return value is an empty Metablock map and the second
365
return value is the error.
366
*/
367
func ReduceStepsMetadata(layout Layout,
368
        stepsMetadata map[string]map[string]Metadata) (map[string]Metadata,
369
        error) {
24✔
370
        stepsMetadataReduced := make(map[string]Metadata)
24✔
371

24✔
372
        for _, step := range layout.Steps {
62✔
373
                linksPerStep, ok := stepsMetadata[step.Name]
38✔
374
                // We should never get here, layout verification must fail earlier
38✔
375
                if !ok || len(linksPerStep) < 1 {
40✔
376
                        panic("Could not reduce metadata for step '" + step.Name +
2✔
377
                                "', no link metadata found.")
2✔
378
                }
379

380
                // Get the first link (could be any link) for the current step, which will
381
                // serve as reference link for below comparisons
382
                var referenceKeyID string
36✔
383
                var referenceLinkEnv Metadata
36✔
384
                for keyID, linkEnv := range linksPerStep {
72✔
385
                        referenceLinkEnv = linkEnv
36✔
386
                        referenceKeyID = keyID
36✔
387
                        break
36✔
388
                }
389

390
                // Only one link, nothing to reduce, take the reference link
391
                if len(linksPerStep) == 1 {
62✔
392
                        stepsMetadataReduced[step.Name] = referenceLinkEnv
26✔
393

26✔
394
                        // Multiple links, reduce but first check
26✔
395
                } else {
36✔
396
                        // Artifact maps must be equal for each type among all links
10✔
397
                        // TODO: What should we do if there are more links, than the
10✔
398
                        // threshold requires, but not all of them are equal? Right now we would
10✔
399
                        // also error.
10✔
400
                        for keyID, linkEnv := range linksPerStep {
30✔
401
                                if !reflect.DeepEqual(linkEnv.GetPayload().(Link).Materials,
20✔
402
                                        referenceLinkEnv.GetPayload().(Link).Materials) ||
20✔
403
                                        !reflect.DeepEqual(linkEnv.GetPayload().(Link).Products,
20✔
404
                                                referenceLinkEnv.GetPayload().(Link).Products) {
28✔
405
                                        return nil, fmt.Errorf("link '%s' and '%s' have different"+
8✔
406
                                                " artifacts",
8✔
407
                                                fmt.Sprintf(LinkNameFormat, step.Name, referenceKeyID),
8✔
408
                                                fmt.Sprintf(LinkNameFormat, step.Name, keyID))
8✔
409
                                }
8✔
410
                        }
411
                        // We haven't errored out, so we can reduce (i.e take the reference link)
412
                        stepsMetadataReduced[step.Name] = referenceLinkEnv
2✔
413
                }
414
        }
415
        return stepsMetadataReduced, nil
14✔
416
}
417

418
/*
419
VerifyStepCommandAlignment (soft) verifies that for each step of the passed
420
layout the command executed, as per the passed link, matches the expected
421
command, as per the layout.  Soft verification means that, in case a command
422
does not align, a warning is issued.
423
*/
424
func VerifyStepCommandAlignment(layout Layout,
425
        stepsMetadata map[string]map[string]Metadata) {
16✔
426
        for _, step := range layout.Steps {
46✔
427
                linksPerStep, ok := stepsMetadata[step.Name]
30✔
428
                // We should never get here, layout verification must fail earlier
30✔
429
                if !ok || len(linksPerStep) < 1 {
32✔
430
                        panic("Could not verify command alignment for step '" + step.Name +
2✔
431
                                "', no link metadata found.")
2✔
432
                }
433

434
                for signerKeyID, linkEnv := range linksPerStep {
56✔
435
                        expectedCommandS := strings.Join(step.ExpectedCommand, " ")
28✔
436
                        executedCommandS := strings.Join(linkEnv.GetPayload().(Link).Command, " ")
28✔
437

28✔
438
                        if expectedCommandS != executedCommandS {
30✔
439
                                linkName := fmt.Sprintf(LinkNameFormat, step.Name, signerKeyID)
2✔
440
                                fmt.Printf("WARNING: Expected command for step '%s' (%s) and command"+
2✔
441
                                        " reported by '%s' (%s) differ.\n",
2✔
442
                                        step.Name, expectedCommandS, linkName, executedCommandS)
2✔
443
                        }
2✔
444
                }
445
        }
446
}
447

448
/*
449
LoadLayoutCertificates loads the root and intermediate CAs from the layout if in the layout.
450
This will be used to check signatures that were used to sign links but not configured
451
in the PubKeys section of the step.  No configured CAs means we don't want to allow this.
452
Returned CertPools will be empty in this case.
453
*/
454
func LoadLayoutCertificates(layout Layout, intermediatePems [][]byte) (*x509.CertPool, *x509.CertPool, error) {
22✔
455
        rootPool := x509.NewCertPool()
22✔
456
        for _, certPem := range layout.RootCas {
36✔
457
                ok := rootPool.AppendCertsFromPEM([]byte(certPem.KeyVal.Certificate))
14✔
458
                if !ok {
16✔
459
                        return nil, nil, fmt.Errorf("failed to load root certificates for layout")
2✔
460
                }
2✔
461
        }
462

463
        intermediatePool := x509.NewCertPool()
20✔
464
        for _, intermediatePem := range layout.IntermediateCas {
32✔
465
                ok := intermediatePool.AppendCertsFromPEM([]byte(intermediatePem.KeyVal.Certificate))
12✔
466
                if !ok {
14✔
467
                        return nil, nil, fmt.Errorf("failed to load intermediate certificates for layout")
2✔
468
                }
2✔
469
        }
470

471
        for _, intermediatePem := range intermediatePems {
22✔
472
                ok := intermediatePool.AppendCertsFromPEM(intermediatePem)
4✔
473
                if !ok {
6✔
474
                        return nil, nil, fmt.Errorf("failed to load provided intermediate certificates")
2✔
475
                }
2✔
476
        }
477

478
        return rootPool, intermediatePool, nil
16✔
479
}
480

481
/*
482
VerifyLinkSignatureThesholds verifies that for each step of the passed layout,
483
there are at least Threshold links, validly signed by different authorized
484
functionaries.  The returned map of link metadata per steps contains only
485
links with valid signatures from distinct functionaries and has the format:
486

487
        {
488
                <step name> : {
489
                <key id>: Metablock,
490
                <key id>: Metablock,
491
                ...
492
                },
493
                <step name> : {
494
                <key id>: Metablock,
495
                <key id>: Metablock,
496
                ...
497
                }
498
                ...
499
        }
500

501
If for any step of the layout there are not enough links available, the first
502
return value is an empty map of Metablock maps and the second return value is
503
the error.
504
*/
505
func VerifyLinkSignatureThesholds(layout Layout,
506
        stepsMetadata map[string]map[string]Metadata, rootCertPool, intermediateCertPool *x509.CertPool) (
507
        map[string]map[string]Metadata, error) {
28✔
508
        // This will stores links with valid signature from an authorized functionary
28✔
509
        // for all steps
28✔
510
        stepsMetadataVerified := make(map[string]map[string]Metadata)
28✔
511

28✔
512
        // Try to find enough (>= threshold) links each with a valid signature from
28✔
513
        // distinct authorized functionaries for each step
28✔
514
        for _, step := range layout.Steps {
70✔
515
                var stepErr error
42✔
516

42✔
517
                // This will store links with valid signature from an authorized
42✔
518
                // functionary for the given step
42✔
519
                linksPerStepVerified := make(map[string]Metadata)
42✔
520

42✔
521
                // Check if there are any links at all for the given step
42✔
522
                linksPerStep, ok := stepsMetadata[step.Name]
42✔
523
                if !ok || len(linksPerStep) < 1 {
46✔
524
                        stepErr = fmt.Errorf("no links found")
4✔
525
                }
4✔
526

527
                // For each link corresponding to a step, check that the signer key was
528
                // authorized, the layout contains a verification key and the signature
529
                // verification passes.  Only good links are stored, to verify thresholds
530
                // below.
531
                isAuthorizedSignature := false
42✔
532
                for signerKeyID, linkEnv := range linksPerStep {
90✔
533
                        for _, authorizedKeyID := range step.PubKeys {
112✔
534
                                if signerKeyID == authorizedKeyID {
112✔
535
                                        if verifierKey, ok := layout.Keys[authorizedKeyID]; ok {
94✔
536
                                                if err := linkEnv.VerifySignature(verifierKey); err == nil {
88✔
537
                                                        linksPerStepVerified[signerKeyID] = linkEnv
42✔
538
                                                        isAuthorizedSignature = true
42✔
539
                                                        break
42✔
540
                                                }
541
                                        }
542
                                }
543
                        }
544

545
                        // If the signer's key wasn't in our step's pubkeys array, check the cert pool to
546
                        // see if the key is known to us.
547
                        if !isAuthorizedSignature {
48✔
548
                                sig, err := linkEnv.GetSignatureForKeyID(signerKeyID)
×
549
                                if err != nil {
×
550
                                        stepErr = err
×
551
                                        continue
×
552
                                }
553

554
                                cert, err := sig.GetCertificate()
×
555
                                if err != nil {
×
556
                                        stepErr = err
×
557
                                        continue
×
558
                                }
559

560
                                // test certificate against the step's constraints to make sure it's a valid functionary
561
                                err = step.CheckCertConstraints(cert, layout.RootCAIDs(), rootCertPool, intermediateCertPool)
×
562
                                if err != nil {
×
563
                                        stepErr = err
×
564
                                        continue
×
565
                                }
566

567
                                err = linkEnv.VerifySignature(cert)
×
568
                                if err != nil {
×
569
                                        stepErr = err
×
570
                                        continue
×
571
                                }
572

573
                                linksPerStepVerified[signerKeyID] = linkEnv
×
574
                        }
575
                }
576

577
                // Store all good links for a step
578
                stepsMetadataVerified[step.Name] = linksPerStepVerified
42✔
579

42✔
580
                if len(linksPerStepVerified) < step.Threshold {
52✔
581
                        linksPerStep := stepsMetadata[step.Name]
10✔
582
                        return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s)."+
10✔
583
                                " '%d' out of '%d' available link(s) have a valid signature from an"+
10✔
584
                                " authorized signer: %v", step.Name, step.Threshold,
10✔
585
                                len(linksPerStepVerified), len(linksPerStep), stepErr)
10✔
586
                }
10✔
587
        }
588
        return stepsMetadataVerified, nil
18✔
589
}
590

591
/*
592
LoadLinksForLayout loads for every Step of the passed Layout a Metablock
593
containing the corresponding Link.  A base path to a directory that contains
594
the links may be passed using linkDir.  Link file names are constructed,
595
using LinkNameFormat together with the corresponding step name and authorized
596
functionary key ids.  A map of link metadata is returned and has the following
597
format:
598

599
        {
600
                <step name> : {
601
                        <key id>: Metablock,
602
                        <key id>: Metablock,
603
                        ...
604
                },
605
                <step name> : {
606
                <key id>: Metablock,
607
                <key id>: Metablock,
608
                ...
609
                }
610
                ...
611
        }
612

613
If a link cannot be loaded at a constructed link name or is invalid, it is
614
ignored. Only a preliminary threshold check is performed, that is, if there
615
aren't at least Threshold links for any given step, the first return value
616
is an empty map of Metablock maps and the second return value is the error.
617
*/
618
func LoadLinksForLayout(layout Layout, linkDir string) (map[string]map[string]Metadata, error) {
18✔
619
        stepsMetadata := make(map[string]map[string]Metadata)
18✔
620

18✔
621
        for _, step := range layout.Steps {
50✔
622
                linksPerStep := make(map[string]Metadata)
32✔
623
                // Since we can verify against certificates belonging to a CA, we need to
32✔
624
                // load any possible links
32✔
625
                linkFiles, err := filepath.Glob(path.Join(linkDir, fmt.Sprintf(LinkGlobFormat, step.Name)))
32✔
626
                if err != nil {
32✔
627
                        return nil, err
×
628
                }
×
629

630
                for _, linkPath := range linkFiles {
68✔
631
                        linkEnv, err := LoadMetadata(linkPath)
36✔
632
                        if err != nil {
36✔
633
                                continue
×
634
                        }
635

636
                        // To get the full key from the metadata's signatures, we have to check
637
                        // for one with the same short id...
638
                        signerShortKeyID := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(linkPath), step.Name+"."), ".link")
36✔
639
                        for _, sig := range linkEnv.Sigs() {
72✔
640
                                if strings.HasPrefix(sig.KeyID, signerShortKeyID) {
72✔
641
                                        linksPerStep[sig.KeyID] = linkEnv
36✔
642
                                        break
36✔
643
                                }
644
                        }
645
                }
646

647
                if len(linksPerStep) < step.Threshold {
34✔
648
                        return nil, fmt.Errorf("step '%s' requires '%d' link metadata file(s),"+
2✔
649
                                " found '%d'", step.Name, step.Threshold, len(linksPerStep))
2✔
650
                }
2✔
651

652
                stepsMetadata[step.Name] = linksPerStep
30✔
653
        }
654

655
        return stepsMetadata, nil
16✔
656
}
657

658
/*
659
VerifyLayoutExpiration verifies that the passed Layout has not expired.  It
660
returns an error if the (zulu) date in the Expires field is in the past.
661
*/
662
func VerifyLayoutExpiration(layout Layout) error {
18✔
663
        expires, err := time.Parse(ISO8601DateSchema, layout.Expires)
18✔
664
        if err != nil {
20✔
665
                return err
2✔
666
        }
2✔
667
        // Uses timezone of expires, i.e. UTC
668
        if time.Until(expires) < 0 {
18✔
669
                return fmt.Errorf("layout has expired on '%s'", expires)
2✔
670
        }
2✔
671
        return nil
14✔
672
}
673

674
/*
675
VerifyLayoutSignatures verifies for each key in the passed key map the
676
corresponding signature of the Layout in the passed Metablock's Signed field.
677
Signatures and keys are associated by key id.  If the key map is empty, or the
678
Metablock's Signature field does not have a signature for one or more of the
679
passed keys, or a matching signature is invalid, an error is returned.
680
*/
681
func VerifyLayoutSignatures(layoutEnv Metadata,
682
        layoutKeys map[string]Key) error {
18✔
683
        if len(layoutKeys) < 1 {
20✔
684
                return fmt.Errorf("layout verification requires at least one key")
2✔
685
        }
2✔
686

687
        for _, key := range layoutKeys {
32✔
688
                if err := layoutEnv.VerifySignature(key); err != nil {
18✔
689
                        return err
2✔
690
                }
2✔
691
        }
692
        return nil
14✔
693
}
694

695
/*
696
GetSummaryLink merges the materials of the first step (as mentioned in the
697
layout) and the products of the last step and returns a new link. This link
698
reports the materials and products and summarizes the overall software supply
699
chain.
700
NOTE: The assumption is that the steps mentioned in the layout are to be
701
performed sequentially. So, the first step mentioned in the layout denotes what
702
comes into the supply chain and the last step denotes what goes out.
703
*/
704
func GetSummaryLink(layout Layout, stepsMetadataReduced map[string]Metadata,
705
        stepName string, useDSSE bool) (Metadata, error) {
14✔
706
        var summaryLink Link
14✔
707
        if len(layout.Steps) > 0 {
28✔
708
                firstStepLink := stepsMetadataReduced[layout.Steps[0].Name]
14✔
709
                lastStepLink := stepsMetadataReduced[layout.Steps[len(layout.Steps)-1].Name]
14✔
710

14✔
711
                summaryLink.Materials = firstStepLink.GetPayload().(Link).Materials
14✔
712
                summaryLink.Name = stepName
14✔
713
                summaryLink.Type = firstStepLink.GetPayload().(Link).Type
14✔
714

14✔
715
                summaryLink.Products = lastStepLink.GetPayload().(Link).Products
14✔
716
                summaryLink.ByProducts = lastStepLink.GetPayload().(Link).ByProducts
14✔
717
                // Using the last command of the sublayout as the command
14✔
718
                // of the summary link can be misleading. Is it necessary to
14✔
719
                // include all the commands executed as part of sublayout?
14✔
720
                summaryLink.Command = lastStepLink.GetPayload().(Link).Command
14✔
721
        }
14✔
722

723
        if useDSSE {
18✔
724
                env := &Envelope{}
4✔
725
                if err := env.SetPayload(summaryLink); err != nil {
4✔
726
                        return nil, err
×
727
                }
×
728

729
                return env, nil
4✔
730
        }
731

732
        return &Metablock{Signed: summaryLink}, nil
10✔
733
}
734

735
/*
736
VerifySublayouts checks if any step in the supply chain is a sublayout, and if
737
so, recursively resolves it and replaces it with a summary link summarizing the
738
steps carried out in the sublayout.
739
*/
740
func VerifySublayouts(layout Layout,
741
        stepsMetadataVerified map[string]map[string]Metadata,
742
        superLayoutLinkPath string, intermediatePems [][]byte, lineNormalization bool) (map[string]map[string]Metadata, error) {
14✔
743
        for stepName, linkData := range stepsMetadataVerified {
42✔
744
                for keyID, metadata := range linkData {
56✔
745
                        if _, ok := metadata.GetPayload().(Layout); ok {
30✔
746
                                layoutKeys := make(map[string]Key)
2✔
747
                                layoutKeys[keyID] = layout.Keys[keyID]
2✔
748

2✔
749
                                sublayoutLinkDir := fmt.Sprintf(SublayoutLinkDirFormat,
2✔
750
                                        stepName, keyID)
2✔
751
                                sublayoutLinkPath := filepath.Join(superLayoutLinkPath,
2✔
752
                                        sublayoutLinkDir)
2✔
753
                                summaryLink, err := InTotoVerify(metadata, layoutKeys,
2✔
754
                                        sublayoutLinkPath, stepName, make(map[string]string), intermediatePems, lineNormalization)
2✔
755
                                if err != nil {
2✔
756
                                        return nil, err
×
757
                                }
×
758
                                linkData[keyID] = summaryLink
2✔
759
                        }
760

761
                }
762
        }
763
        return stepsMetadataVerified, nil
14✔
764
}
765

766
// TODO: find a better way than two helper functions for the replacer op
767

768
func substituteParamatersInSlice(replacer *strings.Replacer, slice []string) []string {
12✔
769
        newSlice := make([]string, 0)
12✔
770
        for _, item := range slice {
52✔
771
                newSlice = append(newSlice, replacer.Replace(item))
40✔
772
        }
40✔
773
        return newSlice
12✔
774
}
775

776
func substituteParametersInSliceOfSlices(replacer *strings.Replacer,
777
        slice [][]string) [][]string {
8✔
778
        newSlice := make([][]string, 0)
8✔
779
        for _, item := range slice {
16✔
780
                newSlice = append(newSlice, substituteParamatersInSlice(replacer,
8✔
781
                        item))
8✔
782
        }
8✔
783
        return newSlice
8✔
784
}
785

786
/*
787
SubstituteParameters performs parameter substitution in steps and inspections
788
in the following fields:
789
- Expected Materials and Expected Products of both
790
- Run of inspections
791
- Expected Command of steps
792
The substitution marker is '{}' and the keyword within the braces is replaced
793
by a value found in the substitution map passed, parameterDictionary. The
794
layout with parameters substituted is returned to the calling function.
795
*/
796
func SubstituteParameters(layout Layout,
797
        parameterDictionary map[string]string) (Layout, error) {
16✔
798

16✔
799
        if len(parameterDictionary) == 0 {
28✔
800
                return layout, nil
12✔
801
        }
12✔
802

803
        parameters := make([]string, 0)
4✔
804

4✔
805
        re := regexp.MustCompile("^[a-zA-Z0-9_-]+$")
4✔
806

4✔
807
        for parameter, value := range parameterDictionary {
16✔
808
                parameterFormatCheck := re.MatchString(parameter)
12✔
809
                if !parameterFormatCheck {
14✔
810
                        return layout, fmt.Errorf("invalid format for parameter")
2✔
811
                }
2✔
812

813
                parameters = append(parameters, "{"+parameter+"}")
10✔
814
                parameters = append(parameters, value)
10✔
815
        }
816

817
        replacer := strings.NewReplacer(parameters...)
2✔
818

2✔
819
        for i := range layout.Steps {
4✔
820
                layout.Steps[i].ExpectedMaterials = substituteParametersInSliceOfSlices(
2✔
821
                        replacer, layout.Steps[i].ExpectedMaterials)
2✔
822
                layout.Steps[i].ExpectedProducts = substituteParametersInSliceOfSlices(
2✔
823
                        replacer, layout.Steps[i].ExpectedProducts)
2✔
824
                layout.Steps[i].ExpectedCommand = substituteParamatersInSlice(replacer,
2✔
825
                        layout.Steps[i].ExpectedCommand)
2✔
826
        }
2✔
827

828
        for i := range layout.Inspect {
4✔
829
                layout.Inspect[i].ExpectedMaterials =
2✔
830
                        substituteParametersInSliceOfSlices(replacer,
2✔
831
                                layout.Inspect[i].ExpectedMaterials)
2✔
832
                layout.Inspect[i].ExpectedProducts =
2✔
833
                        substituteParametersInSliceOfSlices(replacer,
2✔
834
                                layout.Inspect[i].ExpectedProducts)
2✔
835
                layout.Inspect[i].Run = substituteParamatersInSlice(replacer,
2✔
836
                        layout.Inspect[i].Run)
2✔
837
        }
2✔
838

839
        return layout, nil
2✔
840
}
841

842
/*
843
InTotoVerify can be used to verify an entire software supply chain according to
844
the in-toto specification.  It requires the metadata of the root layout, a map
845
that contains public keys to verify the root layout signatures, a path to a
846
directory from where it can load link metadata files, which are treated as
847
signed evidence for the steps defined in the layout, a step name, and a
848
paramater dictionary used for parameter substitution. The step name only
849
matters for sublayouts, where it's important to associate the summary of that
850
step with a unique name. The verification routine is as follows:
851

852
1. Verify layout signature(s) using passed key(s)
853
2. Verify layout expiration date
854
3. Substitute parameters in layout
855
4. Load link metadata files for steps of layout
856
5. Verify signatures and signature thresholds for steps of layout
857
6. Verify sublayouts recursively
858
7. Verify command alignment for steps of layout (only warns)
859
8. Verify artifact rules for steps of layout
860
9. Execute inspection commands (generates link metadata for each inspection)
861
10. Verify artifact rules for inspections of layout
862

863
InTotoVerify returns a summary link wrapped in a Metablock object and an error
864
value. If any of the verification routines fail, verification is aborted and
865
error is returned. In such an instance, the first value remains an empty
866
Metablock object.
867

868
NOTE: Artifact rules of type "create", "modify"
869
and "delete" are currently not supported.
870
*/
871
func InTotoVerify(layoutEnv Metadata, layoutKeys map[string]Key,
872
        linkDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) (
873
        Metadata, error) {
10✔
874

10✔
875
        // Verify root signatures
10✔
876
        if err := VerifyLayoutSignatures(layoutEnv, layoutKeys); err != nil {
10✔
877
                return nil, err
×
878
        }
×
879

880
        useDSSE := false
10✔
881
        if _, ok := layoutEnv.(*Envelope); ok {
14✔
882
                useDSSE = true
4✔
883
        }
4✔
884

885
        // Extract the layout from its Metadata container (for further processing)
886
        layout, ok := layoutEnv.GetPayload().(Layout)
10✔
887
        if !ok {
10✔
888
                return nil, ErrNotLayout
×
889
        }
×
890

891
        // Verify layout expiration
892
        if err := VerifyLayoutExpiration(layout); err != nil {
10✔
893
                return nil, err
×
894
        }
×
895

896
        // Substitute parameters in layout
897
        layout, err := SubstituteParameters(layout, parameterDictionary)
10✔
898
        if err != nil {
10✔
899
                return nil, err
×
900
        }
×
901

902
        rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems)
10✔
903
        if err != nil {
10✔
904
                return nil, err
×
905
        }
×
906

907
        // Load links for layout
908
        stepsMetadata, err := LoadLinksForLayout(layout, linkDir)
10✔
909
        if err != nil {
10✔
910
                return nil, err
×
911
        }
×
912

913
        // Verify link signatures
914
        stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout,
10✔
915
                stepsMetadata, rootCertPool, intermediateCertPool)
10✔
916
        if err != nil {
10✔
917
                return nil, err
×
918
        }
×
919

920
        // Verify and resolve sublayouts
921
        stepsSublayoutVerified, err := VerifySublayouts(layout,
10✔
922
                stepsMetadataVerified, linkDir, intermediatePems, lineNormalization)
10✔
923
        if err != nil {
10✔
924
                return nil, err
×
925
        }
×
926

927
        // Verify command alignment (WARNING only)
928
        VerifyStepCommandAlignment(layout, stepsSublayoutVerified)
10✔
929

10✔
930
        // Given that signature thresholds have been checked above and the rest of
10✔
931
        // the relevant link properties, i.e. materials and products, have to be
10✔
932
        // exactly equal, we can reduce the map of steps metadata. However, we error
10✔
933
        // if the relevant properties are not equal among links of a step.
10✔
934
        stepsMetadataReduced, err := ReduceStepsMetadata(layout,
10✔
935
                stepsSublayoutVerified)
10✔
936
        if err != nil {
10✔
937
                return nil, err
×
938
        }
×
939

940
        // Verify artifact rules
941
        if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(),
10✔
942
                stepsMetadataReduced); err != nil {
10✔
943
                return nil, err
×
944
        }
×
945

946
        inspectionMetadata, err := RunInspections(layout, "", lineNormalization, useDSSE)
10✔
947
        if err != nil {
10✔
948
                return nil, err
×
949
        }
×
950

951
        // Add steps metadata to inspection metadata, because inspection artifact
952
        // rules may also refer to artifacts reported by step links
953
        for k, v := range stepsMetadataReduced {
32✔
954
                inspectionMetadata[k] = v
22✔
955
        }
22✔
956

957
        if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(),
10✔
958
                inspectionMetadata); err != nil {
10✔
959
                return nil, err
×
960
        }
×
961

962
        summaryLink, err := GetSummaryLink(layout, stepsMetadataReduced, stepName, useDSSE)
10✔
963
        if err != nil {
10✔
964
                return nil, err
×
965
        }
×
966

967
        return summaryLink, nil
10✔
968
}
969

970
/*
971
InTotoVerifyWithDirectory provides the same functionality as InTotoVerify, but
972
adds the possibility to select a local directory from where the inspections are run.
973
*/
974
func InTotoVerifyWithDirectory(layoutEnv Metadata, layoutKeys map[string]Key,
975
        linkDir string, runDir string, stepName string, parameterDictionary map[string]string, intermediatePems [][]byte, lineNormalization bool) (
976
        Metadata, error) {
2✔
977

2✔
978
        // runDir sanity checks
2✔
979
        // check if path exists
2✔
980
        info, err := os.Stat(runDir)
2✔
981
        if err != nil {
2✔
982
                return nil, err
×
983
        }
×
984

985
        // check if runDir is a symlink
986
        if info.Mode()&os.ModeSymlink == os.ModeSymlink {
2✔
987
                return nil, ErrInspectionRunDirIsSymlink
×
988
        }
×
989

990
        // check if runDir is writable and a directory
991
        err = isWritable(runDir)
2✔
992
        if err != nil {
2✔
993
                return nil, err
×
994
        }
×
995

996
        // check if runDir is empty (we do not want to overwrite files)
997
        // We abuse File.Readdirnames for this action.
998
        f, err := os.Open(runDir)
2✔
999
        if err != nil {
2✔
1000
                return nil, err
×
1001
        }
×
1002
        defer f.Close()
2✔
1003
        // We use Readdirnames(1) for performance reasons, one child node
2✔
1004
        // is enough to proof that the directory is not empty
2✔
1005
        _, err = f.Readdirnames(1)
2✔
1006
        // if io.EOF gets returned as error the directory is empty
2✔
1007
        if err == io.EOF {
2✔
1008
                return nil, err
×
1009
        }
×
1010
        err = f.Close()
2✔
1011
        if err != nil {
2✔
1012
                return nil, err
×
1013
        }
×
1014

1015
        // Verify root signatures
1016
        if err := VerifyLayoutSignatures(layoutEnv, layoutKeys); err != nil {
2✔
1017
                return nil, err
×
1018
        }
×
1019

1020
        useDSSE := false
2✔
1021
        if _, ok := layoutEnv.(*Envelope); ok {
2✔
1022
                useDSSE = true
×
1023
        }
×
1024

1025
        // Extract the layout from its Metadata container (for further processing)
1026
        layout, ok := layoutEnv.GetPayload().(Layout)
2✔
1027
        if !ok {
2✔
1028
                return nil, ErrNotLayout
×
1029
        }
×
1030

1031
        // Verify layout expiration
1032
        if err := VerifyLayoutExpiration(layout); err != nil {
2✔
1033
                return nil, err
×
1034
        }
×
1035

1036
        // Substitute parameters in layout
1037
        layout, err = SubstituteParameters(layout, parameterDictionary)
2✔
1038
        if err != nil {
2✔
1039
                return nil, err
×
1040
        }
×
1041

1042
        rootCertPool, intermediateCertPool, err := LoadLayoutCertificates(layout, intermediatePems)
2✔
1043
        if err != nil {
2✔
1044
                return nil, err
×
1045
        }
×
1046

1047
        // Load links for layout
1048
        stepsMetadata, err := LoadLinksForLayout(layout, linkDir)
2✔
1049
        if err != nil {
2✔
1050
                return nil, err
×
1051
        }
×
1052

1053
        // Verify link signatures
1054
        stepsMetadataVerified, err := VerifyLinkSignatureThesholds(layout,
2✔
1055
                stepsMetadata, rootCertPool, intermediateCertPool)
2✔
1056
        if err != nil {
2✔
1057
                return nil, err
×
1058
        }
×
1059

1060
        // Verify and resolve sublayouts
1061
        stepsSublayoutVerified, err := VerifySublayouts(layout,
2✔
1062
                stepsMetadataVerified, linkDir, intermediatePems, lineNormalization)
2✔
1063
        if err != nil {
2✔
1064
                return nil, err
×
1065
        }
×
1066

1067
        // Verify command alignment (WARNING only)
1068
        VerifyStepCommandAlignment(layout, stepsSublayoutVerified)
2✔
1069

2✔
1070
        // Given that signature thresholds have been checked above and the rest of
2✔
1071
        // the relevant link properties, i.e. materials and products, have to be
2✔
1072
        // exactly equal, we can reduce the map of steps metadata. However, we error
2✔
1073
        // if the relevant properties are not equal among links of a step.
2✔
1074
        stepsMetadataReduced, err := ReduceStepsMetadata(layout,
2✔
1075
                stepsSublayoutVerified)
2✔
1076
        if err != nil {
2✔
1077
                return nil, err
×
1078
        }
×
1079

1080
        // Verify artifact rules
1081
        if err = VerifyArtifacts(layout.stepsAsInterfaceSlice(),
2✔
1082
                stepsMetadataReduced); err != nil {
2✔
1083
                return nil, err
×
1084
        }
×
1085

1086
        inspectionMetadata, err := RunInspections(layout, runDir, lineNormalization, useDSSE)
2✔
1087
        if err != nil {
2✔
1088
                return nil, err
×
1089
        }
×
1090

1091
        // Add steps metadata to inspection metadata, because inspection artifact
1092
        // rules may also refer to artifacts reported by step links
1093
        for k, v := range stepsMetadataReduced {
6✔
1094
                inspectionMetadata[k] = v
4✔
1095
        }
4✔
1096

1097
        if err = VerifyArtifacts(layout.inspectAsInterfaceSlice(),
2✔
1098
                inspectionMetadata); err != nil {
2✔
1099
                return nil, err
×
1100
        }
×
1101

1102
        summaryLink, err := GetSummaryLink(layout, stepsMetadataReduced, stepName, useDSSE)
2✔
1103
        if err != nil {
2✔
1104
                return nil, err
×
1105
        }
×
1106

1107
        return summaryLink, nil
2✔
1108
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc