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 }