veyron2/ipc: Support for granting credentials to servers in the form of blessings.
This change makes it possible for a client to provide additional credentials,
bound to the identity of the server, in the form of a blessed identity to
a server when making the request.
The server can access this credential via ipc.Context.Blessing.
Change-Id: I67dde80b7038b0ebb9b27f4917ae218b62ec13f0
diff --git a/runtimes/google/ipc/client.go b/runtimes/google/ipc/client.go
index 16a711c..d974020 100644
--- a/runtimes/google/ipc/client.go
+++ b/runtimes/google/ipc/client.go
@@ -1,6 +1,7 @@
package ipc
import (
+ "fmt"
"io"
"sync"
"time"
@@ -141,32 +142,15 @@
}
// Validate caveats on the server's identity for the context associated with this call.
- remoteID := flow.RemoteID()
- if remoteID == nil {
- lastErr = verror.NotAuthorizedf("ipc: server identity cannot be nil")
- continue
- }
- // TODO(ataly): Fetch third-party discharges from the server.
- // TODO(ataly): What should the label be for the context? Typically the label is the security.Label
- // of the method but we don't have that information here at the client.
- authorizedRemoteID, err := remoteID.Authorize(isecurity.NewContext(
- isecurity.ContextArgs{
- LocalID: flow.LocalID(),
- RemoteID: remoteID,
- }))
+ blessing, err := authorizeServer(flow.LocalID(), flow.RemoteID(), opts)
if err != nil {
- lastErr = verror.NotAuthorizedf("ipc: server identity %q has one or more invalid caveats: %v", remoteID, err)
+ lastErr = verror.NotAuthorizedf("ipc: client unwilling to talk to server %q: %v", flow.RemoteID(), err)
+ flow.Close()
continue
}
-
- if lastErr = matchServerID(authorizedRemoteID, opts); lastErr != nil {
- continue
- }
-
- // remoteID is authorized for the context associated with this call.
lastErr = nil
fc := newFlowClient(flow)
- if verr := fc.start(suffix, method, args, timeout); verr != nil {
+ if verr := fc.start(suffix, method, args, timeout, blessing); verr != nil {
return nil, verr
}
return fc, nil
@@ -177,6 +161,45 @@
return nil, errNoServers
}
+// authorizeServer validates that server has an identity that the client is willing to converse
+// with, and if so returns a blessing to be provided to the server. This blessing can be nil,
+// which indicates that the client does wish to talk to the server but not provide any blessings.
+func authorizeServer(client, server security.PublicID, opts []ipc.CallOpt) (security.PublicID, error) {
+ if server == nil {
+ return nil, fmt.Errorf("server identity cannot be nil")
+ }
+ // TODO(ataly): Fetch third-party discharges from the server.
+ // TODO(ataly): What should the label be for the context? Typically the label is the security.Label
+ // of the method but we don't have that information here at the client.
+ authID, err := server.Authorize(isecurity.NewContext(isecurity.ContextArgs{
+ LocalID: client,
+ RemoteID: server,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ var granter ipc.Granter
+ for _, o := range opts {
+ switch v := o.(type) {
+ case veyron2.RemoteID:
+ if !authID.Match(security.PrincipalPattern(v)) {
+ return nil, fmt.Errorf("server %q does not match the provided pattern %q", authID, v)
+ }
+ case ipc.Granter:
+ // Later Granters take precedence over earlier ones.
+ // Or should fail if there are multiple provided?
+ granter = v
+ }
+ }
+ var blessing security.PublicID
+ if granter != nil {
+ if blessing, err = granter.Grant(authID); err != nil {
+ return nil, fmt.Errorf("failed to grant credentials to server %q: %v", authID, err)
+ }
+ }
+ return blessing, nil
+}
+
func (c *client) getCallTimeout(opts []ipc.CallOpt) time.Duration {
timeout := c.callTimeout
for _, opt := range opts {
@@ -229,16 +252,22 @@
return verr
}
-func (fc *flowClient) start(suffix, method string, args []interface{}, timeout time.Duration) verror.E {
+func (fc *flowClient) start(suffix, method string, args []interface{}, timeout time.Duration, blessing security.PublicID) verror.E {
req := ipc.Request{
- Suffix: suffix,
- Method: method,
- NumPosArgs: uint64(len(args)),
- Timeout: int64(timeout),
+ Suffix: suffix,
+ Method: method,
+ NumPosArgs: uint64(len(args)),
+ Timeout: int64(timeout),
+ HasBlessing: blessing != nil,
}
if err := fc.enc.Encode(req); err != nil {
return fc.close(verror.BadProtocolf("ipc: request encoding failed: %v", err))
}
+ if blessing != nil {
+ if err := fc.enc.Encode(blessing); err != nil {
+ return fc.close(verror.BadProtocolf("ipc: blessing encoding failed: %v", err))
+ }
+ }
for ix, arg := range args {
if err := fc.enc.Encode(arg); err != nil {
return fc.close(verror.BadProtocolf("ipc: arg %d encoding failed: %v", ix, err))
@@ -357,12 +386,3 @@
func (fc *flowClient) Cancel() {
fc.flow.Cancel()
}
-
-func matchServerID(id security.PublicID, opts []ipc.CallOpt) verror.E {
- for _, opt := range opts {
- if pattern, ok := opt.(veyron2.RemoteID); ok && !id.Match(security.PrincipalPattern(pattern)) {
- return verror.NotAuthorizedf("ipc: server identity %q does not have a name matching the provided pattern %q", id, pattern)
- }
- }
- return nil
-}
diff --git a/runtimes/google/ipc/flow_test.go b/runtimes/google/ipc/flow_test.go
index 0a60b99..35f7662 100644
--- a/runtimes/google/ipc/flow_test.go
+++ b/runtimes/google/ipc/flow_test.go
@@ -122,7 +122,7 @@
clientFlow, serverFlow := newTestFlows()
client := newFlowClient(clientFlow)
server := newFlowServer(serverFlow, ipcServer)
- err := client.start(test.suffix, test.method, test.args, time.Duration(0))
+ err := client.start(test.suffix, test.method, test.args, time.Duration(0), nil)
if err != nil {
t.Errorf("%s client.start unexpected error: %v", name(test), err)
}
diff --git a/runtimes/google/ipc/full_test.go b/runtimes/google/ipc/full_test.go
index 4b53246..89af1cd 100644
--- a/runtimes/google/ipc/full_test.go
+++ b/runtimes/google/ipc/full_test.go
@@ -63,6 +63,10 @@
return fmt.Sprintf("%v", call.LocalID()), fmt.Sprintf("%v", call.RemoteID())
}
+func (*testServer) EchoBlessing(call ipc.ServerCall, arg string) (result, blessing string) {
+ return arg, fmt.Sprintf("%v", call.Blessing())
+}
+
func (*testServer) EchoAndError(call ipc.ServerCall, arg string) (string, error) {
result := fmt.Sprintf("method:%q,suffix:%q,arg:%q", call.Method(), call.Suffix(), arg)
if arg == "error" {
@@ -278,16 +282,20 @@
return
}
+func bless(blessor security.PrivateID, blessee security.PublicID, name string, caveats ...security.ServiceCaveat) security.PublicID {
+ blessed, err := blessor.Bless(blessee, name, 24*time.Hour, caveats)
+ if err != nil {
+ panic(err)
+ }
+ return blessed
+}
+
func derive(blessor security.PrivateID, name string, caveats ...security.ServiceCaveat) security.PrivateID {
id, err := isecurity.NewPrivateID("irrelevant")
if err != nil {
panic(err)
}
- blessedID, err := blessor.Bless(id.PublicID(), name, 5*time.Minute, caveats)
- if err != nil {
- panic(err)
- }
- derivedID, err := id.Derive(blessedID)
+ derivedID, err := id.Derive(bless(blessor, id.PublicID(), name, caveats...))
if err != nil {
panic(err)
}
@@ -302,8 +310,8 @@
}
func TestStartCall(t *testing.T) {
- authorizeErr := "has one or more invalid caveats"
- nameErr := "does not have a name matching the provided pattern"
+ authorizeErr := "not authorized because"
+ nameErr := "does not match the provided pattern"
cavOnlyV1 := security.UniversalCaveat(caveat.PeerIdentity{"client/v1"})
now := time.Now()
@@ -435,6 +443,49 @@
}
}
+// granter implements ipc.Granter, returning a fixed (security.PublicID, error) pair.
+type granter struct {
+ ipc.CallOpt
+ id security.PublicID
+ err error
+}
+
+func (g granter) Grant(id security.PublicID) (security.PublicID, error) { return g.id, g.err }
+
+func TestBlessing(t *testing.T) {
+ b := createBundle(t, clientID, serverID, &testServer{})
+ defer b.cleanup(t)
+
+ tests := []struct {
+ granter ipc.CallOpt
+ blessing, starterr, finisherr string
+ }{
+ {blessing: "<nil>"},
+ {granter: granter{id: bless(clientID, serverID.PublicID(), "blessed")}, blessing: "client/blessed"},
+ {granter: granter{err: errors.New("hell no")}, starterr: "hell no"},
+ {granter: granter{id: clientID.PublicID()}, finisherr: "blessing provided not bound to this server"},
+ }
+ for _, test := range tests {
+ call, err := b.client.StartCall(&fakeContext{}, "mountpoint/server/suffix", "EchoBlessing", []interface{}{"argument"}, test.granter)
+ if !matchesErrorPattern(err, test.starterr) {
+ t.Errorf("%+v: StartCall returned error %v", test, err)
+ }
+ if err != nil {
+ continue
+ }
+ var result, blessing string
+ if err = call.Finish(&result, &blessing); !matchesErrorPattern(err, test.finisherr) {
+ t.Errorf("%+v: Finish returned error %v", test, err)
+ }
+ if err != nil {
+ continue
+ }
+ if result != "argument" || blessing != test.blessing {
+ t.Errorf("%+v: Got (%q, %q)", test, result, blessing)
+ }
+ }
+}
+
func TestRPCAuthorization(t *testing.T) {
cavOnlyEcho := security.ServiceCaveat{
Service: security.AllPrincipals,
diff --git a/runtimes/google/ipc/server.go b/runtimes/google/ipc/server.go
index cb406cb..222bdac 100644
--- a/runtimes/google/ipc/server.go
+++ b/runtimes/google/ipc/server.go
@@ -4,6 +4,7 @@
"fmt"
"io"
"net"
+ "reflect"
"strings"
"sync"
"time"
@@ -245,6 +246,7 @@
// authorizedRemoteID is the PublicID obtained after authorizing the remoteID
// of the underlying flow for the current request context.
authorizedRemoteID security.PublicID
+ blessing security.PublicID
method, name, suffix string
label security.Label
discharges security.CaveatDischargeMap
@@ -363,6 +365,20 @@
return nil, verr
}
}
+ // If additional credentials are provided, make them available in the context
+ if req.HasBlessing {
+ if err := fs.dec.Decode(&fs.blessing); err != nil {
+ return nil, verror.BadProtocolf("ipc: blessing decoding failed: %v", err)
+ }
+ // Detect unusable blessings now, rather then discovering they are unusable on first use.
+ if !reflect.DeepEqual(fs.blessing.PublicKey(), fs.flow.LocalID().PublicKey()) {
+ return nil, verror.BadProtocolf("ipc: blessing provided not bound to this server")
+ }
+ // TODO(ashankar,ataly): Potential confused deputy attack: The client provides the
+ // server's identity as the blessing. Figure out what we want to do about this -
+ // should servers be able to assume that a blessing is something that does not
+ // have the authorizations that the server's own identity has?
+ }
// Lookup the invoker.
invoker, auth, name, suffix, verr := fs.lookup(req.Suffix)
fs.name = name
@@ -484,6 +500,7 @@
func (fs *flowServer) LocalID() security.PublicID { return fs.flow.LocalID() }
func (fs *flowServer) RemoteID() security.PublicID { return fs.authorizedRemoteID }
func (fs *flowServer) Deadline() time.Time { return fs.deadline }
+func (fs *flowServer) Blessing() security.PublicID { return fs.blessing }
func (fs *flowServer) LocalAddr() net.Addr { return fs.flow.LocalAddr() }
func (fs *flowServer) RemoteAddr() net.Addr { return fs.flow.RemoteAddr() }