Skip to content

Commit 937db82

Browse files
committed
authmailbox: add new server and client implementation
1 parent e55593c commit 937db82

File tree

9 files changed

+2469
-0
lines changed

9 files changed

+2469
-0
lines changed

authmailbox/client.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
package authmailbox
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"errors"
7+
"fmt"
8+
"net"
9+
"sync"
10+
"sync/atomic"
11+
"time"
12+
13+
"github.com/btcsuite/btcd/btcec/v2"
14+
"github.com/btcsuite/btclog/v2"
15+
"github.com/lightninglabs/lndclient"
16+
"github.com/lightninglabs/taproot-assets/proof"
17+
mboxrpc "github.com/lightninglabs/taproot-assets/taprpc/authmailboxrpc"
18+
"github.com/lightningnetwork/lnd/keychain"
19+
"github.com/lightningnetwork/lnd/lnutils"
20+
"github.com/lightningnetwork/lnd/tor"
21+
"google.golang.org/grpc"
22+
"google.golang.org/grpc/credentials"
23+
)
24+
25+
var (
26+
// ErrServerShutdown is the error returned if the mailbox server signals
27+
// it's going to shut down.
28+
ErrServerShutdown = errors.New("server shutting down")
29+
30+
// ErrServerInternal is the error returned if the mailbox server sends
31+
// back an error instead of a proper message.
32+
ErrServerInternal = errors.New("server sent unexpected error")
33+
34+
// ErrClientShutdown is the error returned if the mailbox client itself
35+
// is shutting down.
36+
ErrClientShutdown = errors.New("client shutting down")
37+
38+
// ErrAuthCanceled is returned if the authentication process of a single
39+
// mailbox subscription is aborted.
40+
ErrAuthCanceled = errors.New("authentication was canceled")
41+
)
42+
43+
// ClientConfig holds the configuration options for the mailbox client.
44+
type ClientConfig struct {
45+
// ServerAddress is the domain:port of the mailbox server.
46+
ServerAddress string
47+
48+
// ProxyAddress is the SOCKS proxy that should be used to establish the
49+
// connection.
50+
ProxyAddress string
51+
52+
// Insecure signals that no TLS should be used if set to true.
53+
Insecure bool
54+
55+
// SkipTlsVerify signals that the TLS certificate of the mailbox server
56+
// should not be verified. This is only needed if the server uses a
57+
// self-signed certificate.
58+
SkipTlsVerify bool
59+
60+
// TLSPathServer is the path to a local file that holds the mailbox
61+
// server's TLS certificate. This is only needed if the server is using
62+
// a self-signed cert.
63+
TLSPathServer string
64+
65+
// DialOpts is a list of additional options that should be used when
66+
// dialing the gRPC connection.
67+
DialOpts []grpc.DialOption
68+
69+
// Signer is the signing interface used to sign messages during the
70+
// authentication handshake with the mailbox server.
71+
Signer lndclient.SignerClient
72+
73+
// MinBackoff is the minimum time waited before the next re-connect
74+
// attempt is made. After each try the backoff is doubled until
75+
// MaxBackoff is reached.
76+
MinBackoff time.Duration
77+
78+
// MaxBackoff is the maximum time waited between connection attempts.
79+
MaxBackoff time.Duration
80+
}
81+
82+
// Client performs the client side part of mailbox message exchange.
83+
type Client struct {
84+
cfg *ClientConfig
85+
86+
startOnce sync.Once
87+
stopped atomic.Bool
88+
stopOnce sync.Once
89+
90+
serverConn *grpc.ClientConn
91+
client mboxrpc.MailboxClient
92+
}
93+
94+
// NewClient returns a new instance to initiate mailbox connections with.
95+
func NewClient(cfg *ClientConfig) *Client {
96+
return &Client{
97+
cfg: cfg,
98+
}
99+
}
100+
101+
// Start starts the client, establishing the connection to the server.
102+
func (c *Client) Start() error {
103+
var startErr error
104+
c.startOnce.Do(func() {
105+
dialOpts, err := getServerDialOpts(
106+
c.cfg.Insecure, c.cfg.SkipTlsVerify, c.cfg.ProxyAddress,
107+
c.cfg.TLSPathServer, c.cfg.DialOpts...,
108+
)
109+
if err != nil {
110+
startErr = err
111+
return
112+
}
113+
114+
serverConn, err := grpc.NewClient(
115+
c.cfg.ServerAddress, dialOpts...,
116+
)
117+
if err != nil {
118+
startErr = fmt.Errorf("unable to connect to RPC "+
119+
"server: %w", err)
120+
return
121+
}
122+
123+
c.serverConn = serverConn
124+
c.client = mboxrpc.NewMailboxClient(serverConn)
125+
})
126+
127+
return startErr
128+
}
129+
130+
// Stop shuts down the client connection to the mailbox server.
131+
func (c *Client) Stop() error {
132+
var stopErr error
133+
c.stopOnce.Do(func() {
134+
c.stopped.Store(true)
135+
136+
log.Infof("Shutting down mailbox client")
137+
138+
stopErr = c.serverConn.Close()
139+
})
140+
141+
return stopErr
142+
}
143+
144+
// SendMessage sends a message to the mailbox server. The receiverKey is the
145+
// public key of the receiver, senderEphemeralKey is the ephemeral key used
146+
// to encrypt the message, encryptedPayload is the encrypted message payload
147+
// and txProof is the proof of the transaction that contains the message.
148+
func (c *Client) SendMessage(ctx context.Context, receiverKey btcec.PublicKey,
149+
encryptedPayload []byte, txProof proof.TxProof,
150+
expiryBlockHeight uint32) (uint64, error) {
151+
152+
if c.stopped.Load() {
153+
return 0, ErrClientShutdown
154+
}
155+
156+
rpcProof, err := proof.MarshalTxProof(txProof)
157+
if err != nil {
158+
return 0, fmt.Errorf("unable to marshal tx proof: %w", err)
159+
}
160+
161+
resp, err := c.client.SendMessage(ctx, &mboxrpc.SendMessageRequest{
162+
ReceiverId: receiverKey.SerializeCompressed(),
163+
EncryptedPayload: encryptedPayload,
164+
Proof: &mboxrpc.SendMessageRequest_TxProof{
165+
TxProof: rpcProof,
166+
},
167+
ExpiryBlockHeight: expiryBlockHeight,
168+
})
169+
if err != nil {
170+
return 0, fmt.Errorf("unable to send message: %w", err)
171+
}
172+
173+
return resp.MessageId, nil
174+
}
175+
176+
// StartAccountSubscription opens a stream to the server and subscribes to all
177+
// updates that concern the given account, including all orders that spend from
178+
// that account. Only a single stream is ever open to the server, so a second
179+
// call to this method will send a second subscription over the same stream,
180+
// multiplexing all messages into the same connection. A stream can be
181+
// long-lived, so this can be called for every account as soon as it's confirmed
182+
// open. This method will return as soon as the authentication was successful.
183+
// Messages sent from the server can then be received on the FromServerChan
184+
// channel.
185+
func (c *Client) StartAccountSubscription(ctx context.Context,
186+
msgChan chan<- *ReceivedMessages, receiverKey keychain.KeyDescriptor,
187+
filter MessageFilter) (ReceiveSubscription, error) {
188+
189+
if c.stopped.Load() {
190+
return nil, ErrClientShutdown
191+
}
192+
193+
ctxl := btclog.WithCtx(
194+
ctx, lnutils.LogPubKey("receiver_key", receiverKey.PubKey),
195+
"server", false,
196+
)
197+
198+
return c.connectAndAuthenticate(ctxl, msgChan, receiverKey, filter)
199+
}
200+
201+
// connectAndAuthenticate opens a stream to the server and authenticates the
202+
// account to receive updates.
203+
func (c *Client) connectAndAuthenticate(ctx context.Context,
204+
msgChan chan<- *ReceivedMessages, acctKey keychain.KeyDescriptor,
205+
filter MessageFilter) (*receiveSubscription, error) {
206+
207+
var receiverKey [33]byte
208+
copy(receiverKey[:], acctKey.PubKey.SerializeCompressed())
209+
210+
// Before we can expect to receive any updates, we need to perform the
211+
// 3-way authentication handshake.
212+
sub := newReceiveSubscription(c.cfg, msgChan, acctKey, filter, c.client)
213+
err := sub.connectAndAuthenticate(ctx, 0)
214+
if err != nil {
215+
log.ErrorS(ctx, "Authentication failed", err)
216+
217+
return nil, err
218+
}
219+
220+
return sub, nil
221+
}
222+
223+
// getServerDialOpts returns the dial options to connect to the mailbox server.
224+
func getServerDialOpts(insecure, skipTlsVerify bool, proxyAddress,
225+
tlsPath string, dialOpts ...grpc.DialOption) ([]grpc.DialOption,
226+
error) {
227+
228+
// Create a copy of the dial options array.
229+
opts := dialOpts
230+
231+
// There are four options to connect to a mailbox server, either
232+
// completely skipping TLS verification, using an insecure (h2c)
233+
// transport, using a self-signed certificate or with a certificate
234+
// signed by a public CA.
235+
switch {
236+
case skipTlsVerify:
237+
opts = append(opts, grpc.WithTransportCredentials(
238+
credentials.NewTLS(&tls.Config{
239+
InsecureSkipVerify: true,
240+
}),
241+
))
242+
243+
case insecure:
244+
opts = append(opts, grpc.WithInsecure())
245+
246+
case tlsPath != "":
247+
// Load the specified TLS certificate and build
248+
// transport credentials
249+
creds, err := credentials.NewClientTLSFromFile(tlsPath, "")
250+
if err != nil {
251+
return nil, err
252+
}
253+
opts = append(opts, grpc.WithTransportCredentials(creds))
254+
255+
default:
256+
creds := credentials.NewTLS(&tls.Config{})
257+
opts = append(opts, grpc.WithTransportCredentials(creds))
258+
}
259+
260+
// If a SOCKS proxy address was specified,
261+
// then we should dial through it.
262+
if proxyAddress != "" {
263+
log.Infof("Proxying connection to mailbox server over Tor "+
264+
"SOCKS proxy %v", proxyAddress)
265+
266+
torDialer := func(_ context.Context, addr string) (net.Conn,
267+
error) {
268+
269+
return tor.Dial(
270+
addr, proxyAddress, false, false,
271+
tor.DefaultConnTimeout,
272+
)
273+
}
274+
opts = append(opts, grpc.WithContextDialer(torDialer))
275+
}
276+
277+
return opts, nil
278+
}

0 commit comments

Comments
 (0)