diff --git a/cmd/lotus-wallet/filtered.go b/cmd/lotus-wallet/filtered.go new file mode 100644 index 00000000000..d5c5747aadd --- /dev/null +++ b/cmd/lotus-wallet/filtered.go @@ -0,0 +1,136 @@ +package main + +import ( + "bytes" + "context" + "github.com/ipfs/go-cid" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/types" +) + +type FilteredWallet struct { + under api.Wallet + mustAccept bool +} + +func (c *FilteredWallet) WalletNew(ctx context.Context, typ types.KeyType) (address.Address, error) { + log.Infow("WalletNew", "type", typ) + // TODO FILTER! + + return c.under.WalletNew(ctx, typ) +} + +func (c *FilteredWallet) WalletHas(ctx context.Context, addr address.Address) (bool, error) { + log.Infow("WalletHas", "address", addr) + // TODO FILTER! + + return c.under.WalletHas(ctx, addr) +} + +func (c *FilteredWallet) WalletList(ctx context.Context) ([]address.Address, error) { + // TODO FILTER! + + return c.under.WalletList(ctx) +} + +func (c *FilteredWallet) WalletSign(ctx context.Context, k address.Address, msg []byte, meta api.MsgMeta) (*crypto.Signature, error) { + p, ok := ctx.Value(tokenKey).(jwtPayload) + if !ok { + return nil, xerrors.Errorf("jwt payload not set on request context") + } + if c.mustAccept && p.Rules == nil { + return nil, xerrors.Errorf("token with no rules") + } + + var filterParams map[FilterParam]interface{} + + switch meta.Type { + case api.MTChainMsg: + var cmsg types.Message + if err := cmsg.UnmarshalCBOR(bytes.NewReader(meta.Extra)); err != nil { + return nil, xerrors.Errorf("unmarshalling message: %w", err) + } + + _, bc, err := cid.CidFromBytes(msg) + if err != nil { + return nil, xerrors.Errorf("getting cid from signing bytes: %w", err) + } + + if !cmsg.Cid().Equals(bc) { + return nil, xerrors.Errorf("cid(meta.Extra).bytes() != msg") + } + + filterParams = map[FilterParam]interface{}{ + ParamAction: ActionSign, + ParamSignType: api.MTChainMsg, + + ParamSource: cmsg.From, + ParamDestination: cmsg.To, + ParamValue: cmsg.Value, + ParamMethod: cmsg.Method, + ParamMaxFee: cmsg.RequiredFunds(), + } + case api.MTBlock: + // TODO FILTER PARAMS! + filterParams = map[FilterParam]interface{}{ + ParamAction: ActionSign, + ParamSignType: api.MTBlock, + } + case api.MTDealProposal: + // TODO FILTER PARAMS! + filterParams = map[FilterParam]interface{}{ + ParamAction: ActionSign, + ParamSignType: api.MTDealProposal, + } + default: + // TODO FILTER PARAMS! + filterParams = map[FilterParam]interface{}{ + ParamAction: ActionSign, + ParamSignType: api.MTUnknown, + } + } + + if p.Rules != nil { + f, err := ParseRule(ctx, *p.Rules) + if err != nil { + return nil, xerrors.Errorf("parsing rules: %w", err) + } + + fres := f(filterParams) + switch fres { + case nil: + if c.mustAccept { + return nil, xerrors.Errorf("filter didn't accept") + } + fallthrough + case ErrAccept: + + default: + return nil, xerrors.Errorf("filter error: %w", fres) + } + } + + return c.under.WalletSign(ctx, k, msg, meta) +} + +func (c *FilteredWallet) WalletExport(ctx context.Context, a address.Address) (*types.KeyInfo, error) { + log.Infow("WalletExport", "address", a) + // TODO FILTER! + return c.under.WalletExport(ctx, a) +} + +func (c *FilteredWallet) WalletImport(ctx context.Context, ki *types.KeyInfo) (address.Address, error) { + log.Infow("WalletImport", "type", ki.Type) + // TODO FILTER! + return c.under.WalletImport(ctx, ki) +} + +func (c *FilteredWallet) WalletDelete(ctx context.Context, addr address.Address) error { + log.Infow("WalletDelete", "address", addr) + // TODO FILTER! + return c.under.WalletDelete(ctx, addr) +} diff --git a/cmd/lotus-wallet/main.go b/cmd/lotus-wallet/main.go index e9fdd55b3d1..212124fdd17 100644 --- a/cmd/lotus-wallet/main.go +++ b/cmd/lotus-wallet/main.go @@ -2,10 +2,12 @@ package main import ( "context" + "encoding/json" "fmt" "net" "net/http" "os" + "strings" "time" "github.com/filecoin-project/lotus/api/v0api" @@ -99,6 +101,12 @@ To configure your lotus node to use a remote wallet: var getApiKeyCmd = &cli.Command{ Name: "get-api-key", Usage: "Generate API Key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "rules", + Usage: "filtering rules", + }, + }, Action: func(cctx *cli.Context) error { lr, ks, err := openRepo(cctx) if err != nil { @@ -106,9 +114,25 @@ var getApiKeyCmd = &cli.Command{ } defer lr.Close() // nolint + var rules Rule + if cctx.IsSet("rules") { + var r interface{} + if err := json.Unmarshal([]byte(cctx.String("rules")), &r); err != nil { + return xerrors.Errorf("unmarshalling rules: %w", err) + } + + _, err := ParseRule(cctx.Context, r) + if err != nil { + return xerrors.Errorf("parsing rules: %w", err) + } + + rules = &r + } + p := jwtPayload{ Allow: []auth.Permission{api.PermAdmin}, Created: time.Now(), + Rules: &rules, } authKey, err := modules.APISecret(ks, lr) @@ -126,6 +150,10 @@ var getApiKeyCmd = &cli.Command{ }, } +type jwtTok struct{} + +var tokenKey jwtTok + var runCmd = &cli.Command{ Name: "run", Usage: "Start lotus wallet", @@ -152,6 +180,10 @@ var runCmd = &cli.Command{ Usage: "(insecure) disable api auth", Hidden: true, }, + &cli.BoolFlag{ + Name: "rule-must-accept", + Usage: "require all operations to be accepted by rule filters", + }, }, Description: "For setup instructions see 'lotus-wallet --help'", Action: func(cctx *cli.Context) error { @@ -192,6 +224,11 @@ var runCmd = &cli.Command{ } } + w = &FilteredWallet{ + under: w, + mustAccept: cctx.Bool("rule-must-accept"), + } + address := cctx.String("listen") mux := mux.NewRouter() @@ -234,19 +271,47 @@ var runCmd = &cli.Command{ } authVerify := func(ctx context.Context, token string) ([]auth.Permission, error) { - var payload jwtPayload - if _, err := jwt.Verify([]byte(token), (*jwt.HMACSHA)(authKey), &payload); err != nil { - return nil, xerrors.Errorf("JWT Verification failed: %w", err) + payload, ok := ctx.Value(tokenKey).(jwtPayload) + if !ok { + return nil, xerrors.Errorf("jwt payload not set on request context") } return payload.Allow, nil } log.Info("API auth enabled, use 'lotus-wallet get-api-key' to get API key") - handler = &auth.Handler{ + ah := &auth.Handler{ Verify: authVerify, Next: mux.ServeHTTP, } + + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + token = r.FormValue("token") + if token != "" { + token = "Bearer " + token + } + } + + if token != "" { + if !strings.HasPrefix(token, "Bearer ") { + log.Warn("missing Bearer prefix in auth header") + w.WriteHeader(401) + return + } + token = strings.TrimPrefix(token, "Bearer ") + + var payload jwtPayload + if _, err := jwt.Verify([]byte(token), (*jwt.HMACSHA)(authKey), &payload); err != nil { + http.Error(w, fmt.Sprintf("JWT Verification failed: %s", err), http.StatusForbidden) + } + + r = r.Clone(context.WithValue(r.Context(), tokenKey, payload)) + } + + ah.ServeHTTP(w, r) + }) } srv := &http.Server{ diff --git a/cmd/lotus-wallet/rules.go b/cmd/lotus-wallet/rules.go index 5af504faf3c..9d229d165aa 100644 --- a/cmd/lotus-wallet/rules.go +++ b/cmd/lotus-wallet/rules.go @@ -113,7 +113,7 @@ func finalRule(err error) func(ctx context.Context, r Rule) (Filter, error) { } if len(rules) != 0 { - return nil, xerrors.Errorf("final rule must be an empty map", rules) + return nil, xerrors.Errorf("final rule must be an empty map, had %d elements", rules) } return func(params map[FilterParam]interface{}) error { @@ -157,11 +157,11 @@ func AnyAccepts(ctx context.Context, r Rule) (Filter, error) { func ParseRule(ctx context.Context, r Rule) (Filter, error) { rules, ok := r.(map[string]interface{}) if !ok { - return nil, xerrors.Errorf("expected rule to be a map") + return nil, xerrors.Errorf("expected rule to be a map, was %T", r) } if len(rules) != 1 { - return nil, xerrors.Errorf("expected one rule, had %d", rules) + return nil, xerrors.Errorf("expected one rule, had %d", len(rules)) } for p, r := range rules { @@ -196,7 +196,7 @@ func Sign(ctx context.Context, r Rule) (Filter, error) { sub, err := ParseRule(ctx, r) if err != nil { - return nil, xerrors.Errorf("parsing next rule %d: %w", err) + return nil, xerrors.Errorf("parsing next rule: %w", err) } return func(params map[FilterParam]interface{}) error { @@ -215,7 +215,7 @@ func Message(ctx context.Context, r Rule) (Filter, error) { sub, err := ParseRule(ctx, r) if err != nil { - return nil, xerrors.Errorf("parsing next rule %d: %w", err) + return nil, xerrors.Errorf("parsing next rule: %w", err) } return func(params map[FilterParam]interface{}) error { @@ -249,7 +249,7 @@ func DealProposal(ctx context.Context, r Rule) (Filter, error) { sub, err := ParseRule(ctx, r) if err != nil { - return nil, xerrors.Errorf("parsing next rule %d: %w", err) + return nil, xerrors.Errorf("parsing next rule: %w", err) } return func(params map[FilterParam]interface{}) error {