walletrpc: add sign/verify methods

Adding the grpc functionality to sign and verify messages with
single addresses
This commit is contained in:
ziggie 2022-12-03 16:19:10 +01:00
parent 266cd97573
commit 7b68289a7a
No known key found for this signature in database
GPG Key ID: 1AFF9C4DCED6D666
9 changed files with 1735 additions and 652 deletions

File diff suppressed because it is too large Load Diff

View File

@ -362,6 +362,74 @@ func local_request_WalletKit_ListAddresses_0(ctx context.Context, marshaler runt
}
func request_WalletKit_SignMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SignMessageWithAddrRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.SignMessageWithAddr(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_WalletKit_SignMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SignMessageWithAddrRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.SignMessageWithAddr(ctx, &protoReq)
return msg, metadata, err
}
func request_WalletKit_VerifyMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq VerifyMessageWithAddrRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.VerifyMessageWithAddr(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_WalletKit_VerifyMessageWithAddr_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq VerifyMessageWithAddrRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.VerifyMessageWithAddr(ctx, &protoReq)
return msg, metadata, err
}
func request_WalletKit_ImportAccount_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ImportAccountRequest
var metadata runtime.ServerMetadata
@ -1044,6 +1112,52 @@ func RegisterWalletKitHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_WalletKit_SignMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/SignMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/signmessage"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_WalletKit_SignMessageWithAddr_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_SignMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WalletKit_VerifyMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/VerifyMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/verifymessage"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_WalletKit_VerifyMessageWithAddr_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_VerifyMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WalletKit_ImportAccount_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1584,6 +1698,46 @@ func RegisterWalletKitHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_WalletKit_SignMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/SignMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/signmessage"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WalletKit_SignMessageWithAddr_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_SignMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WalletKit_VerifyMessageWithAddr_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/VerifyMessageWithAddr", runtime.WithHTTPPathPattern("/v2/wallet/address/verifymessage"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WalletKit_VerifyMessageWithAddr_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WalletKit_VerifyMessageWithAddr_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WalletKit_ImportAccount_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1868,6 +2022,10 @@ var (
pattern_WalletKit_ListAddresses_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "wallet", "addresses"}, ""))
pattern_WalletKit_SignMessageWithAddr_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "address", "signmessage"}, ""))
pattern_WalletKit_VerifyMessageWithAddr_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "address", "verifymessage"}, ""))
pattern_WalletKit_ImportAccount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "accounts", "import"}, ""))
pattern_WalletKit_ImportPublicKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "key", "import"}, ""))
@ -1916,6 +2074,10 @@ var (
forward_WalletKit_ListAddresses_0 = runtime.ForwardResponseMessage
forward_WalletKit_SignMessageWithAddr_0 = runtime.ForwardResponseMessage
forward_WalletKit_VerifyMessageWithAddr_0 = runtime.ForwardResponseMessage
forward_WalletKit_ImportAccount_0 = runtime.ForwardResponseMessage
forward_WalletKit_ImportPublicKey_0 = runtime.ForwardResponseMessage

View File

@ -272,6 +272,56 @@ func RegisterWalletKitJSONCallbacks(registry map[string]func(ctx context.Context
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.SignMessageWithAddr"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &SignMessageWithAddrRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewWalletKitClient(conn)
resp, err := client.SignMessageWithAddr(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.VerifyMessageWithAddr"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &VerifyMessageWithAddrRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewWalletKitClient(conn)
resp, err := client.VerifyMessageWithAddr(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["walletrpc.WalletKit.ImportAccount"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {

View File

@ -79,6 +79,47 @@ service WalletKit {
*/
rpc ListAddresses (ListAddressesRequest) returns (ListAddressesResponse);
/*
SignMessageWithAddr returns the compact signature (base64 encoded) created
with the private key of the provided address. This requires the address
to be solely based on a public key lock (no scripts). Obviously the internal
lnd wallet has to possess the private key of the address otherwise
an error is returned.
This method aims to provide full compatibility with the bitcoin-core and
btcd implementation. Bitcoin-core's algorithm is not specified in a
BIP and only applicable for legacy addresses. This method enhances the
signing for additional address types: P2WKH, NP2WKH, P2TR.
For P2TR addresses this represents a special case. ECDSA is used to create
a compact signature which makes the public key of the signature recoverable.
*/
rpc SignMessageWithAddr (SignMessageWithAddrRequest)
returns (SignMessageWithAddrResponse);
/*
VerifyMessageWithAddr returns the validity and the recovered public key of
the provided compact signature (base64 encoded). The verification is
twofold. First the validity of the signature itself is checked and then
it is verified that the recovered public key of the signature equals
the public key of the provided address. There is no dependence on the
private key of the address therefore also external addresses are allowed
to verify signatures.
Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
This method is the counterpart of the related signing method
(SignMessageWithAddr) and aims to provide full compatibility to
bitcoin-core's implementation. Although bitcoin-core/btcd only provide
this functionality for legacy addresses this function enhances it to
the address types: P2PKH, P2WKH, NP2WKH, P2TR.
The verification for P2TR addresses is a special case and requires the
ECDSA compact signature to compare the reovered public key to the internal
taproot key. The compact ECDSA signature format was used because there
are still no known compact signature schemes for schnorr signatures.
*/
rpc VerifyMessageWithAddr (VerifyMessageWithAddrRequest)
returns (VerifyMessageWithAddrResponse);
/*
ImportAccount imports an account backed by an account extended public key.
The master key fingerprint denotes the fingerprint of the root key
@ -497,6 +538,43 @@ message ListAddressesResponse {
repeated AccountWithAddresses account_with_addresses = 1;
}
message SignMessageWithAddrRequest {
// The message to be signed. When using REST, this field must be encoded as
// base64.
bytes msg = 1;
// The address which will be used to look up the private key and sign the
// corresponding message.
string addr = 2;
}
message SignMessageWithAddrResponse {
// The compact ECDSA signature for the given message encoded in base64.
string signature = 1;
}
message VerifyMessageWithAddrRequest {
// The message to be signed. When using REST, this field must be encoded as
// base64.
bytes msg = 1;
// The compact ECDSA signature to be verified over the given message
// ecoded in base64.
string signature = 2;
// The address which will be used to look up the public key and verify the
// the signature.
string addr = 3;
}
message VerifyMessageWithAddrResponse {
// Whether the signature was valid over the given message.
bool valid = 1;
// The pubkey recovered from the signature.
bytes pubkey = 2;
}
message ImportAccountRequest {
// A name to identify the account with.
string name = 1;

View File

@ -130,6 +130,74 @@
]
}
},
"/v2/wallet/address/signmessage": {
"post": {
"summary": "SignMessageWithAddr returns the compact signature (base64 encoded) created\nwith the private key of the provided address. This requires the address\nto be solely based on a public key lock (no scripts). Obviously the internal\nlnd wallet has to possess the private key of the address otherwise\nan error is returned.",
"description": "This method aims to provide full compatibility with the bitcoin-core and\nbtcd implementation. Bitcoin-core's algorithm is not specified in a\nBIP and only applicable for legacy addresses. This method enhances the\nsigning for additional address types: P2WKH, NP2WKH, P2TR.\nFor P2TR addresses this represents a special case. ECDSA is used to create\na compact signature which makes the public key of the signature recoverable.",
"operationId": "WalletKit_SignMessageWithAddr",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/walletrpcSignMessageWithAddrResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/walletrpcSignMessageWithAddrRequest"
}
}
],
"tags": [
"WalletKit"
]
}
},
"/v2/wallet/address/verifymessage": {
"post": {
"summary": "VerifyMessageWithAddr returns the validity and the recovered public key of\nthe provided compact signature (base64 encoded). The verification is\ntwofold. First the validity of the signature itself is checked and then\nit is verified that the recovered public key of the signature equals\nthe public key of the provided address. There is no dependence on the\nprivate key of the address therefore also external addresses are allowed\nto verify signatures.\nSupported address types are P2PKH, P2WKH, NP2WKH, P2TR.",
"description": "This method is the counterpart of the related signing method\n(SignMessageWithAddr) and aims to provide full compatibility to\nbitcoin-core's implementation. Although bitcoin-core/btcd only provide\nthis functionality for legacy addresses this function enhances it to\nthe address types: P2PKH, P2WKH, NP2WKH, P2TR.\n\nThe verification for P2TR addresses is a special case and requires the\nECDSA compact signature to compare the reovered public key to the internal\ntaproot key. The compact ECDSA signature format was used because there\nare still no known compact signature schemes for schnorr signatures.",
"operationId": "WalletKit_VerifyMessageWithAddr",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/walletrpcVerifyMessageWithAddrResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/walletrpcVerifyMessageWithAddrRequest"
}
}
],
"tags": [
"WalletKit"
]
}
},
"/v2/wallet/addresses": {
"get": {
"summary": "ListAddresses retrieves all the addresses along with their balance. An\naccount name filter can be provided to filter through all of the\nwallet accounts and return the addresses of only those matching.",
@ -1714,6 +1782,29 @@
}
}
},
"walletrpcSignMessageWithAddrRequest": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"format": "byte",
"description": "The message to be signed. When using REST, this field must be encoded as\nbase64."
},
"addr": {
"type": "string",
"description": "The address which will be used to look up the private key and sign the\ncorresponding message."
}
}
},
"walletrpcSignMessageWithAddrResponse": {
"type": "object",
"properties": {
"signature": {
"type": "string",
"description": "The compact ECDSA signature for the given message encoded in base64."
}
}
},
"walletrpcSignPsbtRequest": {
"type": "object",
"properties": {
@ -1846,6 +1937,38 @@
}
}
},
"walletrpcVerifyMessageWithAddrRequest": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"format": "byte",
"description": "The message to be signed. When using REST, this field must be encoded as\nbase64."
},
"signature": {
"type": "string",
"description": "The compact ECDSA signature to be verified over the given message\necoded in base64."
},
"addr": {
"type": "string",
"description": "The address which will be used to look up the public key and verify the\nthe signature."
}
}
},
"walletrpcVerifyMessageWithAddrResponse": {
"type": "object",
"properties": {
"valid": {
"type": "boolean",
"description": "Whether the signature was valid over the given message."
},
"pubkey": {
"type": "string",
"format": "byte",
"description": "The pubkey recovered from the signature."
}
}
},
"walletrpcWitnessType": {
"type": "string",
"enum": [

View File

@ -59,9 +59,15 @@ http:
- selector: walletrpc.WalletKit.ListAccounts
get: "/v2/wallet/accounts"
- selector: walletrpc.WalletKit.RequiredReserve
get: "/v2/wallet/reserve"
get: "/v2/wallet/reserve"
- selector: walletrpc.WalletKit.ListAddresses
get: "/v2/wallet/addresses"
- selector: walletrpc.WalletKit.ImportAccount
post: "/v2/wallet/accounts/import"
body: "*"
- selector: walletrpc.WalletKit.SignMessageWithAddr
post: "/v2/wallet/address/signmessage"
body: "*"
- selector: walletrpc.WalletKit.VerifyMessageWithAddr
post: "/v2/wallet/address/verifymessage"
body: "*"

View File

@ -57,6 +57,39 @@ type WalletKitClient interface {
// account name filter can be provided to filter through all of the
// wallet accounts and return the addresses of only those matching.
ListAddresses(ctx context.Context, in *ListAddressesRequest, opts ...grpc.CallOption) (*ListAddressesResponse, error)
// SignMessageWithAddr returns the compact signature (base64 encoded) created
// with the private key of the provided address. This requires the address
// to be solely based on a public key lock (no scripts). Obviously the internal
// lnd wallet has to possess the private key of the address otherwise
// an error is returned.
//
// This method aims to provide full compatibility with the bitcoin-core and
// btcd implementation. Bitcoin-core's algorithm is not specified in a
// BIP and only applicable for legacy addresses. This method enhances the
// signing for additional address types: P2WKH, NP2WKH, P2TR.
// For P2TR addresses this represents a special case. ECDSA is used to create
// a compact signature which makes the public key of the signature recoverable.
SignMessageWithAddr(ctx context.Context, in *SignMessageWithAddrRequest, opts ...grpc.CallOption) (*SignMessageWithAddrResponse, error)
// VerifyMessageWithAddr returns the validity and the recovered public key of
// the provided compact signature (base64 encoded). The verification is
// twofold. First the validity of the signature itself is checked and then
// it is verified that the recovered public key of the signature equals
// the public key of the provided address. There is no dependence on the
// private key of the address therefore also external addresses are allowed
// to verify signatures.
// Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
//
// This method is the counterpart of the related signing method
// (SignMessageWithAddr) and aims to provide full compatibility to
// bitcoin-core's implementation. Although bitcoin-core/btcd only provide
// this functionality for legacy addresses this function enhances it to
// the address types: P2PKH, P2WKH, NP2WKH, P2TR.
//
// The verification for P2TR addresses is a special case and requires the
// ECDSA compact signature to compare the reovered public key to the internal
// taproot key. The compact ECDSA signature format was used because there
// are still no known compact signature schemes for schnorr signatures.
VerifyMessageWithAddr(ctx context.Context, in *VerifyMessageWithAddrRequest, opts ...grpc.CallOption) (*VerifyMessageWithAddrResponse, error)
// ImportAccount imports an account backed by an account extended public key.
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
@ -303,6 +336,24 @@ func (c *walletKitClient) ListAddresses(ctx context.Context, in *ListAddressesRe
return out, nil
}
func (c *walletKitClient) SignMessageWithAddr(ctx context.Context, in *SignMessageWithAddrRequest, opts ...grpc.CallOption) (*SignMessageWithAddrResponse, error) {
out := new(SignMessageWithAddrResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/SignMessageWithAddr", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletKitClient) VerifyMessageWithAddr(ctx context.Context, in *VerifyMessageWithAddrRequest, opts ...grpc.CallOption) (*VerifyMessageWithAddrResponse, error) {
out := new(VerifyMessageWithAddrResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/VerifyMessageWithAddr", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletKitClient) ImportAccount(ctx context.Context, in *ImportAccountRequest, opts ...grpc.CallOption) (*ImportAccountResponse, error) {
out := new(ImportAccountResponse)
err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/ImportAccount", in, out, opts...)
@ -462,6 +513,39 @@ type WalletKitServer interface {
// account name filter can be provided to filter through all of the
// wallet accounts and return the addresses of only those matching.
ListAddresses(context.Context, *ListAddressesRequest) (*ListAddressesResponse, error)
// SignMessageWithAddr returns the compact signature (base64 encoded) created
// with the private key of the provided address. This requires the address
// to be solely based on a public key lock (no scripts). Obviously the internal
// lnd wallet has to possess the private key of the address otherwise
// an error is returned.
//
// This method aims to provide full compatibility with the bitcoin-core and
// btcd implementation. Bitcoin-core's algorithm is not specified in a
// BIP and only applicable for legacy addresses. This method enhances the
// signing for additional address types: P2WKH, NP2WKH, P2TR.
// For P2TR addresses this represents a special case. ECDSA is used to create
// a compact signature which makes the public key of the signature recoverable.
SignMessageWithAddr(context.Context, *SignMessageWithAddrRequest) (*SignMessageWithAddrResponse, error)
// VerifyMessageWithAddr returns the validity and the recovered public key of
// the provided compact signature (base64 encoded). The verification is
// twofold. First the validity of the signature itself is checked and then
// it is verified that the recovered public key of the signature equals
// the public key of the provided address. There is no dependence on the
// private key of the address therefore also external addresses are allowed
// to verify signatures.
// Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
//
// This method is the counterpart of the related signing method
// (SignMessageWithAddr) and aims to provide full compatibility to
// bitcoin-core's implementation. Although bitcoin-core/btcd only provide
// this functionality for legacy addresses this function enhances it to
// the address types: P2PKH, P2WKH, NP2WKH, P2TR.
//
// The verification for P2TR addresses is a special case and requires the
// ECDSA compact signature to compare the reovered public key to the internal
// taproot key. The compact ECDSA signature format was used because there
// are still no known compact signature schemes for schnorr signatures.
VerifyMessageWithAddr(context.Context, *VerifyMessageWithAddrRequest) (*VerifyMessageWithAddrResponse, error)
// ImportAccount imports an account backed by an account extended public key.
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
@ -645,6 +729,12 @@ func (UnimplementedWalletKitServer) RequiredReserve(context.Context, *RequiredRe
func (UnimplementedWalletKitServer) ListAddresses(context.Context, *ListAddressesRequest) (*ListAddressesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListAddresses not implemented")
}
func (UnimplementedWalletKitServer) SignMessageWithAddr(context.Context, *SignMessageWithAddrRequest) (*SignMessageWithAddrResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SignMessageWithAddr not implemented")
}
func (UnimplementedWalletKitServer) VerifyMessageWithAddr(context.Context, *VerifyMessageWithAddrRequest) (*VerifyMessageWithAddrResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method VerifyMessageWithAddr not implemented")
}
func (UnimplementedWalletKitServer) ImportAccount(context.Context, *ImportAccountRequest) (*ImportAccountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ImportAccount not implemented")
}
@ -877,6 +967,42 @@ func _WalletKit_ListAddresses_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _WalletKit_SignMessageWithAddr_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SignMessageWithAddrRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletKitServer).SignMessageWithAddr(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/walletrpc.WalletKit/SignMessageWithAddr",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletKitServer).SignMessageWithAddr(ctx, req.(*SignMessageWithAddrRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletKit_VerifyMessageWithAddr_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(VerifyMessageWithAddrRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletKitServer).VerifyMessageWithAddr(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/walletrpc.WalletKit/VerifyMessageWithAddr",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletKitServer).VerifyMessageWithAddr(ctx, req.(*VerifyMessageWithAddrRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletKit_ImportAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ImportAccountRequest)
if err := dec(in); err != nil {
@ -1158,6 +1284,14 @@ var WalletKit_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListAddresses",
Handler: _WalletKit_ListAddresses_Handler,
},
{
MethodName: "SignMessageWithAddr",
Handler: _WalletKit_SignMessageWithAddr_Handler,
},
{
MethodName: "VerifyMessageWithAddr",
Handler: _WalletKit_VerifyMessageWithAddr_Handler,
},
{
MethodName: "ImportAccount",
Handler: _WalletKit_ImportAccount_Handler,

View File

@ -6,6 +6,7 @@ package walletrpc
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
@ -17,6 +18,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
@ -126,6 +128,14 @@ var (
Entity: "onchain",
Action: "read",
}},
"/walletrpc.WalletKit/SignMessageWithAddr": {{
Entity: "onchain",
Action: "write",
}},
"/walletrpc.WalletKit/VerifyMessageWithAddr": {{
Entity: "onchain",
Action: "write",
}},
"/walletrpc.WalletKit/FundPsbt": {{
Entity: "onchain",
Action: "write",
@ -1625,6 +1635,182 @@ func parseAddrType(addrType AddressType,
}
}
// msgSignaturePrefix is a prefix used to prevent inadvertently signing a
// transaction or a signature. It is prepended in front of the message and
// follows the same standard as bitcoin core and btcd.
const msgSignaturePrefix = "Bitcoin Signed Message:\n"
// SignMessageWithAddr signs a message with the private key of the provided
// address. The address needs to belong to the lnd wallet.
func (w *WalletKit) SignMessageWithAddr(ctx context.Context,
req *SignMessageWithAddrRequest) (*SignMessageWithAddrResponse, error) {
addr, err := btcutil.DecodeAddress(req.Addr, w.cfg.ChainParams)
if err != nil {
return nil, fmt.Errorf("unable to decode address: %w", err)
}
if !addr.IsForNet(w.cfg.ChainParams) {
return nil, fmt.Errorf("encoded address is for "+
"the wrong network %s", req.Addr)
}
// Fetch address infos from own wallet and check whether it belongs
// to the lnd wallet.
managedAddr, err := w.cfg.Wallet.AddressInfo(addr)
if err != nil {
return nil, fmt.Errorf("address could not be found in the "+
"wallet database: %w", err)
}
// Verifying by checking the interface type that the wallet knows about
// the public and private keys so it can sign the message with the
// private key of this address.
pubKey, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return nil, fmt.Errorf("private key to address is unknown")
}
digest, err := doubleHashMessage(msgSignaturePrefix, string(req.Msg))
if err != nil {
return nil, err
}
// For all address types (P2WKH, NP2WKH,P2TR) the ECDSA compact signing
// algorithm is used. For P2TR addresses this represents a special case.
// ECDSA is used to create a compact signature which makes the public
// key of the signature recoverable. For Schnorr no known compact
// signing algorithm exists yet.
privKey, err := pubKey.PrivKey()
if err != nil {
return nil, fmt.Errorf("no private key could be "+
"fetched from wallet database: %w", err)
}
sigBytes, err := ecdsa.SignCompact(privKey, digest, pubKey.Compressed())
if err != nil {
return nil, fmt.Errorf("failed to create signature: %w", err)
}
// Bitcoin signatures are base64 encoded (being compatible with
// bitcoin-core and btcd).
sig := base64.StdEncoding.EncodeToString(sigBytes)
return &SignMessageWithAddrResponse{
Signature: sig,
}, nil
}
// VerifyMessageWithAddr verifies a signature on a message with a provided
// address, it checks both the validity of the signature itself and then
// verifies whether the signature corresponds to the public key of the
// provided address. There is no dependence on the private key of the address
// therefore also external addresses are allowed to verify signatures.
// Supported address types are P2PKH, P2WKH, NP2WKH, P2TR.
func (w *WalletKit) VerifyMessageWithAddr(ctx context.Context,
req *VerifyMessageWithAddrRequest) (*VerifyMessageWithAddrResponse,
error) {
sig, err := base64.StdEncoding.DecodeString(req.Signature)
if err != nil {
return nil, fmt.Errorf("malformed base64 encoding of "+
"the signature: %w", err)
}
digest, err := doubleHashMessage(msgSignaturePrefix, string(req.Msg))
if err != nil {
return nil, err
}
pk, wasCompressed, err := ecdsa.RecoverCompact(sig, digest)
if err != nil {
return nil, fmt.Errorf("unable to recover public key "+
"from compact signature: %w", err)
}
var serializedPubkey []byte
if wasCompressed {
serializedPubkey = pk.SerializeCompressed()
} else {
serializedPubkey = pk.SerializeUncompressed()
}
addr, err := btcutil.DecodeAddress(req.Addr, w.cfg.ChainParams)
if err != nil {
return nil, fmt.Errorf("unable to decode address: %w", err)
}
if !addr.IsForNet(w.cfg.ChainParams) {
return nil, fmt.Errorf("encoded address is for"+
"the wrong network %s", req.Addr)
}
var (
address btcutil.Address
pubKeyHash = btcutil.Hash160(serializedPubkey)
)
// Ensure the address is one of the supported types.
switch addr.(type) {
case *btcutil.AddressPubKeyHash:
address, err = btcutil.NewAddressPubKeyHash(
pubKeyHash, w.cfg.ChainParams,
)
if err != nil {
return nil, err
}
case *btcutil.AddressWitnessPubKeyHash:
address, err = btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash, w.cfg.ChainParams,
)
if err != nil {
return nil, err
}
case *btcutil.AddressScriptHash:
// Check if address is a Nested P2WKH (NP2WKH).
address, err = btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash, w.cfg.ChainParams,
)
if err != nil {
return nil, err
}
witnessScript, err := txscript.PayToAddrScript(address)
if err != nil {
return nil, err
}
address, err = btcutil.NewAddressScriptHashFromHash(
btcutil.Hash160(witnessScript), w.cfg.ChainParams,
)
if err != nil {
return nil, err
}
case *btcutil.AddressTaproot:
// Only addresses without a tapscript are allowed because
// the verification is using the internal key.
tapKey := txscript.ComputeTaprootKeyNoScript(pk)
address, err = btcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey),
w.cfg.ChainParams,
)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported address type")
}
return &VerifyMessageWithAddrResponse{
Valid: req.Addr == address.EncodeAddress(),
Pubkey: serializedPubkey,
}, nil
}
// ImportAccount imports an account backed by an account extended public key.
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with

View File

@ -1,10 +1,13 @@
package walletrpc
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc"
)
@ -74,3 +77,22 @@ func parseDerivationPath(path string) ([]uint32, error) {
}
return indices, nil
}
// doubleHashMessage creates the double hash (sha256) of a message
// prepended with a specified prefix.
func doubleHashMessage(prefix string, msg string) ([]byte, error) {
var buf bytes.Buffer
err := wire.WriteVarString(&buf, 0, prefix)
if err != nil {
return nil, err
}
err = wire.WriteVarString(&buf, 0, msg)
if err != nil {
return nil, err
}
digest := chainhash.DoubleHashB(buf.Bytes())
return digest, nil
}