diff --git a/api-spec/openapi/swagger/signer/v1/service.openapi.json b/api-spec/openapi/swagger/signer/v1/service.openapi.json index 35adc9078..514c8ca7a 100644 --- a/api-spec/openapi/swagger/signer/v1/service.openapi.json +++ b/api-spec/openapi/swagger/signer/v1/service.openapi.json @@ -35,6 +35,46 @@ } } }, + "/v1/sign-message": { + "post": { + "tags": [ + "SignerService" + ], + "operationId": "SignerService_SignMessage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignMessageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "a successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignMessageResponse" + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + } + } + } + }, "/v1/sign-transaction": { "post": { "tags": [ @@ -184,6 +224,30 @@ } } }, + "SignMessageRequest": { + "title": "SignMessageRequest", + "type": "object", + "properties": { + "message": { + "pattern": "^(?:[0-9a-fA-F]{2})*$", + "type": "string", + "description": "hex-encoded message to sign" + } + } + }, + "SignMessageResponse": { + "title": "SignMessageResponse", + "type": "object", + "properties": { + "signature": { + "pattern": "^[0-9a-fA-F]{128}$", + "maxLength": 128, + "minLength": 128, + "type": "string", + "description": "hex-encoded Schnorr signature" + } + } + }, "SignTransactionRequest": { "title": "SignTransactionRequest", "type": "object", diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.go b/api-spec/protobuf/gen/signer/v1/service.pb.go index 61a86e027..e157af4cb 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.go @@ -374,6 +374,96 @@ func (x *SignTransactionTapscriptResponse) GetSignedTx() string { return "" } +type SignMessageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // hex-encoded message to sign + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignMessageRequest) Reset() { + *x = SignMessageRequest{} + mi := &file_signer_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageRequest) ProtoMessage() {} + +func (x *SignMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_signer_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageRequest.ProtoReflect.Descriptor instead. +func (*SignMessageRequest) Descriptor() ([]byte, []int) { + return file_signer_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *SignMessageRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type SignMessageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // hex-encoded Schnorr signature + Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignMessageResponse) Reset() { + *x = SignMessageResponse{} + mi := &file_signer_v1_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageResponse) ProtoMessage() {} + +func (x *SignMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_signer_v1_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageResponse.ProtoReflect.Descriptor instead. +func (*SignMessageResponse) Descriptor() ([]byte, []int) { + return file_signer_v1_service_proto_rawDescGZIP(), []int{9} +} + +func (x *SignMessageResponse) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + var File_signer_v1_service_proto protoreflect.FileDescriptor const file_signer_v1_service_proto_rawDesc = "" + @@ -396,14 +486,19 @@ const file_signer_v1_service_proto_rawDesc = "" + "partial_tx\x18\x01 \x01(\tR\tpartialTx\x12#\n" + "\rinput_indexes\x18\x02 \x03(\x05R\finputIndexes\"?\n" + " SignTransactionTapscriptResponse\x12\x1b\n" + - "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx2\xd7\x03\n" + + "\tsigned_tx\x18\x01 \x01(\tR\bsignedTx\"J\n" + + "\x12SignMessageRequest\x124\n" + + "\amessage\x18\x01 \x01(\tB\x1a\xbaJ\x17b\x15^(?:[0-9a-fA-F]{2})*$R\amessage\"T\n" + + "\x13SignMessageResponse\x12=\n" + + "\tsignature\x18\x01 \x01(\tB\x1f\xbaJ\x1cb\x12^[0-9a-fA-F]{128}$\xa0\x01\x80\x01\xa8\x01\x80\x01R\tsignature2\xbf\x04\n" + "\rSignerService\x12W\n" + "\tGetStatus\x12\x1b.signer.v1.GetStatusRequest\x1a\x1c.signer.v1.GetStatusResponse\"\x0f\xb2J\f\x12\n" + "/v1/status\x12W\n" + "\tGetPubkey\x12\x1b.signer.v1.GetPubkeyRequest\x1a\x1c.signer.v1.GetPubkeyResponse\"\x0f\xb2J\f\x12\n" + "/v1/pubkey\x12v\n" + "\x0fSignTransaction\x12!.signer.v1.SignTransactionRequest\x1a\".signer.v1.SignTransactionResponse\"\x1c\xb2J\x19B\x01*\"\x14/v1/sign-transaction\x12\x9b\x01\n" + - "\x18SignTransactionTapscript\x12*.signer.v1.SignTransactionTapscriptRequest\x1a+.signer.v1.SignTransactionTapscriptResponse\"&\xb2J#B\x01*\"\x1e/v1/sign-transaction-tapscriptB\x90\x01\n" + + "\x18SignTransactionTapscript\x12*.signer.v1.SignTransactionTapscriptRequest\x1a+.signer.v1.SignTransactionTapscriptResponse\"&\xb2J#B\x01*\"\x1e/v1/sign-transaction-tapscript\x12f\n" + + "\vSignMessage\x12\x1d.signer.v1.SignMessageRequest\x1a\x1e.signer.v1.SignMessageResponse\"\x18\xb2J\x15B\x01*\"\x10/v1/sign-messageB\x90\x01\n" + "\rcom.signer.v1B\fServiceProtoP\x01Z,github.com/arkade-os/arkd/signer/v1;signerv1\xa2\x02\x03SXX\xaa\x02\tSigner.V1\xca\x02\tSigner\\V1\xe2\x02\x15Signer\\V1\\GPBMetadata\xea\x02\n" + "Signer::V1b\x06proto3" @@ -419,7 +514,7 @@ func file_signer_v1_service_proto_rawDescGZIP() []byte { return file_signer_v1_service_proto_rawDescData } -var file_signer_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_signer_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_signer_v1_service_proto_goTypes = []any{ (*GetStatusRequest)(nil), // 0: signer.v1.GetStatusRequest (*GetStatusResponse)(nil), // 1: signer.v1.GetStatusResponse @@ -429,18 +524,22 @@ var file_signer_v1_service_proto_goTypes = []any{ (*SignTransactionResponse)(nil), // 5: signer.v1.SignTransactionResponse (*SignTransactionTapscriptRequest)(nil), // 6: signer.v1.SignTransactionTapscriptRequest (*SignTransactionTapscriptResponse)(nil), // 7: signer.v1.SignTransactionTapscriptResponse + (*SignMessageRequest)(nil), // 8: signer.v1.SignMessageRequest + (*SignMessageResponse)(nil), // 9: signer.v1.SignMessageResponse } var file_signer_v1_service_proto_depIdxs = []int32{ 0, // 0: signer.v1.SignerService.GetStatus:input_type -> signer.v1.GetStatusRequest 2, // 1: signer.v1.SignerService.GetPubkey:input_type -> signer.v1.GetPubkeyRequest 4, // 2: signer.v1.SignerService.SignTransaction:input_type -> signer.v1.SignTransactionRequest 6, // 3: signer.v1.SignerService.SignTransactionTapscript:input_type -> signer.v1.SignTransactionTapscriptRequest - 1, // 4: signer.v1.SignerService.GetStatus:output_type -> signer.v1.GetStatusResponse - 3, // 5: signer.v1.SignerService.GetPubkey:output_type -> signer.v1.GetPubkeyResponse - 5, // 6: signer.v1.SignerService.SignTransaction:output_type -> signer.v1.SignTransactionResponse - 7, // 7: signer.v1.SignerService.SignTransactionTapscript:output_type -> signer.v1.SignTransactionTapscriptResponse - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type + 8, // 4: signer.v1.SignerService.SignMessage:input_type -> signer.v1.SignMessageRequest + 1, // 5: signer.v1.SignerService.GetStatus:output_type -> signer.v1.GetStatusResponse + 3, // 6: signer.v1.SignerService.GetPubkey:output_type -> signer.v1.GetPubkeyResponse + 5, // 7: signer.v1.SignerService.SignTransaction:output_type -> signer.v1.SignTransactionResponse + 7, // 8: signer.v1.SignerService.SignTransactionTapscript:output_type -> signer.v1.SignTransactionTapscriptResponse + 9, // 9: signer.v1.SignerService.SignMessage:output_type -> signer.v1.SignMessageResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -457,7 +556,7 @@ func file_signer_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_signer_v1_service_proto_rawDesc), len(file_signer_v1_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go b/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go index 30374e624..e3f8b0012 100644 --- a/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go +++ b/api-spec/protobuf/gen/signer/v1/service.pb.rgw.go @@ -63,6 +63,19 @@ func request_SignerService_SignTransactionTapscript_0(ctx context.Context, marsh } +func request_SignerService_SignMessage_0(ctx context.Context, marshaler gateway.Marshaler, mux *gateway.ServeMux, client SignerServiceClient, req *http.Request, pathParams gateway.Params) (proto.Message, gateway.ServerMetadata, error) { + var protoReq SignMessageRequest + var metadata gateway.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, gateway.ErrMarshal{Err: err, Inbound: true} + } + + msg, err := client.SignMessage(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + // RegisterSignerServiceHandlerFromEndpoint is same as RegisterSignerServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterSignerServiceHandlerFromEndpoint(ctx context.Context, mux *gateway.ServeMux, endpoint string, opts []grpc.DialOption) error { @@ -190,4 +203,26 @@ func RegisterSignerServiceHandlerClient(ctx context.Context, mux *gateway.ServeM mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) }) + mux.HandleWithParams("POST", "/v1/sign-message", func(w http.ResponseWriter, req *http.Request, pathParams gateway.Params) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := mux.MarshalerForRequest(req) + var err error + var annotatedContext context.Context + annotatedContext, err = gateway.AnnotateContext(ctx, mux, req, "/signer.v1.SignerService/SignMessage", gateway.WithHTTPPathPattern("/v1/sign-message")) + if err != nil { + mux.HTTPError(ctx, outboundMarshaler, w, req, err) + return + } + + resp, md, err := request_SignerService_SignMessage_0(annotatedContext, inboundMarshaler, mux, client, req, pathParams) + annotatedContext = gateway.NewServerMetadataContext(annotatedContext, md) + if err != nil { + mux.HTTPError(annotatedContext, outboundMarshaler, w, req, err) + return + } + + mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) + }) + } diff --git a/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go b/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go index e937ce06e..dbec1c1f0 100644 --- a/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go +++ b/api-spec/protobuf/gen/signer/v1/service_grpc.pb.go @@ -23,6 +23,7 @@ const ( SignerService_GetPubkey_FullMethodName = "/signer.v1.SignerService/GetPubkey" SignerService_SignTransaction_FullMethodName = "/signer.v1.SignerService/SignTransaction" SignerService_SignTransactionTapscript_FullMethodName = "/signer.v1.SignerService/SignTransactionTapscript" + SignerService_SignMessage_FullMethodName = "/signer.v1.SignerService/SignMessage" ) // SignerServiceClient is the client API for SignerService service. @@ -35,6 +36,7 @@ type SignerServiceClient interface { GetPubkey(ctx context.Context, in *GetPubkeyRequest, opts ...grpc.CallOption) (*GetPubkeyResponse, error) SignTransaction(ctx context.Context, in *SignTransactionRequest, opts ...grpc.CallOption) (*SignTransactionResponse, error) SignTransactionTapscript(ctx context.Context, in *SignTransactionTapscriptRequest, opts ...grpc.CallOption) (*SignTransactionTapscriptResponse, error) + SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) } type signerServiceClient struct { @@ -85,6 +87,16 @@ func (c *signerServiceClient) SignTransactionTapscript(ctx context.Context, in * return out, nil } +func (c *signerServiceClient) SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SignMessageResponse) + err := c.cc.Invoke(ctx, SignerService_SignMessage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // SignerServiceServer is the server API for SignerService service. // All implementations should embed UnimplementedSignerServiceServer // for forward compatibility. @@ -95,6 +107,7 @@ type SignerServiceServer interface { GetPubkey(context.Context, *GetPubkeyRequest) (*GetPubkeyResponse, error) SignTransaction(context.Context, *SignTransactionRequest) (*SignTransactionResponse, error) SignTransactionTapscript(context.Context, *SignTransactionTapscriptRequest) (*SignTransactionTapscriptResponse, error) + SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) } // UnimplementedSignerServiceServer should be embedded to have @@ -116,6 +129,9 @@ func (UnimplementedSignerServiceServer) SignTransaction(context.Context, *SignTr func (UnimplementedSignerServiceServer) SignTransactionTapscript(context.Context, *SignTransactionTapscriptRequest) (*SignTransactionTapscriptResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SignTransactionTapscript not implemented") } +func (UnimplementedSignerServiceServer) SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignMessage not implemented") +} func (UnimplementedSignerServiceServer) testEmbeddedByValue() {} // UnsafeSignerServiceServer may be embedded to opt out of forward compatibility for this service. @@ -208,6 +224,24 @@ func _SignerService_SignTransactionTapscript_Handler(srv interface{}, ctx contex return interceptor(ctx, in, info, handler) } +func _SignerService_SignMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SignerServiceServer).SignMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SignerService_SignMessage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SignerServiceServer).SignMessage(ctx, req.(*SignMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + // SignerService_ServiceDesc is the grpc.ServiceDesc for SignerService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -231,6 +265,10 @@ var SignerService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SignTransactionTapscript", Handler: _SignerService_SignTransactionTapscript_Handler, }, + { + MethodName: "SignMessage", + Handler: _SignerService_SignMessage_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "signer/v1/service.proto", diff --git a/api-spec/protobuf/signer/v1/service.proto b/api-spec/protobuf/signer/v1/service.proto index c694fd5e2..1ffbdf2f3 100644 --- a/api-spec/protobuf/signer/v1/service.proto +++ b/api-spec/protobuf/signer/v1/service.proto @@ -28,6 +28,12 @@ service SignerService { body: "*" }; } + rpc SignMessage(SignMessageRequest) returns (SignMessageResponse) { + option (meshapi.gateway.http) = { + post: "/v1/sign-message" + body: "*" + }; + } } message GetStatusRequest {} @@ -54,4 +60,19 @@ message SignTransactionTapscriptRequest { } message SignTransactionTapscriptResponse { string signed_tx = 1; +} + +message SignMessageRequest { + // hex-encoded message to sign + string message = 1 [(meshapi.gateway.openapi_field) = { + pattern: "^(?:[0-9a-fA-F]{2})*$" + }]; +} +message SignMessageResponse { + // hex-encoded Schnorr signature + string signature = 1 [(meshapi.gateway.openapi_field) = { + pattern: "^[0-9a-fA-F]{128}$" + min_length: 128 + max_length: 128 + }]; } \ No newline at end of file diff --git a/internal/core/ports/signer.go b/internal/core/ports/signer.go index e825aa362..6c0d6fd1f 100644 --- a/internal/core/ports/signer.go +++ b/internal/core/ports/signer.go @@ -13,4 +13,5 @@ type SignerService interface { SignTransactionTapscript( ctx context.Context, partialTx string, inputIndexes []int, // inputIndexes == nil means sign all inputs ) (string, error) + SignMessage(ctx context.Context, message []byte) ([]byte, error) } diff --git a/internal/infrastructure/signer/client.go b/internal/infrastructure/signer/client.go index e508ac851..45b6c5913 100644 --- a/internal/infrastructure/signer/client.go +++ b/internal/infrastructure/signer/client.go @@ -99,3 +99,22 @@ func (c *signerClient) SignTransactionTapscript( } return resp.GetSignedTx(), nil } + +func (c *signerClient) SignMessage( + ctx context.Context, message []byte, +) ([]byte, error) { + resp, err := c.client.SignMessage(ctx, &signerv1.SignMessageRequest{ + Message: hex.EncodeToString(message), + }) + if err != nil { + return nil, err + } + sig, err := hex.DecodeString(resp.GetSignature()) + if err != nil { + return nil, fmt.Errorf("failed to decode signature hex: %w", err) + } + if len(sig) != 64 { + return nil, fmt.Errorf("invalid signature length: expected 64 bytes, got %d", len(sig)) + } + return sig, nil +} diff --git a/internal/infrastructure/tx-builder/covenantless/mocks_test.go b/internal/infrastructure/tx-builder/covenantless/mocks_test.go index 4de5dfdb0..2a0fef6a0 100644 --- a/internal/infrastructure/tx-builder/covenantless/mocks_test.go +++ b/internal/infrastructure/tx-builder/covenantless/mocks_test.go @@ -396,3 +396,6 @@ func (s *staticSigner) SignTransaction(_ context.Context, _ string, _ bool) (str func (s *staticSigner) SignTransactionTapscript(_ context.Context, _ string, _ []int) (string, error) { return "", nil } +func (s *staticSigner) SignMessage(_ context.Context, _ []byte) ([]byte, error) { + return nil, nil +} diff --git a/pkg/arkd-wallet/core/application/types.go b/pkg/arkd-wallet/core/application/types.go index b16a5e5b9..5fc9c36a4 100644 --- a/pkg/arkd-wallet/core/application/types.go +++ b/pkg/arkd-wallet/core/application/types.go @@ -46,6 +46,7 @@ type WalletService interface { // Withdraw both main and connectors account funds WithdrawAll(ctx context.Context, destinationAddress string) (string, error) LoadSignerKey(ctx context.Context, prvkey *btcec.PrivateKey) error + SignMessage(ctx context.Context, message []byte) ([]byte, error) Close() } diff --git a/pkg/arkd-wallet/core/application/wallet/service.go b/pkg/arkd-wallet/core/application/wallet/service.go index 95c574acd..5de76fa3e 100644 --- a/pkg/arkd-wallet/core/application/wallet/service.go +++ b/pkg/arkd-wallet/core/application/wallet/service.go @@ -733,6 +733,18 @@ func (w *wallet) LoadSignerKey(ctx context.Context, prvkey *btcec.PrivateKey) er return nil } +func (w *wallet) SignMessage(ctx context.Context, message []byte) ([]byte, error) { + if w.SignerKey == nil { + return nil, ErrSignerDisabled + } + + msgHash := chainhash.HashB(message) + sig, err := schnorr.Sign(w.SignerKey, msgHash) + if err != nil { + return nil, fmt.Errorf("failed to sign message: %w", err) + } + return sig.Serialize(), nil +} func (w *wallet) Close() { // nolint:errcheck diff --git a/pkg/arkd-wallet/core/application/wallet/service_test.go b/pkg/arkd-wallet/core/application/wallet/service_test.go new file mode 100644 index 000000000..8d814b47f --- /dev/null +++ b/pkg/arkd-wallet/core/application/wallet/service_test.go @@ -0,0 +1,101 @@ +package wallet + +import ( + "context" + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/stretchr/testify/require" +) + +type signMessageFixtures struct { + Valid []signMessageTestCase `json:"valid"` + Invalid []signMessageInvalidCase `json:"invalid"` +} + +type signMessageTestCase struct { + Name string `json:"name"` + MessageHex string `json:"message_hex"` + PrivateKeyHex string `json:"private_key_hex"` +} + +type signMessageInvalidCase struct { + Name string `json:"name"` + MessageHex string `json:"message_hex"` + ExpectedError string `json:"expected_error"` +} + +func loadSignMessageFixtures(t *testing.T) *signMessageFixtures { + t.Helper() + data, err := os.ReadFile("testdata/signmessage_fixtures.json") + require.NoError(t, err) + + var f signMessageFixtures + err = json.Unmarshal(data, &f) + require.NoError(t, err) + + return &f +} + +func TestSignMessage(t *testing.T) { + fixtures := loadSignMessageFixtures(t) + ctx := context.Background() + + t.Run("valid", func(t *testing.T) { + for _, tc := range fixtures.Valid { + t.Run(tc.Name, func(t *testing.T) { + privKeyBytes, err := hex.DecodeString(tc.PrivateKeyHex) + require.NoError(t, err) + privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) + + w := &wallet{ + WalletOptions: WalletOptions{ + SignerKey: privKey, + }, + } + + message, err := hex.DecodeString(tc.MessageHex) + require.NoError(t, err) + + signature, err := w.SignMessage(ctx, message) + require.NoError(t, err) + require.NotNil(t, signature) + + require.Len(t, signature, 64, "schnorr signature should be 64 bytes") + + msgHash := chainhash.HashB(message) + sig, err := schnorr.ParseSignature(signature) + require.NoError(t, err) + + pubKey := privKey.PubKey() + valid := sig.Verify(msgHash, pubKey) + require.True(t, valid, "signature should be valid") + }) + } + }) + + t.Run("invalid", func(t *testing.T) { + for _, tc := range fixtures.Invalid { + t.Run(tc.Name, func(t *testing.T) { + w := &wallet{ + WalletOptions: WalletOptions{ + SignerKey: nil, + }, + } + + message, err := hex.DecodeString(tc.MessageHex) + require.NoError(t, err) + + signature, err := w.SignMessage(ctx, message) + require.Error(t, err) + require.Nil(t, signature) + require.Contains(t, err.Error(), tc.ExpectedError) + }) + } + }) +} diff --git a/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json new file mode 100644 index 000000000..aa42a515b --- /dev/null +++ b/pkg/arkd-wallet/core/application/wallet/testdata/signmessage_fixtures.json @@ -0,0 +1,41 @@ +{ + "valid": [ + { + "name": "simple_message", + "message_hex": "48656c6c6f20576f726c64", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "empty_message", + "message_hex": "", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "32_byte_message", + "message_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "36_byte_auth_token_message", + "message_hex": "000000000000000000000000000000000000000000000000000000000000000100000000", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "large_message", + "message_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "name": "message_with_different_key", + "message_hex": "48656c6c6f20576f726c64", + "private_key_hex": "0000000000000000000000000000000000000000000000000000000000000002" + } + ], + "invalid": [ + { + "name": "no_signer_key", + "message_hex": "48656c6c6f20576f726c64", + "expected_error": "signer not enabled" + } + ] +} diff --git a/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go b/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go index 663369a58..a3f0b91b5 100644 --- a/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go +++ b/pkg/arkd-wallet/interface/grpc/handlers/signer_handler.go @@ -2,9 +2,12 @@ package handlers import ( "context" + "encoding/hex" signerv1 "github.com/arkade-os/arkd/api-spec/protobuf/gen/signer/v1" application "github.com/arkade-os/arkd/pkg/arkd-wallet/core/application" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type signerHandler struct { @@ -61,3 +64,16 @@ func (h *signerHandler) SignTransactionTapscript( return &signerv1.SignTransactionTapscriptResponse{SignedTx: tx}, nil } +func (h *signerHandler) SignMessage( + ctx context.Context, req *signerv1.SignMessageRequest, +) (*signerv1.SignMessageResponse, error) { + message, err := hex.DecodeString(req.GetMessage()) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid message hex: %s", err) + } + signature, err := h.wallet.SignMessage(ctx, message) + if err != nil { + return nil, err + } + return &signerv1.SignMessageResponse{Signature: hex.EncodeToString(signature)}, nil +}