| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package security |
| |
| import ( |
| "fmt" |
| "reflect" |
| "testing" |
| "time" |
| |
| "v.io/v23/context" |
| "v.io/v23/uniqueid" |
| "v.io/v23/vdl" |
| "v.io/v23/verror" |
| ) |
| |
| func TestStandardCaveatFactories(t *testing.T) { |
| var ( |
| self = newPrincipal(t) |
| balice, _ = UnionOfBlessings(blessSelf(t, self, "alice:phone"), blessSelf(t, self, "alice:tablet")) |
| bother = blessSelf(t, self, "other") |
| b, _ = UnionOfBlessings(balice, bother) |
| now = time.Now() |
| call = NewCall(&CallParams{ |
| Timestamp: now, |
| Method: "Foo", |
| LocalPrincipal: self, |
| LocalBlessings: b, |
| }) |
| C = newCaveat |
| tests = []struct { |
| cav Caveat |
| ok bool |
| }{ |
| // ExpiryCaveat |
| {C(NewExpiryCaveat(now.Add(time.Second))), true}, |
| {C(NewExpiryCaveat(now.Add(-1 * time.Second))), false}, |
| // MethodCaveat |
| {C(NewMethodCaveat("Foo")), true}, |
| {C(NewMethodCaveat("Bar")), false}, |
| {C(NewMethodCaveat("Foo", "Bar")), true}, |
| {C(NewMethodCaveat("Bar", "Baz")), false}, |
| // PeerBlesingsCaveat |
| {C(NewCaveat(PeerBlessingsCaveat, []BlessingPattern{"alice:phone"})), true}, |
| {C(NewCaveat(PeerBlessingsCaveat, []BlessingPattern{"alice:tablet", "other"})), true}, |
| {C(NewCaveat(PeerBlessingsCaveat, []BlessingPattern{"alice:laptop", "other", "alice:$"})), false}, |
| {C(NewCaveat(PeerBlessingsCaveat, []BlessingPattern{"alice:laptop", "other", "alice"})), true}, |
| } |
| ) |
| |
| AddToRoots(self, balice) |
| |
| ctx, cancel := context.RootContext() |
| defer cancel() |
| for idx, test := range tests { |
| err := test.cav.Validate(ctx, call) |
| if test.ok && err != nil { |
| t.Errorf("#%d: %v.Validate(...) failed validation: %v", idx, test.cav, err) |
| } else if !test.ok && verror.ErrorID(err) != ErrCaveatValidation.ID { |
| t.Errorf("#%d: %v.Validate(...) returned error='%v' (errorid=%v), want errorid=%v", idx, test.cav, err, verror.ErrorID(err), ErrCaveatValidation.ID) |
| } |
| } |
| } |
| |
| func TestPublicKeyThirdPartyCaveat(t *testing.T) { |
| var ( |
| now = time.Now() |
| valid = newCaveat(NewExpiryCaveat(now.Add(time.Second))) |
| expired = newCaveat(NewExpiryCaveat(now.Add(-1 * time.Second))) |
| discharger = newPrincipal(t) |
| randomserver = newPrincipal(t) |
| ctxAndCall = func(method string, discharges ...Discharge) (*context.T, Call) { |
| params := &CallParams{ |
| Timestamp: now, |
| Method: method, |
| RemoteDischarges: make(map[string]Discharge), |
| } |
| for _, d := range discharges { |
| params.RemoteDischarges[d.ID()] = d |
| } |
| root, _ := context.RootContext() |
| return root, NewCall(params) |
| } |
| ) |
| |
| tpc, err := NewPublicKeyCaveat(discharger.PublicKey(), "location", ThirdPartyRequirements{}, valid) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // Caveat should fail validation without a discharge |
| if err := matchesError(tpc.Validate(ctxAndCall("Method1")), "missing discharge"); err != nil { |
| t.Fatal(err) |
| } |
| // Should validate when the discharge is present (and caveats on the discharge are met). |
| d, err := discharger.MintDischarge(tpc, newCaveat(NewMethodCaveat("Method1"))) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := tpc.Validate(ctxAndCall("Method1", d)); err != nil { |
| t.Fatal(err) |
| } |
| // Should fail validation when caveats on the discharge are not met. |
| if err := matchesError(tpc.Validate(ctxAndCall("Method2", d)), "discharge failed to validate"); err != nil { |
| t.Fatal(err) |
| } |
| // Discharge can be converted to and from wire format: |
| var d2 Discharge |
| if err := roundTrip(d, &d2); err != nil || !reflect.DeepEqual(d, d2) { |
| t.Errorf("Got (%#v, %v), want (%#v, nil)", d2, err, d) |
| } |
| // A discharge minted by another principal should not be respected. |
| if d, err = randomserver.MintDischarge(tpc, UnconstrainedUse()); err == nil { |
| if err := matchesError(tpc.Validate(ctxAndCall("Method1", d)), "signature verification on discharge"); err != nil { |
| t.Fatal(err) |
| } |
| } |
| // And ThirdPartyCaveat should not be dischargeable if caveats encoded within it fail validation. |
| tpc, err = NewPublicKeyCaveat(discharger.PublicKey(), "location", ThirdPartyRequirements{}, expired) |
| if err != nil { |
| t.Fatal(err) |
| } |
| ctx, cancel := context.RootContext() |
| defer cancel() |
| call := NewCall(&CallParams{Timestamp: now}) |
| if merr := matchesError(tpc.ThirdPartyDetails().Dischargeable(ctx, call), "could not validate embedded restriction"); merr != nil { |
| t.Fatal(merr) |
| } |
| } |
| |
| func TestCaveat(t *testing.T) { |
| uid, err := uniqueid.Random() |
| if err != nil { |
| t.Fatal(err) |
| } |
| anyCd := CaveatDescriptor{ |
| Id: uid, |
| ParamType: vdl.AnyType, |
| } |
| if _, err := NewCaveat(anyCd, 9); verror.ErrorID(err) != ErrCaveatParamAny.ID { |
| t.Errorf("Got '%v' (errorid=%v), want errorid=%v", err, verror.ErrorID(err), ErrCaveatParamAny.ID) |
| } |
| cd := CaveatDescriptor{ |
| Id: uid, |
| ParamType: vdl.TypeOf(string("")), |
| } |
| if _, err := NewCaveat(cd, nil); verror.ErrorID(err) != ErrCaveatParamTypeMismatch.ID { |
| t.Errorf("Got '%v' (errorid=%v), want errorid=%v", err, verror.ErrorID(err), ErrCaveatParamTypeMismatch.ID) |
| } |
| // A param of type *vdl.Value with underlying type string should succeed. |
| if _, err := NewCaveat(cd, vdl.StringValue(nil, "")); err != nil { |
| t.Errorf("vdl value should have succeeded: %v", err) |
| } |
| call := NewCall(&CallParams{ |
| Method: "Foo", |
| }) |
| ctx, cancel := context.RootContext() |
| defer cancel() |
| c1, err := NewCaveat(cd, "Foo") |
| if err != nil { |
| t.Fatal(err) |
| } |
| c2, err := NewCaveat(cd, "Bar") |
| if err != nil { |
| t.Fatal(err) |
| } |
| // Validation will fail when the validation function isn't registered. |
| if err := c1.Validate(ctx, call); verror.ErrorID(err) != ErrCaveatNotRegistered.ID { |
| t.Errorf("Got '%v' (errorid=%v), want errorid=%v", err, verror.ErrorID(err), ErrCaveatNotRegistered.ID) |
| } |
| // Once registered, then it should be invoked |
| RegisterCaveatValidator(cd, func(ctx *context.T, call Call, param string) error { |
| if call.Method() == param { |
| return nil |
| } |
| return fmt.Errorf("na na na") |
| }) |
| if err := c1.Validate(ctx, call); err != nil { |
| t.Error(err) |
| } |
| if err := c2.Validate(ctx, call); verror.ErrorID(err) != ErrCaveatValidation.ID { |
| t.Errorf("Got '%v' (errorid=%v), want errorid=%v", err, verror.ErrorID(err), ErrCaveatValidation.ID) |
| } |
| // If a malformed caveat was received, then validation should fail |
| c3 := Caveat{Id: cd.Id, ParamVom: nil} |
| if err := c3.Validate(ctx, call); verror.ErrorID(err) != ErrCaveatParamCoding.ID { |
| t.Errorf("Got '%v' (errorid=%v), want errorid=%v", err, verror.ErrorID(err), ErrCaveatParamCoding.ID) |
| } |
| } |
| |
| func TestRegisterCaveat(t *testing.T) { |
| uid, err := uniqueid.Random() |
| if err != nil { |
| t.Fatal(err) |
| } |
| var ( |
| cd = CaveatDescriptor{ |
| Id: uid, |
| ParamType: vdl.TypeOf(string("")), |
| } |
| npanics int |
| expectPanic = func(details string) { |
| npanics++ |
| if err := recover(); err == nil { |
| t.Errorf("%s: expected a panic", details) |
| } |
| } |
| ) |
| func() { |
| defer expectPanic("not a function") |
| RegisterCaveatValidator(cd, "not a function") |
| }() |
| func() { |
| defer expectPanic("wrong #outputs") |
| RegisterCaveatValidator(cd, func(*context.T, Call, string) (error, error) { return nil, nil }) |
| }() |
| func() { |
| defer expectPanic("bad output type") |
| RegisterCaveatValidator(cd, func(*context.T, Call, string) int { return 0 }) |
| }() |
| func() { |
| defer expectPanic("wrong #inputs") |
| RegisterCaveatValidator(cd, func(*context.T, string) error { return nil }) |
| }() |
| func() { |
| defer expectPanic("bad input arg 0") |
| RegisterCaveatValidator(cd, func(int, Call, string) error { return nil }) |
| }() |
| func() { |
| defer expectPanic("bad input arg 1") |
| RegisterCaveatValidator(cd, func(*context.T, int, string) error { return nil }) |
| }() |
| func() { |
| defer expectPanic("bad input arg 2") |
| RegisterCaveatValidator(cd, func(*context.T, Call, int) error { return nil }) |
| }() |
| func() { |
| // Successful registration: No panic: |
| RegisterCaveatValidator(cd, func(*context.T, Call, string) error { return nil }) |
| }() |
| func() { |
| defer expectPanic("Duplication registration") |
| RegisterCaveatValidator(cd, func(*context.T, Call, string) error { return nil }) |
| }() |
| if got, want := npanics, 8; got != want { |
| t.Errorf("Got %d panics, want %d", got, want) |
| } |
| } |
| |
| func TestThirdPartyDetails(t *testing.T) { |
| niltests := []Caveat{ |
| newCaveat(NewExpiryCaveat(time.Now())), |
| newCaveat(NewMethodCaveat("Foo", "Bar")), |
| } |
| for _, c := range niltests { |
| if tp := c.ThirdPartyDetails(); tp != nil { |
| t.Errorf("Caveat [%v] recognized as a third-party caveat: %v", c, tp) |
| } |
| } |
| req := ThirdPartyRequirements{ReportMethod: true} |
| c, err := NewPublicKeyCaveat(newPrincipal(t).PublicKey(), "location", req, newCaveat(NewExpiryCaveat(time.Now()))) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got := c.ThirdPartyDetails(); got.Location() != "location" { |
| t.Errorf("Got location %q, want %q", got.Location(), "location") |
| } else if !reflect.DeepEqual(got.Requirements(), req) { |
| t.Errorf("Got requirements %+v, want %+v", got.Requirements(), req) |
| } |
| } |
| |
| func TestPublicKeyDischargeExpiry(t *testing.T) { |
| var ( |
| discharger = newPrincipal(t) |
| now = time.Now() |
| oneh = newCaveat(NewExpiryCaveat(now.Add(time.Hour))) |
| twoh = newCaveat(NewExpiryCaveat(now.Add(2 * time.Hour))) |
| threeh = newCaveat(NewExpiryCaveat(now.Add(3 * time.Hour))) |
| ) |
| |
| tpc, err := NewPublicKeyCaveat(discharger.PublicKey(), "location", ThirdPartyRequirements{}, oneh) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Mint three discharges; one with no ExpiryCaveat... |
| noExpiry, err := discharger.MintDischarge(tpc, newCaveat(NewMethodCaveat("Method1"))) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // another with an ExpiryCaveat one hour from now... |
| oneCav, err := discharger.MintDischarge(tpc, oneh) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // and finally, one with an ExpiryCaveat of one, two, and three hours from now. |
| // Use a random order to help test that Expiry always returns the earliest time. |
| threeCav, err := discharger.MintDischarge(tpc, threeh, oneh, twoh) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if exp := noExpiry.Expiry(); !exp.IsZero() { |
| t.Errorf("got %v, want %v", exp, time.Time{}) |
| } |
| if got, want := oneCav.Expiry().UTC(), now.Add(time.Hour).UTC(); got != want { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| if got, want := threeCav.Expiry().UTC(), now.Add(time.Hour).UTC(); got != want { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| } |
| |
| // Benchmark creation of a new caveat using one of the simplest caveats |
| // (expiry) |
| func BenchmarkNewCaveat(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| if _, err := NewExpiryCaveat(time.Time{}); err != nil { |
| b.Fatal(err) |
| } |
| } |
| } |
| |
| // Benchmark caveat valdation using one of the simplest caveats (expiry). |
| func BenchmarkValidateCaveat(b *testing.B) { |
| cav, err := NewExpiryCaveat(time.Now()) |
| if err != nil { |
| b.Fatal(err) |
| } |
| call := NewCall(&CallParams{}) |
| ctx, cancel := context.RootContext() |
| defer cancel() |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| cav.Validate(ctx, call) |
| } |
| } |