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  }