Support macaroons and TLS && Add arkd wallet cmds (#232)

* Update protos

* Update handlers

* Support macaroons and TLS

* Add arkd cli

* Minor fixes

* Update deps

* Fixes

* Update makefile

* Fixes

* Fix

* Fix

* Fix

* Remove trusted onboarding from client

* Completely remove trusted onboarding

* Fix compose files and add --no-macaroon flag to arkd cli

* Lint

* Remove e2e for trusted onboarding

* Add sleep time
This commit is contained in:
Pietralberto Mazza
2024-08-09 17:59:31 +02:00
committed by GitHub
parent 059e837794
commit 57ce08f239
105 changed files with 12111 additions and 1617 deletions

View File

@@ -1,27 +1,25 @@
package grpcservice
import (
"crypto/rand"
"crypto/tls"
"fmt"
"net"
"path/filepath"
"golang.org/x/net/http2"
)
type Config struct {
Port uint32
NoTLS bool
AuthUser string
AuthPass string
Datadir string
Port uint32
NoTLS bool
NoMacaroons bool
TLSExtraIPs []string
TLSExtraDomains []string
}
func (c Config) Validate() error {
if len(c.AuthUser) == 0 {
return fmt.Errorf("missing auth user")
}
if len(c.AuthPass) == 0 {
return fmt.Errorf("missing auth password")
}
lis, err := net.Listen("tcp", c.address())
if err != nil {
return fmt.Errorf("invalid port: %s", err)
@@ -29,7 +27,40 @@ func (c Config) Validate() error {
defer lis.Close()
if !c.NoTLS {
return fmt.Errorf("tls termination not supported yet")
tlsDir := c.tlsDatadir()
tlsKeyExists := pathExists(filepath.Join(tlsDir, tlsKeyFile))
tlsCertExists := pathExists(filepath.Join(tlsDir, tlsCertFile))
if !tlsKeyExists && tlsCertExists {
return fmt.Errorf(
"found %s file but %s is missing. Please delete %s to make the "+
"daemon recreating both files in path %s",
tlsCertFile, tlsKeyFile, tlsCertFile, tlsDir,
)
}
if len(c.TLSExtraIPs) > 0 {
for _, ip := range c.TLSExtraIPs {
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid operator extra ip %s", ip)
}
}
}
}
if !c.NoMacaroons {
macDir := c.macaroonsDatadir()
adminMacExists := pathExists(filepath.Join(macDir, adminMacaroonFile))
roMacExists := pathExists(filepath.Join(macDir, roMacaroonFile))
walletMacExists := pathExists(filepath.Join(macDir, walletMacaroonFile))
managerMacExists := pathExists(filepath.Join(macDir, managerMacaroonFile))
if adminMacExists != roMacExists ||
adminMacExists != walletMacExists ||
adminMacExists != managerMacExists {
return fmt.Errorf(
"all macaroons must be either existing or not in path %s", macDir,
)
}
}
return nil
}
@@ -46,6 +77,52 @@ func (c Config) gatewayAddress() string {
return fmt.Sprintf("localhost:%d", c.Port)
}
func (c Config) tlsConfig() *tls.Config {
return nil
func (c Config) macaroonsDatadir() string {
return filepath.Join(c.Datadir, macaroonsFolder)
}
func (c Config) tlsDatadir() string {
return filepath.Join(c.Datadir, tlsFolder)
}
func (c Config) tlsKey() string {
if c.NoTLS {
return ""
}
return filepath.Join(c.tlsDatadir(), tlsKeyFile)
}
func (c Config) tlsCert() string {
if c.NoTLS {
return ""
}
return filepath.Join(c.tlsDatadir(), tlsCertFile)
}
func (c Config) tlsConfig() (*tls.Config, error) {
if c.NoTLS {
return nil, nil
}
if c.tlsKey() == "" || c.tlsCert() == "" {
return nil, fmt.Errorf("tls_key and tls_cert both needs to be provided")
}
certificate, err := tls.LoadX509KeyPair(c.tlsCert(), c.tlsKey())
if err != nil {
return nil, err
}
config := &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1", http2.NextProtoTLS, "h2-14"}, // h2-14 is just for compatibility. will be eventually removed.
Certificates: []tls.Certificate{certificate},
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
}
config.Rand = rand.Reader
return config, nil
}

View File

@@ -40,31 +40,6 @@ func NewHandler(service application.Service) arkv1.ArkServiceServer {
return h
}
func (h *handler) TrustedOnboarding(ctx context.Context, req *arkv1.TrustedOnboardingRequest) (*arkv1.TrustedOnboardingResponse, error) {
if req.GetUserPubkey() == "" {
return nil, status.Error(codes.InvalidArgument, "missing user pubkey")
}
pubKey, err := hex.DecodeString(req.GetUserPubkey())
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
}
decodedPubKey, err := secp256k1.ParsePubKey(pubKey)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user pubkey")
}
address, err := h.svc.TrustedOnboarding(ctx, decodedPubKey)
if err != nil {
return nil, err
}
return &arkv1.TrustedOnboardingResponse{
Address: address,
}, nil
}
func (h *handler) Onboard(ctx context.Context, req *arkv1.OnboardRequest) (*arkv1.OnboardResponse, error) {
if req.GetUserPubkey() == "" {
return nil, status.Error(codes.InvalidArgument, "missing user pubkey")

View File

@@ -8,16 +8,19 @@ import (
"github.com/ark-network/ark/internal/core/ports"
)
type walletHandler struct {
type walletInitHandler struct {
walletService ports.WalletService
onUnlock func()
onInit func(password string)
onUnlock func(password string)
}
func NewWalletHandler(walletService ports.WalletService, onUnlock func()) arkv1.WalletServiceServer {
return &walletHandler{walletService, onUnlock}
func NewWalletInitializerHandler(
walletService ports.WalletService, onInit, onUnlock func(string),
) arkv1.WalletInitializerServiceServer {
return &walletInitHandler{walletService, onInit, onUnlock}
}
func (a *walletHandler) GenSeed(ctx context.Context, _ *arkv1.GenSeedRequest) (*arkv1.GenSeedResponse, error) {
func (a *walletInitHandler) GenSeed(ctx context.Context, _ *arkv1.GenSeedRequest) (*arkv1.GenSeedResponse, error) {
seed, err := a.walletService.GenSeed(ctx)
if err != nil {
return nil, err
@@ -26,7 +29,7 @@ func (a *walletHandler) GenSeed(ctx context.Context, _ *arkv1.GenSeedRequest) (*
return &arkv1.GenSeedResponse{Seed: seed}, nil
}
func (a *walletHandler) Create(ctx context.Context, req *arkv1.CreateRequest) (*arkv1.CreateResponse, error) {
func (a *walletInitHandler) Create(ctx context.Context, req *arkv1.CreateRequest) (*arkv1.CreateResponse, error) {
if len(req.GetSeed()) <= 0 {
return nil, fmt.Errorf("missing wallet seed")
}
@@ -40,10 +43,12 @@ func (a *walletHandler) Create(ctx context.Context, req *arkv1.CreateRequest) (*
return nil, err
}
go a.onInit(req.GetPassword())
return &arkv1.CreateResponse{}, nil
}
func (a *walletHandler) Restore(ctx context.Context, req *arkv1.RestoreRequest) (*arkv1.RestoreResponse, error) {
func (a *walletInitHandler) Restore(ctx context.Context, req *arkv1.RestoreRequest) (*arkv1.RestoreResponse, error) {
if len(req.GetSeed()) <= 0 {
return nil, fmt.Errorf("missing wallet seed")
}
@@ -57,10 +62,12 @@ func (a *walletHandler) Restore(ctx context.Context, req *arkv1.RestoreRequest)
return nil, err
}
go a.onInit(req.GetPassword())
return &arkv1.RestoreResponse{}, nil
}
func (a *walletHandler) Unlock(ctx context.Context, req *arkv1.UnlockRequest) (*arkv1.UnlockResponse, error) {
func (a *walletInitHandler) Unlock(ctx context.Context, req *arkv1.UnlockRequest) (*arkv1.UnlockResponse, error) {
if len(req.GetPassword()) <= 0 {
return nil, fmt.Errorf("missing wallet password")
}
@@ -68,11 +75,32 @@ func (a *walletHandler) Unlock(ctx context.Context, req *arkv1.UnlockRequest) (*
return nil, err
}
go a.onUnlock()
go a.onUnlock(req.GetPassword())
return &arkv1.UnlockResponse{}, nil
}
func (a *walletInitHandler) GetStatus(ctx context.Context, _ *arkv1.GetStatusRequest) (*arkv1.GetStatusResponse, error) {
status, err := a.walletService.Status(ctx)
if err != nil {
return nil, err
}
return &arkv1.GetStatusResponse{
Initialized: status.IsInitialized(),
Unlocked: status.IsUnlocked(),
Synced: status.IsSynced(),
}, nil
}
type walletHandler struct {
walletService ports.WalletService
}
func NewWalletHandler(walletService ports.WalletService) arkv1.WalletServiceServer {
return &walletHandler{walletService}
}
func (a *walletHandler) Lock(ctx context.Context, req *arkv1.LockRequest) (*arkv1.LockResponse, error) {
if len(req.GetPassword()) <= 0 {
return nil, fmt.Errorf("missing wallet password")
@@ -84,19 +112,6 @@ func (a *walletHandler) Lock(ctx context.Context, req *arkv1.LockRequest) (*arkv
return &arkv1.LockResponse{}, nil
}
func (a *walletHandler) GetStatus(ctx context.Context, _ *arkv1.GetStatusRequest) (*arkv1.GetStatusResponse, error) {
status, err := a.walletService.Status(ctx)
if err != nil {
return nil, err
}
return &arkv1.GetStatusResponse{
Initialized: status.IsInitialized(),
Unlocked: status.IsUnlocked(),
Synced: status.IsSynced(),
}, nil
}
func (a *walletHandler) DeriveAddress(ctx context.Context, _ *arkv1.DeriveAddressRequest) (*arkv1.DeriveAddressResponse, error) {
addr, err := a.walletService.DeriveAddresses(ctx, 1)
if err != nil {

View File

@@ -2,41 +2,70 @@ package interceptors
import (
"context"
"encoding/base64"
"fmt"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
"github.com/ark-network/ark/internal/interface/grpc/permissions"
"github.com/ark-network/tools/macaroons"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func unaryAuthenticator(user, pass string) grpc.UnaryServerInterceptor {
adminToken := fmt.Sprintf("%s:%s", user, pass)
adminTokenEncoded := base64.StdEncoding.EncodeToString([]byte(adminToken))
func unaryMacaroonAuthHandler(
macaroonSvc *macaroons.Service,
) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// whitelist the ArkService
if strings.Contains(info.FullMethod, arkv1.ArkService_ServiceDesc.ServiceName) {
return handler(ctx, req)
}
token, err := grpc_auth.AuthFromMD(ctx, "basic")
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "no basic header found: %v", err)
}
if token != adminTokenEncoded {
return nil, status.Errorf(codes.Unauthenticated, "invalid auth credentials: %v", err)
if err := checkMacaroon(ctx, info.FullMethod, macaroonSvc); err != nil {
return nil, err
}
return handler(ctx, req)
}
}
func streamMacaroonAuthHandler(
macaroonSvc *macaroons.Service,
) grpc.StreamServerInterceptor {
return func(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
if err := checkMacaroon(ss.Context(), info.FullMethod, macaroonSvc); err != nil {
return err
}
return handler(srv, ss)
}
}
func checkMacaroon(
ctx context.Context, fullMethod string, svc *macaroons.Service,
) error {
if svc == nil {
return nil
}
// Check whether the method is whitelisted, if so we'll allow it regardless
// of macaroons.
if _, ok := permissions.Whitelist()[fullMethod]; ok {
return nil
}
uriPermissions, ok := permissions.AllPermissionsByMethod()[fullMethod]
if !ok {
return fmt.Errorf("%s: unknown permissions required for method", fullMethod)
}
// Find out if there is an external validator registered for
// this method. Fall back to the internal one if there isn't.
validator, ok := svc.ExternalValidators[fullMethod]
if !ok {
validator = svc
}
// Now that we know what validator to use, let it do its work.
return validator.ValidateMacaroon(ctx, uriPermissions, fullMethod)
}

View File

@@ -1,19 +1,23 @@
package interceptors
import (
"github.com/ark-network/tools/macaroons"
middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"google.golang.org/grpc"
)
// UnaryInterceptor returns the unary interceptor
func UnaryInterceptor(user, pass string) grpc.ServerOption {
func UnaryInterceptor(svc *macaroons.Service) grpc.ServerOption {
return grpc.UnaryInterceptor(middleware.ChainUnaryServer(
unaryAuthenticator(user, pass),
unaryLogger,
unaryMacaroonAuthHandler(svc),
))
}
// StreamInterceptor returns the stream interceptor with a logrus log
func StreamInterceptor() grpc.ServerOption {
return grpc.StreamInterceptor(middleware.ChainStreamServer(streamLogger))
func StreamInterceptor(svc *macaroons.Service) grpc.ServerOption {
return grpc.StreamInterceptor(middleware.ChainStreamServer(
streamLogger,
streamMacaroonAuthHandler(svc),
))
}

View File

@@ -0,0 +1,82 @@
package grpcservice
import (
"context"
"io/fs"
"os"
"path/filepath"
"github.com/ark-network/ark/internal/interface/grpc/permissions"
"github.com/ark-network/tools/macaroons"
"gopkg.in/macaroon-bakery.v2/bakery"
)
var (
adminMacaroonFile = "admin.macaroon"
walletMacaroonFile = "wallet.macaroon"
managerMacaroonFile = "manager.macaroon"
roMacaroonFile = "readonly.macaroon"
macFiles = map[string][]bakery.Op{
adminMacaroonFile: permissions.AdminPermissions(),
walletMacaroonFile: permissions.WalletPermissions(),
managerMacaroonFile: permissions.ManagerPermissions(),
roMacaroonFile: permissions.ReadOnlyPermissions(),
}
)
// genMacaroons generates four macaroon files; one admin-level, one for
// updating the strategy of a market, one for updating its price and one
// read-only. Admin and read-only can also be used to generate more granular
// macaroons.
func genMacaroons(
ctx context.Context, svc *macaroons.Service, datadir string,
) (bool, error) {
adminMacFile := filepath.Join(datadir, adminMacaroonFile)
walletMacFile := filepath.Join(datadir, walletMacaroonFile)
managerMacFile := filepath.Join(datadir, managerMacaroonFile)
roMacFile := filepath.Join(datadir, roMacaroonFile)
if pathExists(adminMacFile) || pathExists(walletMacFile) ||
pathExists(managerMacFile) || pathExists(roMacFile) {
return false, nil
}
// Let's create the datadir if it doesn't exist.
if err := makeDirectoryIfNotExists(datadir); err != nil {
return false, err
}
for macFilename, macPermissions := range macFiles {
mktMacBytes, err := svc.BakeMacaroon(ctx, macPermissions)
if err != nil {
return false, err
}
macFile := filepath.Join(datadir, macFilename)
perms := fs.FileMode(0644)
if macFilename == adminMacaroonFile {
perms = 0600
}
if err := os.WriteFile(macFile, mktMacBytes, perms); err != nil {
os.Remove(macFile)
return false, err
}
}
return true, nil
}
func makeDirectoryIfNotExists(path string) error {
if pathExists(path) {
return nil
}
return os.MkdirAll(path, os.ModeDir|0755)
}
func pathExists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}

View File

@@ -0,0 +1,188 @@
package permissions
import (
"fmt"
"gopkg.in/macaroon-bakery.v2/bakery"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
grpchealth "google.golang.org/grpc/health/grpc_health_v1"
)
const (
EntityWallet = "wallet"
EntityAdmin = "admin"
EntityManager = "manager"
EntityArk = "ark"
EntityHealth = "health"
)
// ReadOnlyPermissions returns the permissions of the macaroon readonly.macaroon.
// This grants access to the read action for all entities.
func ReadOnlyPermissions() []bakery.Op {
return []bakery.Op{
{
Entity: EntityWallet,
Action: "read",
},
{
Entity: EntityManager,
Action: "read",
},
}
}
// WalletPermissions returns the permissions of the macaroon wallet.macaroon.
// This grants access to the all actions for the wallet entity.
func WalletPermissions() []bakery.Op {
return []bakery.Op{
{
Entity: EntityWallet,
Action: "read",
},
{
Entity: EntityWallet,
Action: "write",
},
}
}
// ManagerPermissions returns the permissions of the macaroon manager.macaroon.
// This grants access to the all actions for the manager entity.
func ManagerPermissions() []bakery.Op {
return []bakery.Op{
{
Entity: EntityManager,
Action: "read",
},
{
Entity: EntityManager,
Action: "write",
},
}
}
// AdminPermissions returns the permissions of the macaroon admin.macaroon.
// This grants access to the all actions for all entities.
func AdminPermissions() []bakery.Op {
return []bakery.Op{
{
Entity: EntityManager,
Action: "read",
},
{
Entity: EntityManager,
Action: "write",
},
{
Entity: EntityWallet,
Action: "read",
},
{
Entity: EntityWallet,
Action: "write",
},
}
}
// Whitelist returns the list of all whitelisted methods with the relative
// entity and action.
func Whitelist() map[string][]bakery.Op {
return map[string][]bakery.Op{
fmt.Sprintf("/%s/GenSeed", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "read",
}},
fmt.Sprintf("/%s/Create", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "write",
}},
fmt.Sprintf("/%s/Restore", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "write",
}},
fmt.Sprintf("/%s/Unlock", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "write",
}},
fmt.Sprintf("/%s/GetStatus", arkv1.WalletInitializerService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "read",
}},
fmt.Sprintf("/%s/RegisterPayment", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/ClaimPayment", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/FinalizePayment", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/GetRound", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/GetRoundById", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/GetEventStream", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/Ping", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/ListVtxos", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/GetInfo", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "read",
}},
fmt.Sprintf("/%s/Onboard", arkv1.ArkService_ServiceDesc.ServiceName): {{
Entity: EntityArk,
Action: "write",
}},
fmt.Sprintf("/%s/Check", grpchealth.Health_ServiceDesc.ServiceName): {{
Entity: EntityHealth,
Action: "read",
}},
}
}
// AllPermissionsByMethod returns a mapping of the RPC server calls to the
// permissions they require.
func AllPermissionsByMethod() map[string][]bakery.Op {
return map[string][]bakery.Op{
fmt.Sprintf("/%s/Lock", arkv1.WalletService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "write",
}},
fmt.Sprintf("/%s/DeriveAddress", arkv1.WalletService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "write",
}},
fmt.Sprintf("/%s/GetBalance", arkv1.WalletService_ServiceDesc.ServiceName): {{
Entity: EntityWallet,
Action: "read",
}},
fmt.Sprintf("/%s/GetScheduledSweep", arkv1.AdminService_ServiceDesc.ServiceName): {{
Entity: EntityManager,
Action: "read",
}},
fmt.Sprintf("/%s/GetRoundDetails", arkv1.AdminService_ServiceDesc.ServiceName): {{
Entity: EntityManager,
Action: "read",
}},
fmt.Sprintf("/%s/GetRounds", arkv1.AdminService_ServiceDesc.ServiceName): {{
Entity: EntityManager,
Action: "read",
}},
}
}

View File

@@ -0,0 +1,48 @@
package permissions_test
import (
"fmt"
"testing"
grpchealth "google.golang.org/grpc/health/grpc_health_v1"
"github.com/ark-network/ark/internal/interface/grpc/permissions"
"github.com/stretchr/testify/require"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
)
func TestRestrictedMethods(t *testing.T) {
allMethods := make([]string, 0)
for _, m := range arkv1.AdminService_ServiceDesc.Methods {
allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.AdminService_ServiceDesc.ServiceName, m.MethodName))
}
for _, m := range arkv1.WalletService_ServiceDesc.Methods {
allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.WalletService_ServiceDesc.ServiceName, m.MethodName))
}
allPermissions := permissions.AllPermissionsByMethod()
for _, method := range allMethods {
_, ok := allPermissions[method]
require.True(t, ok, fmt.Sprintf("missing permission for %s", method))
}
}
func TestWhitelistedMethods(t *testing.T) {
allMethods := make([]string, 0)
for _, m := range arkv1.ArkService_ServiceDesc.Methods {
allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.ArkService_ServiceDesc.ServiceName, m.MethodName))
}
for _, v := range arkv1.WalletInitializerService_ServiceDesc.Methods {
allMethods = append(allMethods, fmt.Sprintf("/%s/%s", arkv1.WalletInitializerService_ServiceDesc.ServiceName, v.MethodName))
}
allMethods = append(allMethods, fmt.Sprintf("/%s/%s", grpchealth.Health_ServiceDesc.ServiceName, "Check"))
whitelist := permissions.Whitelist()
for _, m := range allMethods {
_, ok := whitelist[m]
require.True(t, ok, fmt.Sprintf("missing %s in whitelist", m))
}
}

View File

@@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"net/http"
"path/filepath"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
@@ -13,6 +14,8 @@ import (
interfaces "github.com/ark-network/ark/internal/interface"
"github.com/ark-network/ark/internal/interface/grpc/handlers"
"github.com/ark-network/ark/internal/interface/grpc/interceptors"
"github.com/ark-network/tools/kvdb"
"github.com/ark-network/tools/macaroons"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
@@ -24,11 +27,22 @@ import (
"google.golang.org/protobuf/encoding/protojson"
)
const (
macaroonsLocation = "ark"
macaroonsDbFile = "macaroons.db"
macaroonsFolder = "macaroons"
tlsKeyFile = "key.pem"
tlsCertFile = "cert.pem"
tlsFolder = "tls"
)
type service struct {
config Config
appConfig *appconfig.Config
server *http.Server
grpcServer *grpc.Server
config Config
appConfig *appconfig.Config
server *http.Server
grpcServer *grpc.Server
macaroonSvc *macaroons.Service
}
func NewService(
@@ -41,7 +55,41 @@ func NewService(
return nil, fmt.Errorf("invalid app config: %s", err)
}
return &service{svcConfig, appConfig, nil, nil}, nil
var macaroonSvc *macaroons.Service
if !svcConfig.NoMacaroons {
macaroonDB, err := kvdb.Create(
kvdb.BoltBackendName,
filepath.Join(svcConfig.Datadir, macaroonsDbFile),
true,
kvdb.DefaultDBTimeout,
)
if err != nil {
return nil, err
}
keyStore, err := macaroons.NewRootKeyStorage(macaroonDB)
if err != nil {
return nil, err
}
svc, err := macaroons.NewService(
keyStore, macaroonsLocation, false, macaroons.IPLockChecker,
)
if err != nil {
return nil, err
}
macaroonSvc = svc
}
if !svcConfig.insecure() {
if err := generateOperatorTLSKeyCert(
svcConfig.tlsDatadir(), svcConfig.TLSExtraIPs, svcConfig.TLSExtraDomains,
); err != nil {
return nil, err
}
log.Debugf("generated TLS key pair at path: %s", svcConfig.tlsDatadir())
}
return &service{svcConfig, appConfig, nil, nil, macaroonSvc}, nil
}
func (s *service) Start() error {
@@ -55,7 +103,12 @@ func (s *service) Stop() {
}
func (s *service) start(withAppSvc bool) error {
if err := s.newServer(withAppSvc); err != nil {
tlsConfig, err := s.config.tlsConfig()
if err != nil {
return err
}
if err := s.newServer(tlsConfig, withAppSvc); err != nil {
return err
}
@@ -92,17 +145,14 @@ func (s *service) stop(withAppSvc bool) {
}
}
func (s *service) newServer(withAppSvc bool) error {
func (s *service) newServer(tlsConfig *tls.Config, withAppSvc bool) error {
grpcConfig := []grpc.ServerOption{
interceptors.UnaryInterceptor(s.config.AuthUser, s.config.AuthPass),
interceptors.StreamInterceptor(),
}
if !s.config.NoTLS {
return fmt.Errorf("tls termination not supported yet")
interceptors.UnaryInterceptor(s.macaroonSvc),
interceptors.StreamInterceptor(s.macaroonSvc),
}
creds := insecure.NewCredentials()
if !s.config.insecure() {
creds = credentials.NewTLS(s.config.tlsConfig())
creds = credentials.NewTLS(tlsConfig)
}
grpcConfig = append(grpcConfig, grpc.Creds(creds))
@@ -123,9 +173,14 @@ func (s *service) newServer(withAppSvc bool) error {
adminHandler := handlers.NewAdminHandler(s.appConfig.AdminService(), appSvc)
arkv1.RegisterAdminServiceServer(grpcServer, adminHandler)
walletHandler := handlers.NewWalletHandler(s.appConfig.WalletService(), s.onUnlock)
walletHandler := handlers.NewWalletHandler(s.appConfig.WalletService())
arkv1.RegisterWalletServiceServer(grpcServer, walletHandler)
walletInitHandler := handlers.NewWalletInitializerHandler(
s.appConfig.WalletService(), s.onInit, s.onUnlock,
)
arkv1.RegisterWalletInitializerServiceServer(grpcServer, walletInitHandler)
healthHandler := handlers.NewHealthHandler()
grpchealth.RegisterHealthServer(grpcServer, healthHandler)
@@ -143,8 +198,18 @@ func (s *service) newServer(withAppSvc bool) error {
if err != nil {
return err
}
customMatcher := func(key string) (string, bool) {
switch key {
case "X-Macaroon":
return "macaroon", true
default:
return key, false
}
}
// Reverse proxy grpc-gateway.
gwmux := runtime.NewServeMux(
runtime.WithIncomingHeaderMatcher(customMatcher),
runtime.WithHealthzEndpoint(grpchealth.NewHealthClient(conn)),
runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
@@ -167,6 +232,11 @@ func (s *service) newServer(withAppSvc bool) error {
); err != nil {
return err
}
if err := arkv1.RegisterWalletInitializerServiceHandler(
ctx, gwmux, conn,
); err != nil {
return err
}
if withAppSvc {
if err := arkv1.RegisterArkServiceHandler(
ctx, gwmux, conn,
@@ -188,13 +258,13 @@ func (s *service) newServer(withAppSvc bool) error {
s.server = &http.Server{
Addr: s.config.address(),
Handler: httpServerHandler,
TLSConfig: s.config.tlsConfig(),
TLSConfig: tlsConfig,
}
return nil
}
func (s *service) onUnlock() {
func (s *service) onUnlock(password string) {
withoutAppSvc := false
s.stop(withoutAppSvc)
@@ -202,6 +272,46 @@ func (s *service) onUnlock() {
if err := s.start(withAppSvc); err != nil {
panic(err)
}
if s.config.NoMacaroons {
return
}
pwd := []byte(password)
datadir := s.config.macaroonsDatadir()
if err := s.macaroonSvc.CreateUnlock(&pwd); err != nil {
if err != macaroons.ErrAlreadyUnlocked {
log.WithError(err).Warn("failed to unlock macaroon store")
}
}
done, err := genMacaroons(
context.Background(), s.macaroonSvc, datadir,
)
if err != nil {
log.WithError(err).Warn("failed to create macaroons")
}
if done {
log.Debugf("created and stored macaroons at path %s", datadir)
}
}
func (s *service) onInit(password string) {
if s.config.NoMacaroons {
return
}
pwd := []byte(password)
datadir := s.config.macaroonsDatadir()
if err := s.macaroonSvc.CreateUnlock(&pwd); err != nil {
log.WithError(err).Warn("failed to initialize macaroon store")
}
if _, err := genMacaroons(
context.Background(), s.macaroonSvc, datadir,
); err != nil {
log.WithError(err).Warn("failed to create macaroons")
}
log.Debugf("generated macaroons at path %s", datadir)
}
func router(

View File

@@ -0,0 +1,205 @@
package grpcservice
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"os"
"path/filepath"
"strings"
"time"
)
func generateOperatorTLSKeyCert(
datadir string, extraIPs, extraDomains []string,
) error {
if err := makeDirectoryIfNotExists(datadir); err != nil {
return err
}
keyPath := filepath.Join(datadir, tlsKeyFile)
certPath := filepath.Join(datadir, tlsCertFile)
// if key and cert files already exist nothing to do here.
if pathExists(keyPath) && pathExists(certPath) {
return nil
}
organization := "ark"
now := time.Now()
validUntil := now.AddDate(1, 0, 0)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
// Generate a serial number that's below the serialNumberLimit.
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %s", err)
}
// Collect the host's IP addresses, including loopback, in a slice.
ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
if len(extraIPs) > 0 {
for _, ip := range extraIPs {
ipAddresses = append(ipAddresses, net.ParseIP(ip))
}
}
// addIP appends an IP address only if it isn't already in the slice.
addIP := func(ipAddr net.IP) {
for _, ip := range ipAddresses {
if net.IP.Equal(ip, ipAddr) {
return
}
}
ipAddresses = append(ipAddresses, ipAddr)
}
// Add all the interface IPs that aren't already in the slice.
addrs, err := net.InterfaceAddrs()
if err != nil {
return err
}
for _, a := range addrs {
ipAddr, _, err := net.ParseCIDR(a.String())
if err == nil {
addIP(ipAddr)
}
}
host, err := os.Hostname()
if err != nil {
return err
}
dnsNames := []string{host}
if host != "localhost" {
dnsNames = append(dnsNames, "localhost")
}
if len(extraDomains) > 0 {
dnsNames = append(dnsNames, extraDomains...)
}
dnsNames = append(dnsNames, "unix", "unixpacket")
priv, err := createOrLoadTLSKey(keyPath)
if err != nil {
return err
}
keybytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return err
}
// construct certificate template
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{organization},
CommonName: host,
},
NotBefore: now.Add(-time.Hour * 24),
NotAfter: validUntil,
KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}
derBytes, err := x509.CreateCertificate(
rand.Reader, &template, &template, &priv.PublicKey, priv,
)
if err != nil {
return fmt.Errorf("failed to create certificate: %v", err)
}
certBuf := &bytes.Buffer{}
if err := pem.Encode(
certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes},
); err != nil {
return fmt.Errorf("failed to encode certificate: %v", err)
}
keyBuf := &bytes.Buffer{}
if err := pem.Encode(
keyBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes},
); err != nil {
return fmt.Errorf("failed to encode private key: %v", err)
}
if err := os.WriteFile(certPath, certBuf.Bytes(), 0644); err != nil {
return err
}
if err := os.WriteFile(keyPath, keyBuf.Bytes(), 0600); err != nil {
os.Remove(certPath)
return err
}
return nil
}
func createOrLoadTLSKey(keyPath string) (*ecdsa.PrivateKey, error) {
if !pathExists(keyPath) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
b, err := os.ReadFile(keyPath)
if err != nil {
return nil, err
}
key, err := privateKeyFromPEM(b)
if err != nil {
return nil, err
}
return key.(*ecdsa.PrivateKey), nil
}
func privateKeyFromPEM(pemBlock []byte) (crypto.PrivateKey, error) {
var derBlock *pem.Block
for {
derBlock, pemBlock = pem.Decode(pemBlock)
if derBlock == nil {
return nil, fmt.Errorf("tls: failed to find any PEM data in key input")
}
if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
break
}
}
return parsePrivateKey(derBlock.Bytes)
}
func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
return key, nil
default:
return nil, fmt.Errorf("tls: found unknown private key type in PKCS#8 wrapping")
}
}
if key, err := x509.ParseECPrivateKey(der); err == nil {
return key, nil
}
return nil, fmt.Errorf("tls: failed to parse private key")
}