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 }