carlos / platform Public

boxid: a box record is enrollment evidence, and one instance never holds two references

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 9be38241d76fcf838fc5a32f4cea4c1100a20458 parent 06b05b2
3 files changed, +237 −174
  • internal/boxid/boxid_test.go +98 −90
  • internal/boxid/record.go +24 −46
  • internal/boxid/verify.go +115 −38
diff --git a/internal/boxid/boxid_test.go b/internal/boxid/boxid_test.go
index bf3c0c6e9..f153f4c49 100644
--- a/internal/boxid/boxid_test.go
+++ b/internal/boxid/boxid_test.go
@@ -13,6 +13,7 @@ import (
"encoding/json"
"errors"
"math/big"
+ "os"
"testing"
"time"
@@ -53,146 +54,153 @@ func (s signer) record(t *testing.T, box, account, region, instance string) (Rec
t.Fatal(err)
}
rec := Record{V: 1, Box: box, IdentityDocument: base64.StdEncoding.EncodeToString(doc),
- IdentitySignature: base64.StdEncoding.EncodeToString(sig), WrittenAt: "2026-10-05T00:00:00Z"}
+ IdentitySignature: base64.StdEncoding.EncodeToString(sig), WrittenAt: "2026-10-06T00:00:00Z"}
raw, _ := json.Marshal(rec)
return rec, raw
}
+// verifier certifies eu-west-1 and eu-west-2 with the same key, so a test
+// can show that a supported but wrong region is still refused.
func (s signer) verifier() Verifier {
- return Verifier{Account: "111122223333", Certs: map[string]*x509.Certificate{"eu-west-1": s.cert}}
+ return Verifier{Account: "111122223333", Certs: map[string]*x509.Certificate{"eu-west-1": s.cert, "eu-west-2": s.cert}}
}
-func TestAutoScalingBoxVerifiesWhenIDIsItsInstance(t *testing.T) {
+func expect(box string) Expect { return Expect{Box: box, AWSRegion: "eu-west-1"} }
+
+func TestEnrollProvesTheCloudOfALabel(t *testing.T) {
s := newSigner(t)
- rec, raw := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
- id, err := s.verifier().Verify(rec, raw, nil)
+ _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
+ ev, err := s.verifier().Enroll(raw, expect("annie-welding"))
if err != nil {
t.Fatal(err)
}
- if id.InstanceID != "i-0abc" || id.Region != "eu-west-1" || id.AccountID != "111122223333" {
- t.Fatalf("identity = %+v", id)
+ sum := sha256.Sum256(raw)
+ want := Evidence{Box: "annie-welding", RecordSHA256: hex.EncodeToString(sum[:]),
+ Cloud: Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}}
+ if ev != want {
+ t.Fatalf("evidence = %+v\nwant %+v", ev, want)
}
-}
-
-// Review Focus 5: a valid document for instance A under box id B.
-func TestRecordUnderAnotherInstancesIDIsRefused(t *testing.T) {
- s := newSigner(t)
- rec, raw := s.record(t, "i-0other", "111122223333", "eu-west-1", "i-0abc")
- if _, err := s.verifier().Verify(rec, raw, nil); !errors.Is(err, ErrUnverified) {
- t.Fatalf("err = %v, want ErrUnverified", err)
+ if !ev.Cloud.Complete() {
+ t.Fatal("a proven cloud is not complete")
}
}
-func TestRefusals(t *testing.T) {
+func TestEnrollRefusals(t *testing.T) {
s, other := newSigner(t), newSigner(t)
- cases := map[string]func() (Record, []byte, Verifier){
- "wrong account": func() (Record, []byte, Verifier) {
- r, raw := s.record(t, "i-0abc", "999999999999", "eu-west-1", "i-0abc")
- return r, raw, s.verifier()
- },
- "region with no certificate": func() (Record, []byte, Verifier) {
- r, raw := s.record(t, "i-0abc", "111122223333", "ap-south-1", "i-0abc")
- return r, raw, s.verifier()
- },
- "signed by someone else": func() (Record, []byte, Verifier) {
- r, raw := other.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
- return r, raw, s.verifier()
- },
- "document altered after signing": func() (Record, []byte, Verifier) {
- r, _ := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
+ good := func() []byte {
+ _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
+ return raw
+ }
+ cases := map[string]struct {
+ raw []byte
+ e Expect
+ }{
+ "wrong account": {func() []byte {
+ _, raw := s.record(t, "annie-welding", "999999999999", "eu-west-1", "i-0abc")
+ return raw
+ }(), expect("annie-welding")},
+ "region with no certificate": {func() []byte {
+ _, raw := s.record(t, "annie-welding", "111122223333", "ap-south-1", "i-0abc")
+ return raw
+ }(), Expect{Box: "annie-welding", AWSRegion: "ap-south-1"}},
+ "a supported region that is not the placement's": {func() []byte {
+ _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-2", "i-0abc")
+ return raw
+ }(), expect("annie-welding")},
+ "signed by someone else": {func() []byte {
+ _, raw := other.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
+ return raw
+ }(), expect("annie-welding")},
+ "document altered after signing": {func() []byte {
+ r, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
doc, _ := json.Marshal(map[string]string{"accountId": "111122223333", "region": "eu-west-1", "instanceId": "i-0zzz"})
- r.IdentityDocument, r.Box = base64.StdEncoding.EncodeToString(doc), "i-0zzz"
+ r.IdentityDocument = base64.StdEncoding.EncodeToString(doc)
raw, _ := json.Marshal(r)
- return r, raw, s.verifier()
- },
- "empty box": func() (Record, []byte, Verifier) {
- r, raw := s.record(t, "", "111122223333", "eu-west-1", "i-0abc")
- return r, raw, s.verifier()
- },
- }
- for name, mk := range cases {
+ return raw
+ }(), expect("annie-welding")},
+ "record names another box": {good(), expect("bert-sawing")},
+ "reference is not a label": {good(), expect("annie.welding")},
+ "no expected region": {good(), Expect{Box: "annie-welding"}},
+ "tofu names another instance": {good(), Expect{Box: "annie-welding", AWSRegion: "eu-west-1", TofuInstanceID: "i-0zzz"}},
+ "unknown field in the record": {[]byte(`{"v":1,"box":"annie-welding","extra":1}`), expect("annie-welding")},
+ "trailing data": {append(good(), []byte(`{}`)...), expect("annie-welding")},
+ "version 2": {func() []byte {
+ r, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
+ r.V = 2
+ raw, _ := json.Marshal(r)
+ return raw
+ }(), expect("annie-welding")},
+ "reference enrolled elsewhere": {good(), Expect{Box: "annie-welding", AWSRegion: "eu-west-1", Roster: []Enrolled{{Box: "annie-welding", Cloud: Cloud{InstanceID: "i-0old"}}}}},
+ }
+ for name, tc := range cases {
t.Run(name, func(t *testing.T) {
- r, raw, v := mk()
- if _, err := v.Verify(r, raw, nil); !errors.Is(err, ErrUnverified) {
- t.Fatalf("err = %v, want ErrUnverified", err)
+ if ev, err := s.verifier().Enroll(tc.raw, tc.e); !errors.Is(err, ErrUnverified) {
+ t.Fatalf("evidence = %+v, err = %v; want ErrUnverified", ev, err)
}
})
}
}
-func TestLegacyLabelNeedsABindNamingThisExactRecord(t *testing.T) {
+// Review Focus 1: box A's record, intact and validly signed, copied under
+// box B's key with its box field rewritten. The signature still verifies,
+// because the box field is not signed. Only the roster check refuses it.
+func TestEnrollRefusesAnInstanceAlreadyEnrolledUnderAnotherReference(t *testing.T) {
s := newSigner(t)
- rec, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
- if _, err := s.verifier().Verify(rec, raw, nil); !errors.Is(err, ErrUnverified) {
- t.Fatalf("no bind: err = %v, want ErrUnverified", err)
+ _, raw := s.record(t, "bert-sawing", "111122223333", "eu-west-1", "i-0abc")
+ roster := []Enrolled{{Box: "annie-welding", Cloud: Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}}}
+ if _, err := s.verifier().Enroll(raw, Expect{Box: "bert-sawing", AWSRegion: "eu-west-1", Roster: roster}); !errors.Is(err, ErrUnverified) {
+ t.Fatalf("err = %v, want ErrUnverified", err)
}
- sum := sha256.Sum256(raw)
- good := &Bind{RecordSHA256: hex.EncodeToString(sum[:]), InstanceID: "i-0abc", TofuInstanceID: "i-0abc", Operator: "paul", At: "2026-10-05T00:00:00Z"}
- if _, err := s.verifier().Verify(rec, raw, good); err != nil {
- t.Fatalf("good bind: %v", err)
- }
- for name, b := range map[string]Bind{
- "another record's hash": {RecordSHA256: "00", InstanceID: "i-0abc", TofuInstanceID: "i-0abc", Operator: "paul"},
- "tofu names another box": {RecordSHA256: good.RecordSHA256, InstanceID: "i-0abc", TofuInstanceID: "i-0zzz", Operator: "paul"},
- "bind names another box": {RecordSHA256: good.RecordSHA256, InstanceID: "i-0zzz", TofuInstanceID: "i-0zzz", Operator: "paul"},
- "no operator": {RecordSHA256: good.RecordSHA256, InstanceID: "i-0abc", TofuInstanceID: "i-0abc"},
- } {
- b := b
- t.Run(name, func(t *testing.T) {
- if _, err := s.verifier().Verify(rec, raw, &b); !errors.Is(err, ErrUnverified) {
- t.Fatalf("err = %v, want ErrUnverified", err)
- }
- })
+ // The same box re-enrolled as the same instance is not a conflict.
+ roster[0].Box = "bert-sawing"
+ if _, err := s.verifier().Enroll(raw, Expect{Box: "bert-sawing", AWSRegion: "eu-west-1", Roster: roster}); err != nil {
+ t.Fatalf("re-enrolling the same instance: %v", err)
+ }
+}
+
+func TestEnrollAcceptsTheTofuInstanceForALegacyReference(t *testing.T) {
+ s := newSigner(t)
+ _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
+ if _, err := s.verifier().Enroll(raw, Expect{Box: "annie-welding", AWSRegion: "eu-west-1", TofuInstanceID: "i-0abc"}); err != nil {
+ t.Fatal(err)
}
}
func TestPublishIsCreateOnlyAndIdempotent(t *testing.T) {
ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t)
- rec, _ := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
+ rec, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
if err := Publish(ctx, st, rec); err != nil {
t.Fatal(err)
}
if err := Publish(ctx, st, rec); err != nil {
t.Fatalf("republishing the same record: %v", err)
}
- other, _ := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
+ other := rec
other.WrittenAt = "2027-01-01T00:00:00Z"
if err := Publish(ctx, st, other); !errors.Is(err, release.ErrCASConflict) {
t.Fatalf("a different record for the same box: err = %v, want ErrCASConflict", err)
}
- got, raw, bind, err := Load(ctx, st, "i-0abc")
- if err != nil || got.WrittenAt != rec.WrittenAt || len(raw) == 0 || bind != nil {
- t.Fatalf("Load = %+v, %d bytes, bind %v, err %v", got, len(raw), bind, err)
- }
-}
-
-func TestLoadReadsABindWhenThereIsOne(t *testing.T) {
- ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t)
- rec, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
- if err := Publish(ctx, st, rec); err != nil {
- t.Fatal(err)
- }
- body, _ := json.Marshal(Bind{RecordSHA256: "aa", InstanceID: "i-0abc", TofuInstanceID: "i-0abc", Operator: "paul"})
- if _, err := st.Put(ctx, BindKey("annie-welding"), body, ""); err != nil {
+ raw, err := Load(ctx, st, "annie-welding")
+ if err != nil {
t.Fatal(err)
}
- _, _, bind, err := Load(ctx, st, "annie-welding")
- if err != nil || bind == nil || bind.Operator != "paul" {
- t.Fatalf("bind = %+v, err %v", bind, err)
+ if _, err := s.verifier().Enroll(raw, expect("annie-welding")); err != nil {
+ t.Fatalf("the published record does not enroll: %v", err)
}
}
-// Review Focus 5, the storage half: box A's own record, unchanged and
-// validly signed, stored under box B's key must not load as B.
-func TestLoadRefusesAnotherBoxesRecordUnderThisKey(t *testing.T) {
+func TestPublishAndLoadRefuseAReferenceThatIsNotALabel(t *testing.T) {
ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t)
- _, raw := s.record(t, "i-0aaa", "111122223333", "eu-west-1", "i-0aaa")
- if _, err := st.Put(ctx, Key("i-0bbb"), raw, ""); err != nil {
- t.Fatal(err)
+ rec, _ := s.record(t, "x.bind", "111122223333", "eu-west-1", "i-0abc")
+ if err := Publish(ctx, st, rec); err == nil {
+ t.Fatal("published under a reference with a dot")
}
- if _, _, _, err := Load(ctx, st, "i-0bbb"); !errors.Is(err, ErrUnverified) {
+ if _, err := Load(ctx, st, "../edges/x"); !errors.Is(err, ErrUnverified) {
t.Fatalf("err = %v, want ErrUnverified", err)
}
+ if _, err := Load(ctx, st, "nobody-here"); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("err = %v, want os.ErrNotExist", err)
+ }
}
func TestEmbeddedCertsCoverTheFlagshipRegions(t *testing.T) {
diff --git a/internal/boxid/record.go b/internal/boxid/record.go
index a743ca0ea..5bbcac646 100644
--- a/internal/boxid/record.go
+++ b/internal/boxid/record.go
@@ -1,7 +1,10 @@
-// Package boxid binds the name a claim uses for a box to one AWS instance
-// (region HA spec 1 §5.1). The binding is the box's own AWS-signed identity
-// document, written once. Nothing else is evidence of a box's identity: not
-// an edges file, not a status snapshot.
+// Package boxid is the evidence a platform box presents to be enrolled in
+// the roster (region HA spec 1 §5.1, ruling H11): the box's own AWS-signed
+// instance identity document, published once under its box reference. The
+// record grants nothing by itself. Enrollment verifies it (Verifier.Enroll)
+// and copies the proven account, region and instance id into the roster
+// entry's cloud field; from then on the roster, never this record, is what
+// binds a reference to an instance.
package boxid
import (
@@ -10,15 +13,19 @@ import (
"encoding/json"
"errors"
"fmt"
- "os"
+ "github.com/carlosframework/platform/internal/controlname"
"github.com/carlosframework/platform/internal/release"
)
const Prefix = "control/boxes/"
-func Key(box string) string { return Prefix + box + ".json" }
-func BindKey(box string) string { return Prefix + box + ".bind.json" }
+// ValidBox reports whether ref can be a box reference: one DNS label, so
+// it can never contain "." or "/" and no two references share a key.
+func ValidBox(ref string) bool { return controlname.ValidLabel(ref) }
+
+// Key is where a box's record lives. Callers check ValidBox first.
+func Key(box string) string { return Prefix + box + ".json" }
type Record struct {
V int `json:"v"`
@@ -28,20 +35,13 @@ type Record struct {
WrittenAt string `json:"written_at"`
}
-// Bind is an operator's statement that a legacy label belongs to one
-// instance. It names the exact record bytes it vouches for.
-type Bind struct {
- RecordSHA256 string `json:"record_sha256"`
- InstanceID string `json:"instance_id"`
- TofuInstanceID string `json:"tofu_instance_id"`
- Operator string `json:"operator"`
- At string `json:"at"`
-}
-
// Publish writes a box's record create-only. Writing the same bytes again
// succeeds; a different record for the same box is ErrCASConflict, because
// a record is never replaced.
func Publish(ctx context.Context, s release.Store, rec Record) error {
+ if !ValidBox(rec.Box) {
+ return fmt.Errorf("boxid: %q is not a box reference", rec.Box)
+ }
body, err := json.Marshal(rec)
if err != nil {
return fmt.Errorf("boxid: marshal: %w", err)
@@ -60,34 +60,12 @@ func Publish(ctx context.Context, s release.Store, rec Record) error {
return fmt.Errorf("boxid: %s already has a different record: %w", rec.Box, release.ErrCASConflict)
}
-// Load returns a box's record, its exact bytes (which a Bind's hash is over)
-// and its bind when one exists. A missing record is os.ErrNotExist-wrapped.
-// A record that names a different box than the key it is stored under is
-// ErrUnverified: the key is part of the binding.
-func Load(ctx context.Context, s release.Store, box string) (Record, []byte, *Bind, error) {
- raw, _, err := s.Get(ctx, Key(box))
- if err != nil {
- return Record{}, nil, nil, err
- }
- var rec Record
- if err := json.Unmarshal(raw, &rec); err != nil {
- return Record{}, nil, nil, fmt.Errorf("boxid: %s: %w", box, err)
+// Load returns the exact bytes of a box's record, which is what enrollment
+// verifies and hashes. A missing record is os.ErrNotExist-wrapped.
+func Load(ctx context.Context, s release.Store, box string) ([]byte, error) {
+ if !ValidBox(box) {
+ return nil, fmt.Errorf("%w: %q is not a box reference", ErrUnverified, box)
}
- if rec.Box != box {
- // Another box's record, intact and validly signed, copied under
- // this name. Verify would accept it as that other box.
- return Record{}, nil, nil, fmt.Errorf("%w: the record stored for %q names %q", ErrUnverified, box, rec.Box)
- }
- braw, _, err := s.Get(ctx, BindKey(box))
- if errors.Is(err, os.ErrNotExist) {
- return rec, raw, nil, nil
- }
- if err != nil {
- return Record{}, nil, nil, err
- }
- var b Bind
- if err := json.Unmarshal(braw, &b); err != nil {
- return Record{}, nil, nil, fmt.Errorf("boxid: %s bind: %w", box, err)
- }
- return rec, raw, &b, nil
+ raw, _, err := s.Get(ctx, Key(box))
+ return raw, err
}
diff --git a/internal/boxid/verify.go b/internal/boxid/verify.go
index 52f922971..033404a8d 100644
--- a/internal/boxid/verify.go
+++ b/internal/boxid/verify.go
@@ -1,6 +1,7 @@
package boxid
import (
+ "bytes"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
@@ -8,38 +9,115 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io"
)
-// ErrUnverified wraps every reason a record is not evidence of identity.
+// ErrUnverified wraps every reason a record is not evidence for enrollment.
var ErrUnverified = errors.New("boxid: record does not verify")
+// Identity is what an identity document proves. termfence binds to it
+// until it moves to Cloud; nothing else uses it.
type Identity struct{ AccountID, Region, InstanceID string }
+// Cloud is the roster entry's cloud field (spec §2, §5.1): the instance a
+// box reference is enrolled as. Enrollment fills it from a verified record;
+// every later check (the termination fence, volume records) compares
+// against it and never against anything a box says about itself.
+type Cloud struct {
+ Provider string `json:"provider"`
+ Account string `json:"account"`
+ Region string `json:"region"`
+ InstanceID string `json:"instance_id"`
+}
+
+// Complete reports whether every field is set and the provider is AWS.
+func (c Cloud) Complete() bool {
+ return c.Provider == "aws" && c.Account != "" && c.Region != "" && c.InstanceID != ""
+}
+
type Verifier struct {
- Account string
- Certs map[string]*x509.Certificate
+ Account string // the deployment's AWS account id
+ Certs map[string]*x509.Certificate // by AWS region
+}
+
+// Enrolled is one roster entry as enrollment needs to see it: its
+// reference and the instance it was enrolled as. Entries in every state
+// count, retired ones included: a retired reference never names a working
+// box again (spec §5.9), and a terminated instance never comes back.
+type Enrolled struct {
+ Box string
+ Cloud Cloud
+}
+
+// Expect is what the enrolling side knows before it reads the record.
+type Expect struct {
+ Box string // the reference being enrolled
+ AWSRegion string // the AWS region of the placement it is enrolled into
+ // TofuInstanceID is the region's tofu region-host instance_id output.
+ // The caller sets it when Box already appears in claims or edges files,
+ // as a box predating region HA does, and leaves it "" otherwise.
+ TofuInstanceID string
+ Roster []Enrolled
+}
+
+// Evidence is what enrollment records: the roster entry's cloud, and the
+// SHA-256 of the exact record bytes it was proven from.
+type Evidence struct {
+ Box string
+ Cloud Cloud
+ RecordSHA256 string
}
func refuse(format string, a ...any) error {
return fmt.Errorf("%w: %s", ErrUnverified, fmt.Sprintf(format, a...))
}
-// Verify checks, in order: the signature under the certificate for the
-// document's own region, the deployment account, and the binding of the
-// record's box id to the signed instance id. A box whose id is its instance
-// id needs nothing more. Any other id is a legacy label and needs a bind
-// that names these exact record bytes and agrees on the instance.
-func (v Verifier) Verify(rec Record, raw []byte, bind *Bind) (Identity, error) {
- if rec.Box == "" {
- return Identity{}, refuse("no box id")
+func decodeRecord(raw []byte) (Record, error) {
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.DisallowUnknownFields()
+ var rec Record
+ if err := dec.Decode(&rec); err != nil {
+ return Record{}, refuse("record is not JSON: %v", err)
+ }
+ if _, err := dec.Token(); err != io.EOF {
+ return Record{}, refuse("trailing data after the record")
+ }
+ return rec, nil
+}
+
+// Enroll checks raw, the exact bytes stored at Key(e.Box), as enrollment
+// evidence (spec §5.1, H11). In order: the reference, the record's own box
+// field, AWS's signature under the certificate for the document's region,
+// the deployment account, the AWS region expected for the placement, the
+// tofu instance id for a reference predating region HA, and the roster:
+// the reference enrolled as another instance, or the instance enrolled
+// under another reference, is refused. A record copied from one box to
+// another's key carries the first box's signed instance id, so the last
+// check is what keeps one machine from holding two references.
+func (v Verifier) Enroll(raw []byte, e Expect) (Evidence, error) {
+ if !ValidBox(e.Box) {
+ return Evidence{}, refuse("%q is not a box reference", e.Box)
+ }
+ if e.AWSRegion == "" {
+ return Evidence{}, refuse("no expected AWS region for %s", e.Box)
+ }
+ rec, err := decodeRecord(raw)
+ if err != nil {
+ return Evidence{}, err
+ }
+ if rec.V != 1 {
+ return Evidence{}, refuse("record version %d", rec.V)
+ }
+ if rec.Box != e.Box {
+ return Evidence{}, refuse("the record stored for %q names %q", e.Box, rec.Box)
}
doc, err := base64.StdEncoding.DecodeString(rec.IdentityDocument)
if err != nil {
- return Identity{}, refuse("document is not base64")
+ return Evidence{}, refuse("document is not base64")
}
sig, err := base64.StdEncoding.DecodeString(rec.IdentitySignature)
if err != nil {
- return Identity{}, refuse("signature is not base64")
+ return Evidence{}, refuse("signature is not base64")
}
var d struct {
AccountID string `json:"accountId"`
@@ -47,38 +125,37 @@ func (v Verifier) Verify(rec Record, raw []byte, bind *Bind) (Identity, error) {
InstanceID string `json:"instanceId"`
}
if err := json.Unmarshal(doc, &d); err != nil {
- return Identity{}, refuse("document is not JSON")
+ return Evidence{}, refuse("document is not JSON")
}
cert := v.Certs[d.Region]
if cert == nil {
- return Identity{}, refuse("no signing certificate for region %q", d.Region)
+ return Evidence{}, refuse("no signing certificate for region %q", d.Region)
}
if err := cert.CheckSignature(x509.SHA256WithRSA, doc, sig); err != nil {
- return Identity{}, refuse("signature: %v", err)
- }
- if d.InstanceID == "" {
- return Identity{}, refuse("document names no instance")
- }
- if v.Account == "" || d.AccountID != v.Account {
- return Identity{}, refuse("account %q is not the deployment's", d.AccountID)
+ return Evidence{}, refuse("signature: %v", err)
}
- id := Identity{AccountID: d.AccountID, Region: d.Region, InstanceID: d.InstanceID}
- if rec.Box == d.InstanceID {
- return id, nil
+ switch {
+ case d.InstanceID == "":
+ return Evidence{}, refuse("document names no instance")
+ case v.Account == "" || d.AccountID != v.Account:
+ return Evidence{}, refuse("account %q is not the deployment's", d.AccountID)
+ case d.Region != e.AWSRegion:
+ return Evidence{}, refuse("the box is in %q, the placement is in %q", d.Region, e.AWSRegion)
+ case e.TofuInstanceID != "" && e.TofuInstanceID != d.InstanceID:
+ return Evidence{}, refuse("tofu records instance %q for this region, the record %q", e.TofuInstanceID, d.InstanceID)
}
- if bind == nil {
- return Identity{}, refuse("%q is a legacy label with no operator bind", rec.Box)
+ for _, en := range e.Roster {
+ if en.Box == e.Box && en.Cloud.InstanceID != d.InstanceID {
+ return Evidence{}, refuse("%s is enrolled as instance %q", e.Box, en.Cloud.InstanceID)
+ }
+ if en.Box != e.Box && en.Cloud.InstanceID == d.InstanceID {
+ return Evidence{}, refuse("instance %s is enrolled as %s", d.InstanceID, en.Box)
+ }
}
sum := sha256.Sum256(raw)
- switch {
- case bind.RecordSHA256 != hex.EncodeToString(sum[:]):
- return Identity{}, refuse("bind is for a different record")
- case bind.InstanceID != d.InstanceID:
- return Identity{}, refuse("bind names instance %q, the record %q", bind.InstanceID, d.InstanceID)
- case bind.TofuInstanceID != d.InstanceID:
- return Identity{}, refuse("tofu records instance %q, the record %q", bind.TofuInstanceID, d.InstanceID)
- case bind.Operator == "":
- return Identity{}, refuse("bind names no operator")
- }
- return id, nil
+ return Evidence{
+ Box: e.Box,
+ Cloud: Cloud{Provider: "aws", Account: d.AccountID, Region: d.Region, InstanceID: d.InstanceID},
+ RecordSHA256: hex.EncodeToString(sum[:]),
+ }, nil
}