github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/ephemeral/team_ek.go (about)

     1  package ephemeral
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  
     7  	"github.com/keybase/client/go/kbcrypto"
     8  	"github.com/keybase/client/go/libkb"
     9  	"github.com/keybase/client/go/protocol/gregor1"
    10  	"github.com/keybase/client/go/protocol/keybase1"
    11  	"github.com/keybase/client/go/teams"
    12  )
    13  
    14  type TeamEKSeed keybase1.Bytes32
    15  
    16  func newTeamEphemeralSeed() (seed TeamEKSeed, err error) {
    17  	randomSeed, err := makeNewRandomSeed()
    18  	if err != nil {
    19  		return seed, err
    20  	}
    21  	return TeamEKSeed(randomSeed), nil
    22  }
    23  
    24  func newTeamEKSeedFromBytes(b []byte) (s TeamEKSeed, err error) {
    25  	seed, err := newEKSeedFromBytes(b)
    26  	if err != nil {
    27  		return s, err
    28  	}
    29  	return TeamEKSeed(seed), nil
    30  }
    31  
    32  func (s *TeamEKSeed) DeriveDHKey() *libkb.NaclDHKeyPair {
    33  	return deriveDHKey(keybase1.Bytes32(*s), libkb.DeriveReasonTeamEKEncryption)
    34  }
    35  
    36  type TeamEphemeralKeyer struct{}
    37  
    38  var _ EphemeralKeyer = (*TeamEphemeralKeyer)(nil)
    39  
    40  func NewTeamEphemeralKeyer() *TeamEphemeralKeyer {
    41  	return &TeamEphemeralKeyer{}
    42  }
    43  
    44  func (k *TeamEphemeralKeyer) Type() keybase1.TeamEphemeralKeyType {
    45  	return keybase1.TeamEphemeralKeyType_TEAM
    46  }
    47  
    48  func postNewTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID, sig string,
    49  	boxes *[]keybase1.TeamEkBoxMetadata) (err error) {
    50  	defer mctx.Trace("postNewTeamEK", &err)()
    51  
    52  	boxesJSON, err := json.Marshal(*boxes)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	apiArg := libkb.APIArg{
    57  		Endpoint:    "team/team_ek",
    58  		SessionType: libkb.APISessionTypeREQUIRED,
    59  		Args: libkb.HTTPArgs{
    60  			"team_id": libkb.S{Val: string(teamID)},
    61  			"sig":     libkb.S{Val: sig},
    62  			"boxes":   libkb.S{Val: string(boxesJSON)},
    63  		},
    64  	}
    65  	_, err = mctx.G().GetAPI().Post(mctx, apiArg)
    66  	return err
    67  }
    68  
    69  func prepareNewTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID,
    70  	signingKey libkb.NaclSigningKeyPair, membersMetadata map[keybase1.UID]keybase1.UserEkMetadata,
    71  	merkleRoot libkb.MerkleRoot) (sig string, boxes *[]keybase1.TeamEkBoxMetadata,
    72  	metadata keybase1.TeamEkMetadata, myBox *keybase1.TeamEkBoxed, err error) {
    73  	defer mctx.Trace("prepareNewTeamEK", &err)()
    74  
    75  	seed, err := newTeamEphemeralSeed()
    76  	if err != nil {
    77  		return "", nil, metadata, nil, err
    78  	}
    79  
    80  	prevStatement, latestGeneration, wrongKID, err := fetchTeamEKStatement(mctx, teamID)
    81  	if !wrongKID && err != nil {
    82  		return "", nil, metadata, nil, err
    83  	}
    84  	var generation keybase1.EkGeneration
    85  	if prevStatement == nil {
    86  		// Even if the teamEK statement was signed by the wrong key (this can
    87  		// happen when legacy clients roll the PTK), fetchTeamEKStatement will
    88  		// return the generation number from the last (unverifiable) statement.
    89  		// If there was never any statement, latestGeneration will be 0, so
    90  		// adding one is correct in all cases.
    91  		generation = latestGeneration + 1
    92  	} else {
    93  		generation = prevStatement.CurrentTeamEkMetadata.Generation + 1
    94  	}
    95  
    96  	dhKeypair := seed.DeriveDHKey()
    97  
    98  	metadata = keybase1.TeamEkMetadata{
    99  		Kid:        dhKeypair.GetKID(),
   100  		Generation: generation,
   101  		HashMeta:   merkleRoot.HashMeta(),
   102  		// The ctime is derivable from the hash meta, by fetching the hashed
   103  		// root from the server, but including it saves readers a potential
   104  		// extra round trip.
   105  		Ctime: keybase1.TimeFromSeconds(merkleRoot.Ctime()),
   106  	}
   107  
   108  	statement := keybase1.TeamEkStatement{
   109  		CurrentTeamEkMetadata: metadata,
   110  	}
   111  	statementJSON, err := json.Marshal(statement)
   112  	if err != nil {
   113  		return "", nil, metadata, nil, err
   114  	}
   115  
   116  	sig, _, err = signingKey.SignToString(statementJSON)
   117  	if err != nil {
   118  		return "", nil, metadata, nil, err
   119  	}
   120  
   121  	teamEK := keybase1.TeamEk{
   122  		Seed:     keybase1.Bytes32(seed),
   123  		Metadata: metadata,
   124  	}
   125  	boxes, myTeamEKBoxed, err := boxTeamEKForUsers(mctx, membersMetadata, teamEK)
   126  	if err != nil {
   127  		return "", nil, metadata, nil, err
   128  	}
   129  	return sig, boxes, metadata, myTeamEKBoxed, nil
   130  }
   131  
   132  func publishNewTeamEK(mctx libkb.MetaContext, teamID keybase1.TeamID,
   133  	merkleRoot libkb.MerkleRoot, forceCreateGeneration *keybase1.EkGeneration) (metadata keybase1.TeamEkMetadata, err error) {
   134  	defer mctx.Trace("publishNewTeamEK", &err)()
   135  
   136  	team, err := teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{
   137  		ID: teamID,
   138  	})
   139  	if err != nil {
   140  		return metadata, err
   141  	}
   142  	signingKey, err := team.SigningKey(mctx.Ctx())
   143  	if err != nil {
   144  		return metadata, err
   145  	}
   146  
   147  	statementMap, err := fetchTeamMemberStatements(mctx, teamID)
   148  	if err != nil {
   149  		return metadata, err
   150  	}
   151  	membersMetadata, err := activeUserEKMetadata(mctx, statementMap, merkleRoot)
   152  	if err != nil {
   153  		return metadata, err
   154  	}
   155  
   156  	sig, boxes, teamEKMetadata, myBox, err := prepareNewTeamEK(mctx, teamID, signingKey, membersMetadata, merkleRoot)
   157  	if err != nil {
   158  		return metadata, err
   159  	}
   160  
   161  	if forceCreateGeneration != nil {
   162  		if *forceCreateGeneration+1 != teamEKMetadata.Generation {
   163  			return metadata, fmt.Errorf("Not posting new teamEK, expected %d, found %d", *forceCreateGeneration+1, teamEKMetadata.Generation)
   164  		}
   165  		mctx.Debug("forceCreateGeneration set to: %d", *forceCreateGeneration)
   166  	}
   167  
   168  	if err = postNewTeamEK(mctx, teamID, sig, boxes); err != nil {
   169  		return metadata, err
   170  	}
   171  
   172  	if myBox == nil {
   173  		mctx.Debug("No box made for own teamEK")
   174  	} else {
   175  		storage := mctx.G().GetTeamEKBoxStorage()
   176  		boxed := keybase1.NewTeamEphemeralKeyBoxedWithTeam(*myBox)
   177  		if err = storage.Put(mctx, teamID, teamEKMetadata.Generation, boxed); err != nil {
   178  			return metadata, err
   179  		}
   180  	}
   181  	return teamEKMetadata, nil
   182  }
   183  
   184  func (k *TeamEphemeralKeyer) Fetch(mctx libkb.MetaContext, teamID keybase1.TeamID, generation keybase1.EkGeneration, contentCtime *gregor1.Time) (teamEK keybase1.TeamEphemeralKeyBoxed, err error) {
   185  	apiArg := libkb.APIArg{
   186  		Endpoint:    "team/team_ek_box",
   187  		SessionType: libkb.APISessionTypeREQUIRED,
   188  		Args: libkb.HTTPArgs{
   189  			"team_id":    libkb.S{Val: string(teamID)},
   190  			"generation": libkb.U{Val: uint64(generation)},
   191  		},
   192  	}
   193  
   194  	var result TeamEKBoxedResponse
   195  	res, err := mctx.G().GetAPI().Get(mctx, apiArg)
   196  	if err != nil {
   197  		err = errFromAppStatus(err)
   198  		return teamEK, err
   199  	}
   200  
   201  	err = res.Body.UnmarshalAgain(&result)
   202  	if err != nil {
   203  		return teamEK, err
   204  	}
   205  
   206  	if result.Result == nil {
   207  		err = newEKMissingBoxErr(mctx, TeamEKKind, generation)
   208  		return teamEK, err
   209  	}
   210  
   211  	// Although we verify the signature is valid, it's possible that this key
   212  	// was signed with a PTK that is not our latest and greatest. We allow this
   213  	// when we are using this ek for *decryption*. When getting a key for
   214  	// *encryption* callers are responsible for verifying the signature is
   215  	// signed by the latest PTK or generating a new EK. This logic currently
   216  	// lives in ephemeral/lib.go#GetOrCreateLatestTeamEK (#newTeamEKNeeded)
   217  	_, teamEKStatement, err := extractTeamEKStatementFromSig(result.Result.Sig)
   218  	if err != nil {
   219  		return teamEK, err
   220  	} else if teamEKStatement == nil { // shouldn't happen
   221  		return teamEK, fmt.Errorf("unable to fetch valid teamEKStatement")
   222  	}
   223  
   224  	teamEKMetadata := teamEKStatement.CurrentTeamEkMetadata
   225  	if generation != teamEKMetadata.Generation {
   226  		// sanity check that we got the right generation
   227  		return teamEK, newEKCorruptedErr(mctx, TeamEKKind, generation, teamEKMetadata.Generation)
   228  	}
   229  	teamEKBoxed := keybase1.TeamEkBoxed{
   230  		Box:              result.Result.Box,
   231  		UserEkGeneration: result.Result.UserEKGeneration,
   232  		Metadata:         teamEKMetadata,
   233  	}
   234  
   235  	return keybase1.NewTeamEphemeralKeyBoxedWithTeam(teamEKBoxed), nil
   236  }
   237  
   238  func (k *TeamEphemeralKeyer) Unbox(mctx libkb.MetaContext, boxed keybase1.TeamEphemeralKeyBoxed,
   239  	contentCtime *gregor1.Time) (ek keybase1.TeamEphemeralKey, err error) {
   240  	defer mctx.Trace(fmt.Sprintf("TeamEKBoxStorage#unbox: teamEKGeneration: %v", boxed.Generation()),
   241  		&err)()
   242  
   243  	typ, err := boxed.KeyType()
   244  	if err != nil {
   245  		return ek, err
   246  	}
   247  	if !typ.IsTeam() {
   248  		return ek, NewIncorrectTeamEphemeralKeyTypeError(typ, keybase1.TeamEphemeralKeyType_TEAM)
   249  	}
   250  
   251  	teamEKBoxed := boxed.Team()
   252  	teamEKGeneration := teamEKBoxed.Metadata.Generation
   253  	userEKBoxStorage := mctx.G().GetUserEKBoxStorage()
   254  	userEK, err := userEKBoxStorage.Get(mctx, teamEKBoxed.UserEkGeneration, contentCtime)
   255  	if err != nil {
   256  		mctx.Debug("unable to get from userEKStorage %v", err)
   257  		if _, ok := err.(EphemeralKeyError); ok {
   258  			return ek, newEKUnboxErr(mctx, TeamEKKind, teamEKGeneration, UserEKKind,
   259  				teamEKBoxed.UserEkGeneration, contentCtime)
   260  		}
   261  		return ek, err
   262  	}
   263  
   264  	userSeed := UserEKSeed(userEK.Seed)
   265  	userKeypair := userSeed.DeriveDHKey()
   266  
   267  	msg, _, err := userKeypair.DecryptFromString(teamEKBoxed.Box)
   268  	if err != nil {
   269  		mctx.Debug("unable to decrypt teamEKBoxed %v", err)
   270  		return ek, newEKUnboxErr(mctx, TeamEKKind, teamEKGeneration, UserEKKind,
   271  			teamEKBoxed.UserEkGeneration, contentCtime)
   272  	}
   273  
   274  	seed, err := newTeamEKSeedFromBytes(msg)
   275  	if err != nil {
   276  		return ek, err
   277  	}
   278  
   279  	keypair := seed.DeriveDHKey()
   280  	if !keypair.GetKID().Equal(teamEKBoxed.Metadata.Kid) {
   281  		return ek, fmt.Errorf("Failed to verify server given seed [%s] against signed KID [%s]. Box: %+v",
   282  			teamEKBoxed.Metadata.Kid, keypair.GetKID(), teamEKBoxed)
   283  	}
   284  
   285  	return keybase1.NewTeamEphemeralKeyWithTeam(keybase1.TeamEk{
   286  		Seed:     keybase1.Bytes32(seed),
   287  		Metadata: teamEKBoxed.Metadata,
   288  	}), nil
   289  }
   290  
   291  // There are plenty of race conditions where the PTK or teamEK or
   292  // membership list can change out from under us while we're in the middle
   293  // of posting a new key, causing the post to fail. Detect these conditions
   294  // and retry.
   295  func teamEKRetryWrapper(mctx libkb.MetaContext, retryFn func() error) (err error) {
   296  	for tries := 0; tries < maxRetries; tries++ {
   297  		if err = retryFn(); err == nil {
   298  			return nil
   299  		}
   300  		if !libkb.IsEphemeralRetryableError(err) {
   301  			return err
   302  		}
   303  		mctx.Debug("teamEKRetryWrapper found a retryable error on try %d: %v",
   304  			tries, err)
   305  		select {
   306  		case <-mctx.Ctx().Done():
   307  			return mctx.Ctx().Err()
   308  		default:
   309  			// continue retrying
   310  		}
   311  	}
   312  	return err
   313  }
   314  
   315  func ForcePublishNewTeamEKForTesting(mctx libkb.MetaContext, teamID keybase1.TeamID,
   316  	merkleRoot libkb.MerkleRoot) (metadata keybase1.TeamEkMetadata, err error) {
   317  	defer mctx.Trace("ForcePublishNewTeamEKForTesting", &err)()
   318  	err = teamEKRetryWrapper(mctx, func() error {
   319  		metadata, err = publishNewTeamEK(mctx, teamID, merkleRoot, nil)
   320  		return err
   321  	})
   322  	return metadata, err
   323  }
   324  
   325  func boxTeamEKForUsers(mctx libkb.MetaContext, usersMetadata map[keybase1.UID]keybase1.UserEkMetadata,
   326  	teamEK keybase1.TeamEk) (teamBoxes *[]keybase1.TeamEkBoxMetadata, myTeamEKBoxed *keybase1.TeamEkBoxed, err error) {
   327  	defer mctx.Trace("boxTeamEKForUsers", &err)()
   328  
   329  	myUID := mctx.G().Env.GetUID()
   330  	boxes := make([]keybase1.TeamEkBoxMetadata, 0, len(usersMetadata))
   331  	for uid, metadata := range usersMetadata {
   332  		recipientKey, err := libkb.ImportKeypairFromKID(metadata.Kid)
   333  		if err != nil {
   334  			return nil, nil, err
   335  		}
   336  		// Encrypting with a nil sender means we'll generate a random sender private key.
   337  		box, err := recipientKey.EncryptToString(teamEK.Seed[:], nil)
   338  		if err != nil {
   339  			return nil, nil, err
   340  		}
   341  		boxMetadata := keybase1.TeamEkBoxMetadata{
   342  			RecipientUID:        uid,
   343  			RecipientGeneration: metadata.Generation,
   344  			Box:                 box,
   345  		}
   346  		boxes = append(boxes, boxMetadata)
   347  
   348  		if uid == myUID {
   349  			myTeamEKBoxed = &keybase1.TeamEkBoxed{
   350  				Box:              box,
   351  				UserEkGeneration: metadata.Generation,
   352  				Metadata:         teamEK.Metadata,
   353  			}
   354  		}
   355  	}
   356  	return &boxes, myTeamEKBoxed, err
   357  }
   358  
   359  type teamEKStatementResponse struct {
   360  	Sig *string `json:"sig"`
   361  }
   362  
   363  // Returns nil if the team has never published a teamEK. If the team has
   364  // published a teamEK, but has since rolled their PTK without publishing a new
   365  // one, this function will also return nil and log a warning. This is a
   366  // transitional thing, and eventually when all "reasonably up to date" clients
   367  // in the wild have EK support, we will make that case an error.
   368  func fetchTeamEKStatement(mctx libkb.MetaContext, teamID keybase1.TeamID) (
   369  	statement *keybase1.TeamEkStatement, latestGeneration keybase1.EkGeneration, wrongKID bool, err error) {
   370  	defer mctx.Trace("fetchTeamEKStatement", &err)()
   371  
   372  	apiArg := libkb.APIArg{
   373  		Endpoint:    "team/team_ek",
   374  		SessionType: libkb.APISessionTypeREQUIRED,
   375  		Args: libkb.HTTPArgs{
   376  			"team_id": libkb.S{Val: string(teamID)},
   377  		},
   378  	}
   379  	res, err := mctx.G().GetAPI().Get(mctx, apiArg)
   380  	if err != nil {
   381  		return nil, latestGeneration, false, err
   382  	}
   383  
   384  	parsedResponse := teamEKStatementResponse{}
   385  	err = res.Body.UnmarshalAgain(&parsedResponse)
   386  	if err != nil {
   387  		return nil, latestGeneration, false, err
   388  	}
   389  
   390  	// If the result field in the response is null, the server is saying that
   391  	// the team has never published a teamEKStatement, stale or otherwise.
   392  	if parsedResponse.Sig == nil {
   393  		mctx.Debug("team has no teamEKStatement at all")
   394  		return nil, latestGeneration, false, nil
   395  	}
   396  
   397  	statement, latestGeneration, wrongKID, err = verifySigWithLatestPTK(mctx, teamID, *parsedResponse.Sig)
   398  	// Check the wrongKID condition before checking the error, since an error
   399  	// is still returned in this case. TODO: Turn this warning into an error
   400  	// after EK support is sufficiently widespread.
   401  	if wrongKID {
   402  		mctx.Debug("It looks like someone rolled the PTK without generating new ephemeral keys. They might be on an old version.")
   403  		return nil, latestGeneration, true, nil
   404  	} else if err != nil {
   405  		return nil, latestGeneration, false, err
   406  	}
   407  
   408  	return statement, latestGeneration, false, nil
   409  }
   410  
   411  func extractTeamEKStatementFromSig(sig string) (signerKey *kbcrypto.NaclSigningKeyPublic, statement *keybase1.TeamEkStatement, err error) {
   412  	signerKey, payload, _, err := kbcrypto.NaclVerifyAndExtract(sig)
   413  	if err != nil {
   414  		return signerKey, nil, err
   415  	}
   416  
   417  	parsedStatement := keybase1.TeamEkStatement{}
   418  	if err = json.Unmarshal(payload, &parsedStatement); err != nil {
   419  		return signerKey, nil, err
   420  	}
   421  	return signerKey, &parsedStatement, nil
   422  }
   423  
   424  // Verify that the blob is validly signed, and that the signing key is the
   425  // given team's latest PTK, then parse its contents. If the blob is signed by
   426  // the wrong KID, that's still an error, but we'll also return this special
   427  // `wrongKID` flag. As a transitional measure while we wait for all clients in
   428  // the wild to have EK support, callers will treat that case as "there is no
   429  // key" and convert the error to a warning.
   430  func verifySigWithLatestPTK(mctx libkb.MetaContext, teamID keybase1.TeamID,
   431  	sig string) (statement *keybase1.TeamEkStatement, latestGeneration keybase1.EkGeneration, wrongKID bool, err error) {
   432  	defer mctx.Trace("verifySigWithLatestPTK", &err)()
   433  
   434  	// Parse the statement before we verify the signing key. Even if the
   435  	// signing key is bad (likely because of a legacy PTK roll that didn't
   436  	// include a teamEK statement), we'll still return the generation number.
   437  	signerKey, parsedStatement, err := extractTeamEKStatementFromSig(sig)
   438  	if err != nil {
   439  		return nil, latestGeneration, false, err
   440  	}
   441  	latestGeneration = parsedStatement.CurrentTeamEkMetadata.Generation
   442  
   443  	// Verify the signing key corresponds to the latest PTK. We load the team's
   444  	// from cache, but if the KID doesn't match, we try a forced reload to see
   445  	// if the cache might've been stale. Only if the KID still doesn't match
   446  	// after the reload do we complain.
   447  	team, err := teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{
   448  		ID: teamID,
   449  	})
   450  	if err != nil {
   451  		return nil, latestGeneration, false, err
   452  	}
   453  	teamSigningKey, err := team.SigningKey(mctx.Ctx())
   454  	if err != nil {
   455  		return nil, latestGeneration, false, err
   456  	}
   457  	if !teamSigningKey.GetKID().Equal(signerKey.GetKID()) {
   458  		// The latest PTK might be stale. Force a reload, then check this over again.
   459  		team, err := teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{
   460  			ID:          teamID,
   461  			ForceRepoll: true,
   462  		})
   463  		if err != nil {
   464  			return nil, latestGeneration, false, err
   465  		}
   466  		teamSigningKey, err = team.SigningKey(mctx.Ctx())
   467  		if err != nil {
   468  			return nil, latestGeneration, false, err
   469  		}
   470  		if !teamSigningKey.GetKID().Equal(signerKey.GetKID()) {
   471  			return nil, latestGeneration, true, fmt.Errorf("teamEK returned for PTK signing KID %s, but latest is %s",
   472  				signerKey.GetKID(), teamSigningKey.GetKID())
   473  		}
   474  	}
   475  
   476  	// If we didn't short circuit above, then the signing key is correct.
   477  	// Return the parsed statement.
   478  	return parsedStatement, latestGeneration, false, nil
   479  }
   480  
   481  type teamMemberEKStatementResponse struct {
   482  	Sigs map[keybase1.UID]string `json:"sigs"`
   483  }
   484  
   485  // Returns nil if all team members have never published a teamEK. Verifies that
   486  // the map of users the server returns are indeed valid team members of the
   487  // team and all signatures verify correctly for the users.
   488  func fetchTeamMemberStatements(mctx libkb.MetaContext,
   489  	teamID keybase1.TeamID) (statements map[keybase1.UID]*keybase1.UserEkStatement, err error) {
   490  	defer mctx.Trace("fetchTeamMemberStatements", &err)()
   491  
   492  	apiArg := libkb.APIArg{
   493  		Endpoint:    "team/member_eks",
   494  		SessionType: libkb.APISessionTypeREQUIRED,
   495  		Args: libkb.HTTPArgs{
   496  			"team_id": libkb.S{Val: string(teamID)},
   497  		},
   498  	}
   499  	res, err := mctx.G().GetAPI().Get(mctx, apiArg)
   500  	if err != nil {
   501  		return nil, err
   502  	}
   503  
   504  	memberEKStatements := teamMemberEKStatementResponse{}
   505  	if err = res.Body.UnmarshalAgain(&memberEKStatements); err != nil {
   506  		return nil, err
   507  	}
   508  
   509  	team, err := teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{
   510  		ID: teamID,
   511  	})
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  	var uids []keybase1.UID
   516  	for uid := range memberEKStatements.Sigs {
   517  		uids = append(uids, uid)
   518  	}
   519  
   520  	getArg := func(i int) *libkb.LoadUserArg {
   521  		if i >= len(uids) {
   522  			return nil
   523  		}
   524  		tmp := libkb.NewLoadUserArgWithMetaContext(mctx).WithUID(uids[i])
   525  		return &tmp
   526  	}
   527  
   528  	var upaks []*keybase1.UserPlusKeysV2AllIncarnations
   529  	statements = make(map[keybase1.UID]*keybase1.UserEkStatement)
   530  	processResult := func(i int, upak *keybase1.UserPlusKeysV2AllIncarnations) error {
   531  		mctx.Debug("processing member %d/%d %.2f%% complete", i, len(uids), (float64(i) / float64(len(uids)) * 100))
   532  		if upak == nil {
   533  			mctx.Debug("Unable to load user %v", uids[i])
   534  			return nil
   535  		}
   536  		uv := upak.Current.ToUserVersion()
   537  		if !team.IsMember(mctx.Ctx(), uv) {
   538  			// Team membership may be stale, force a reload and check again
   539  			team, err = teams.Load(mctx.Ctx(), mctx.G(), keybase1.LoadTeamArg{
   540  				ID:          teamID,
   541  				ForceRepoll: true,
   542  			})
   543  			if err != nil {
   544  				return err
   545  			}
   546  			if !team.IsMember(mctx.Ctx(), uv) {
   547  				mctx.Debug("%v is not a member of team %v", uv, teamID)
   548  				return nil
   549  			}
   550  		}
   551  		upaks = append(upaks, upak)
   552  		return nil
   553  	}
   554  	if err = mctx.G().GetUPAKLoader().Batcher(mctx.Ctx(), getArg, processResult, 0); err != nil {
   555  		return nil, err
   556  	}
   557  	for _, upak := range upaks {
   558  		uid := upak.GetUID()
   559  		sig, ok := memberEKStatements.Sigs[uid]
   560  		if !ok {
   561  			mctx.Debug("missing memberEK statement for UID %v in team %v", uid, teamID)
   562  			continue
   563  		}
   564  		statement, _, wrongKID, err := verifySigWithLatestPUK(mctx, uid,
   565  			upak.Current.GetLatestPerUserKey(), sig)
   566  		if wrongKID {
   567  			mctx.Debug("UID %v has a statement signed with the wrongKID, skipping", uid)
   568  			// Don't box for this member since they have no valid userEK
   569  			continue
   570  		} else if err != nil {
   571  			return nil, err
   572  		}
   573  		statements[uid] = statement
   574  	}
   575  
   576  	return statements, nil
   577  }