github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/engine/prove.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package engine 5 6 import ( 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/keybase/client/go/externals" 12 libkb "github.com/keybase/client/go/libkb" 13 keybase1 "github.com/keybase/client/go/protocol/keybase1" 14 "golang.org/x/net/context" 15 ) 16 17 // Prove is an engine used for proving ownership of remote accounts, 18 // like Twitter, GitHub, etc. 19 type Prove struct { 20 arg *keybase1.StartProofArg 21 me *libkb.User 22 serviceType libkb.ServiceType 23 serviceParameters *keybase1.ProveParameters 24 supersede bool 25 proof *libkb.ProofMetadataRes 26 sig string 27 sigID keybase1.SigID 28 linkID libkb.LinkID 29 postRes *libkb.PostProofRes 30 signingKey libkb.GenericKey 31 sigInner []byte 32 33 remoteNameNormalized string 34 35 libkb.Contextified 36 } 37 38 // NewProve makes a new Prove Engine given an RPC-friendly ProveArg. 39 func NewProve(g *libkb.GlobalContext, arg *keybase1.StartProofArg) *Prove { 40 if arg.SigVersion == nil || libkb.SigVersion(*arg.SigVersion) == libkb.KeybaseNullSigVersion { 41 tmp := keybase1.SigVersion(libkb.GetDefaultSigVersion(g)) 42 arg.SigVersion = &tmp 43 } 44 return &Prove{ 45 arg: arg, 46 Contextified: libkb.NewContextified(g), 47 } 48 } 49 50 // Name provides the name of this engine for the engine interface contract 51 func (p *Prove) Name() string { 52 return "Prove" 53 } 54 55 // GetPrereqs returns the engine prereqs. 56 func (p *Prove) Prereqs() Prereqs { 57 return Prereqs{Device: true} 58 } 59 60 // RequiredUIs returns the required UIs. 61 func (p *Prove) RequiredUIs() []libkb.UIKind { 62 return []libkb.UIKind{ 63 libkb.LogUIKind, 64 libkb.ProveUIKind, 65 libkb.SecretUIKind, 66 } 67 } 68 69 // SubConsumers returns the other UI consumers for this engine. 70 func (p *Prove) SubConsumers() []libkb.UIConsumer { 71 return nil 72 } 73 74 func (p *Prove) loadMe(m libkb.MetaContext) (err error) { 75 p.me, err = libkb.LoadMe(libkb.NewLoadUserArgWithMetaContext(m).WithForceReload()) 76 return err 77 } 78 79 func (p *Prove) checkExists1(m libkb.MetaContext) (err error) { 80 proofs := p.me.IDTable().GetActiveProofsFor(p.serviceType) 81 if len(proofs) != 0 && !p.arg.Force && p.serviceType.LastWriterWins() { 82 lst := proofs[len(proofs)-1] 83 var redo bool 84 redo, err = m.UIs().ProveUI.PromptOverwrite(m.Ctx(), keybase1.PromptOverwriteArg{ 85 Account: lst.ToDisplayString(), 86 Typ: keybase1.PromptOverwriteType_SOCIAL, 87 }) 88 if err != nil { 89 return err 90 } 91 if !redo { 92 return libkb.NotConfirmedError{} 93 } 94 p.supersede = true 95 } 96 return nil 97 } 98 99 func (p *Prove) promptRemoteName(m libkb.MetaContext) error { 100 // If the name is already supplied, there's no need to prompt. 101 if len(p.arg.Username) > 0 { 102 remoteNameNormalized, err := p.serviceType.NormalizeRemoteName(m, p.arg.Username) 103 if err == nil { 104 p.remoteNameNormalized = remoteNameNormalized 105 } 106 return err 107 } 108 109 // Prompt for the name, retrying if it's invalid. 110 var normalizationError error 111 for { 112 un, err := m.UIs().ProveUI.PromptUsername(m.Ctx(), keybase1.PromptUsernameArg{ 113 Prompt: p.serviceType.GetPrompt(), 114 PrevError: libkb.ExportErrorAsStatus(m.G(), normalizationError), 115 Parameters: p.serviceParameters, 116 }) 117 if err != nil { 118 // Errors here are conditions like EOF. Return them rather than retrying. 119 return err 120 } 121 var remoteNameNormalized string 122 remoteNameNormalized, normalizationError = p.serviceType.NormalizeRemoteName(m, un) 123 if normalizationError == nil { 124 p.remoteNameNormalized = remoteNameNormalized 125 return nil 126 } 127 } 128 } 129 130 func (p *Prove) checkExists2(m libkb.MetaContext) (err error) { 131 defer m.Trace("Prove#CheckExists2", &err)() 132 if !p.serviceType.LastWriterWins() { 133 var found libkb.RemoteProofChainLink 134 for _, proof := range p.me.IDTable().GetActiveProofsFor(p.serviceType) { 135 _, name := proof.ToKeyValuePair() 136 if libkb.Cicmp(name, p.remoteNameNormalized) { 137 found = proof 138 break 139 } 140 } 141 if found != nil { 142 var redo bool 143 redo, err = m.UIs().ProveUI.PromptOverwrite(m.Ctx(), keybase1.PromptOverwriteArg{ 144 Account: found.ToDisplayString(), 145 Typ: keybase1.PromptOverwriteType_SITE, 146 }) 147 if err != nil { 148 return err 149 } 150 if !redo { 151 err = libkb.NotConfirmedError{} 152 return err 153 } 154 p.supersede = true 155 } 156 } 157 return nil 158 } 159 160 func (p *Prove) doPrechecks(m libkb.MetaContext) (err error) { 161 var w *libkb.Markup 162 w, err = p.serviceType.PreProofCheck(m, p.remoteNameNormalized) 163 if w != nil { 164 if uierr := m.UIs().ProveUI.OutputPrechecks(m.Ctx(), keybase1.OutputPrechecksArg{Text: w.Export()}); uierr != nil { 165 m.Warning("prove ui OutputPrechecks call error: %s", uierr) 166 } 167 } 168 return err 169 } 170 171 func (p *Prove) doWarnings(m libkb.MetaContext) (err error) { 172 if mu := p.serviceType.PreProofWarning(p.remoteNameNormalized); mu != nil { 173 var ok bool 174 arg := keybase1.PreProofWarningArg{Text: mu.Export()} 175 if ok, err = m.UIs().ProveUI.PreProofWarning(m.Ctx(), arg); err == nil && !ok { 176 err = libkb.NotConfirmedError{} 177 } 178 if err != nil { 179 return err 180 } 181 } 182 return nil 183 } 184 185 func (p *Prove) generateProof(m libkb.MetaContext) (err error) { 186 ska := libkb.SecretKeyArg{ 187 Me: p.me, 188 KeyType: libkb.DeviceSigningKeyType, 189 } 190 191 p.signingKey, err = m.G().Keyrings.GetSecretKeyWithPrompt(m, m.SecretKeyPromptArg(ska, "tracking signature")) 192 if err != nil { 193 return err 194 } 195 196 sigVersion := libkb.SigVersion(*p.arg.SigVersion) 197 198 if p.proof, err = p.me.ServiceProof(m, p.signingKey, p.serviceType, p.remoteNameNormalized, sigVersion); err != nil { 199 return err 200 } 201 202 if p.sigInner, err = p.proof.J.Marshal(); err != nil { 203 return err 204 } 205 206 p.sig, p.sigID, p.linkID, err = libkb.MakeSig( 207 m, 208 p.signingKey, 209 libkb.LinkTypeWebServiceBinding, 210 p.sigInner, 211 libkb.SigHasRevokes(false), 212 keybase1.SeqType_PUBLIC, 213 libkb.SigIgnoreIfUnsupported(false), 214 p.me, 215 sigVersion, 216 ) 217 218 return err 219 } 220 221 func (p *Prove) postProofToServer(m libkb.MetaContext) (err error) { 222 arg := libkb.PostProofArg{ 223 UID: p.me.GetUID(), 224 Seqno: p.proof.Seqno, 225 Sig: p.sig, 226 ProofType: p.serviceType.GetProofType(), 227 RemoteServiceType: p.serviceType.GetTypeName(), 228 SigID: p.sigID, 229 LinkID: p.linkID, 230 Supersede: p.supersede, 231 RemoteUsername: p.remoteNameNormalized, 232 RemoteKey: p.serviceType.GetAPIArgKey(), 233 SigningKey: p.signingKey, 234 } 235 if libkb.SigVersion(*p.arg.SigVersion) == libkb.KeybaseSignatureV2 { 236 arg.SigInner = p.sigInner 237 } 238 p.postRes, err = libkb.PostProof(m, arg) 239 return err 240 } 241 242 func (p *Prove) instructAction(m libkb.MetaContext) (err error) { 243 mkp := p.serviceType.PostInstructions(p.remoteNameNormalized) 244 var txt string 245 if txt, err = p.serviceType.FormatProofText(m, p.postRes, p.me.GetNormalizedName().String(), p.remoteNameNormalized, p.sigID); err != nil { 246 return err 247 } 248 err = m.UIs().ProveUI.OutputInstructions(m.Ctx(), keybase1.OutputInstructionsArg{ 249 Instructions: mkp.Export(), 250 // If we don't trim newlines here, we'll run into an issue where e.g. 251 // Facebook links get corrupted on iOS. See: 252 // - https://keybase.atlassian.net/browse/DESKTOP-3335 253 // - https://keybase.atlassian.net/browse/CORE-4941 254 // All of our proof verifying code (PVL) should already be flexible 255 // with surrounding whitespace, because users are pasting proofs by 256 // hand anyway. 257 Proof: strings.TrimSpace(txt), 258 Parameters: p.serviceParameters, 259 }) 260 if err != nil { 261 return err 262 } 263 264 return p.checkAutoPost(m, txt) 265 } 266 267 func (p *Prove) checkAutoPost(m libkb.MetaContext, txt string) error { 268 if !p.arg.Auto { 269 return nil 270 } 271 if libkb.RemoteServiceTypes[p.arg.Service] != keybase1.ProofType_ROOTER { 272 return nil 273 } 274 m.Debug("making automatic post of proof to rooter") 275 apiArg := libkb.APIArg{ 276 Endpoint: "rooter", 277 SessionType: libkb.APISessionTypeREQUIRED, 278 Args: libkb.HTTPArgs{ 279 "post": libkb.S{Val: txt}, 280 "username": libkb.S{Val: p.arg.Username}, 281 }, 282 } 283 if _, err := m.G().API.Post(m, apiArg); err != nil { 284 m.Debug("error posting to rooter: %s", err) 285 return err 286 } 287 return nil 288 } 289 290 // Keep asking the user whether they posted the proof 291 // until it works or they give up. 292 func (p *Prove) promptPostedLoop(m libkb.MetaContext) (err error) { 293 found := false 294 for i := 0; ; i++ { 295 var retry bool 296 var status keybase1.ProofStatus 297 var warn *libkb.Markup 298 retry, err = m.UIs().ProveUI.OkToCheck(m.Ctx(), keybase1.OkToCheckArg{ 299 Name: p.serviceType.DisplayName(), 300 Attempt: i, 301 }) 302 if !retry || err != nil { 303 break 304 } 305 found, status, _, err = libkb.CheckPosted(m, p.sigID) 306 if found || err != nil { 307 break 308 } 309 warn, err = p.serviceType.RecheckProofPosting(i, status, p.remoteNameNormalized) 310 if warn != nil { 311 uierr := m.UIs().ProveUI.DisplayRecheckWarning(m.Ctx(), keybase1.DisplayRecheckWarningArg{ 312 Text: warn.Export(), 313 }) 314 if uierr != nil { 315 m.Warning("prove ui DisplayRecheckWarning call error: %s", uierr) 316 } 317 } 318 if err != nil { 319 break 320 } 321 } 322 if !found && err == nil { 323 err = libkb.ProofNotYetAvailableError{} 324 } 325 326 return err 327 } 328 329 // Poll until the proof succeeds, limited to an hour. 330 func (p *Prove) verifyLoop(m libkb.MetaContext) (err error) { 331 timeout := time.Hour 332 m, cancel := m.WithTimeout(timeout) 333 defer cancel() 334 defer func() { 335 if err != nil && m.Ctx().Err() == context.DeadlineExceeded { 336 m.Debug("Prove.verifyLoop rewriting error after timeout: %v", err) 337 err = fmt.Errorf("Timed out after looking for proof for %v", timeout) 338 } 339 }() 340 uierr := m.UIs().ProveUI.Checking(m.Ctx(), keybase1.CheckingArg{ 341 Name: p.serviceType.DisplayName(), 342 }) 343 if uierr != nil { 344 m.Warning("prove ui Checking call error: %s", uierr) 345 } 346 for i := 0; ; i++ { 347 if shouldContinue, uierr := m.UIs().ProveUI.ContinueChecking(m.Ctx(), 0); !shouldContinue || uierr != nil { 348 if uierr != nil { 349 m.Warning("prove ui ContinueChecking call error: %s", uierr) 350 } 351 return libkb.CanceledError{} 352 } 353 found, status, _, err := libkb.CheckPosted(m, p.sigID) 354 if err != nil { 355 return err 356 } 357 m.Debug("Prove.verifyLoop round:%v found:%v status:%v", i, found, status) 358 if found { 359 return nil 360 } 361 wakeAt := m.G().Clock().Now().Add(2 * time.Second) 362 err = libkb.SleepUntilWithContext(m.Ctx(), m.G().Clock(), wakeAt) 363 if err != nil { 364 return err 365 } 366 } 367 } 368 369 func (p *Prove) checkProofText(m libkb.MetaContext) error { 370 m.Debug("p.postRes.Text: %q", p.postRes.Text) 371 m.Debug("p.sigID: %q", p.sigID) 372 return p.serviceType.CheckProofText(p.postRes.Text, p.sigID, p.sig) 373 } 374 375 func (p *Prove) getServiceType(m libkb.MetaContext) (err error) { 376 p.serviceType = m.G().GetProofServices().GetServiceType(m.Ctx(), p.arg.Service) 377 if p.serviceType == nil { 378 return libkb.BadServiceError{Service: p.arg.Service} 379 } 380 if !p.serviceType.CanMakeNewProofs(m) { 381 return libkb.ServiceDoesNotSupportNewProofsError{Service: p.arg.Service} 382 } 383 if serviceType, ok := p.serviceType.(*externals.GenericSocialProofServiceType); ok { 384 tmp := serviceType.ProveParameters(m) 385 p.serviceParameters = &tmp 386 } 387 return nil 388 } 389 390 // SigID returns the signature id of the proof posted to the 391 // server. 392 func (p *Prove) SigID() keybase1.SigID { 393 return p.sigID 394 } 395 396 // Run runs the Prove engine, performing all steps of the proof process. 397 func (p *Prove) Run(m libkb.MetaContext) (err error) { 398 defer m.Trace("ProofEngine.Run", &err)() 399 400 stage := func(s string) { 401 m.Debug("| ProofEngine.Run() %s", s) 402 } 403 404 stage("GetServiceType") 405 if err = p.getServiceType(m); err != nil { 406 return err 407 } 408 stage("LoadMe") 409 if err = p.loadMe(m); err != nil { 410 return err 411 } 412 stage("CheckExists1") 413 if err = p.checkExists1(m); err != nil { 414 return err 415 } 416 stage("PromptRemoteName") 417 if err = p.promptRemoteName(m); err != nil { 418 return err 419 } 420 stage("CheckExists2") 421 if err = p.checkExists2(m); err != nil { 422 return err 423 } 424 stage("DoPrechecks") 425 if err = p.doPrechecks(m); err != nil { 426 return err 427 } 428 stage("DoWarnings") 429 if err = p.doWarnings(m); err != nil { 430 return err 431 } 432 m.G().LocalSigchainGuard().Set(m.Ctx(), "Prove") 433 defer m.G().LocalSigchainGuard().Clear(m.Ctx(), "Prove") 434 stage("GenerateProof") 435 if err = p.generateProof(m); err != nil { 436 return err 437 } 438 stage("PostProofToServer") 439 if err = p.postProofToServer(m); err != nil { 440 return err 441 } 442 m.G().LocalSigchainGuard().Clear(m.Ctx(), "Prove") 443 stage("CheckProofText") 444 if err = p.checkProofText(m); err != nil { 445 return err 446 } 447 stage("InstructAction") 448 if err = p.instructAction(m); err != nil { 449 return err 450 } 451 452 if !p.arg.PromptPosted { 453 m.Debug("PromptPosted not set, prove run finished") 454 return nil 455 } 456 457 stage("CheckStart") 458 if p.serviceParameters == nil { 459 stage("PromptPostedLoop") 460 if err = p.promptPostedLoop(m); err != nil { 461 return err 462 } 463 } else { 464 stage("VerifyLoop") 465 if err = p.verifyLoop(m); err != nil { 466 return err 467 } 468 } 469 m.UIs().LogUI.Notice("Success!") 470 return nil 471 }