package release import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" ) const generationCompletenessKey = "completeness.json" type GenerationEvidence struct { Box, Enrollment, Boot string Migration uint64 Installations, Launches, Kernel string } type GenerationCompleteness struct { Schema int Revision uint64 Complete bool Reservations string Evidence GenerationEvidence } func generationDigest(v any) string { raw, _ := json.Marshal(v) sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) } func generationHash(s string) bool { b, err := hex.DecodeString(s) return err == nil && len(b) == sha256.Size && hex.EncodeToString(b) == s } func (e GenerationEvidence) valid() bool { return e.Box != "" && e.Enrollment != "" && e.Boot != "" && e.Migration > 0 && generationHash(e.Installations) && generationHash(e.Launches) && generationHash(e.Kernel) } // Completeness is evidence, not a cached permission. A destructive reader must // Certify again under release exclusion and retain that exclusion through use. func (g *RootGenerations) Completeness() (GenerationCompleteness, error) { var c GenerationCompleteness if g == nil || g.Store == nil { return c, os.ErrPermission } raw, err := g.Store.Read(generationCompletenessKey) if err != nil { return c, err } if len(raw) > rootGenerationCompletenessLimit || strictCanonicalJSON(raw, &c) != nil || c.Schema != 1 || c.Revision == 0 || !generationHash(c.Reservations) || !c.Evidence.valid() { return GenerationCompleteness{}, os.ErrPermission } if err := g.Store.Write(generationCompletenessKey, raw); err != nil { return GenerationCompleteness{}, err } return c, nil } const rootGenerationCompletenessLimit = 4096 func (g *RootGenerations) writeCompleteness(c GenerationCompleteness) error { if c.Revision == 0 { return os.ErrPermission } raw, err := json.Marshal(c) if err != nil { return err } if len(raw) > rootGenerationCompletenessLimit { return os.ErrPermission } return g.Store.Write(generationCompletenessKey, raw) } // Invalidation precedes each reservation transition. A crash between the two // writes leaves the old obligation with an unusable certificate. func (g *RootGenerations) invalidateCompleteness() error { c, err := g.Completeness() if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } c.Complete = false c.Revision++ return g.writeCompleteness(c) } // Certify accepts only independently reconciled installation, launch and kernel // evidence. The verifier must not acquire release exclusion: its caller holds it. func (g *RootGenerations) Certify(ctx context.Context, expected []GenerationReservation, verify func(context.Context, []GenerationReservation) (GenerationEvidence, error)) (GenerationCompleteness, error) { var zero GenerationCompleteness if verify == nil { return zero, os.ErrPermission } before, err := g.Reservations(ctx) if err != nil { return zero, err } digest := generationDigest(before) if digest != generationDigest(expected) { return zero, os.ErrPermission } // A failed reconciliation must not leave the previous positive record live. if err := g.invalidateCompleteness(); err != nil { return zero, err } e, err := verify(ctx, before) if err != nil { return zero, err } if !e.valid() { return zero, os.ErrPermission } for _, r := range before { if r.Binding.Target.Box != e.Box || r.Binding.Target.Enrollment != e.Enrollment { return zero, os.ErrPermission } } after, err := g.Reservations(ctx) if err != nil { return zero, err } if generationDigest(after) != digest { return zero, os.ErrPermission } old, err := g.Completeness() if err != nil && !errors.Is(err, os.ErrNotExist) { return zero, err } c := GenerationCompleteness{Schema: 1, Revision: old.Revision + 1, Complete: true, Reservations: digest, Evidence: e} if err := ctx.Err(); err != nil { return zero, err } if err := g.writeCompleteness(c); err != nil { return zero, err } return c, nil }