diff --git a/darepod/config.go b/darepod/config.go index 2238d8537..8bbad154f 100644 --- a/darepod/config.go +++ b/darepod/config.go @@ -337,9 +337,8 @@ type SwapConfig struct { // instead of system roots or insecure local credentials. ServerTLSCertPath string `mapstructure:"servertlscertpath"` - // ServerInsecure disables TLS for the swapdk-server connection. Local - // loopback endpoints are also treated as insecure by default for - // regtest and integration-test ergonomics. + // ServerInsecure disables TLS for the swapdk-server connection. This + // should only be used for explicit regtest/dev deployments. ServerInsecure bool `mapstructure:"serverinsecure"` // DatabaseFileName is the daemon-owned swap SQLite database path. When diff --git a/darepod/rpc_server.go b/darepod/rpc_server.go index 19652e971..7e965dfd7 100644 --- a/darepod/rpc_server.go +++ b/darepod/rpc_server.go @@ -3,6 +3,7 @@ package darepod import ( "bytes" "context" + "crypto/tls" "database/sql" "encoding/hex" "errors" @@ -33,6 +34,7 @@ import ( "github.com/lightninglabs/darepo-client/lwwallet" "github.com/lightninglabs/darepo-client/oor" "github.com/lightninglabs/darepo-client/round" + "github.com/lightninglabs/darepo-client/serverconn" "github.com/lightninglabs/darepo-client/unroll" "github.com/lightninglabs/darepo-client/vtxo" "github.com/lightninglabs/darepo-client/wallet" @@ -100,6 +102,51 @@ func (r *RPCServer) SubLogger(tag string) btclog.Logger { return r.server.subLogger(tag) } +// SignMailboxAuth returns a hex-encoded Schnorr mailbox auth signature for +// the daemon identity key bound to the recipient mailbox ID. Optional +// subservers use this to authenticate mailbox RPCs without learning how the +// daemon wallet backend stores or signs with the identity key. +func (r *RPCServer) SignMailboxAuth(ctx context.Context, + recipientMailboxID string) (string, error) { + + if r == nil || r.server == nil { + return "", fmt.Errorf("daemon server unavailable") + } + if r.server.clientKeyDesc.PubKey == nil { + return "", fmt.Errorf("identity key not yet derived; wallet " + + "not ready") + } + + sig, err := r.server.signMailboxAuth(ctx, recipientMailboxID) + if err != nil { + return "", err + } + + return hex.EncodeToString(sig.Serialize()), nil +} + +// ClientTLSCerts returns the daemon identity client certificate used when +// optional subservers dial mTLS-protected mailbox edges. +func (r *RPCServer) ClientTLSCerts() ([]tls.Certificate, error) { + if r == nil || r.server == nil { + return nil, fmt.Errorf("daemon server unavailable") + } + + if r.server.clientKeyDesc.PubKey == nil { + return nil, fmt.Errorf("identity key not yet derived; wallet " + + "not ready") + } + + clientCert, err := serverconn.GenerateClientTLSCert( + r.server.clientKeyDesc.PubKey, + ) + if err != nil { + return nil, fmt.Errorf("generate client TLS cert: %w", err) + } + + return []tls.Certificate{clientCert}, nil +} + // reserveCustomInputs atomically claims every outpoint in the supplied // slice. If any outpoint is already reserved by another in-flight call, // no claims are taken and an error is returned. The returned release diff --git a/serverconn/mailbox_auth_rpc.go b/serverconn/mailbox_auth_rpc.go new file mode 100644 index 000000000..3838c1d1f --- /dev/null +++ b/serverconn/mailbox_auth_rpc.go @@ -0,0 +1,116 @@ +package serverconn + +import ( + "context" + "fmt" + + mailboxpb "github.com/lightninglabs/darepo-client/mailbox/pb" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// MailboxAuthSigner returns the hex-encoded x-mailbox-auth-sig value for a +// recipient mailbox ID. +type MailboxAuthSigner func(context.Context, string) (string, error) + +// authenticatedMailboxClient adds mailbox auth metadata before forwarding to +// the configured mailbox transport. +type authenticatedMailboxClient struct { + next mailboxpb.MailboxServiceClient + sign MailboxAuthSigner +} + +// NewAuthenticatedMailboxClient wraps a mailbox edge with per-mailbox auth +// metadata. Nil signers return next unchanged for unauthenticated test +// transports. +func NewAuthenticatedMailboxClient(next mailboxpb.MailboxServiceClient, + sign MailboxAuthSigner) mailboxpb.MailboxServiceClient { + + if sign == nil { + return next + } + + return &authenticatedMailboxClient{ + next: next, + sign: sign, + } +} + +// Send authenticates the sender for the envelope's recipient mailbox. +func (c *authenticatedMailboxClient) Send(ctx context.Context, + req *mailboxpb.SendRequest, opts ...grpc.CallOption) ( + *mailboxpb.SendResponse, error) { + + recipient := "" + if req != nil && req.Envelope != nil { + recipient = req.Envelope.Recipient + } + + ctx, err := c.authContext(ctx, recipient) + if err != nil { + return nil, err + } + + return c.next.Send(ctx, req, opts...) +} + +// Pull authenticates access to the requested mailbox. +func (c *authenticatedMailboxClient) Pull(ctx context.Context, + req *mailboxpb.PullRequest, opts ...grpc.CallOption) ( + *mailboxpb.PullResponse, error) { + + recipient := "" + if req != nil { + recipient = req.MailboxId + } + + ctx, err := c.authContext(ctx, recipient) + if err != nil { + return nil, err + } + + return c.next.Pull(ctx, req, opts...) +} + +// AckUpTo authenticates access to the requested mailbox cursor. +func (c *authenticatedMailboxClient) AckUpTo(ctx context.Context, + req *mailboxpb.AckUpToRequest, opts ...grpc.CallOption) ( + *mailboxpb.AckUpToResponse, error) { + + recipient := "" + if req != nil { + recipient = req.MailboxId + } + + ctx, err := c.authContext(ctx, recipient) + if err != nil { + return nil, err + } + + return c.next.AckUpTo(ctx, req, opts...) +} + +// authContext adds the mailbox auth metadata understood by both gRPC and the +// REST client's outgoing metadata bridge. +func (c *authenticatedMailboxClient) authContext(ctx context.Context, + recipient string) (context.Context, error) { + + if recipient == "" { + return nil, fmt.Errorf("mailbox recipient is required") + } + + sig, err := c.sign(ctx, recipient) + if err != nil { + return nil, fmt.Errorf("sign mailbox auth: %w", err) + } + + md, ok := metadata.FromOutgoingContext(ctx) + if ok { + md = md.Copy() + } else { + md = metadata.New(nil) + } + md.Set(AuthHeaderKey, sig) + + return metadata.NewOutgoingContext(ctx, md), nil +} diff --git a/serverconn/mailbox_auth_rpc_test.go b/serverconn/mailbox_auth_rpc_test.go new file mode 100644 index 000000000..76e544ccd --- /dev/null +++ b/serverconn/mailbox_auth_rpc_test.go @@ -0,0 +1,86 @@ +package serverconn + +import ( + "context" + "testing" + + mailboxpb "github.com/lightninglabs/darepo-client/mailbox/pb" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func TestAuthenticatedMailboxClientAddsMetadata(t *testing.T) { + t.Parallel() + + next := &capturingMailboxClient{} + sign := func(_ context.Context, recipient string) (string, error) { + return "auth-" + recipient, nil + } + client := NewAuthenticatedMailboxClient( + next, sign, + ) + + _, err := client.Pull(t.Context(), &mailboxpb.PullRequest{ + MailboxId: "mailbox", + }) + require.NoError(t, err) + require.Equal(t, []string{"auth-mailbox"}, next.authHeader) +} + +func TestAuthenticatedMailboxClientReplacesMetadata(t *testing.T) { + t.Parallel() + + next := &capturingMailboxClient{} + sign := func(_ context.Context, recipient string) (string, error) { + return "auth-" + recipient, nil + } + client := NewAuthenticatedMailboxClient( + next, sign, + ) + + ctx := metadata.AppendToOutgoingContext( + t.Context(), AuthHeaderKey, "caller-auth", + ) + _, err := client.Pull(ctx, &mailboxpb.PullRequest{ + MailboxId: "mailbox", + }) + require.NoError(t, err) + require.Equal(t, []string{"auth-mailbox"}, next.authHeader) +} + +type capturingMailboxClient struct { + authHeader []string +} + +func (c *capturingMailboxClient) Send(ctx context.Context, + _ *mailboxpb.SendRequest, _ ...grpc.CallOption) ( + *mailboxpb.SendResponse, error) { + + c.capture(ctx) + + return &mailboxpb.SendResponse{}, nil +} + +func (c *capturingMailboxClient) Pull(ctx context.Context, + _ *mailboxpb.PullRequest, _ ...grpc.CallOption) ( + *mailboxpb.PullResponse, error) { + + c.capture(ctx) + + return &mailboxpb.PullResponse{}, nil +} + +func (c *capturingMailboxClient) AckUpTo(ctx context.Context, + _ *mailboxpb.AckUpToRequest, _ ...grpc.CallOption) ( + *mailboxpb.AckUpToResponse, error) { + + c.capture(ctx) + + return &mailboxpb.AckUpToResponse{}, nil +} + +func (c *capturingMailboxClient) capture(ctx context.Context) { + md, _ := metadata.FromOutgoingContext(ctx) + c.authHeader = md.Get(AuthHeaderKey) +} diff --git a/swapclientserver/service.go b/swapclientserver/service.go index 8f603585a..1634d3c71 100644 --- a/swapclientserver/service.go +++ b/swapclientserver/service.go @@ -28,6 +28,7 @@ import ( "github.com/lightninglabs/darepo-client/rpc/swapclientrpc" sdkark "github.com/lightninglabs/darepo-client/sdk/ark" "github.com/lightninglabs/darepo-client/sdk/swaps" + "github.com/lightninglabs/darepo-client/serverconn" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" @@ -186,6 +187,10 @@ type swapServerClients struct { cleanup func() error } +// clientTLSCertProvider returns the daemon identity certificate at handshake +// time, after the wallet has had a chance to derive the identity key. +type clientTLSCertProvider func() ([]tls.Certificate, error) + // receiveSessionAdapter adds method accessors around sdk/swaps.ReceiveSession's // public start-response fields so both production code and tests share the // same receiveSwapSession interface. @@ -266,6 +271,8 @@ func RegisterGateway(ctx context.Context, mux *runtime.ServeMux, // its own receive-auth key material. It also returns a cleanup function that // must be called during daemon shutdown so the root worker context is canceled // before the Ark, swapdk-server, and store resources are closed. +// +//nolint:contextcheck func newSwapClientService(ctx context.Context, rpcServer *darepod.RPCServer, daemonCfg *darepod.Config) (*swapClientService, func(), error) { @@ -299,7 +306,14 @@ func newSwapClientService(ctx context.Context, rpcServer *darepod.RPCServer, swapAddr = "localhost:10030" } - swapClients, err := newSwapServerClients(cfg, swapAddr) + var clientCerts clientTLSCertProvider + if !cfg.ServerInsecure { + clientCerts = rpcServer.ClientTLSCerts + } + + swapClients, err := newSwapServerClients( + cfg, swapAddr, rpcServer.SignMailboxAuth, clientCerts, + ) if err != nil { _ = store.Close() @@ -383,12 +397,15 @@ func newSwapClientService(ctx context.Context, rpcServer *darepod.RPCServer, // newSwapServerClients builds the swapdk-server clients for the configured // daemon-owned outbound transport. -func newSwapServerClients(cfg *darepod.SwapConfig, - swapAddr string) (*swapServerClients, error) { +func newSwapServerClients(cfg *darepod.SwapConfig, swapAddr string, + sign serverconn.MailboxAuthSigner, + clientCerts clientTLSCertProvider) (*swapServerClients, error) { switch cfg.ServerTransport { case "", darepod.RPCTransportGRPC: - dialOpts, err := swapServerDialOptions(cfg, swapAddr) + dialOpts, err := swapServerDialOptions( + cfg, swapAddr, clientCerts, + ) if err != nil { return nil, err } @@ -400,13 +417,16 @@ func newSwapServerClients(cfg *darepod.SwapConfig, } return &swapServerClients{ - server: swaps.NewGRPCSwapServerConn(swapConn), - mailbox: mailboxpb.NewMailboxServiceClient(swapConn), + server: swaps.NewGRPCSwapServerConn(swapConn), + mailbox: serverconn.NewAuthenticatedMailboxClient( + mailboxpb.NewMailboxServiceClient(swapConn), + sign, + ), cleanup: swapConn.Close, }, nil case darepod.RPCTransportREST: - opts, err := swapServerRESTOptions(cfg) + opts, err := swapServerRESTOptions(cfg, clientCerts) if err != nil { return nil, err } @@ -416,8 +436,11 @@ func newSwapServerClients(cfg *darepod.SwapConfig, return &swapServerClients{ server: swaps.NewRESTSwapServerConn(baseURL, opts...), - mailbox: restclient.NewMailboxServiceClientFromClient( - transport, + mailbox: serverconn.NewAuthenticatedMailboxClient( + restclient.NewMailboxServiceClientFromClient( + transport, + ), + sign, ), cleanup: func() error { return nil }, }, nil @@ -581,27 +604,26 @@ func (d *daemonAuthOnlyInvoiceCreator) CreateInvoiceWithKey(ctx context.Context, } // swapServerDialOptions maps daemon swap config into gRPC transport options -// for swapdk-server. Loopback and unix-socket endpoints default to insecure -// transport for regtest ergonomics; non-local endpoints use TLS unless the -// caller explicitly provides ServerInsecure. -func swapServerDialOptions(cfg *darepod.SwapConfig, - addr string) ([]grpc.DialOption, error) { +// for swapdk-server. +func swapServerDialOptions(cfg *darepod.SwapConfig, addr string, + clientCerts clientTLSCertProvider) ([]grpc.DialOption, error) { switch { case cfg.ServerTLSCertPath != "": - creds, err := credentials.NewClientTLSFromFile( - cfg.ServerTLSCertPath, "", + tlsCfg, err := swapServerTLSConfig( + cfg.ServerTLSCertPath, clientCerts, ) if err != nil { - return nil, fmt.Errorf("load swap server TLS "+ - "certificate: %w", err) + return nil, err } return []grpc.DialOption{ - grpc.WithTransportCredentials(creds), + grpc.WithTransportCredentials( + credentials.NewTLS(tlsCfg), + ), }, nil - case isLocalSwapServerAddr(addr) || cfg.ServerInsecure: + case useInsecureSwapServerTransport(cfg, addr): return []grpc.DialOption{ grpc.WithTransportCredentials( insecure.NewCredentials(), @@ -609,13 +631,16 @@ func swapServerDialOptions(cfg *darepod.SwapConfig, }, nil default: + tlsCfg := &tls.Config{ + GetClientCertificate: swapClientCertificate( + clientCerts, + ), + MinVersion: tls.VersionTLS12, + } + return []grpc.DialOption{ grpc.WithTransportCredentials( - credentials.NewTLS( - &tls.Config{ - MinVersion: tls.VersionTLS12, - }, - ), + credentials.NewTLS(tlsCfg), ), }, nil } @@ -623,25 +648,21 @@ func swapServerDialOptions(cfg *darepod.SwapConfig, // swapServerRESTOptions maps the swapdk-server TLS config into the shared REST // transport. -func swapServerRESTOptions(cfg *darepod.SwapConfig) ([]restclient.Option, - error) { +func swapServerRESTOptions(cfg *darepod.SwapConfig, + clientCerts clientTLSCertProvider) ([]restclient.Option, error) { tlsCfg := &tls.Config{ - MinVersion: tls.VersionTLS12, + GetClientCertificate: swapClientCertificate(clientCerts), + MinVersion: tls.VersionTLS12, } if cfg.ServerTLSCertPath != "" { - certBytes, err := os.ReadFile(cfg.ServerTLSCertPath) + var err error + tlsCfg, err = swapServerTLSConfig( + cfg.ServerTLSCertPath, clientCerts, + ) if err != nil { - return nil, fmt.Errorf("load swap server TLS "+ - "certificate: %w", err) - } - - pool := x509.NewCertPool() - if !pool.AppendCertsFromPEM(certBytes) { - return nil, fmt.Errorf("unable to parse swap server "+ - "TLS certificate at %s", cfg.ServerTLSCertPath) + return nil, err } - tlsCfg.RootCAs = pool } httpTransport := cloneDefaultHTTPTransport() @@ -654,6 +675,55 @@ func swapServerRESTOptions(cfg *darepod.SwapConfig) ([]restclient.Option, }, nil } +// swapServerTLSConfig builds a client TLS config pinned to the configured +// swapd certificate and carrying the daemon identity client certificate. +func swapServerTLSConfig(certPath string, + clientCerts clientTLSCertProvider) (*tls.Config, error) { + + certBytes, err := os.ReadFile(certPath) // #nosec G304 + if err != nil { + return nil, fmt.Errorf("load swap server TLS certificate: %w", + err) + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(certBytes) { + return nil, fmt.Errorf("unable to parse swap server TLS "+ + "certificate at %s", certPath) + } + + return &tls.Config{ + RootCAs: pool, + GetClientCertificate: swapClientCertificate(clientCerts), + MinVersion: tls.VersionTLS12, + }, nil +} + +// swapClientCertificate adapts the daemon identity certificate provider into a +// TLS handshake callback. The callback is intentionally lazy because the swap +// subserver can be registered before the daemon wallet derives its identity +// key; gRPC/HTTP retries will ask again once the wallet is ready. +func swapClientCertificate( + clientCerts clientTLSCertProvider, +) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + + if clientCerts == nil { + return nil + } + + return func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { + certs, err := clientCerts() + if err != nil { + return nil, err + } + if len(certs) == 0 { + return &tls.Certificate{}, nil + } + + return &certs[0], nil + } +} + // cloneDefaultHTTPTransport returns a mutable copy of the default HTTP // transport without relying on a forced package-global type assertion. func cloneDefaultHTTPTransport() *http.Transport { @@ -678,40 +748,24 @@ func swapServerRESTBaseURL(cfg *darepod.SwapConfig, addr string) string { if cfg.ServerTLSCertPath != "" { return "https://" + addr } - if cfg.ServerInsecure || isLocalSwapServerAddr(addr) { + if useInsecureSwapServerTransport(cfg, addr) { return "http://" + addr } return "https://" + addr } -// chainParamsForNetwork converts the daemon's configured network string into -// the btcd chain parameters required by the invoice generator. -func chainParamsForNetwork(network string) (*chaincfg.Params, error) { - switch network { - case "mainnet", "bitcoin": - return &chaincfg.MainNetParams, nil - - case "testnet", "testnet3": - return &chaincfg.TestNet3Params, nil - - case "regtest": - return &chaincfg.RegressionNetParams, nil - - case "simnet": - return &chaincfg.SimNetParams, nil - - case "signet": - return &chaincfg.SigNetParams, nil - - default: - return nil, fmt.Errorf("unknown network %q", network) - } +// useInsecureSwapServerTransport reports whether the swapserver connection +// should use plaintext transport. Explicit TLS certificate pinning always wins; +// otherwise local loopback endpoints keep the historical regtest default. +func useInsecureSwapServerTransport(cfg *darepod.SwapConfig, addr string) bool { + return cfg.ServerTLSCertPath == "" && + (cfg.ServerInsecure || isLocalSwapServerAddr(addr)) } // isLocalSwapServerAddr reports whether a configured swapdk-server address is // scoped to the local machine. Local endpoints are treated as development -// endpoints and may be dialed with insecure gRPC credentials by default. +// endpoints and may be dialed with plaintext credentials by default. func isLocalSwapServerAddr(addr string) bool { if strings.HasPrefix(addr, "unix:") { return true @@ -733,6 +787,30 @@ func isLocalSwapServerAddr(addr string) bool { return ip != nil && ip.IsLoopback() } +// chainParamsForNetwork converts the daemon's configured network string into +// the btcd chain parameters required by the invoice generator. +func chainParamsForNetwork(network string) (*chaincfg.Params, error) { + switch network { + case "mainnet", "bitcoin": + return &chaincfg.MainNetParams, nil + + case "testnet", "testnet3": + return &chaincfg.TestNet3Params, nil + + case "regtest": + return &chaincfg.RegressionNetParams, nil + + case "simnet": + return &chaincfg.SimNetParams, nil + + case "signet": + return &chaincfg.SigNetParams, nil + + default: + return nil, fmt.Errorf("unknown network %q", network) + } +} + // StartPay persists a pay swap through sdk/swaps, starts or reuses the daemon // background worker for the resulting payment hash, and returns the initial // durable summary to the RPC caller. diff --git a/swapclientserver/service_test.go b/swapclientserver/service_test.go index 177a02aa4..d1ef914d5 100644 --- a/swapclientserver/service_test.go +++ b/swapclientserver/service_test.go @@ -19,6 +19,7 @@ import ( mailboxpb "github.com/lightninglabs/darepo-client/mailbox/pb" "github.com/lightninglabs/darepo-client/rpc/swapclientrpc" "github.com/lightninglabs/darepo-client/sdk/swaps" + "github.com/lightninglabs/darepo-client/serverconn" "github.com/lightninglabs/darepo-client/swaprpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" @@ -280,6 +281,9 @@ func TestNewSwapServerClientsREST(t *testing.T) { FeeProportionalPpm: 2, CltvExpiryDelta: 40, } + channelIDResp := &swaprpc.RequestChannelIdResponse{ + RouteHint: routeHint, + } server := httptest.NewServer( http.HandlerFunc( @@ -295,16 +299,30 @@ func TestNewSwapServerClientsREST(t *testing.T) { switch r.URL.Path { case "/v1/swap/request-channel-id": msg, marshalErr = protojson.Marshal( - &swaprpc.RequestChannelIdResponse{ - RouteHint: routeHint, - }, + channelIDResp, ) case "/v1/mailbox/pull": + requireMailboxAuth(t, r) + msg, marshalErr = protojson.Marshal( &mailboxpb.PullResponse{}, ) + case "/v1/mailbox/send": + requireMailboxAuth(t, r) + + msg, marshalErr = protojson.Marshal( + &mailboxpb.SendResponse{}, + ) + + case "/v1/mailbox/ack-up-to": + requireMailboxAuth(t, r) + + msg, marshalErr = protojson.Marshal( + &mailboxpb.AckUpToResponse{}, + ) + default: http.NotFound(w, r) @@ -322,7 +340,11 @@ func TestNewSwapServerClientsREST(t *testing.T) { clients, err := newSwapServerClients(&darepod.SwapConfig{ ServerTransport: darepod.RPCTransportREST, ServerInsecure: true, - }, server.URL) + }, server.URL, func(_ context.Context, recipient string) (string, + error) { + + return "auth-" + recipient, nil + }, nil) require.NoError(t, err) require.NotNil(t, clients.server) require.NotNil(t, clients.mailbox) @@ -339,7 +361,26 @@ func TestNewSwapServerClientsREST(t *testing.T) { require.Equal(t, nodeID, hint.NodeID) _, err = clients.mailbox.Pull( - t.Context(), &mailboxpb.PullRequest{}, + t.Context(), &mailboxpb.PullRequest{ + MailboxId: "mailbox", + }, + ) + require.NoError(t, err) + + _, err = clients.mailbox.Send( + t.Context(), &mailboxpb.SendRequest{ + Envelope: &mailboxpb.Envelope{ + Recipient: "mailbox", + }, + }, + ) + require.NoError(t, err) + + _, err = clients.mailbox.AckUpTo( + t.Context(), &mailboxpb.AckUpToRequest{ + MailboxId: "mailbox", + Cursor: 1, + }, ) require.NoError(t, err) } @@ -349,10 +390,68 @@ func TestNewSwapServerClientsUnknownTransport(t *testing.T) { _, err := newSwapServerClients(&darepod.SwapConfig{ ServerTransport: "webdav", - }, "localhost:10030") + }, "localhost:10030", nil, nil) require.ErrorContains(t, err, "unknown swap server transport") } +func TestDefaultLocalSwapServerUsesInsecureTransport(t *testing.T) { + t.Parallel() + + cfg := &darepod.SwapConfig{} + + require.True( + t, useInsecureSwapServerTransport( + cfg, "localhost:10030", + ), + ) + require.Equal( + t, "http://localhost:10030", + swapServerRESTBaseURL(cfg, "localhost:10030"), + ) +} + +func TestRemoteSwapServerUsesTLSByDefault(t *testing.T) { + t.Parallel() + + cfg := &darepod.SwapConfig{} + + require.False( + t, useInsecureSwapServerTransport( + cfg, "swap.example.com:10030", + ), + ) + require.Equal( + t, "https://swap.example.com:10030", + swapServerRESTBaseURL(cfg, "swap.example.com:10030"), + ) +} + +func TestSwapServerTLSCertPathOverridesLocalFallback(t *testing.T) { + t.Parallel() + + cfg := &darepod.SwapConfig{ + ServerTLSCertPath: "/tmp/swapd.pem", + } + + require.False( + t, useInsecureSwapServerTransport( + cfg, "localhost:10030", + ), + ) + require.Equal( + t, "https://localhost:10030", + swapServerRESTBaseURL(cfg, "localhost:10030"), + ) +} + +func requireMailboxAuth(t *testing.T, r *http.Request) { + t.Helper() + + require.Equal( + t, "auth-mailbox", r.Header.Get(serverconn.AuthHeaderKey), + ) +} + func newTestSwapClientService(client swapRuntimeClient) *swapClientService { ctx, cancel := context.WithCancel(context.Background())