package release import ( "context" "crypto/ed25519" "errors" "fmt" "os" "time" ) // PointerDigestsKey is the account's emission marker (#253, spec §4.6 // "Writers before readers"). While it verifies, upgraded writers sign // digests into the pointers they write; writers from before #253 do not know // it, which is why every writer is upgraded before it is written. func PointerDigestsKey(acct string) string { return "releases/" + acct + "/pointer-digests" } // DigestMarker is the marker's root-signed payload. At is when emission // started: a pointer without digests promoted after At came from a writer // that was not upgraded (DigestStateOf's straggler). type DigestMarker struct { Schema int `json:"schema"` Account string `json:"account"` At time.Time `json:"at"` By string `json:"by,omitempty"` } // WriteDigestMarker turns emission on for acct. Root key only: the marker is // deployment control, not channel publication (SignRootEnvelope refuses a // scoped signer). A marker that already verifies is left alone, so At stays // the first time emission went on. func WriteDigestMarker(ctx context.Context, s Store, sg Signer, acct, by string) error { if !ValidAccount(acct) { return fmt.Errorf("pointer digests: invalid account %q", acct) } key := PointerDigestsKey(acct) _, etag, err := s.Get(ctx, key) switch { case errors.Is(err, os.ErrNotExist): etag = "" case err != nil: return err default: if m, verr := ReadDigestMarker(ctx, s, sg.RootPublicKey(), acct); verr == nil && m != nil { return nil } } wire, err := SignRootEnvelope(sg, DigestMarker{Schema: 1, Account: acct, At: time.Now().UTC().Truncate(time.Second), By: by}) if err != nil { return fmt.Errorf("pointer digests: the marker needs the deployment's root key: %w", err) } _, err = s.Put(ctx, key, wire, etag) return err } // RemoveDigestMarker suspends emission for acct. It does not clear any box's // ratchet: a box that has seen digests for a channel keeps refusing pointers // without them (spec §4.6). func RemoveDigestMarker(ctx context.Context, s Store, acct string) error { if !ValidAccount(acct) { return fmt.Errorf("pointer digests: invalid account %q", acct) } if err := s.Delete(ctx, PointerDigestsKey(acct)); err != nil && !errors.Is(err, os.ErrNotExist) { return err } return nil } // ReadDigestMarker is (nil, nil) when acct has no marker, the marker when it // verifies against the root, and an error for anything else: a marker that is // present but cannot be trusted is never read as "off" or as "on". func ReadDigestMarker(ctx context.Context, s Store, pub ed25519.PublicKey, acct string) (*DigestMarker, error) { raw, _, err := s.Get(ctx, PointerDigestsKey(acct)) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, err } var m DigestMarker if err := VerifyRootEnvelope(pub, raw, &m); err != nil { return nil, fmt.Errorf("pointer digest marker for %s: %w", acct, err) } if m.Schema != 1 || m.Account != acct || m.At.IsZero() { return nil, fmt.Errorf("pointer digest marker for %s names account %q, schema %d", acct, m.Account, m.Schema) } return &m, nil }