| 1 | package boxid |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/rsa" |
| 7 | "crypto/sha256" |
| 8 | "crypto/x509" |
| 9 | "encoding/base64" |
| 10 | "encoding/hex" |
| 11 | "encoding/json" |
| 12 | "errors" |
| 13 | "os" |
| 14 | "testing" |
| 15 | |
| 16 | "github.com/carlosframework/platform/internal/boxid/internal/signtest" |
| 17 | "github.com/carlosframework/platform/internal/release" |
| 18 | ) |
| 19 | |
| 20 | // signer stands in for AWS (signtest, shared with boxidtest): one key and |
| 21 | // certificate per test. |
| 22 | type signer struct { |
| 23 | key *rsa.PrivateKey |
| 24 | cert *x509.Certificate |
| 25 | } |
| 26 | |
| 27 | func newSigner(t *testing.T) signer { |
| 28 | t.Helper() |
| 29 | s := signtest.New(t) |
| 30 | return signer{s.Key, s.Cert} |
| 31 | } |
| 32 | |
| 33 | func (s signer) record(t *testing.T, box, account, region, instance string) (Record, []byte) { |
| 34 | t.Helper() |
| 35 | doc, sig := (&signtest.Signer{Key: s.key, Cert: s.cert}).Sign(t, account, region, instance) |
| 36 | rec := Record{V: 1, Box: box, IdentityDocument: base64.StdEncoding.EncodeToString(doc), |
| 37 | IdentitySignature: base64.StdEncoding.EncodeToString(sig), WrittenAt: "2026-10-06T00:00:00Z"} |
| 38 | raw, _ := json.Marshal(rec) |
| 39 | return rec, raw |
| 40 | } |
| 41 | |
| 42 | // verifier certifies eu-west-1 and eu-west-2 with the same key, so a test |
| 43 | // can show that a supported but wrong region is still refused. |
| 44 | func (s signer) verifier() Verifier { |
| 45 | return Verifier{Account: "111122223333", Certs: map[string]*x509.Certificate{"eu-west-1": s.cert, "eu-west-2": s.cert}} |
| 46 | } |
| 47 | |
| 48 | func expect(box string) Expect { return Expect{Box: box, AWSRegion: "eu-west-1"} } |
| 49 | |
| 50 | func TestEnrollProvesTheCloudOfALabel(t *testing.T) { |
| 51 | s := newSigner(t) |
| 52 | _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 53 | ev, err := s.verifier().Enroll(raw, expect("annie-welding")) |
| 54 | if err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | sum := sha256.Sum256(raw) |
| 58 | want := Evidence{Box: "annie-welding", RecordSHA256: hex.EncodeToString(sum[:]), |
| 59 | Cloud: Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}} |
| 60 | if ev != want { |
| 61 | t.Fatalf("evidence = %+v\nwant %+v", ev, want) |
| 62 | } |
| 63 | if !ev.Cloud.Complete() { |
| 64 | t.Fatal("a proven cloud is not complete") |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | func TestEnrollRefusals(t *testing.T) { |
| 69 | s, other := newSigner(t), newSigner(t) |
| 70 | good := func() []byte { |
| 71 | _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 72 | return raw |
| 73 | } |
| 74 | cases := map[string]struct { |
| 75 | raw []byte |
| 76 | e Expect |
| 77 | }{ |
| 78 | "wrong account": {func() []byte { |
| 79 | _, raw := s.record(t, "annie-welding", "999999999999", "eu-west-1", "i-0abc") |
| 80 | return raw |
| 81 | }(), expect("annie-welding")}, |
| 82 | "region with no certificate": {func() []byte { |
| 83 | _, raw := s.record(t, "annie-welding", "111122223333", "ap-south-1", "i-0abc") |
| 84 | return raw |
| 85 | }(), Expect{Box: "annie-welding", AWSRegion: "ap-south-1"}}, |
| 86 | "a supported region that is not the placement's": {func() []byte { |
| 87 | _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-2", "i-0abc") |
| 88 | return raw |
| 89 | }(), expect("annie-welding")}, |
| 90 | "signed by someone else": {func() []byte { |
| 91 | _, raw := other.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 92 | return raw |
| 93 | }(), expect("annie-welding")}, |
| 94 | "document altered after signing": {func() []byte { |
| 95 | r, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 96 | doc, _ := json.Marshal(map[string]string{"accountId": "111122223333", "region": "eu-west-1", "instanceId": "i-0zzz"}) |
| 97 | r.IdentityDocument = base64.StdEncoding.EncodeToString(doc) |
| 98 | raw, _ := json.Marshal(r) |
| 99 | return raw |
| 100 | }(), expect("annie-welding")}, |
| 101 | "record names another box": {good(), expect("bert-sawing")}, |
| 102 | "reference is not a label": {good(), expect("annie.welding")}, |
| 103 | "no expected region": {good(), Expect{Box: "annie-welding"}}, |
| 104 | "tofu names another instance": {good(), Expect{Box: "annie-welding", AWSRegion: "eu-west-1", TofuInstanceID: "i-0zzz"}}, |
| 105 | // A good, signed record plus one field: only DisallowUnknownFields |
| 106 | // can refuse it. |
| 107 | "unknown field in the record": {append(bytes.TrimSuffix(good(), []byte("}")), []byte(`,"extra":1}`)...), expect("annie-welding")}, |
| 108 | "trailing data": {append(good(), []byte(`{}`)...), expect("annie-welding")}, |
| 109 | "version 2": {func() []byte { |
| 110 | r, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 111 | r.V = 2 |
| 112 | raw, _ := json.Marshal(r) |
| 113 | return raw |
| 114 | }(), expect("annie-welding")}, |
| 115 | "reference enrolled elsewhere": {good(), Expect{Box: "annie-welding", AWSRegion: "eu-west-1", Roster: []Enrolled{{Box: "annie-welding", Cloud: Cloud{InstanceID: "i-0old"}}}}}, |
| 116 | } |
| 117 | for name, tc := range cases { |
| 118 | t.Run(name, func(t *testing.T) { |
| 119 | if ev, err := s.verifier().Enroll(tc.raw, tc.e); !errors.Is(err, ErrUnverified) { |
| 120 | t.Fatalf("evidence = %+v, err = %v; want ErrUnverified", ev, err) |
| 121 | } |
| 122 | }) |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | // Review Focus 1: box A's record, intact and validly signed, copied under |
| 127 | // box B's key with its box field rewritten. The signature still verifies, |
| 128 | // because the box field is not signed. Only the roster check refuses it. |
| 129 | func TestEnrollRefusesAnInstanceAlreadyEnrolledUnderAnotherReference(t *testing.T) { |
| 130 | s := newSigner(t) |
| 131 | _, raw := s.record(t, "bert-sawing", "111122223333", "eu-west-1", "i-0abc") |
| 132 | roster := []Enrolled{{Box: "annie-welding", Cloud: Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}}} |
| 133 | if _, err := s.verifier().Enroll(raw, Expect{Box: "bert-sawing", AWSRegion: "eu-west-1", Roster: roster}); !errors.Is(err, ErrUnverified) { |
| 134 | t.Fatalf("err = %v, want ErrUnverified", err) |
| 135 | } |
| 136 | // The same box re-enrolled as the same instance is not a conflict. |
| 137 | roster[0].Box = "bert-sawing" |
| 138 | if _, err := s.verifier().Enroll(raw, Expect{Box: "bert-sawing", AWSRegion: "eu-west-1", Roster: roster}); err != nil { |
| 139 | t.Fatalf("re-enrolling the same instance: %v", err) |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | func TestEnrollAcceptsTheTofuInstanceForALegacyReference(t *testing.T) { |
| 144 | s := newSigner(t) |
| 145 | _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 146 | if _, err := s.verifier().Enroll(raw, Expect{Box: "annie-welding", AWSRegion: "eu-west-1", TofuInstanceID: "i-0abc"}); err != nil { |
| 147 | t.Fatal(err) |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | func TestPublishIsCreateOnlyAndIdempotent(t *testing.T) { |
| 152 | ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t) |
| 153 | rec, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 154 | if err := Publish(ctx, st, rec); err != nil { |
| 155 | t.Fatal(err) |
| 156 | } |
| 157 | if err := Publish(ctx, st, rec); err != nil { |
| 158 | t.Fatalf("republishing the same record: %v", err) |
| 159 | } |
| 160 | other := rec |
| 161 | other.WrittenAt = "2027-01-01T00:00:00Z" |
| 162 | if err := Publish(ctx, st, other); !errors.Is(err, release.ErrCASConflict) { |
| 163 | t.Fatalf("a different record for the same box: err = %v, want ErrCASConflict", err) |
| 164 | } |
| 165 | raw, err := Load(ctx, st, "annie-welding") |
| 166 | if err != nil { |
| 167 | t.Fatal(err) |
| 168 | } |
| 169 | if _, err := s.verifier().Enroll(raw, expect("annie-welding")); err != nil { |
| 170 | t.Fatalf("the published record does not enroll: %v", err) |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | func TestPublishAndLoadRefuseAReferenceThatIsNotALabel(t *testing.T) { |
| 175 | ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t) |
| 176 | rec, _ := s.record(t, "x.bind", "111122223333", "eu-west-1", "i-0abc") |
| 177 | if err := Publish(ctx, st, rec); err == nil { |
| 178 | t.Fatal("published under a reference with a dot") |
| 179 | } |
| 180 | if _, err := Load(ctx, st, "../edges/x"); !errors.Is(err, ErrUnverified) { |
| 181 | t.Fatalf("err = %v, want ErrUnverified", err) |
| 182 | } |
| 183 | if _, err := Load(ctx, st, "nobody-here"); !errors.Is(err, os.ErrNotExist) { |
| 184 | t.Fatalf("err = %v, want os.ErrNotExist", err) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | func TestEmbeddedCertsCoverTheFlagshipRegions(t *testing.T) { |
| 189 | certs, err := EmbeddedCerts() |
| 190 | if err != nil { |
| 191 | t.Fatal(err) |
| 192 | } |
| 193 | for _, region := range []string{"eu-west-1", "eu-west-2", "us-east-1", "ap-southeast-2"} { |
| 194 | c := certs[region] |
| 195 | if c == nil { |
| 196 | t.Errorf("no certificate for %s", region) |
| 197 | continue |
| 198 | } |
| 199 | if c.PublicKeyAlgorithm != x509.RSA { |
| 200 | t.Errorf("%s: %v key; the base64-signature certificate is RSA (the PKCS7 one is DSA: wrong tab)", region, c.PublicKeyAlgorithm) |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | // Final review 2: a reference already on the roster with no proven cloud |
| 206 | // yet (a box predating region HA) enrolls once, against its tofu id. Only |
| 207 | // an entry that already names another instance refuses the reference. |
| 208 | func TestEnrollAcceptsARosterEntryWithNoCloudYet(t *testing.T) { |
| 209 | s := newSigner(t) |
| 210 | _, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc") |
| 211 | roster := []Enrolled{{Box: "annie-welding"}} |
| 212 | if _, err := s.verifier().Enroll(raw, Expect{Box: "annie-welding", AWSRegion: "eu-west-1", TofuInstanceID: "i-0abc", Roster: roster}); err != nil { |
| 213 | t.Fatalf("a roster entry with no cloud yet: %v", err) |
| 214 | } |
| 215 | } |
| 216 | |