package hostactors import ( "bytes" "context" "crypto/ed25519" "encoding/json" "math" "time" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // CensusKeyBinding is approved by the root enrollment coordinator after its // authenticated box session supplies a proof of possession. It is not an // unauthenticated enrollment request or a console-generated box report. type CensusKeyBinding struct { Bucket string `json:"bucket"` Box string `json:"box"` Enrollment string `json:"enrollment"` PublicKey ed25519.PublicKey `json:"public_key"` } func (d Directory) BindCensusKey(ctx context.Context, signer release.Signer, before Observed, binding CensusKeyBinding, proof []byte) (Observed, error) { // This write bypasses Replace's membership gate, so it decides against // the signed roster at the caller's ETag, never a caller-supplied copy. fresh, err := d.Read(ctx) if err != nil { return Observed{}, err } if before.ETag == "" || fresh.ETag != before.ETag { return Observed{}, ErrRoster } before = fresh b, ok := before.Roster.Boxes[binding.Box] if !ok || binding.Bucket != d.Bucket || binding.Enrollment != b.Enrollment || len(binding.PublicKey) != ed25519.PublicKeySize || before.Roster.Revision == math.MaxUint64 { return Observed{}, ErrRoster } raw, err := json.Marshal(binding) if err != nil { return Observed{}, err } if !ed25519.Verify(binding.PublicKey, raw, proof) { return Observed{}, ErrRoster } // I2: binaries older than census_public_key refuse the strict-decoded // roster once a key is bound, so binding waits for the published floor // that excludes them. Only an active, unfenced enrollment is bound. if before.Roster.Publication != "published" || b.State != "active" { return Observed{}, ErrRoster } floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket) if err != nil { return Observed{}, err } if floor.Digest != before.Roster.FloorSHA256 { return Observed{}, ErrRoster } fenced, err := d.TerminationFence(ctx, before, binding.Box, time.Now()) if err != nil { return Observed{}, err } if fenced { return Observed{}, ErrRoster } if len(b.CensusPublicKey) != 0 { if bytes.Equal(b.CensusPublicKey, binding.PublicKey) { return before, nil } return Observed{}, ErrRoster } // One write-once extension to an existing enrollment. Ordinary Replace // continues to reject changes to this field, including deletion/rotation. next := Clone(before.Roster) b.CensusPublicKey = append(ed25519.PublicKey(nil), binding.PublicKey...) next.Boxes[binding.Box] = b next.Revision++ if err = next.Validate(d.Bucket); err != nil { return Observed{}, err } wire, err := release.SignRootEnvelope(signer, next) if err != nil { return Observed{}, err } tag, err := d.Store.Put(ctx, Key, wire, before.ETag) if err != nil { return Observed{}, err } return Observed{Roster: next, ETag: tag}, nil }