github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/auth/credential_authority.go (about)

     1  package auth
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"time"
     7  
     8  	libkb "github.com/keybase/client/go/libkb"
     9  	logger "github.com/keybase/client/go/logger"
    10  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    11  	context "golang.org/x/net/context"
    12  )
    13  
    14  const (
    15  	userTimeout  = 3 * time.Hour
    16  	cacheTimeout = 8 * time.Hour
    17  )
    18  
    19  // CredentialAuthority should be allocated as a singleton object. It validates UID<->Username<->ActiveKey
    20  // triples for all users across a service. It keeps a cache and subscribes for updates,
    21  // so you can call into it as much as you'd like without fear of spamming the network.
    22  type CredentialAuthority struct {
    23  	log           logger.Logger
    24  	api           UserKeyAPIer
    25  	invalidateCh  chan keybase1.UID
    26  	checkCh       chan checkArg
    27  	shutdownCh    chan struct{}
    28  	cleanItemCh   chan cleanItem
    29  	users         map[keybase1.UID](*userWrapper)
    30  	cleanSchedule []cleanItem
    31  	eng           engine
    32  }
    33  
    34  // checkArgs are sent over the checkCh to the core loop of a CredentialAuthority
    35  type checkArg struct {
    36  	uid         keybase1.UID
    37  	username    *libkb.NormalizedUsername
    38  	kid         *keybase1.KID
    39  	sibkeys     []keybase1.KID
    40  	subkeys     []keybase1.KID
    41  	loadDeleted bool
    42  	retCh       chan error
    43  }
    44  
    45  // String implements the Stringer interface for checkArg.
    46  func (ca checkArg) String() string {
    47  	return fmt.Sprintf("{uid: %s, username: %s, kid: %s, sibkeys: %v, subkeys: %v}",
    48  		ca.uid, ca.username, ca.kid, ca.sibkeys, ca.subkeys)
    49  }
    50  
    51  // userWrapper contains two fields -- one is the user object itself, which will
    52  // spawn a go-routine that is largely off-limits to the main thread aside from
    53  // over channels. the second field is the `atime`, or *access* time, which the main
    54  // thread can touch to compute eviction mechanics.
    55  type userWrapper struct {
    56  	u     *user
    57  	atime time.Time
    58  }
    59  
    60  // String implements the Stringer interface for userWrapper.
    61  func (uw userWrapper) String() string {
    62  	return fmt.Sprintf("{user: %s, atime: %s}", uw.u, uw.atime)
    63  }
    64  
    65  // cleanItems are items to consider cleaning out of the cache. they sit in a queue
    66  // until they are up for review. When the review happens, the user object they
    67  // refer to can still persist in the cache, if it's been accessed recently.
    68  type cleanItem struct {
    69  	uid   keybase1.UID
    70  	ctime time.Time
    71  }
    72  
    73  // String implements the Stringer interface for cleanItem.
    74  func (ci cleanItem) String() string {
    75  	return fmt.Sprintf("{uid: %s, ctime: %s}", ci.uid, ci.ctime)
    76  }
    77  
    78  // user wraps a user who is currently active in the system. Each user has a run
    79  // method that runs its own goRoutine, so many items, aside from the two channels,
    80  // are off-limits to the main thread.
    81  type user struct {
    82  	lock      sync.RWMutex
    83  	uid       keybase1.UID
    84  	username  libkb.NormalizedUsername
    85  	sibkeys   map[keybase1.KID]struct{}
    86  	subkeys   map[keybase1.KID]struct{}
    87  	isOK      bool
    88  	isDeleted bool
    89  	ctime     time.Time
    90  	ca        *CredentialAuthority
    91  }
    92  
    93  // String implements the stringer interface for user.
    94  func (u *user) String() string {
    95  	u.lock.RLock()
    96  	defer u.lock.RUnlock()
    97  	return fmt.Sprintf("{uid: %s, username: %s, sibkeys: %v, subkeys: %v, isOK: %v, ctime: %s, isDeleted: %v}",
    98  		u.uid, u.username, u.sibkeys, u.subkeys, u.isOK, u.ctime, u.isDeleted)
    99  }
   100  
   101  // newUser makes a new user with the given UID for use in the given
   102  // CredentialAuthority. This constructor sets up the necessary maps and
   103  // channels to make the user work as expected.
   104  func newUser(uid keybase1.UID, ca *CredentialAuthority) *user {
   105  	ca.log.Debug("newUser, uid %s", uid)
   106  	ret := &user{
   107  		uid:     uid,
   108  		sibkeys: make(map[keybase1.KID]struct{}),
   109  		subkeys: make(map[keybase1.KID]struct{}),
   110  		ca:      ca,
   111  	}
   112  	return ret
   113  }
   114  
   115  // UserKeyAPIer is an interface that specifies the UserKeyAPI that
   116  // will eventually be used to get information about the users from the trusted
   117  // server authority.
   118  type UserKeyAPIer interface {
   119  	// GetUser looks up the username and KIDS active for the given user.
   120  	// Deleted users are loaded by default.
   121  	GetUser(context.Context, keybase1.UID) (
   122  		un libkb.NormalizedUsername, sibkeys, subkeys []keybase1.KID, deleted bool, err error)
   123  	// PollForChanges returns the UIDs that have recently changed on the server
   124  	// side. It will be called in a poll loop. This call should function as
   125  	// a *long poll*, meaning, it should not return unless there is a change
   126  	// to report, or a sufficient amount of time has passed. If an error occurred,
   127  	// then PollForChanges should delay before return, so we don't wind up
   128  	// busy-waiting.
   129  	PollForChanges(context.Context) ([]keybase1.UID, error)
   130  }
   131  
   132  // engine specifies the internal mechanics of how this CredentialAuthority
   133  // works. It's only really useful for testing, since tests will want to change
   134  // the definition of time, poke the main loop into action at certain points,
   135  // and get callback hooks when items are evicted.
   136  type engine interface {
   137  	Now() time.Time             // we can overload this for debugging
   138  	Evicted(uid keybase1.UID)   // called when this uid is evicted
   139  	GetPokeCh() <-chan struct{} // Return a channel that can poke the main loop
   140  }
   141  
   142  // standardEngine is the engine that's used in production when the CredentailAuthority
   143  // actually runs. It does very little.
   144  type standardEngine struct {
   145  	pokeCh <-chan struct{}
   146  }
   147  
   148  // Now returns time.Now
   149  func (se *standardEngine) Now() time.Time { return time.Now() }
   150  
   151  // Evicted is a Noop, called whenever a user object for the given UID is evicted.
   152  func (se *standardEngine) Evicted(uid keybase1.UID) {}
   153  
   154  // GetPokeCh returns a dummy channel that's never sent to
   155  func (se *standardEngine) GetPokeCh() <-chan struct{} { return se.pokeCh }
   156  
   157  // newStandardEngine creates and initializes a standardEngine for use in the
   158  // production run of a CredentialAuthority.
   159  func newStandardEngine() engine {
   160  	return &standardEngine{
   161  		pokeCh: make(chan struct{}),
   162  	}
   163  }
   164  
   165  // NewCredentialAuthority makes a new signleton CredentialAuthority an start it running. It takes as input
   166  // a logger and an API for making keybase API calls
   167  func NewCredentialAuthority(log logger.Logger, api UserKeyAPIer) *CredentialAuthority {
   168  	return newCredentialAuthorityWithEngine(log, api, newStandardEngine())
   169  }
   170  
   171  // newCredentialAuthoirutyWithEngine is an internal call that can specify the non-standard
   172  // engine. We'd only need to call this directly from testing to specify a testingEngine.
   173  func newCredentialAuthorityWithEngine(log logger.Logger, api UserKeyAPIer, eng engine) *CredentialAuthority {
   174  	ret := &CredentialAuthority{
   175  		log:          log,
   176  		api:          api,
   177  		invalidateCh: make(chan keybase1.UID, 100),
   178  		checkCh:      make(chan checkArg),
   179  		shutdownCh:   make(chan struct{}),
   180  		users:        make(map[keybase1.UID](*userWrapper)),
   181  		cleanItemCh:  make(chan cleanItem),
   182  		eng:          eng,
   183  	}
   184  	ret.run()
   185  	return ret
   186  }
   187  
   188  // run two loops in goroutines: one to poll for updates from the server, and
   189  // another to poll for incoming requests and maintenance events.
   190  func (v *CredentialAuthority) run() {
   191  	go v.pollLoop()
   192  	go v.runLoop()
   193  }
   194  
   195  // pollOnce polls the API server once for which users have changed.
   196  func (v *CredentialAuthority) pollOnce() error {
   197  	var err error
   198  	var uids []keybase1.UID
   199  	err = v.runWithCancel(func(ctx context.Context) error {
   200  		var err error
   201  		uids, err = v.api.PollForChanges(ctx)
   202  		return err
   203  	})
   204  	if err == nil {
   205  		for _, uid := range uids {
   206  			v.invalidateCh <- uid
   207  		}
   208  	}
   209  	return err
   210  }
   211  
   212  // runWithCancel runs an API call while listening for a shutdown of the CredentialAuthority.
   213  // If it gets one, it uses context-based cancellation to cancel the outstanding API call
   214  // (or sleep in the case of Poll()'ing).
   215  func (v *CredentialAuthority) runWithCancel(body func(ctx context.Context) error) error {
   216  	ctx, cancel := context.WithCancel(context.Background())
   217  	doneCh := make(chan error)
   218  	var err error
   219  
   220  	go func() {
   221  		doneCh <- body(ctx)
   222  	}()
   223  
   224  	select {
   225  	case err = <-doneCh:
   226  	case <-v.shutdownCh:
   227  		cancel()
   228  		err = ErrShutdown
   229  	}
   230  	return err
   231  }
   232  
   233  // pollLoop() keeps running until the CA is shut down via Shutdown(). It calls Poll()
   234  // on the UserKeyAPIer once per iteration.
   235  func (v *CredentialAuthority) pollLoop() {
   236  	for {
   237  		// We rely on pollOnce to not return right away, so we don't busy loop.
   238  		if v.pollOnce() == ErrShutdown {
   239  			break
   240  		}
   241  	}
   242  }
   243  
   244  // runLoop() keeps running until the CA is shut down via Shutdown(). It listens
   245  // for incoming client requests, and also for various maintenance takes, and
   246  // cache invalidations.
   247  func (v *CredentialAuthority) runLoop() {
   248  	done := false
   249  	for !done {
   250  		select {
   251  		case <-v.eng.GetPokeCh():
   252  			// Noop, but poke main loop for testing.
   253  		case <-v.shutdownCh:
   254  			done = true
   255  		case ca := <-v.checkCh:
   256  			v.log.Debug("Checking %s", ca)
   257  			u := v.makeUser(ca.uid)
   258  			go u.check(ca)
   259  		case uid := <-v.invalidateCh:
   260  			if uw := v.users[uid]; uw != nil {
   261  				v.log.Debug("Invalidating %s", uw)
   262  				delete(v.users, uid)
   263  				v.eng.Evicted(uid)
   264  			}
   265  		case ci := <-v.cleanItemCh:
   266  			v.cleanSchedule = append(v.cleanSchedule, ci)
   267  		}
   268  		v.clean()
   269  	}
   270  	for uid := range v.users {
   271  		v.eng.Evicted(uid)
   272  	}
   273  }
   274  
   275  // clean out in-memory data, going through in FIFO order. Stop once we've hit
   276  // a cleanItem that's too recent. When we iterate over cleanItems, we don't
   277  // need to throw them out necessarily, if they've been accessed recently. In that
   278  // case, just skip and keep going.
   279  //
   280  // We'll get an entry in the cleanSchedule once for ever call to GetUser() on
   281  // the API server.
   282  func (v *CredentialAuthority) clean() {
   283  	cutoff := v.eng.Now().Add(-cacheTimeout)
   284  	for i, e := range v.cleanSchedule {
   285  		if e.ctime.After(cutoff) {
   286  			v.cleanSchedule = v.cleanSchedule[i:]
   287  			return
   288  		}
   289  		if uw := v.users[e.uid]; uw != nil && !uw.atime.After(e.ctime) {
   290  			v.log.Debug("Cleaning %s, clean entry: %s", uw, e)
   291  			delete(v.users, e.uid)
   292  			v.eng.Evicted(e.uid)
   293  		}
   294  	}
   295  	v.cleanSchedule = nil
   296  }
   297  
   298  // makeUser either pulls a user from the in-memory table, or constructs a new
   299  // one. In either case, it updates the CA's `atime` bit for now for this user
   300  // record.
   301  func (v *CredentialAuthority) makeUser(uid keybase1.UID) *user {
   302  	uw := v.users[uid]
   303  	if uw == nil {
   304  		u := newUser(uid, v)
   305  		uw = &userWrapper{u: u}
   306  		v.users[uid] = uw
   307  	}
   308  	uw.atime = v.eng.Now()
   309  	return uw.u
   310  }
   311  
   312  // Now return this CA's idea of what time Now is.
   313  func (u *user) Now() time.Time { return u.ca.eng.Now() }
   314  
   315  // repopulate is intended to repopulate our representation of the user with the
   316  // server's up-to-date notion of what the user looks like. If our version is recent
   317  // enough, this is a no-op.  If not, we'll go to the server and send the main loop
   318  // a "Cleanup" event to eventually clean us out.
   319  func (u *user) repopulate() error {
   320  	u.lock.RLock()
   321  	alreadyPopulated := u.isPopulatedRLocked()
   322  	u.lock.RUnlock()
   323  	if alreadyPopulated {
   324  		return nil
   325  	}
   326  
   327  	u.lock.Lock()
   328  
   329  	// Check again in case another goroutine has already filled this before
   330  	// we took the write lock.
   331  	alreadyPopulated = u.isPopulatedRLocked()
   332  	defer u.lock.Unlock()
   333  	if alreadyPopulated {
   334  		return nil
   335  	}
   336  
   337  	// Register that this item should eventually be cleaned out by the cleaner
   338  	// thread. Don't block on the send, though, since that could deadlock the process
   339  	// (since the process is blocked on sending to us).
   340  	ctime := u.Now()
   341  	go func() {
   342  		u.ca.cleanItemCh <- cleanItem{uid: u.uid, ctime: ctime}
   343  	}()
   344  
   345  	un, sibkeys, subkeys, isDeleted, err := u.ca.getUserFromServer(u.uid)
   346  	if err != nil {
   347  		u.isOK = false
   348  		return err
   349  	}
   350  	u.username = un
   351  	for _, k := range sibkeys {
   352  		u.sibkeys[k] = struct{}{}
   353  	}
   354  	for _, k := range subkeys {
   355  		u.subkeys[k] = struct{}{}
   356  	}
   357  	u.isOK = true
   358  	u.ctime = ctime
   359  	u.isDeleted = isDeleted
   360  	u.ca.log.Debug("Repopulated info for %s", u)
   361  	return nil
   362  }
   363  
   364  // isPopulated returned true if this user is populated and current enough to
   365  // trust.
   366  func (u *user) isPopulatedRLocked() bool {
   367  	return u.isOK && u.Now().Sub(u.ctime) <= userTimeout
   368  }
   369  
   370  func (u *user) checkAfterPopulatedRLocked(ca checkArg) (err error) {
   371  	if !ca.loadDeleted && u.isDeleted {
   372  		return ErrUserDeleted
   373  	}
   374  
   375  	if ca.username != nil {
   376  		if err = u.checkUsernameRLocked(*ca.username); err != nil {
   377  			return err
   378  		}
   379  	}
   380  
   381  	if ca.kid != nil {
   382  		if err = u.checkKeyRLocked(*ca.kid); err != nil {
   383  			return err
   384  		}
   385  	}
   386  
   387  	if ca.sibkeys != nil {
   388  		if err = u.compareSibkeysRLocked(ca.sibkeys); err != nil {
   389  			return err
   390  		}
   391  	}
   392  
   393  	if ca.subkeys != nil {
   394  		if err = u.compareSubkeysRLocked(ca.subkeys); err != nil {
   395  			return err
   396  		}
   397  	}
   398  
   399  	return nil
   400  }
   401  
   402  // check that a user matches the given username and has the given key as one of
   403  // its valid keys. This is where the actually work of this whole library happens.
   404  func (u *user) check(ca checkArg) {
   405  	var err error
   406  
   407  	defer func() {
   408  		u.ca.log.Debug("Check %s, err: %v", ca, err)
   409  		ca.retCh <- err
   410  	}()
   411  
   412  	if err = u.repopulate(); err != nil {
   413  		return
   414  	}
   415  
   416  	u.lock.RLock()
   417  	defer u.lock.RUnlock()
   418  
   419  	err = u.checkAfterPopulatedRLocked(ca)
   420  }
   421  
   422  // getUserFromServer runs the UserKeyAPIer GetUser() API call while paying
   423  // attention to any shutdown events that might interrupt it.
   424  func (v *CredentialAuthority) getUserFromServer(uid keybase1.UID) (
   425  	un libkb.NormalizedUsername, sibkeys, subkeys []keybase1.KID, deleted bool, err error) {
   426  	err = v.runWithCancel(func(ctx context.Context) error {
   427  		var err error
   428  		un, sibkeys, subkeys, deleted, err = v.api.GetUser(ctx, uid)
   429  		return err
   430  	})
   431  	return un, sibkeys, subkeys, deleted, err
   432  }
   433  
   434  // checkUsernameRLocked checks that a username is a match for this user.
   435  func (u *user) checkUsernameRLocked(un libkb.NormalizedUsername) error {
   436  	var err error
   437  	if !u.username.Eq(un) {
   438  		err = BadUsernameError{u.username, un}
   439  	}
   440  	return err
   441  }
   442  
   443  // compareSibkeysRLocked returns true if the passed set of sibkeys is equal.
   444  func (u *user) compareSibkeysRLocked(sibkeys []keybase1.KID) error {
   445  	return compareKeys(sibkeys, u.sibkeys)
   446  }
   447  
   448  // compareSubkeysRLocked returns true if the passed set of subkeys is equal.
   449  func (u *user) compareSubkeysRLocked(subkeys []keybase1.KID) error {
   450  	return compareKeys(subkeys, u.subkeys)
   451  }
   452  
   453  // Helper method for the two above.
   454  func compareKeys(keys []keybase1.KID, expected map[keybase1.KID]struct{}) error {
   455  	if len(keys) != len(expected) {
   456  		return ErrKeysNotEqual
   457  	}
   458  	for _, kid := range keys {
   459  		if _, ok := expected[kid]; !ok {
   460  			return ErrKeysNotEqual
   461  		}
   462  	}
   463  	return nil
   464  }
   465  
   466  // checkKeyRLocked checks that the given key is still valid for this user.
   467  func (u *user) checkKeyRLocked(kid keybase1.KID) error {
   468  	var err error
   469  	if _, ok := u.sibkeys[kid]; !ok {
   470  		if _, ok := u.subkeys[kid]; !ok {
   471  			err = BadKeyError{u.uid, kid}
   472  		}
   473  	}
   474  	return err
   475  }
   476  
   477  // CheckUserKey is the main point of entry to this library. It takes as input a UID, a
   478  // username and a kid that should refer to a current valid triple, perhaps
   479  // extracted from a signed authentication statement. It returns an error if the
   480  // check fails, and nil otherwise. If username or kid are nil they aren't checked.
   481  func (v *CredentialAuthority) CheckUserKey(ctx context.Context, uid keybase1.UID,
   482  	username *libkb.NormalizedUsername, kid *keybase1.KID, loadDeleted bool) (err error) {
   483  	v.log.Debug("CheckUserKey uid %s, kid %s", uid, kid)
   484  	retCh := make(chan error, 1) // buffered in case the ctx is canceled
   485  	v.checkCh <- checkArg{uid: uid, username: username, kid: kid, loadDeleted: loadDeleted, retCh: retCh}
   486  	select {
   487  	case <-ctx.Done():
   488  		err = ctx.Err()
   489  	case err = <-retCh:
   490  	}
   491  	return err
   492  }
   493  
   494  // CheckUsers is used to validate all provided UIDs are known.
   495  func (v *CredentialAuthority) CheckUsers(ctx context.Context, users []keybase1.UID) (err error) {
   496  	for _, uid := range users {
   497  		if uid == keybase1.PUBLIC_UID {
   498  			continue
   499  		}
   500  		if err = v.CheckUserKey(ctx, uid, nil, nil, false); err != nil {
   501  			break
   502  		}
   503  	}
   504  	return err
   505  }
   506  
   507  // CompareUserKeys compares the passed sets to the sets known by the API server.
   508  // It returns true if the sets are equal.
   509  func (v *CredentialAuthority) CompareUserKeys(ctx context.Context, uid keybase1.UID, sibkeys, subkeys []keybase1.KID) (
   510  	err error) {
   511  	retCh := make(chan error, 1) // buffered in case the ctx is canceled
   512  	v.checkCh <- checkArg{uid: uid, sibkeys: sibkeys, subkeys: subkeys, retCh: retCh}
   513  	select {
   514  	case <-ctx.Done():
   515  		err = ctx.Err()
   516  	case err = <-retCh:
   517  	}
   518  	return err
   519  }
   520  
   521  // Shutdown the credentialAuthority and delete all internal state.
   522  func (v *CredentialAuthority) Shutdown() {
   523  	close(v.shutdownCh)
   524  }