carlos / platform Public

boxid: a box record verifies only under AWS's signature, for its own instance

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev fff400c0bb7432218855d51f9355ce80a5cbed04 parent 7b43f19
9 files changed, +516 −0
  • internal/boxid/boxid_test.go +213 −0
  • internal/boxid/certs.go +43 −0
  • internal/boxid/certs/README.md +7 −0
  • internal/boxid/certs/ap-southeast-2.pem +19 −0
  • internal/boxid/certs/eu-west-1.pem +19 −0
  • internal/boxid/certs/eu-west-2.pem +19 −0
  • internal/boxid/certs/us-east-1.pem +19 −0
  • internal/boxid/record.go +93 −0
  • internal/boxid/verify.go +84 −0
diff --git a/internal/boxid/boxid_test.go b/internal/boxid/boxid_test.go
new file mode 100644
index 000000000..bf3c0c6e9
--- /dev/null
+++ b/internal/boxid/boxid_test.go
@@ -0,0 +1,213 @@
+package boxid
+
+import (
+ "context"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "math/big"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// signer stands in for AWS: one key and certificate per test.
+type signer struct {
+ key *rsa.PrivateKey
+ cert *x509.Certificate
+}
+
+func newSigner(t *testing.T) signer {
+ t.Helper()
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test"},
+ NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour)}
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cert, err := x509.ParseCertificate(der)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return signer{key, cert}
+}
+
+func (s signer) record(t *testing.T, box, account, region, instance string) (Record, []byte) {
+ t.Helper()
+ doc, _ := json.Marshal(map[string]string{"accountId": account, "region": region, "instanceId": instance})
+ sum := sha256.Sum256(doc)
+ sig, err := rsa.SignPKCS1v15(rand.Reader, s.key, crypto.SHA256, sum[:])
+ if err != nil {
+ 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"}
+ raw, _ := json.Marshal(rec)
+ return rec, raw
+}
+
+func (s signer) verifier() Verifier {
+ return Verifier{Account: "111122223333", Certs: map[string]*x509.Certificate{"eu-west-1": s.cert}}
+}
+
+func TestAutoScalingBoxVerifiesWhenIDIsItsInstance(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)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if id.InstanceID != "i-0abc" || id.Region != "eu-west-1" || id.AccountID != "111122223333" {
+ t.Fatalf("identity = %+v", id)
+ }
+}
+
+// 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)
+ }
+}
+
+func TestRefusals(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")
+ 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"
+ 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 {
+ 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)
+ }
+ })
+ }
+}
+
+func TestLegacyLabelNeedsABindNamingThisExactRecord(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)
+ }
+ 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)
+ }
+ })
+ }
+}
+
+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")
+ 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.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 {
+ 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)
+ }
+}
+
+// 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) {
+ 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)
+ }
+ if _, _, _, err := Load(ctx, st, "i-0bbb"); !errors.Is(err, ErrUnverified) {
+ t.Fatalf("err = %v, want ErrUnverified", err)
+ }
+}
+
+func TestEmbeddedCertsCoverTheFlagshipRegions(t *testing.T) {
+ certs, err := EmbeddedCerts()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, region := range []string{"eu-west-1", "eu-west-2", "us-east-1", "ap-southeast-2"} {
+ c := certs[region]
+ if c == nil {
+ t.Errorf("no certificate for %s", region)
+ continue
+ }
+ if c.PublicKeyAlgorithm != x509.RSA {
+ t.Errorf("%s: %v key; the base64-signature certificate is RSA (the PKCS7 one is DSA: wrong tab)", region, c.PublicKeyAlgorithm)
+ }
+ }
+}
diff --git a/internal/boxid/certs.go b/internal/boxid/certs.go
new file mode 100644
index 000000000..292d7876f
--- /dev/null
+++ b/internal/boxid/certs.go
@@ -0,0 +1,43 @@
+package boxid
+
+import (
+ "crypto/x509"
+ "embed"
+ "encoding/pem"
+ "fmt"
+ "strings"
+)
+
+//go:embed certs/*.pem
+var certFS embed.FS
+
+// EmbeddedCerts returns AWS's instance-identity signing certificates by
+// region, from certs/<aws-region>.pem. A region with no file has no
+// certificate, so its boxes never verify: that is the safe default.
+func EmbeddedCerts() (map[string]*x509.Certificate, error) {
+ entries, err := certFS.ReadDir("certs")
+ if err != nil {
+ return nil, err
+ }
+ out := map[string]*x509.Certificate{}
+ for _, e := range entries {
+ region, ok := strings.CutSuffix(e.Name(), ".pem")
+ if !ok {
+ continue
+ }
+ raw, err := certFS.ReadFile("certs/" + e.Name())
+ if err != nil {
+ return nil, err
+ }
+ block, _ := pem.Decode(raw)
+ if block == nil {
+ return nil, fmt.Errorf("boxid: certs/%s is not PEM", e.Name())
+ }
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("boxid: certs/%s: %w", e.Name(), err)
+ }
+ out[region] = cert
+ }
+ return out, nil
+}
diff --git a/internal/boxid/certs/README.md b/internal/boxid/certs/README.md
new file mode 100644
index 000000000..2993de1a6
--- /dev/null
+++ b/internal/boxid/certs/README.md
@@ -0,0 +1,7 @@
+AWS's public certificates for the base64-encoded instance identity
+signature (IMDS `/latest/dynamic/instance-identity/signature`), one per
+region, from
+https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/regions-certs.html
+(the RSA certificate (the one for the base64-encoded signature; not DSA, not RSA-2048)). A region with no file here has no
+certificate, and a box there never verifies. Add a region's file before
+running a second box in it.
diff --git a/internal/boxid/certs/ap-southeast-2.pem b/internal/boxid/certs/ap-southeast-2.pem
new file mode 100644
index 000000000..1411c269a
--- /dev/null
+++ b/internal/boxid/certs/ap-southeast-2.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAoqgAwIBAgIUFxWyAdk4oiXIOC9PxcgjYYh71mwwDQYJKoZIhvcNAQEL
+BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
+BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
+MB4XDTI0MDQyOTE1MjE0M1oXDTI5MDQyODE1MjE0M1owXDELMAkGA1UEBhMCVVMx
+GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
+BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
+UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
+vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
+o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
+UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
+BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
+ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFxWyAdk4
+oiXIOC9PxcgjYYh71mwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
+AAOBgQByjeQe6lr7fiIhoGdjBXYzDfkX0lGGvMIhRh57G1bbceQfaYdZd7Ptc0jl
+bpycKGaTvhUdkpMOiV2Hi9dOOYawkdhyJDstmDNKu6P9+b6Kak8He5z3NU1tUR2Y
+uTwcz7Ye8Nldx//ws3raErfTI7D6s9m63OX8cAJ/f8bNgikwpw==
+-----END CERTIFICATE-----
diff --git a/internal/boxid/certs/eu-west-1.pem b/internal/boxid/certs/eu-west-1.pem
new file mode 100644
index 000000000..6aec21c96
--- /dev/null
+++ b/internal/boxid/certs/eu-west-1.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAoqgAwIBAgIUakDaQ1Zqy87Hy9ESXA1pFC116HkwDQYJKoZIhvcNAQEL
+BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
+BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
+MB4XDTI0MDQyOTE2MTgxMFoXDTI5MDQyODE2MTgxMFowXDELMAkGA1UEBhMCVVMx
+GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
+BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
+UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
+vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
+o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
+UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
+BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
+ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUakDaQ1Zq
+y87Hy9ESXA1pFC116HkwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
+AAOBgQADIkn/MqaLGPuK5+prZZ5Ox4bBZLPtreO2C7r0pqU2kPM2lVPyYYydkvP0
+lgSmmsErGu/oL9JNztDe2oCA+kNy17ehcsf8cw0uP861czNFKCeU8b7FgBbL+sIm
+qi33rAq6owWGi/5uEcfCR+JP7W+oSYVir5r/yDmWzx+BVH5S/g==
+-----END CERTIFICATE-----
diff --git a/internal/boxid/certs/eu-west-2.pem b/internal/boxid/certs/eu-west-2.pem
new file mode 100644
index 000000000..7ef8e220c
--- /dev/null
+++ b/internal/boxid/certs/eu-west-2.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAoqgAwIBAgIUCgCV/DPxYNND/swDgEKGiC5I+EwwDQYJKoZIhvcNAQEL
+BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
+BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
+MB4XDTI0MDQyOTE2MjkxNFoXDTI5MDQyODE2MjkxNFowXDELMAkGA1UEBhMCVVMx
+GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
+BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
+UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
+vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
+o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
+UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
+BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
+ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUCgCV/DPx
+YNND/swDgEKGiC5I+EwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
+AAOBgQATPu/sOE2esNa4+XPEGKlEJSgqzyBSQLQc+VWo6FAJhGG9fp7D97jhHeLC
+5vwfmtTAfnGBxadfAOT3ASkxnOZhXtnRna460LtnNHm7ArCVgXKJo7uBn6ViXtFh
+uEEw4y6p9YaLQna+VC8Xtgw6WKq2JXuKzuhuNKSFaGGw9vRcHg==
+-----END CERTIFICATE-----
diff --git a/internal/boxid/certs/us-east-1.pem b/internal/boxid/certs/us-east-1.pem
new file mode 100644
index 000000000..be318505b
--- /dev/null
+++ b/internal/boxid/certs/us-east-1.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAoqgAwIBAgIUE1y2NIKCU+Rg4uu4u32koG9QEYIwDQYJKoZIhvcNAQEL
+BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
+BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
+MB4XDTI0MDQyOTE3MzQwMVoXDTI5MDQyODE3MzQwMVowXDELMAkGA1UEBhMCVVMx
+GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
+BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
+UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
+vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
+o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
+UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
+BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
+ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUE1y2NIKC
+U+Rg4uu4u32koG9QEYIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
+AAOBgQAlxSmwcWnhT4uAeSinJuz+1BTcKhVSWb5jT8pYjQb8ZoZkXXRGb09mvYeU
+NeqOBr27rvRAnaQ/9LUQf72+SahDFuS4CMI8nwowytqbmwquqFr4dxA/SDADyRiF
+ea1UoMuNHTY49J/1vPomqsVn7mugTp+TbjqCfOJTpu0temHcFA==
+-----END CERTIFICATE-----
diff --git a/internal/boxid/record.go b/internal/boxid/record.go
new file mode 100644
index 000000000..a743ca0ea
--- /dev/null
+++ b/internal/boxid/record.go
@@ -0,0 +1,93 @@
+// 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
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+
+ "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" }
+
+type Record struct {
+ V int `json:"v"`
+ Box string `json:"box"`
+ IdentityDocument string `json:"identity_document"`
+ IdentitySignature string `json:"identity_signature"`
+ 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 {
+ body, err := json.Marshal(rec)
+ if err != nil {
+ return fmt.Errorf("boxid: marshal: %w", err)
+ }
+ _, err = s.Put(ctx, Key(rec.Box), body, "")
+ if !errors.Is(err, release.ErrCASConflict) {
+ return err
+ }
+ cur, _, gerr := s.Get(ctx, Key(rec.Box))
+ if gerr != nil {
+ return fmt.Errorf("boxid: %s exists and cannot be read: %w", rec.Box, gerr)
+ }
+ if bytes.Equal(cur, body) {
+ return nil
+ }
+ 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)
+ }
+ 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
+}
diff --git a/internal/boxid/verify.go b/internal/boxid/verify.go
new file mode 100644
index 000000000..52f922971
--- /dev/null
+++ b/internal/boxid/verify.go
@@ -0,0 +1,84 @@
+package boxid
+
+import (
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+)
+
+// ErrUnverified wraps every reason a record is not evidence of identity.
+var ErrUnverified = errors.New("boxid: record does not verify")
+
+type Identity struct{ AccountID, Region, InstanceID string }
+
+type Verifier struct {
+ Account string
+ Certs map[string]*x509.Certificate
+}
+
+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")
+ }
+ doc, err := base64.StdEncoding.DecodeString(rec.IdentityDocument)
+ if err != nil {
+ return Identity{}, refuse("document is not base64")
+ }
+ sig, err := base64.StdEncoding.DecodeString(rec.IdentitySignature)
+ if err != nil {
+ return Identity{}, refuse("signature is not base64")
+ }
+ var d struct {
+ AccountID string `json:"accountId"`
+ Region string `json:"region"`
+ InstanceID string `json:"instanceId"`
+ }
+ if err := json.Unmarshal(doc, &d); err != nil {
+ return Identity{}, refuse("document is not JSON")
+ }
+ cert := v.Certs[d.Region]
+ if cert == nil {
+ return Identity{}, 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)
+ }
+ id := Identity{AccountID: d.AccountID, Region: d.Region, InstanceID: d.InstanceID}
+ if rec.Box == d.InstanceID {
+ return id, nil
+ }
+ if bind == nil {
+ return Identity{}, refuse("%q is a legacy label with no operator bind", rec.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
+}