"ref/profiles/iternal/ipc": Support LocalDischarges

This CL accompanies: https://vanadium-review.googlesource.com/#/c/6765/

MultiPart: 2/2
Change-Id: Ia41e9c9c5fea07d16d2b56950e8d2ee0740b080d
diff --git a/profiles/internal/ipc/full_test.go b/profiles/internal/ipc/full_test.go
index 06ed262..bef9dd3 100644
--- a/profiles/internal/ipc/full_test.go
+++ b/profiles/internal/ipc/full_test.go
@@ -110,7 +110,21 @@
 }
 
 func (*testServer) EchoBlessings(call ipc.ServerCall) (server, client string, _ error) {
-	local, _ := call.LocalBlessings().ForCall(call)
+	// TODO(ataly, ashankar): This is a HACK to create a context
+	// that can be used to validate caveats on the LocalBlessings.
+	// It will go away once we have a security.BlessingNames function
+	// that takes a call and an IPCSide argument.
+	localCall := security.NewCall(&security.CallParams{
+		Context:          call.Context(),
+		LocalPrincipal:   call.LocalPrincipal(),
+		RemoteBlessings:  call.LocalBlessings(),
+		RemoteDischarges: call.LocalDischarges(),
+		LocalEndpoint:    call.LocalEndpoint(),
+		RemoteEndpoint:   call.RemoteEndpoint(),
+		Method:           call.Method(),
+		Suffix:           call.Suffix(),
+	})
+	local, _ := call.LocalBlessings().ForCall(localCall)
 	remote, _ := call.RemoteBlessings().ForCall(call)
 	return fmt.Sprintf("%v", local), fmt.Sprintf("%v", remote), nil
 }
@@ -150,10 +164,34 @@
 type testServerAuthorizer struct{}
 
 func (testServerAuthorizer) Authorize(c security.Call) error {
-	if c.Method() != "Unauthorized" {
-		return nil
+	// Verify that the Call object seen by the authorizer
+	// has the necessary fields.
+	lb := c.LocalBlessings()
+	if lb.IsZero() {
+		return fmt.Errorf("testServerAuthorzer: Call object %v has no LocalBlessings", c)
 	}
-	return fmt.Errorf("testServerAuthorizer denied access")
+	if tpcavs := lb.ThirdPartyCaveats(); len(tpcavs) > 0 && c.LocalDischarges() == nil {
+		return fmt.Errorf("testServerAuthorzer: Call object %v has no LocalDischarges even when LocalBlessings have third-party caveats", c)
+
+	}
+	if c.LocalPrincipal() == nil {
+		return fmt.Errorf("testServerAuthorzer: Call object %v has no LocalPrincipal", c)
+	}
+	if c.Method() == "" {
+		return fmt.Errorf("testServerAuthorzer: Call object %v has no Method", c)
+	}
+	if c.LocalEndpoint() == nil {
+		return fmt.Errorf("testServerAuthorzer: Call object %v has no LocalEndpoint", c)
+	}
+	if c.RemoteEndpoint() == nil {
+		return fmt.Errorf("testServerAuthorzer: Call object %v has no RemoteEndpoint", c)
+	}
+
+	// Do not authorize the method "Unauthorized".
+	if c.Method() == "Unauthorized" {
+		return fmt.Errorf("testServerAuthorizer denied access")
+	}
+	return nil
 }
 
 type testServerDisp struct{ server interface{} }
@@ -988,7 +1026,7 @@
 	var (
 		// Principals
 		pclient, pserver = tsecurity.NewPrincipal("client"), tsecurity.NewPrincipal("server")
-		pdischarger      = pserver
+		pdischarger      = tsecurity.NewPrincipal("discharger")
 
 		now = time.Now()
 
@@ -999,8 +1037,8 @@
 		cavOnlyEcho = mkCaveat(security.MethodCaveat("Echo"))
 		cavExpired  = mkCaveat(security.ExpiryCaveat(now.Add(-1 * time.Second)))
 		// Caveats on blessings to the client: Third-party caveats
-		cavTPValid   = mkThirdPartyCaveat(pdischarger.PublicKey(), "mountpoint/server/discharger", mkCaveat(security.ExpiryCaveat(now.Add(24*time.Hour))))
-		cavTPExpired = mkThirdPartyCaveat(pdischarger.PublicKey(), "mountpoint/server/discharger", mkCaveat(security.ExpiryCaveat(now.Add(-1*time.Second))))
+		cavTPValid   = mkThirdPartyCaveat(pdischarger.PublicKey(), dischargeServerName, mkCaveat(security.ExpiryCaveat(now.Add(24*time.Hour))))
+		cavTPExpired = mkThirdPartyCaveat(pdischarger.PublicKey(), dischargeServerName, mkCaveat(security.ExpiryCaveat(now.Add(-1*time.Second))))
 
 		// Client blessings that will be tested.
 		bServerClientOnlyEcho  = bless(pserver, pclient, "onlyecho", cavOnlyEcho)
@@ -1088,11 +1126,14 @@
 	// And the client needs to recognize the server's and discharger's blessings to decide which of its
 	// own blessings to share.
 	pclient.AddToRoots(pserver.BlessingStore().Default())
+	pclient.AddToRoots(pdischarger.BlessingStore().Default())
+	// Set a blessing on the client's blessing store to be presented to the discharge server.
+	pclient.BlessingStore().Set(pclient.BlessingStore().Default(), "discharger")
 	// tsecurity.NewPrincipal sets up a principal that shares blessings with all servers, undo that.
 	pclient.BlessingStore().Set(security.Blessings{}, security.AllPrincipals)
 
-	for _, test := range tests {
-		name := fmt.Sprintf("%q.%s(%v) by %v", test.name, test.method, test.args, test.blessings)
+	for i, test := range tests {
+		name := fmt.Sprintf("#%d: %q.%s(%v) by %v", i, test.name, test.method, test.args, test.blessings)
 		client, err := InternalNewClient(mgr, ns, vc.LocalPrincipal{pclient})
 		if err != nil {
 			t.Fatalf("InternalNewClient failed: %v", err)
@@ -1209,6 +1250,60 @@
 	}
 }
 
+func TestServerLocalBlessings(t *testing.T) {
+	var (
+		pprovider, pclient, pserver = tsecurity.NewPrincipal("root"), tsecurity.NewPrincipal(), tsecurity.NewPrincipal()
+		pdischarger                 = pprovider
+
+		mgr = imanager.InternalNew(naming.FixedRoutingID(0x1111111))
+		ns  = tnaming.NewSimpleNamespace()
+
+		tpCav = mkThirdPartyCaveat(pdischarger.PublicKey(), "mountpoint/dischargeserver", mkCaveat(security.ExpiryCaveat(time.Now().Add(time.Hour))))
+
+		bserver = bless(pprovider, pserver, "server", tpCav)
+		bclient = bless(pprovider, pclient, "client")
+	)
+
+	// Start the server and the discharger.
+	_, server := startServer(t, pserver, mgr, ns, "mountpoint/server", testServerDisp{&testServer{}})
+	defer stopServer(t, server, ns, "mountpoint/server")
+
+	_, dischargeServer := startServer(t, pdischarger, mgr, ns, "mountpoint/dischargeserver", testutil.LeafDispatcher(&dischargeServer{}, &acceptAllAuthorizer{}))
+	defer stopServer(t, dischargeServer, ns, "mountpoint/dischargeserver")
+
+	// Make the client and server principals trust root certificates from
+	// pprovider.
+	pclient.AddToRoots(pprovider.BlessingStore().Default())
+	pserver.AddToRoots(pprovider.BlessingStore().Default())
+
+	// Make the server present bserver to all clients.
+	pserver.BlessingStore().SetDefault(bserver)
+
+	// Make the client present bclient to all servers that are blessed
+	// by pprovider.
+	pclient.BlessingStore().Set(bclient, "root")
+
+	client, err := InternalNewClient(mgr, ns, vc.LocalPrincipal{pclient})
+	if err != nil {
+		t.Fatalf("InternalNewClient failed: %v", err)
+	}
+	defer client.Close()
+
+	call, err := client.StartCall(testContext(), "mountpoint/server/suffix", "EchoBlessings", nil)
+	if err != nil {
+		t.Fatalf("StartCall failed: %v", err)
+	}
+
+	type v []interface{}
+	var gotServer, gotClient string
+	if err := call.Finish(&gotServer, &gotClient); err != nil {
+		t.Fatalf("Finish failed: %v", err)
+	}
+	if wantServer, wantClient := "[root/server]", "[root/client]"; gotServer != wantServer || gotClient != wantClient {
+		t.Fatalf("EchoBlessings: got %v, %v want %v, %v", gotServer, gotClient, wantServer, wantClient)
+	}
+}
+
 func TestDischargePurgeFromCache(t *testing.T) {
 	var (
 		pserver     = tsecurity.NewPrincipal("server")
diff --git a/profiles/internal/ipc/reserved.go b/profiles/internal/ipc/reserved.go
index 95589c5..3eeca20 100644
--- a/profiles/internal/ipc/reserved.go
+++ b/profiles/internal/ipc/reserved.go
@@ -360,6 +360,9 @@
 func (c *mutableServerCall) RemoteBlessings() security.Blessings { return c.M.RemoteBlessings }
 func (c *mutableServerCall) LocalEndpoint() naming.Endpoint      { return c.M.LocalEndpoint }
 func (c *mutableServerCall) RemoteEndpoint() naming.Endpoint     { return c.M.RemoteEndpoint }
+func (c *mutableServerCall) LocalDischarges() map[string]security.Discharge {
+	return c.M.LocalDischarges
+}
 func (c *mutableServerCall) RemoteDischarges() map[string]security.Discharge {
 	return c.M.RemoteDischarges
 }
diff --git a/profiles/internal/ipc/server.go b/profiles/internal/ipc/server.go
index 9439086..7dddf07 100644
--- a/profiles/internal/ipc/server.go
+++ b/profiles/internal/ipc/server.go
@@ -1238,6 +1238,10 @@
 
 // Implementations of ipc.ServerCall methods.
 
+func (fs *flowServer) LocalDischarges() map[string]security.Discharge {
+	//nologcall
+	return fs.flow.LocalDischarges()
+}
 func (fs *flowServer) RemoteDischarges() map[string]security.Discharge {
 	//nologcall
 	return fs.discharges
diff --git a/profiles/internal/ipc/stream/model.go b/profiles/internal/ipc/stream/model.go
index b98e3db..630fe1e 100644
--- a/profiles/internal/ipc/stream/model.go
+++ b/profiles/internal/ipc/stream/model.go
@@ -26,7 +26,11 @@
 	LocalBlessings() security.Blessings
 	// RemoteBlessings returns the blessings presented by the remote end of the flow during authentication.
 	RemoteBlessings() security.Blessings
-	// RemoteDischarges() returns the discharges presented by the remote end of the flow during authentication.
+	// LocalDischarges returns the discharges presented by the local end of the flow during authentication.
+	//
+	// The discharges are organized in a map keyed by the discharge-identifier.
+	LocalDischarges() map[string]security.Discharge
+	// RemoteDischarges returns the discharges presented by the remote end of the flow during authentication.
 	//
 	// The discharges are organized in a map keyed by the discharge-identifier.
 	RemoteDischarges() map[string]security.Discharge
diff --git a/profiles/internal/ipc/stream/vc/auth.go b/profiles/internal/ipc/stream/vc/auth.go
index beb8773..c467555 100644
--- a/profiles/internal/ipc/stream/vc/auth.go
+++ b/profiles/internal/ipc/stream/vc/auth.go
@@ -28,55 +28,61 @@
 	errSingleCertificateRequired = errors.New("exactly one X.509 certificate chain with exactly one certificate is required")
 )
 
-// AuthenticateAsServer executes the authentication protocol at the server and
-// returns the blessings used to authenticate the client.
-func AuthenticateAsServer(conn io.ReadWriteCloser, principal security.Principal, server security.Blessings, dc DischargeClient, crypter crypto.Crypter, v version.IPCVersion) (client security.Blessings, err error) {
+// AuthenticateAsServer executes the authentication protocol at the server.
+// It returns the blessings shared by the client, and the discharges shared
+// by the server.
+func AuthenticateAsServer(conn io.ReadWriteCloser, principal security.Principal, server security.Blessings, dc DischargeClient, crypter crypto.Crypter, v version.IPCVersion) (security.Blessings, map[string]security.Discharge, error) {
 	if server.IsZero() {
-		return security.Blessings{}, errors.New("no blessings to present as a server")
+		return security.Blessings{}, nil, errors.New("no blessings to present as a server")
 	}
-	var discharges []security.Discharge
+	var serverDischarges []security.Discharge
 	if tpcavs := server.ThirdPartyCaveats(); len(tpcavs) > 0 && dc != nil {
-		discharges = dc.PrepareDischarges(nil, tpcavs, security.DischargeImpetus{})
+		serverDischarges = dc.PrepareDischarges(nil, tpcavs, security.DischargeImpetus{})
 	}
-	if err = writeBlessings(conn, authServerContextTag, crypter, principal, server, discharges, v); err != nil {
-		return
+	if err := writeBlessings(conn, authServerContextTag, crypter, principal, server, serverDischarges, v); err != nil {
+		return security.Blessings{}, nil, err
 	}
-	if client, _, err = readBlessings(conn, authClientContextTag, crypter, v); err != nil {
-		return
+	// Note that since the client uses a self-signed blessing to authenticate
+	// during VC setup, it does not share any discharges.
+	client, _, err := readBlessings(conn, authClientContextTag, crypter, v)
+	if err != nil {
+		return security.Blessings{}, nil, err
 	}
-	return
+	return client, mkDischargeMap(serverDischarges), nil
 }
 
-// AuthenticateAsClient executes the authentication protocol at the client and
-// returns the blessings used to authenticate both ends.
+// AuthenticateAsClient executes the authentication protocol at the client.
+// It returns the blessing shared by the server, the blessings shared by the
+// client, and any discharges shared by the server.
 //
-// The client will only share its blessings if the server (who shares its blessings first)
-// is authorized as per the authorizer for this RPC.
-func AuthenticateAsClient(conn io.ReadWriteCloser, crypter crypto.Crypter, params security.CallParams, auth *ServerAuthorizer, v version.IPCVersion) (server, client security.Blessings, serverDischarges map[string]security.Discharge, err error) {
-	if server, serverDischarges, err = readBlessings(conn, authServerContextTag, crypter, v); err != nil {
-		return
+// The client will only share its blessings if the server (who shares its
+// blessings first) is authorized as per the authorizer for this RPC.
+func AuthenticateAsClient(conn io.ReadWriteCloser, crypter crypto.Crypter, params security.CallParams, auth *ServerAuthorizer, v version.IPCVersion) (security.Blessings, security.Blessings, map[string]security.Discharge, error) {
+	server, serverDischarges, err := readBlessings(conn, authServerContextTag, crypter, v)
+	if err != nil {
+		return security.Blessings{}, security.Blessings{}, nil, err
 	}
 	// Authorize the server based on the provided authorizer.
 	if auth != nil {
 		params.RemoteBlessings = server
 		params.RemoteDischarges = serverDischarges
-		if err = auth.Authorize(params); err != nil {
-			return
+		if err := auth.Authorize(params); err != nil {
+			return security.Blessings{}, security.Blessings{}, nil, err
 		}
 	}
 
-	// The client shares its blessings at RPC time (as the blessings may vary across
-	// RPCs). During VC handshake, the client simply sends a self-signed blessing
-	// in order to reveal its public key to the server.
+	// The client shares its blessings at RPC time (as the blessings may vary
+	// across RPCs). During VC handshake, the client simply sends a self-signed
+	// blessing in order to reveal its public key to the server.
 	principal := params.LocalPrincipal
-	client, err = principal.BlessSelf("vcauth")
+	client, err := principal.BlessSelf("vcauth")
 	if err != nil {
 		return security.Blessings{}, security.Blessings{}, nil, fmt.Errorf("failed to created self blessing: %v", err)
 	}
-	if err = writeBlessings(conn, authClientContextTag, crypter, principal, client, nil, v); err != nil {
-		return
+	if err := writeBlessings(conn, authClientContextTag, crypter, principal, client, nil, v); err != nil {
+		return security.Blessings{}, security.Blessings{}, nil, err
 	}
-	return
+	return server, client, serverDischarges, nil
 }
 
 func writeBlessings(w io.Writer, tag []byte, crypter crypto.Crypter, p security.Principal, b security.Blessings, discharges []security.Discharge, v version.IPCVersion) error {
@@ -142,21 +148,25 @@
 	if err = dec.Decode(&blessings); err != nil {
 		return noBlessings, nil, err
 	}
-	var discharges map[string]security.Discharge
+	var discharges []security.Discharge
 	if v >= version.IPCVersion5 {
-		var list []security.Discharge
-		if err := dec.Decode(&list); err != nil {
+		if err := dec.Decode(&discharges); err != nil {
 			return noBlessings, nil, err
 		}
-		if len(list) > 0 {
-			discharges = make(map[string]security.Discharge)
-			for _, d := range list {
-				discharges[d.ID()] = d
-			}
-		}
 	}
 	if !sig.Verify(blessings.PublicKey(), append(tag, crypter.ChannelBinding()...)) {
 		return noBlessings, nil, errInvalidSignatureInMessage
 	}
-	return blessings, discharges, nil
+	return blessings, mkDischargeMap(discharges), nil
+}
+
+func mkDischargeMap(discharges []security.Discharge) map[string]security.Discharge {
+	if len(discharges) == 0 {
+		return nil
+	}
+	m := make(map[string]security.Discharge, len(discharges))
+	for _, d := range discharges {
+		m[d.ID()] = d
+	}
+	return m
 }
diff --git a/profiles/internal/ipc/stream/vc/flow.go b/profiles/internal/ipc/stream/vc/flow.go
index e57ddc7..da1d64a 100644
--- a/profiles/internal/ipc/stream/vc/flow.go
+++ b/profiles/internal/ipc/stream/vc/flow.go
@@ -18,6 +18,7 @@
 	LocalPrincipal() security.Principal
 	LocalBlessings() security.Blessings
 	RemoteBlessings() security.Blessings
+	LocalDischarges() map[string]security.Discharge
 	RemoteDischarges() map[string]security.Discharge
 }
 
diff --git a/profiles/internal/ipc/stream/vc/listener_test.go b/profiles/internal/ipc/stream/vc/listener_test.go
index f240b3f..15a3bb5 100644
--- a/profiles/internal/ipc/stream/vc/listener_test.go
+++ b/profiles/internal/ipc/stream/vc/listener_test.go
@@ -24,6 +24,7 @@
 func (*noopFlow) LocalPrincipal() security.Principal              { return nil }
 func (*noopFlow) LocalBlessings() security.Blessings              { return security.Blessings{} }
 func (*noopFlow) RemoteBlessings() security.Blessings             { return security.Blessings{} }
+func (*noopFlow) LocalDischarges() map[string]security.Discharge  { return nil }
 func (*noopFlow) RemoteDischarges() map[string]security.Discharge { return nil }
 func (*noopFlow) SetDeadline(<-chan struct{})                     {}
 func (*noopFlow) VCDataCache() stream.VCDataCache                 { return nil }
diff --git a/profiles/internal/ipc/stream/vc/vc.go b/profiles/internal/ipc/stream/vc/vc.go
index 15cf5c2..0860870 100644
--- a/profiles/internal/ipc/stream/vc/vc.go
+++ b/profiles/internal/ipc/stream/vc/vc.go
@@ -56,7 +56,8 @@
 	localEP, remoteEP               naming.Endpoint
 	localPrincipal                  security.Principal
 	localBlessings, remoteBlessings security.Blessings
-	remoteDischarges                map[string]security.Discharge
+	localDischarges                 map[string]security.Discharge // Discharges shared by the local end of the VC.
+	remoteDischarges                map[string]security.Discharge // Discharges shared by the remote end of the VC.
 
 	pool           *iobuf.Pool
 	reserveBytes   uint
@@ -589,7 +590,8 @@
 		vc.mu.Lock()
 		vc.authFID = vc.findFlowLocked(authConn)
 		vc.mu.Unlock()
-		rBlessings, err := AuthenticateAsServer(authConn, principal, lBlessings, dischargeClient, crypter, vc.version)
+
+		rBlessings, lDischarges, err := AuthenticateAsServer(authConn, principal, lBlessings, dischargeClient, crypter, vc.version)
 		if err != nil {
 			authConn.Close()
 			sendErr(fmt.Errorf("authentication failed: %v", err))
@@ -601,6 +603,7 @@
 		vc.localPrincipal = principal
 		vc.localBlessings = lBlessings
 		vc.remoteBlessings = rBlessings
+		vc.localDischarges = lDischarges
 		close(vc.acceptHandshakeDone)
 		vc.acceptHandshakeDone = nil
 		vc.mu.Unlock()
@@ -635,6 +638,17 @@
 				vlog.Errorf("encoding discharges on VC %v failed: %v", vc, err)
 				return
 			}
+			if len(discharges) == 0 {
+				continue
+			}
+			vc.mu.Lock()
+			if vc.localDischarges == nil {
+				vc.localDischarges = make(map[string]security.Discharge)
+			}
+			for _, d := range discharges {
+				vc.localDischarges[d.ID()] = d
+			}
+			vc.mu.Unlock()
 		case <-vc.closeCh:
 			vlog.VI(3).Infof("closing sendDischargesLoop on VC %v", vc)
 			return
@@ -676,7 +690,13 @@
 			vlog.VI(3).Infof("decoding discharges on %v failed: %v", vc, err)
 			return
 		}
+		if len(discharges) == 0 {
+			continue
+		}
 		vc.mu.Lock()
+		if vc.remoteDischarges == nil {
+			vc.remoteDischarges = make(map[string]security.Discharge)
+		}
 		for _, d := range discharges {
 			vc.remoteDischarges[d.ID()] = d
 		}
@@ -761,6 +781,19 @@
 	return vc.remoteBlessings
 }
 
+// LocalDischarges returns the discharges presented by the local end of the VC during
+// authentication.
+func (vc *VC) LocalDischarges() map[string]security.Discharge {
+	vc.mu.Lock()
+	defer vc.mu.Unlock()
+	vc.waitForHandshakeLocked()
+	if len(vc.localDischarges) == 0 {
+		return nil
+	}
+	// Return a copy of the map to prevent racy reads.
+	return copyDischargeMap(vc.localDischarges)
+}
+
 // RemoteDischarges returns the discharges presented by the remote end of the VC during
 // authentication.
 func (vc *VC) RemoteDischarges() map[string]security.Discharge {
@@ -770,12 +803,8 @@
 	if len(vc.remoteDischarges) == 0 {
 		return nil
 	}
-	// Copy the map to prevent racy reads.
-	ret := make(map[string]security.Discharge)
-	for k, v := range vc.remoteDischarges {
-		ret[k] = v
-	}
-	return ret
+	// Return a copy of the map to prevent racy reads.
+	return copyDischargeMap(vc.remoteDischarges)
 }
 
 // waitForHandshakeLocked blocks until an in-progress handshake (encryption
@@ -832,3 +861,12 @@
 func (r readHandlerImpl) HandleRead(bytes uint) {
 	r.vc.helper.AddReceiveBuffers(r.vc.vci, r.fid, bytes)
 }
+
+func copyDischargeMap(m map[string]security.Discharge) map[string]security.Discharge {
+	ret := make(map[string]security.Discharge)
+	for id, d := range m {
+		ret[id] = d
+	}
+	return ret
+
+}
diff --git a/profiles/internal/ipc/stream/vif/auth.go b/profiles/internal/ipc/stream/vif/auth.go
index 1edf8e1..62914ab 100644
--- a/profiles/internal/ipc/stream/vif/auth.go
+++ b/profiles/internal/ipc/stream/vif/auth.go
@@ -180,7 +180,7 @@
 	c := crypto.NewControlCipherIPC6(&box.PublicKey, &pvt.naclBoxPrivateKey, true)
 	sconn := newSetupConn(writer, reader, c)
 	// TODO(jyh): act upon authentication results.
-	_, err := vc.AuthenticateAsServer(sconn, principal, lBlessings, dc, crypto.NewNullCrypter(), version)
+	_, _, err := vc.AuthenticateAsServer(sconn, principal, lBlessings, dc, crypto.NewNullCrypter(), version)
 	if err != nil {
 		return nil, fmt.Errorf("authentication failed: %v", err)
 	}
diff --git a/profiles/internal/ipc/testutil_test.go b/profiles/internal/ipc/testutil_test.go
index 5bff981..533d8d8 100644
--- a/profiles/internal/ipc/testutil_test.go
+++ b/profiles/internal/ipc/testutil_test.go
@@ -87,6 +87,7 @@
 func (c *mockSecurityContext) Method() string                                  { return "" }
 func (c *mockSecurityContext) MethodTags() []*vdl.Value                        { return nil }
 func (c *mockSecurityContext) Suffix() string                                  { return "" }
+func (c *mockSecurityContext) LocalDischarges() map[string]security.Discharge  { return nil }
 func (c *mockSecurityContext) RemoteDischarges() map[string]security.Discharge { return nil }
 func (c *mockSecurityContext) LocalEndpoint() naming.Endpoint                  { return nil }
 func (c *mockSecurityContext) RemoteEndpoint() naming.Endpoint                 { return nil }
diff --git a/services/wsprd/app/app.go b/services/wsprd/app/app.go
index cdbfded..ad05bb6 100644
--- a/services/wsprd/app/app.go
+++ b/services/wsprd/app/app.go
@@ -391,6 +391,7 @@
 func (l *localCall) MethodTags() []*vdl.Value                        { return l.tags }
 func (l *localCall) Name() string                                    { return l.vrpc.Name }
 func (l *localCall) Suffix() string                                  { return "" }
+func (l *localCall) LocalDischarges() map[string]security.Discharge  { return nil }
 func (l *localCall) RemoteDischarges() map[string]security.Discharge { return nil }
 func (l *localCall) LocalPrincipal() security.Principal              { return nil }
 func (l *localCall) LocalBlessings() security.Blessings              { return security.Blessings{} }