github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/libkb/wot.go (about)

     1  package libkb
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/keybase/client/go/kbun"
     9  	"github.com/keybase/client/go/protocol/gregor1"
    10  	"github.com/keybase/client/go/protocol/keybase1"
    11  )
    12  
    13  func getWotVouchChainLink(mctx MetaContext, uid keybase1.UID, sigID keybase1.SigID) (cl *WotVouchChainLink, voucher *User, err error) {
    14  	// requires a full chain load
    15  	user, err := LoadUser(NewLoadUserArgWithMetaContext(mctx).WithUID(uid).WithStubMode(StubModeUnstubbed))
    16  	if err != nil {
    17  		return nil, nil, fmt.Errorf("Error loading user: %v", err)
    18  	}
    19  	link := user.LinkFromSigID(sigID)
    20  	if link == nil {
    21  		return nil, nil, fmt.Errorf("Could not find link from sigID")
    22  	}
    23  	tlink, w := NewTypedChainLink(link)
    24  	if w != nil {
    25  		return nil, nil, fmt.Errorf("Could not get typed chain link: %v", w.Warning())
    26  	}
    27  	vlink, ok := tlink.(*WotVouchChainLink)
    28  	if !ok {
    29  		return nil, nil, fmt.Errorf("Link is not a WotVouchChainLink: %v", tlink)
    30  	}
    31  	return vlink, user, nil
    32  }
    33  
    34  func getWotReactChainLink(mctx MetaContext, user *User, sigID keybase1.SigID) (cl *WotReactChainLink, err error) {
    35  	link := user.LinkFromSigID(sigID)
    36  	if link == nil {
    37  		return nil, fmt.Errorf("Could not find link from sigID")
    38  	}
    39  	tlink, w := NewTypedChainLink(link)
    40  	if w != nil {
    41  		return nil, fmt.Errorf("Could not get typed chain link: %v", w.Warning())
    42  	}
    43  	rlink, ok := tlink.(*WotReactChainLink)
    44  	if !ok {
    45  		return nil, fmt.Errorf("Link is not a WotReactChainLink: %v", tlink)
    46  	}
    47  	return rlink, nil
    48  }
    49  
    50  func assertVouchIsForUser(mctx MetaContext, vouchedUser wotExpansionUser, user *User) (err error) {
    51  	if user.GetName() != vouchedUser.Username {
    52  		return fmt.Errorf("wot username isn't expected %s != %s", user.GetName(), vouchedUser.Username)
    53  	}
    54  	if user.GetUID() != vouchedUser.UID {
    55  		return fmt.Errorf("wot uid isn't me %s != %s", user.GetUID(), vouchedUser.UID)
    56  	}
    57  	if user.GetEldestKID() != vouchedUser.Eldest.KID {
    58  		return fmt.Errorf("wot eldest kid isn't me %s != %s", user.GetEldestKID(), vouchedUser.Eldest.KID)
    59  	}
    60  	return nil
    61  }
    62  
    63  type wotExpansionUser struct {
    64  	Eldest struct {
    65  		KID   keybase1.KID
    66  		Seqno keybase1.Seqno
    67  	}
    68  	SeqTail struct {
    69  		PayloadHash string
    70  		Seqno       keybase1.Seqno
    71  		SigID       string
    72  	}
    73  	UID      keybase1.UID
    74  	Username string
    75  }
    76  
    77  type vouchExpansion struct {
    78  	User       wotExpansionUser    `json:"user"`
    79  	Confidence keybase1.Confidence `json:"confidence"`
    80  	VouchText  string              `json:"vouch_text"`
    81  }
    82  
    83  type reactionExpansion struct {
    84  	SigID    keybase1.SigID `json:"sig_id"`
    85  	Reaction string         `json:"reaction"`
    86  }
    87  
    88  type serverWotVouch struct {
    89  	Voucher               keybase1.UID           `json:"voucher"`
    90  	VoucherEldestSeqno    keybase1.Seqno         `json:"voucher_eldest_seqno"`
    91  	Vouchee               keybase1.UID           `json:"vouchee"`
    92  	VoucheeEldestSeqno    keybase1.Seqno         `json:"vouchee_eldest_seqno"`
    93  	VouchSigID            keybase1.SigID         `json:"vouch_sig"`
    94  	VouchExpansionJSON    string                 `json:"vouch_expansion"`
    95  	ReactionSigID         *keybase1.SigID        `json:"reaction_sig,omitempty"`
    96  	ReactionExpansionJSON *string                `json:"reaction_expansion,omitempty"`
    97  	Status                keybase1.WotStatusType `json:"status"`
    98  }
    99  
   100  func transformUserVouch(mctx MetaContext, serverVouch serverWotVouch, voucheeUser *User) (res keybase1.WotVouch, err error) {
   101  	// load the voucher and fetch the relevant chain link
   102  	wotVouchLink, voucher, err := getWotVouchChainLink(mctx, serverVouch.Voucher, serverVouch.VouchSigID)
   103  	if err != nil {
   104  		return res, fmt.Errorf("error finding the vouch in the voucher's sigchain: %s", err.Error())
   105  	}
   106  	// extract the sig expansion
   107  	expansionObject, err := ExtractExpansionObj(wotVouchLink.ExpansionID, serverVouch.VouchExpansionJSON)
   108  	if err != nil {
   109  		return res, fmt.Errorf("error extracting and validating the vouch expansion: %s", err.Error())
   110  	}
   111  	// load it into the right type for web-of-trust vouching
   112  	var wotObj vouchExpansion
   113  	err = json.Unmarshal(expansionObject, &wotObj)
   114  	if err != nil {
   115  		return res, fmt.Errorf("error casting vouch expansion object to expected web-of-trust schema: %s", err.Error())
   116  	}
   117  
   118  	if voucheeUser == nil || voucheeUser.GetUID() != serverVouch.Vouchee {
   119  		// load vouchee
   120  		voucheeUser, err = LoadUser(NewLoadUserArgWithMetaContext(mctx).WithUID(serverVouch.Vouchee).WithPublicKeyOptional().WithStubMode(StubModeUnstubbed))
   121  		if err != nil {
   122  			return res, fmt.Errorf("error loading vouchee to transform: %s", err.Error())
   123  		}
   124  	}
   125  
   126  	err = assertVouchIsForUser(mctx, wotObj.User, voucheeUser)
   127  	if err != nil {
   128  		mctx.Debug("web-of-trust vouch user-section doesn't look right: %+v", wotObj.User)
   129  		return res, fmt.Errorf("error verifying user section of web-of-trust expansion: %s", err.Error())
   130  	}
   131  
   132  	hasReaction := serverVouch.ReactionSigID != nil
   133  	var reactionObj reactionExpansion
   134  	var reactionStatus keybase1.WotReactionType
   135  	var wotReactLink *WotReactChainLink
   136  	if hasReaction {
   137  		wotReactLink, err = getWotReactChainLink(mctx, voucheeUser, *serverVouch.ReactionSigID)
   138  		if err != nil {
   139  			return res, fmt.Errorf("error finding the vouch in the vouchee's sigchain: %s", err.Error())
   140  		}
   141  		// extract the sig expansion
   142  		expansionObject, err = ExtractExpansionObj(wotReactLink.ExpansionID, *serverVouch.ReactionExpansionJSON)
   143  		if err != nil {
   144  			return res, fmt.Errorf("error extracting and validating the vouch expansion: %s", err.Error())
   145  		}
   146  		// load it into the right type for web-of-trust vouching
   147  		err = json.Unmarshal(expansionObject, &reactionObj)
   148  		if err != nil {
   149  			return res, fmt.Errorf("error casting vouch expansion object to expected web-of-trust schema: %s", err.Error())
   150  		}
   151  		if reactionObj.SigID.String()[:30] != wotVouchLink.GetSigID().String()[:30] {
   152  			return res, fmt.Errorf("reaction sigID doesn't match the original attestation: %s != %s", reactionObj.SigID, wotVouchLink.GetSigID())
   153  		}
   154  		reactionStatus = keybase1.WotReactionTypeMap[strings.ToUpper(reactionObj.Reaction)]
   155  	}
   156  
   157  	var status keybase1.WotStatusType
   158  	switch {
   159  	case wotVouchLink.revoked:
   160  		status = keybase1.WotStatusType_REVOKED
   161  	case wotReactLink != nil && wotReactLink.revoked:
   162  		status = keybase1.WotStatusType_PROPOSED
   163  	case !hasReaction:
   164  		status = keybase1.WotStatusType_PROPOSED
   165  	case reactionStatus == keybase1.WotReactionType_ACCEPT:
   166  		status = keybase1.WotStatusType_ACCEPTED
   167  	case reactionStatus == keybase1.WotReactionType_REJECT:
   168  		status = keybase1.WotStatusType_REJECTED
   169  	default:
   170  		return res, fmt.Errorf("could not determine the status of web-of-trust from %s", voucher.GetName())
   171  	}
   172  
   173  	var proofs []keybase1.WotProofUI
   174  	for _, proof := range wotObj.Confidence.Proofs {
   175  		proofForUI, err := NewWotProofUI(mctx, proof)
   176  		if err != nil {
   177  			return res, err
   178  		}
   179  		proofs = append(proofs, proofForUI)
   180  	}
   181  
   182  	// build a WotVouch
   183  	return keybase1.WotVouch{
   184  		Status:          status,
   185  		Vouchee:         voucheeUser.ToUserVersion(),
   186  		VoucheeUsername: voucheeUser.GetNormalizedName().String(),
   187  		Voucher:         voucher.ToUserVersion(),
   188  		VoucherUsername: voucher.GetNormalizedName().String(),
   189  		VouchText:       wotObj.VouchText,
   190  		VouchProof:      serverVouch.VouchSigID,
   191  		VouchedAt:       keybase1.ToTime(wotVouchLink.GetCTime()),
   192  		Confidence:      wotObj.Confidence,
   193  		Proofs:          proofs,
   194  	}, nil
   195  }
   196  
   197  type apiWot struct {
   198  	AppStatusEmbed
   199  	Vouches []serverWotVouch `json:"webOfTrust"`
   200  }
   201  
   202  type FetchWotVouchesArg struct {
   203  	Vouchee string
   204  	Voucher string
   205  }
   206  
   207  func fetchWot(mctx MetaContext, vouchee string, voucher string) (res []serverWotVouch, err error) {
   208  	defer mctx.Trace("fetchWot", &err)()
   209  	apiArg := APIArg{
   210  		Endpoint:    "wot/get",
   211  		SessionType: APISessionTypeREQUIRED,
   212  	}
   213  	apiArg.Args = HTTPArgs{}
   214  	if len(vouchee) > 0 {
   215  		apiArg.Args["vouchee"] = S{Val: vouchee}
   216  	}
   217  	if len(voucher) > 0 {
   218  		apiArg.Args["voucher"] = S{Val: voucher}
   219  	}
   220  	var response apiWot
   221  	err = mctx.G().API.GetDecode(mctx, apiArg, &response)
   222  	if err != nil {
   223  		mctx.Debug("error fetching web-of-trust vouches: %s", err.Error())
   224  		return nil, err
   225  	}
   226  	mctx.Debug("server returned %d web-of-trust vouches", len(response.Vouches))
   227  	return response.Vouches, nil
   228  }
   229  
   230  // FetchWotVouches gets vouches written for vouchee (if specified) by voucher
   231  // (if specified).
   232  func FetchWotVouches(mctx MetaContext, arg FetchWotVouchesArg) (res []keybase1.WotVouch, err error) {
   233  	vouches, err := fetchWot(mctx, arg.Vouchee, arg.Voucher)
   234  	if err != nil {
   235  		mctx.Debug("error fetching web-of-trust vouches for vouchee=%s by voucher=%s: %s", arg.Vouchee, arg.Voucher, err.Error())
   236  		return nil, err
   237  	}
   238  	var voucheeUser *User
   239  	if len(arg.Vouchee) > 0 {
   240  		voucheeUser, err = LoadUser(NewLoadUserArgWithMetaContext(mctx).WithName(arg.Vouchee).WithPublicKeyOptional())
   241  		if err != nil {
   242  			return nil, fmt.Errorf("error loading vouchee: %s", err.Error())
   243  		}
   244  	}
   245  	for _, serverVouch := range vouches {
   246  		vouch, err := transformUserVouch(mctx, serverVouch, voucheeUser)
   247  		if err != nil {
   248  			mctx.Debug("error validating server-reported web-of-trust vouches for vouchee=%s by voucher=%s: %s", arg.Vouchee, arg.Voucher, err.Error())
   249  			return nil, err
   250  		}
   251  		res = append(res, vouch)
   252  	}
   253  	mctx.Debug("found %d web-of-trust vouches for vouchee=%s by voucher=%s", len(res), arg.Vouchee, arg.Voucher)
   254  	return res, nil
   255  }
   256  
   257  type _wotMsg struct {
   258  	Voucher *string `json:"voucher,omitempty"`
   259  	Vouchee *string `json:"vouchee,omitempty"`
   260  }
   261  
   262  func hasWotMsg(testable string) bool {
   263  	for _, match := range []string{"wot.new_vouch", "wot.accepted", "wot.rejected"} {
   264  		if match == testable {
   265  			return true
   266  		}
   267  	}
   268  	return false
   269  }
   270  
   271  func isDismissable(mctx MetaContext, category string, msg _wotMsg, voucher, vouchee kbun.NormalizedUsername) bool {
   272  	voucherMatches := (msg.Voucher != nil && kbun.NewNormalizedUsername(*msg.Voucher) == voucher)
   273  	voucheeMatches := (msg.Vouchee != nil && kbun.NewNormalizedUsername(*msg.Vouchee) == vouchee)
   274  	me := mctx.ActiveDevice().Username(mctx)
   275  	switch category {
   276  	case "wot.new_vouch":
   277  		return voucherMatches && (voucheeMatches || vouchee == me)
   278  	case "wot.accepted", "wot.rejected":
   279  		return voucheeMatches && (voucherMatches || voucher == me)
   280  	default:
   281  		return false
   282  	}
   283  }
   284  
   285  func DismissWotNotifications(mctx MetaContext, voucherUsername, voucheeUsername string) (err error) {
   286  	dismisser := mctx.G().GregorState
   287  	state, err := mctx.G().GregorState.State(mctx.Ctx())
   288  	if err != nil {
   289  		return err
   290  	}
   291  	categoryPrefix, err := gregor1.ObjFactory{}.MakeCategory("wot")
   292  	if err != nil {
   293  		return err
   294  	}
   295  	items, err := state.ItemsWithCategoryPrefix(categoryPrefix)
   296  	if err != nil {
   297  		return err
   298  	}
   299  	var wotMsg _wotMsg
   300  	for _, item := range items {
   301  		category := item.Category().String()
   302  		if !hasWotMsg(category) {
   303  			continue
   304  		}
   305  		if err := json.Unmarshal(item.Body().Bytes(), &wotMsg); err != nil {
   306  			return err
   307  		}
   308  		if isDismissable(mctx, category, wotMsg, kbun.NewNormalizedUsername(voucherUsername), kbun.NewNormalizedUsername(voucheeUsername)) {
   309  			itemID := item.Metadata().MsgID()
   310  			mctx.Debug("dismissing %s for %s,%s", category, voucherUsername, voucheeUsername)
   311  			if err := dismisser.DismissItem(mctx.Ctx(), nil, itemID); err != nil {
   312  				return err
   313  			}
   314  		}
   315  	}
   316  	return nil
   317  }