package hostbox import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "errors" "reflect" "testing" "github.com/carlosframework/platform/internal/boxid" ) // L1: a roster row read without a binding is not a verified actor. Every // installed-state comparison stamps and compares the binding, so an actor // without one must never leave Verify. func TestIdentityVerifyRequiresBinding(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } actor := Actor{Box: "box", Enrollment: "en", Revision: 1, Binding: "binding", Region: "ie", Bucket: "data", Namespace: "platform", Eligible: true, Cloud: CloudIdentity{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}} doc, err := json.Marshal(map[string]string{"accountId": actor.Cloud.Account, "region": actor.Cloud.Region, "instanceId": actor.Cloud.InstanceID}) if err != nil { t.Fatal(err) } sum := sha256.Sum256(doc) sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, sum[:]) if err != nil { t.Fatal(err) } raw, err := json.Marshal(boxid.Record{V: 1, Box: actor.Box, IdentityDocument: base64.StdEncoding.EncodeToString(doc), IdentitySignature: base64.StdEncoding.EncodeToString(sig)}) if err != nil { t.Fatal(err) } p := IdentityProvider{Account: actor.Cloud.Account, Box: actor.Box, Region: actor.Region, AWSRegion: actor.Cloud.Region, Bucket: actor.Bucket, Namespace: actor.Namespace, Roster: enrollmentRoster{actor}, Evidence: enrollmentEvidence(raw), Certs: map[string]*x509.Certificate{actor.Cloud.Region: {PublicKey: &key.PublicKey, PublicKeyAlgorithm: x509.RSA}}} if got, err := p.Verify(t.Context()); err != nil || got != actor { t.Fatalf("bound actor: got=%+v err=%v", got, err) } unbound := actor unbound.Binding = "" if p.actorMatches(unbound) { t.Fatal("actorMatches admitted an actor without a binding") } p.Roster = enrollmentRoster{unbound} if _, err := p.Verify(t.Context()); !errors.Is(err, ErrChanged) { t.Fatalf("Verify without a binding: %v, want ErrChanged", err) } } // Bound clears only Revision, and SameBinding ignores only Revision: every // other actor field, Binding included, still separates two actors. The // reflection walk covers fields added later. func TestActorBoundIgnoresOnlyRevision(t *testing.T) { base := Actor{FleetHomeRegion: "ie", HomeRegion: "ie", ControlBucket: "deployment", Box: "box", Enrollment: "enrolled", Revision: 7, Binding: "binding", Region: "ie", Bucket: "data", Namespace: "platform", Cloud: CloudIdentity{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}, Eligible: true} if b := base.Bound(); b.Revision != 0 || func() Actor { c := base; c.Revision = 0; return c }() != b { t.Fatalf("Bound = %+v", b) } if !SameBinding(base, base.Bound()) { t.Fatal("an actor is not SameBinding with its bound copy") } var walk func(v reflect.Value, index []int, path string) walk = func(v reflect.Value, index []int, path string) { for i := 0; i < v.NumField(); i++ { f := v.Field(i) at := append(append([]int{}, index...), i) name := path + v.Type().Field(i).Name if f.Kind() == reflect.Struct { walk(f, at, name+".") continue } changed := base field := reflect.ValueOf(&changed).Elem().FieldByIndex(at) switch field.Kind() { case reflect.String: field.SetString(field.String() + "-changed") case reflect.Uint64: field.SetUint(field.Uint() + 1) case reflect.Bool: field.SetBool(!field.Bool()) default: t.Fatalf("%s: unhandled kind %s", name, field.Kind()) } if got, want := SameBinding(changed, base), name == "Revision"; got != want { t.Errorf("%s changed: SameBinding = %v, want %v", name, got, want) } if got, want := changed.Bound() == base.Bound(), name == "Revision"; got != want { t.Errorf("%s changed: Bound equal = %v, want %v", name, got, want) } } } walk(reflect.ValueOf(base), nil, "") }