Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"errors"
"fmt"
"io"
"net/rpc"
"os"
"os/exec"

Expand Down Expand Up @@ -83,7 +82,7 @@ type DecryptArgs struct {
// Key implements credential.Credential by holding the executed signer subprocess.
type Key struct {
cmd *exec.Cmd // Pointer to the signer subprocess.
client *rpc.Client // Pointer to the rpc client that communicates with the signer subprocess.
client *rpcClient // Pointer to the rpc client that communicates with the signer subprocess.
publicKey crypto.PublicKey // Public key of loaded certificate.
chain [][]byte // Certificate chain of loaded certificate.
}
Expand Down Expand Up @@ -179,7 +178,7 @@ func Cred(configFilePath string) (*Key, error) {
if err != nil {
return nil, err
}
k.client = rpc.NewClient(&Connection{kout, kin})
k.client = newRPCClient(&Connection{kout, kin})

if err := k.cmd.Start(); err != nil {
return nil, fmt.Errorf("starting enterprise cert signer subprocess: %w", err)
Expand Down
97 changes: 97 additions & 0 deletions client/rpclite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2022 Google LLC.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

// rpclite is a minimal RPC client that speaks the same gob-based wire protocol
// as net/rpc but does not import net/rpc itself. This avoids pulling in
// net/rpc's debug HTTP handler which depends on html/template, a known blocker
// for the Go linker's method dead code elimination (DCE) optimization.
//
// Wire protocol (identical to net/rpc):
// - Client sends: gob-encoded request header, then gob-encoded args
// - Server sends: gob-encoded response header, then gob-encoded reply
//
// The request/response structs mirror net/rpc.Request and net/rpc.Response
// exactly, so this client is compatible with existing net/rpc servers.

import (
"encoding/gob"
"errors"
"io"
"sync"
)

// request mirrors net/rpc.Request — must match exactly for wire compatibility.
type request struct {
ServiceMethod string
Seq uint64
}

// response mirrors net/rpc.Response — must match exactly for wire compatibility.
type response struct {
ServiceMethod string
Seq uint64
Error string
}

// rpcClient is a minimal replacement for net/rpc.Client.
type rpcClient struct {
conn io.ReadWriteCloser
enc *gob.Encoder
dec *gob.Decoder
mu sync.Mutex
seq uint64
}

// newRPCClient creates a new RPC client, equivalent to rpc.NewClient.
func newRPCClient(conn io.ReadWriteCloser) *rpcClient {
return &rpcClient{
conn: conn,
enc: gob.NewEncoder(conn),
dec: gob.NewDecoder(conn),
}
}

// Call invokes the named method, waits for it to complete, and returns the error status.
func (c *rpcClient) Call(serviceMethod string, args any, reply any) error {
c.mu.Lock()
defer c.mu.Unlock()

c.seq++
req := request{ServiceMethod: serviceMethod, Seq: c.seq}

if err := c.enc.Encode(&req); err != nil {
return err
}
if err := c.enc.Encode(args); err != nil {
return err
}

var resp response
if err := c.dec.Decode(&resp); err != nil {
return err
}
if err := c.dec.Decode(reply); err != nil {
return err
}
if resp.Error != "" {
return errors.New(resp.Error)
}
return nil
}

// Close closes the underlying connection.
func (c *rpcClient) Close() error {
return c.conn.Close()
}
95 changes: 95 additions & 0 deletions client/rpclite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2022 Google LLC.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"io"
"net/rpc"
"testing"
)

// EchoService is a trivial RPC service for testing wire compatibility.
type EchoService struct{}

// EchoArgs are the arguments for the Echo method.
type EchoArgs struct {
Msg string
}

// EchoReply is the reply from the Echo method.
type EchoReply struct {
Msg string
}

// Echo returns the input message as-is.
func (e *EchoService) Echo(args EchoArgs, reply *EchoReply) error {
reply.Msg = args.Msg
return nil
}

// newTestClientServer creates a connected rpclite client and net/rpc server
// pair over an in-memory pipe, verifying wire compatibility.
func newTestClientServer(t *testing.T) *rpcClient {
t.Helper()
clientRead, serverWrite := io.Pipe()
serverRead, clientWrite := io.Pipe()

srv := rpc.NewServer()
if err := srv.Register(&EchoService{}); err != nil {
t.Fatal(err)
}

go srv.ServeConn(&Connection{serverRead, serverWrite})

return newRPCClient(&Connection{clientRead, clientWrite})
}

func TestRPCLite_WireCompatibility(t *testing.T) {
client := newTestClientServer(t)
defer client.Close()

var reply EchoReply
if err := client.Call("EchoService.Echo", EchoArgs{Msg: "hello"}, &reply); err != nil {
t.Fatalf("Call: got %v, want nil", err)
}
if reply.Msg != "hello" {
t.Errorf("Call: got reply %q, want %q", reply.Msg, "hello")
}
}

func TestRPCLite_MultipleCalls(t *testing.T) {
client := newTestClientServer(t)
defer client.Close()

for i, msg := range []string{"first", "second", "third"} {
var reply EchoReply
if err := client.Call("EchoService.Echo", EchoArgs{Msg: msg}, &reply); err != nil {
t.Fatalf("Call %d: got %v, want nil", i, err)
}
if reply.Msg != msg {
t.Errorf("Call %d: got reply %q, want %q", i, reply.Msg, msg)
}
}
}

func TestRPCLite_UnknownMethod(t *testing.T) {
client := newTestClientServer(t)
defer client.Close()

var reply EchoReply
err := client.Call("EchoService.NoSuchMethod", EchoArgs{}, &reply)
if err == nil {
t.Fatal("Call unknown method: got nil, want error")
}
}