diff --git a/go/common/crypto/signature/signer.go b/go/common/crypto/signature/signer.go index 03968180a66..27c48edd7bf 100644 --- a/go/common/crypto/signature/signer.go +++ b/go/common/crypto/signature/signer.go @@ -36,10 +36,19 @@ var ( errUnregisteredContext = errors.New("signature: unregistered context") errNoChainContext = errors.New("signature: chain domain separation context not set") - registeredContexts sync.Map + registeredContexts sync.Map + allowUnregisteredContexts bool chainContextLock sync.RWMutex chainContext Context + + // SignerRoles is the list of all supported signer roles. + SignerRoles = []SignerRole{ + SignerEntity, + SignerNode, + SignerP2P, + SignerConsensus, + } ) type contextOptions struct { @@ -111,6 +120,13 @@ func UnsafeResetChainContext() { chainContext = Context("") } +// UnsafeAllowUnregisteredContexts bypasses the context registration check. +// +// This function is only for the benefit of implementing a remote signer. +func UnsafeAllowUnregisteredContexts() { + allowUnregisteredContexts = true +} + // SetChainContext configures the chain domain separation context that is // used with any contexts constructed using the WithChainSeparation option. func SetChainContext(rawContext string) { @@ -137,6 +153,8 @@ const ( SignerNode SignerP2P SignerConsensus + + // If you add to this, also add the new roles to SignerRoles. ) // SignerFactoryCtor is an SignerFactory constructor. @@ -187,6 +205,17 @@ type UnsafeSigner interface { // PrepareSignerContext prepares a context for use during signing by a Signer. func PrepareSignerContext(context Context) ([]byte, error) { + // The remote signer implementation uses the raw context, and + // registration is dealt with client side. Just check that the + // length is sensible, even though the client should be sending + // something sane. + if allowUnregisteredContexts { + if cLen := len(context); cLen == 0 || cLen > ed25519.ContextMaxSize { + return nil, errMalformedContext + } + return []byte(context), nil + } + // Ensure that the context is registered for use. rawOpts, isRegistered := registeredContexts.Load(context) if !isRegistered { diff --git a/go/common/crypto/signature/signers/remote/grpc.go b/go/common/crypto/signature/signers/remote/grpc.go new file mode 100644 index 00000000000..79fea0164d6 --- /dev/null +++ b/go/common/crypto/signature/signers/remote/grpc.go @@ -0,0 +1,226 @@ +// Package remote provides a gRPC backed signer (both client and server). +package remote + +import ( + "context" + "fmt" + "io" + + "google.golang.org/grpc" + + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + cmnGrpc "github.com/oasislabs/oasis-core/go/common/grpc" +) + +var ( + serviceName = cmnGrpc.NewServiceName("RemoteSigner") + + methodPublicKeys = serviceName.NewMethod("PublicKeys", nil) + methodSign = serviceName.NewMethod("Sign", SignRequest{}) + + serviceDesc = grpc.ServiceDesc{ + ServiceName: string(serviceName), + HandlerType: (*wrapper)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: methodPublicKeys.ShortName(), + Handler: handlerPublicKeys, + }, + { + MethodName: methodSign.ShortName(), + Handler: handlerSign, + }, + }, + } +) + +// PublicKey is a public key supported by the remote signer. +type PublicKey struct { + Role signature.SignerRole `json:"role"` + PublicKey signature.PublicKey `json:"public_key"` +} + +// SignRequest is a signature request. +type SignRequest struct { + Role signature.SignerRole `json:"role"` + Context string `json:"context"` + Message []byte `json:"message"` +} + +type wrapper struct { + signers map[signature.SignerRole]signature.Signer +} + +func (w *wrapper) publicKeys(ctx context.Context) ([]PublicKey, error) { + var resp []PublicKey + for _, v := range signature.SignerRoles { // Return in consistent order. + if signer := w.signers[v]; signer != nil { + resp = append(resp, PublicKey{ + Role: v, + PublicKey: signer.Public(), + }) + } + } + return resp, nil +} + +func (w *wrapper) sign(ctx context.Context, req *SignRequest) ([]byte, error) { + signer, ok := w.signers[req.Role] + if !ok { + return nil, signature.ErrNotExist + } + return signer.ContextSign(signature.Context(req.Context), req.Message) +} + +func handlerPublicKeys( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + if interceptor == nil { + return srv.(*wrapper).publicKeys(ctx) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodPublicKeys.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(*wrapper).publicKeys(ctx) + } + return interceptor(ctx, nil, info, handler) +} + +func handlerSign( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + var req SignRequest + if err := dec(&req); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(*wrapper).sign(ctx, &req) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodSign.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(*wrapper).sign(ctx, req.(*SignRequest)) + } + return interceptor(ctx, &req, info, handler) +} + +// RegisterService registers a new remote signer backend service with the given +// gRPC server. +// +// WARNING: NEVER call this from the actual node. +func RegisterService(server *grpc.Server, signerFactory signature.SignerFactory) { + // Not sure if this is the best place to do this. + signature.UnsafeAllowUnregisteredContexts() + + // Load all signers, ignoring errors. + w := &wrapper{ + signers: make(map[signature.SignerRole]signature.Signer), + } + for _, v := range signature.SignerRoles { + signer, err := signerFactory.Load(v) + if err == nil { + w.signers[v] = signer + } + } + server.RegisterService(&serviceDesc, w) +} + +type remoteFactory struct { + conn *grpc.ClientConn + reqCtx context.Context + + signers map[signature.SignerRole]*remoteSigner +} + +func (rf *remoteFactory) EnsureRole(role signature.SignerRole) error { + if rf.signers[role] != nil { + return signature.ErrNotExist + } + return nil +} + +func (rf *remoteFactory) Generate(role signature.SignerRole, rng io.Reader) (signature.Signer, error) { + return nil, fmt.Errorf("signature/signer/remote: key re-generation prohibited") +} + +func (rf *remoteFactory) Load(role signature.SignerRole) (signature.Signer, error) { + signer := rf.signers[role] + if signer == nil { + return nil, signature.ErrNotExist + } + return signer, nil +} + +type remoteSigner struct { + factory *remoteFactory + + publicKey signature.PublicKey + role signature.SignerRole +} + +func (rs *remoteSigner) Public() signature.PublicKey { + return rs.publicKey +} + +func (rs *remoteSigner) ContextSign(context signature.Context, message []byte) ([]byte, error) { + // Prepare the context (chain separation is done client side). + rawCtx, err := signature.PrepareSignerContext(context) + if err != nil { + return nil, err + } + + req := &SignRequest{ + Role: rs.role, + Context: string(rawCtx), + Message: message, + } + + var rsp []byte + if err := rs.factory.conn.Invoke(rs.factory.reqCtx, methodSign.FullName(), req, &rsp); err != nil { + return nil, err + } + + return rsp, nil +} + +func (rs *remoteSigner) String() string { + return "[redacted remote private key]" +} + +func (rs *remoteSigner) Reset() { + // Nothing to do. +} + +// NewRemoteFactory creates a new gRPC remote signer client service. +func NewRemoteFactory(ctx context.Context, conn *grpc.ClientConn) (signature.SignerFactory, error) { + // Enumerate the keys available, and cache them. + var rsp []PublicKey + if err := conn.Invoke(ctx, methodPublicKeys.FullName(), nil, &rsp); err != nil { + return nil, err + } + + rf := &remoteFactory{ + conn: conn, + reqCtx: ctx, + signers: make(map[signature.SignerRole]*remoteSigner), + } + for _, v := range rsp { + rf.signers[v.Role] = &remoteSigner{ + factory: rf, + publicKey: v.PublicKey, + role: v.Role, + } + } + + return rf, nil +}