package main import ( "context" "crypto/rand" "encoding/json" "fmt" "os" "time" sdk "github.com/nspcc-dev/cdn-sdk" "github.com/nspcc-dev/cdn-sdk/creds/hcs" "github.com/nspcc-dev/cdn-sdk/creds/neofs" "github.com/nspcc-dev/cdn-sdk/grace" "github.com/nspcc-dev/cdn-sdk/pool" "github.com/nspcc-dev/neofs-api-go/pkg/container" "github.com/nspcc-dev/neofs-s3-gw/authmate" "github.com/nspcc-dev/neofs-s3-gw/internal/version" "github.com/urfave/cli/v2" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) type gateKey struct { PrivateKey string `json:"private_key"` PublicKey string `json:"public_key"` } const ( poolConnectTimeout = 5 * time.Second poolRequestTimeout = 5 * time.Second ) var ( neoFSKeyPathFlag string peerAddressFlag string eaclRulesFlag string gatePrivateKeyFlag string secretAddressFlag string ownerPrivateKeyFlag string containerIDFlag string containerFriendlyName string gatesPublicKeysFlag cli.StringSlice gatesKeysCountFlag int logEnabledFlag bool logDebugEnabledFlag bool ) var zapConfig = zap.Config{ Development: true, Encoding: "console", Level: zap.NewAtomicLevelAt(zapcore.FatalLevel), OutputPaths: []string{"stdout"}, EncoderConfig: zapcore.EncoderConfig{ MessageKey: "message", LevelKey: "level", EncodeLevel: zapcore.CapitalLevelEncoder, TimeKey: "time", EncodeTime: zapcore.ISO8601TimeEncoder, CallerKey: "caller", EncodeCaller: zapcore.ShortCallerEncoder, }, } func prepare() (context.Context, *zap.Logger) { var ( err error log = zap.NewNop() ) if !logEnabledFlag { return grace.Context(log), log } else if logDebugEnabledFlag { zapConfig.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) } if log, err = zapConfig.Build(); err != nil { panic(err) } return grace.Context(log), log } func main() { app := &cli.App{ Name: "NeoFS gate authentication manager", Usage: "Helps manage delegated access via gates to data stored in NeoFS network", Version: version.Version, Flags: appFlags(), Commands: appCommands(), } if err := app.Run(os.Args); err != nil { _, _ = fmt.Fprintf(os.Stderr, "error: %s\n", err) os.Exit(100) } } func appFlags() []cli.Flag { return []cli.Flag{ &cli.BoolFlag{ Name: "with-log", Usage: "Enable logger", Destination: &logEnabledFlag, }, &cli.BoolFlag{ Name: "debug", Usage: "Enable debug logger level", Destination: &logDebugEnabledFlag, }, } } func appCommands() []*cli.Command { return []*cli.Command{ issueSecret(), obtainSecret(), generateKeys(), } } func generateGatesKeys(count int) ([]hcs.Credentials, error) { var ( err error res = make([]hcs.Credentials, count) ) for i := 0; i < count; i++ { if res[i], err = hcs.Generate(rand.Reader); err != nil { return nil, err } } return res, nil } func generateKeys() *cli.Command { return &cli.Command{ Name: "generate-keys", Usage: "Generate key pairs for gates", Flags: []cli.Flag{ &cli.IntFlag{ Name: "count", Usage: "number of x25519 key pairs to generate", Value: 1, Destination: &gatesKeysCountFlag, }, }, Action: func(c *cli.Context) error { _, log := prepare() log.Info("start generating x25519 keys") csl, err := generateGatesKeys(gatesKeysCountFlag) if err != nil { return cli.Exit(fmt.Sprintf("failed to create key pairs of gates: %s", err), 1) } log.Info("generated x25519 keys") gatesKeys := make([]gateKey, len(csl)) for i, cs := range csl { privateKey, publicKey := cs.PrivateKey().String(), cs.PublicKey().String() gatesKeys[i] = gateKey{PrivateKey: privateKey, PublicKey: publicKey} } keys, err := json.MarshalIndent(gatesKeys, "", " ") if err != nil { return cli.Exit(fmt.Sprintf("failed to marshal key pairs of gates: %s", err), 2) } fmt.Println(string(keys)) return nil }, } } func issueSecret() *cli.Command { return &cli.Command{ Name: "issue-secret", Usage: "Issue a secret in NeoFS network", Flags: []cli.Flag{ &cli.StringFlag{ Name: "neofs-key", Value: "", Usage: "path to owner's neofs private ecdsa key", Required: true, Destination: &neoFSKeyPathFlag, }, &cli.StringFlag{ Name: "peer", Value: "", Usage: "address of a neofs peer to connect to", Required: true, Destination: &peerAddressFlag, }, &cli.StringFlag{ Name: "rules", Usage: "eacl rules as plain json string", Required: false, Destination: &eaclRulesFlag, }, &cli.StringSliceFlag{ Name: "gate-public-key", Usage: "public x25519 key of a gate (use flags repeatedly for multiple gates)", Required: true, Destination: &gatesPublicKeysFlag, }, &cli.StringFlag{ Name: "owner-private-key", Usage: "owner's private x25519 key", Required: false, Destination: &ownerPrivateKeyFlag, }, &cli.StringFlag{ Name: "container-id", Usage: "auth container id to put the secret into", Required: false, Destination: &containerIDFlag, }, &cli.StringFlag{ Name: "container-friendly-name", Usage: "friendly name of auth container to put the secret into", Required: false, Destination: &containerFriendlyName, Value: "auth-container", }, }, Action: func(c *cli.Context) error { ctx, log := prepare() neofsCreds, err := neofs.New(neoFSKeyPathFlag) if err != nil { return cli.Exit(fmt.Sprintf("failed to load neofs private key: %s", err), 1) } ctx, cancel := context.WithCancel(ctx) defer cancel() client, err := createSDKClient(ctx, log, neofsCreds, peerAddressFlag) if err != nil { return cli.Exit(fmt.Sprintf("failed to create sdk client: %s", err), 2) } agent := authmate.New(log, client) var cid *container.ID if len(containerIDFlag) > 0 { cid = container.NewID() if err := cid.Parse(containerIDFlag); err != nil { return cli.Exit(fmt.Sprintf("failed to parse auth container id: %s", err), 3) } } var owner hcs.Credentials if owner, err = fetchHCSCredentials(ownerPrivateKeyFlag); err != nil { return cli.Exit(fmt.Sprintf("failed to create owner's private key: %s", err), 4) } var gatesPublicKeys []hcs.PublicKey for _, key := range gatesPublicKeysFlag.Value() { gpk, err := hcs.LoadPublicKey(key) if err != nil { return cli.Exit(fmt.Sprintf("failed to load gate's public key: %s", err), 5) } gatesPublicKeys = append(gatesPublicKeys, gpk) } issueSecretOptions := &authmate.IssueSecretOptions{ ContainerID: cid, ContainerFriendlyName: containerFriendlyName, NEOFSCreds: neofsCreds, OwnerPrivateKey: owner.PrivateKey(), GatesPublicKeys: gatesPublicKeys, EACLRules: []byte(eaclRulesFlag), } if err = agent.IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil { return cli.Exit(fmt.Sprintf("failed to issue secret: %s", err), 6) } return nil }, } } func obtainSecret() *cli.Command { command := &cli.Command{ Name: "obtain-secret", Usage: "Obtain a secret from NeoFS network", Flags: []cli.Flag{ &cli.StringFlag{ Name: "neofs-key", Value: "", Usage: "path to owner's neofs private ecdsa key", Required: true, Destination: &neoFSKeyPathFlag, }, &cli.StringFlag{ Name: "peer", Value: "", Usage: "address of neofs peer to connect to", Required: true, Destination: &peerAddressFlag, }, &cli.StringFlag{ Name: "gate-private-key", Usage: "gate's private x25519 key", Required: true, Destination: &gatePrivateKeyFlag, }, &cli.StringFlag{ Name: "secret-address", Usage: "address of a secret (i.e. access key id for s3)", Required: true, Destination: &secretAddressFlag, }, }, Action: func(c *cli.Context) error { ctx, log := prepare() neofsCreds, err := neofs.New(neoFSKeyPathFlag) if err != nil { return cli.Exit(fmt.Sprintf("failed to load neofs private key: %s", err), 1) } ctx, cancel := context.WithCancel(ctx) defer cancel() client, err := createSDKClient(ctx, log, neofsCreds, peerAddressFlag) if err != nil { return cli.Exit(fmt.Sprintf("failed to create sdk client: %s", err), 2) } agent := authmate.New(log, client) var _ = agent gateCreds, err := hcs.NewCredentials(gatePrivateKeyFlag) if err != nil { return cli.Exit(fmt.Sprintf("failed to create owner's private key: %s", err), 4) } obtainSecretOptions := &authmate.ObtainSecretOptions{ SecretAddress: secretAddressFlag, GatePrivateKey: gateCreds.PrivateKey(), } if err = agent.ObtainSecret(ctx, os.Stdout, obtainSecretOptions); err != nil { return cli.Exit(fmt.Sprintf("failed to obtain secret: %s", err), 5) } return nil }, } return command } func fetchHCSCredentials(val string) (hcs.Credentials, error) { if val == "" { return hcs.Generate(rand.Reader) } return hcs.NewCredentials(val) } func createSDKClient(ctx context.Context, log *zap.Logger, neofsCreds neofs.Credentials, peerAddress string) (sdk.Client, error) { log.Debug("prepare connection pool") p, err := pool.New(ctx, pool.WithLogger(log), pool.WithAddress(peerAddress), pool.WithCredentials(neofsCreds), pool.WithAPIPreparer(sdk.APIPreparer), pool.WithConnectTimeout(poolConnectTimeout), pool.WithRequestTimeout(poolRequestTimeout)) if err != nil { return nil, fmt.Errorf("failed to create connection pool: %w", err) } log.Debug("prepare sdk client") return sdk.New(ctx, sdk.WithLogger(log), sdk.WithCredentials(neofsCreds), sdk.WithConnectionPool(p), sdk.WithAPIPreparer(sdk.APIPreparer)) }