Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions free/ci/internal/authz/promote.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package authz
// Outputs: Promotable.
// Constraints: pure. The gate class is forced to release, self-reported
// evidence is never admitted and selection must be full, whatever the caller's
// gate said, and the trust floor is "internal". An artifact of another repo or
// gate said, and the trust floor is at least "internal" (an unknown floor is refused, not raised). An artifact of another repo or
// revision is refused, and so is one whose SHA-256 the evidence does not list
// as a committed artifact: a fork build of the same commit is not the release.

Expand Down Expand Up @@ -42,7 +42,9 @@ func Promotable(a Artifact, ev model.Evidence, g attest.Gate) (bool, []attest.Re
g.Class = attest.ClassRelease
g.FullSelection = true
g.AllowSelfReported = false
if trust.RankTrust(g.MinTrust) < trust.RankTrust(promoteMinTrust) {
// An unknown floor (ranks -1) is left as is so GateAccept rejects it; raising it
// to the default would let a typo'd owner floor weaken to internal.
if r := trust.RankTrust(g.MinTrust); r >= 0 && r < trust.RankTrust(promoteMinTrust) {
g.MinTrust = promoteMinTrust
}
reasons := attest.GateAccept(ev, g)
Expand Down
22 changes: 22 additions & 0 deletions free/ci/internal/authz/promote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,25 @@ func TestPromotable_TrustFloor(t *testing.T) {
}
}
}

// An unknown floor (a typo'd "owner") is refused, not raised to internal, even
// when the evidence would have met internal.
func TestPromotable_UnknownFloorIsRefused(t *testing.T) {
for _, floor := range []model.TrustClass{"", "owner ", "Owner", "unknown"} {
ev := goodEvidence()
ev.SourceTrust = "internal"
g := goodGate(ev)
g.MinTrust = floor
ok, reasons := authz.Promotable(goodArtifact(), ev, g)
if ok {
t.Fatalf("floor %q promoted", floor)
}
found := false
for _, r := range reasons {
found = found || r == attest.ReasonTrustBelowMin
}
if !found {
t.Fatalf("floor %q: want %s in %v", floor, attest.ReasonTrustBelowMin, reasons)
}
}
}
2 changes: 1 addition & 1 deletion free/ci/internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func Parse(name string, data []byte) (*Loaded, error) {
}
return fail(Problem{File: name, Line: line, Message: "more than one YAML document"})
}
w := &walker{file: name, lines: map[string]int{}, budget: maxNodes, bytes: maxBytes}
w := &walker{file: name, lines: map[string]int{}, budget: maxNodes, bytes: maxBytes, ptrs: maxPtrBytes}
value := w.value(doc.Content[0], "", 0)
lines := w.lines
if len(w.probs) > 0 {
Expand Down
14 changes: 14 additions & 0 deletions free/ci/internal/config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,17 @@ func TestWarningNeverContainsAValue(t *testing.T) {
t.Fatalf("value echoed or accepted: %v", err)
}
}

// Every node keeps its full JSON pointer, so a long key above many items is
// refused instead of retaining len(key) bytes per item (a sub-1MiB file could
// otherwise allocate tens of GiB).
func TestLongKeyPointersAreBudgeted(t *testing.T) {
doc := "version: 1\npolicy:\n ? " + strings.Repeat("k", 16<<10) + "\n : [" + strings.Repeat("0,", 10_000) + "0]\n"
if len(doc) >= MaxFileBytes {
t.Fatalf("document is %d bytes, not a sub-limit file", len(doc))
}
_, err := Parse("ci.yaml", []byte(doc))
if err == nil || !strings.Contains(err.Error(), "too large") {
t.Fatalf("long-pointer document accepted: %v", err)
}
}
26 changes: 24 additions & 2 deletions free/ci/internal/config/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const (
maxDepth = 64
maxNodes = 200_000
maxBytes = 4 << 20 // scalar bytes after alias expansion
// maxPtrBytes bounds the JSON pointer strings retained in walker.lines: every
// node keeps its full pointer, so a long key above many items would otherwise
// cost len(key) per item, far past what the node and scalar budgets allow.
maxPtrBytes = 16 << 20
)

type walker struct {
Expand All @@ -32,6 +36,17 @@ type walker struct {
probs []Problem
budget int
bytes int
ptrs int // pointer bytes still available
}

// child returns the pointer of a member of ptr, charging its length to the
// pointer budget. ok is false (and the limit reported) when the budget is spent.
func (w *walker) child(n *yaml.Node, ptr, seg string) (string, bool) {
if w.ptrs -= len(ptr) + 1 + len(seg); w.ptrs < 0 {
w.overLimit(n, ptr)
return "", false
}
return ptr + "/" + seg, true
}

func (w *walker) bad(n *yaml.Node, ptr, msg string) {
Expand Down Expand Up @@ -75,7 +90,10 @@ func (w *walker) value(n *yaml.Node, ptr string, depth int) any {
w.overLimit(k, ptr)
return m
}
child := ptr + "/" + escape(k.Value)
child, ok := w.child(k, ptr, escape(k.Value))
if !ok {
return m
}
if _, dup := m[k.Value]; dup {
w.bad(k, child, "duplicate key")
continue
Expand All @@ -87,7 +105,11 @@ func (w *walker) value(n *yaml.Node, ptr string, depth int) any {
case yaml.SequenceNode:
s := make([]any, len(n.Content))
for i, c := range n.Content {
s[i] = w.value(c, ptr+"/"+strconv.Itoa(i), depth+1)
child, ok := w.child(c, ptr, strconv.Itoa(i))
if !ok {
return s
}
s[i] = w.value(c, child, depth+1)
}
return s
case yaml.ScalarNode:
Expand Down
Loading