mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-18 20:54:20 +01:00
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:
committed by
GitHub
parent
059e837794
commit
57ce08f239
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
82
server/internal/interface/grpc/macaroons.go
Normal file
82
server/internal/interface/grpc/macaroons.go
Normal 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
|
||||
}
|
||||
188
server/internal/interface/grpc/permissions/permissions.go
Normal file
188
server/internal/interface/grpc/permissions/permissions.go
Normal 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",
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
205
server/internal/interface/grpc/tls.go
Normal file
205
server/internal/interface/grpc/tls.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user