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

     1  package teams
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  	"time"
     7  
     8  	"encoding/base64"
     9  
    10  	"golang.org/x/net/context"
    11  
    12  	"github.com/davecgh/go-spew/spew"
    13  	"github.com/keybase/client/go/kbtest"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/keybase1"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  // TestSeitanV2Encryption does an offline test run of seitan crypto
    20  // functions.
    21  func TestSeitanV2Encryption(t *testing.T) {
    22  	tc := SetupTest(t, "team", 1)
    23  	defer tc.Cleanup()
    24  
    25  	user, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
    26  	require.NoError(t, err)
    27  
    28  	name := createTeam(tc)
    29  
    30  	team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{
    31  		Name:        name,
    32  		NeedAdmin:   true,
    33  		ForceRepoll: true,
    34  	})
    35  	require.NoError(t, err)
    36  
    37  	ikey, err := GenerateIKeyV2()
    38  	require.NoError(t, err)
    39  	t.Logf("ikey is: %q (%d)\n", ikey, len(ikey))
    40  
    41  	sikey, err := ikey.GenerateSIKey()
    42  	require.NoError(t, err)
    43  	t.Logf("sikey is: %v (%d)\n", sikey, len(sikey))
    44  
    45  	keyPair, err := sikey.generateKeyPair()
    46  	require.NoError(t, err)
    47  	pubKey := keybase1.SeitanPubKey(keyPair.Public.GetKID()[:])
    48  
    49  	inviteID, err := sikey.GenerateTeamInviteID()
    50  	require.NoError(t, err)
    51  	t.Logf("Invite id is: %s\n", inviteID)
    52  	require.Equal(t, len(string(inviteID)), 32)
    53  
    54  	var expectedLabelSms keybase1.SeitanKeyLabelSms
    55  	expectedLabelSms.F = "edwin powell hubble"
    56  	expectedLabelSms.N = "+48123zz3045"
    57  
    58  	expectedLabel := keybase1.NewSeitanKeyLabelWithSms(expectedLabelSms)
    59  
    60  	pkey, encoded, err := sikey.GeneratePackedEncryptedKey(context.TODO(), team, expectedLabel)
    61  	require.NoError(t, err)
    62  	require.EqualValues(t, pkey.Version, 2)
    63  	require.EqualValues(t, pkey.TeamKeyGeneration, 1)
    64  	require.NotZero(tc.T, pkey.RandomNonce)
    65  
    66  	t.Logf("Encrypted ikey with gen: %d\n", pkey.TeamKeyGeneration)
    67  	t.Logf("Armored output: %s\n", encoded)
    68  
    69  	expectedPKey, err := SeitanDecodePKey(encoded)
    70  	require.NoError(t, err)
    71  	require.Equal(t, expectedPKey.Version, pkey.Version)
    72  	require.Equal(t, expectedPKey.TeamKeyGeneration, pkey.TeamKeyGeneration)
    73  	require.Equal(t, expectedPKey.RandomNonce, pkey.RandomNonce)
    74  	require.Equal(t, expectedPKey.EncryptedKeyAndLabel, pkey.EncryptedKeyAndLabel)
    75  
    76  	keyAndLabel, err := pkey.DecryptKeyAndLabel(context.TODO(), team)
    77  	require.NoError(t, err)
    78  	keyAndLabelType, err := keyAndLabel.V()
    79  	require.NoError(t, err)
    80  	require.Equal(t, keybase1.SeitanKeyAndLabelVersion_V2, keyAndLabelType)
    81  	keyAndLabelV2 := keyAndLabel.V2()
    82  	require.EqualValues(t, pubKey, keyAndLabelV2.K)
    83  
    84  	label := keyAndLabelV2.L
    85  	labelType, err := label.T()
    86  	require.NoError(t, err)
    87  	require.Equal(t, keybase1.SeitanKeyLabelType_SMS, labelType)
    88  
    89  	labelSms := label.Sms()
    90  	require.Equal(t, expectedLabelSms.F, labelSms.F)
    91  	require.Equal(t, expectedLabelSms.N, labelSms.N)
    92  
    93  	t.Logf("Decrypted pubKey is %q\n", keyAndLabelV2.K)
    94  
    95  	uid := user.User.GetUID()
    96  	eldestSeqno := user.EldestSeqno
    97  	ctime := keybase1.ToTime(time.Now())
    98  	msg, err := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID, ctime)
    99  	require.NoError(t, err)
   100  	sig, _, err := sikey.GenerateSignature(uid, eldestSeqno, inviteID, ctime)
   101  	require.NoError(t, err)
   102  
   103  	require.NoError(t, VerifySeitanSignatureMessage(SeitanPubKey(keyPair.Public), msg, sig))
   104  }
   105  
   106  func TestSeitanBadSignatures(t *testing.T) {
   107  	tc := SetupTest(t, "team", 1)
   108  	defer tc.Cleanup()
   109  
   110  	user, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   111  	require.NoError(t, err)
   112  
   113  	ikey1, err := GenerateIKeyV2()
   114  	require.NoError(t, err)
   115  
   116  	sikey1, err := ikey1.GenerateSIKey()
   117  	require.NoError(t, err)
   118  
   119  	inviteID1, err := sikey1.GenerateTeamInviteID()
   120  	require.NoError(t, err)
   121  
   122  	keyPair1, err := sikey1.generateKeyPair()
   123  	require.NoError(t, err)
   124  
   125  	uid := user.User.GetUID()
   126  	eldestSeqno := user.EldestSeqno
   127  	ctime := keybase1.ToTime(time.Now())
   128  	sig, _, err := sikey1.GenerateSignature(uid, eldestSeqno, inviteID1, ctime)
   129  	require.NoError(t, err)
   130  
   131  	// Check signature verification failure for using the wrong sikey
   132  	ikey2, err := GenerateIKeyV2()
   133  	require.NoError(t, err)
   134  
   135  	sikey2, err := ikey2.GenerateSIKey()
   136  	require.NoError(t, err)
   137  
   138  	keyPair2, err := sikey2.generateKeyPair()
   139  	require.NoError(t, err)
   140  
   141  	msg, err := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID1, ctime)
   142  	require.NoError(t, err)
   143  	require.Error(t, VerifySeitanSignatureMessage(SeitanPubKey(keyPair2.Public), msg, sig))
   144  
   145  	type Badmsg struct {
   146  		msg []byte
   147  		err error
   148  	}
   149  	badMsgs := make([]Badmsg, 4)
   150  
   151  	// Check signature verification failure for a bad uid
   152  	msgBadUID, errBadUID := GenerateSeitanSignatureMessage(uid+"a", eldestSeqno, inviteID1, ctime)
   153  	badMsgs = append(badMsgs, Badmsg{msgBadUID, errBadUID})
   154  
   155  	// Check signature verification failure for a bad EldestSeqno
   156  	msgBadEldest, errBadEldest := GenerateSeitanSignatureMessage(uid, eldestSeqno+1, inviteID1, ctime)
   157  	badMsgs = append(badMsgs, Badmsg{msgBadEldest, errBadEldest})
   158  
   159  	// Check signature verification failure for a bad InviteID
   160  	msgBadInviteID, errBadInviteID := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID1+"a", ctime)
   161  	badMsgs = append(badMsgs, Badmsg{msgBadInviteID, errBadInviteID})
   162  
   163  	// Check signature verification failure for a bad ctime
   164  	msgBadCTime, errBadCTime := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID1, ctime+1)
   165  	badMsgs = append(badMsgs, Badmsg{msgBadCTime, errBadCTime})
   166  
   167  	for _, bad := range badMsgs {
   168  		require.NoError(t, bad.err)
   169  		require.Error(t, VerifySeitanSignatureMessage(SeitanPubKey(keyPair1.Public), bad.msg, sig))
   170  	}
   171  
   172  }
   173  
   174  // TestSeitanV2KnownSamples runs offline seitan crypto chain using known
   175  // inputs and compares results with known samples generated using
   176  // server test library.
   177  func TestSeitanV2KnownSamples(t *testing.T) {
   178  	fromB64 := func(b string) (ret []byte) {
   179  		ret, err := base64.StdEncoding.DecodeString(b)
   180  		require.NoError(t, err)
   181  		return ret
   182  	}
   183  
   184  	// secret_key: GBsy8q2vgQ6jEHmQZiNJcxvgxVNlG4IsxKf/zcxtJIA=
   185  	// ikey: 4uywza+b3cga7rd6yc
   186  	// sikey: Il9ZFgI1yP2b6Hvt53jWIoo8sDre3puyNH8b2es9TTQ=
   187  	// inviteID: 6303ec43bd61d21edb95a433faf06227
   188  	// pkey: lAIBxBgxk+0rwtlCacCIzNK8apyoiiN69+tTU1HEgze4fphJmImUv7wFm54ioO9dB876yLUciHsuUItYXH1cSq6cShz2HrjVCSUQCJVxDNQwb3A6x2zv6/mrbUselphhjzxrJFGb6mS7N0cA3cYfdk+WByNEUOVqi6qwzgvAYuwEqM1sAYYb+NgrLEH5+4Tlr5mcWfAtLynLngX3Z4Ef4Mf1
   189  	// label: {"sms":{"f":"Alice","n":"111-555-222"},"t":1}
   190  
   191  	var expectedSIKey SeitanSIKeyV2
   192  	copy(expectedSIKey[:], fromB64("Il9ZFgI1yP2b6Hvt53jWIoo8sDre3puyNH8b2es9TTQ="))
   193  	expectedInviteID := SCTeamInviteID("6303ec43bd61d21edb95a433faf06227")
   194  
   195  	var secretKey keybase1.Bytes32
   196  	copy(secretKey[:], fromB64("GBsy8q2vgQ6jEHmQZiNJcxvgxVNlG4IsxKf/zcxtJIA="))
   197  
   198  	pkeyBase64 := "lAIBxBgxk+0rwtlCacCIzNK8apyoiiN69+tTU1HEgze4fphJmImUv7wFm54ioO9dB876yLUciHsuUItYXH1cSq6cShz2HrjVCSUQCJVxDNQwb3A6x2zv6/mrbUselphhjzxrJFGb6mS7N0cA3cYfdk+WByNEUOVqi6qwzgvAYuwEqM1sAYYb+NgrLEH5+4Tlr5mcWfAtLynLngX3Z4Ef4Mf1"
   199  
   200  	ikey := SeitanIKeyV2("4uywza+b3cga7rd6yc")
   201  	sikey, err := ikey.GenerateSIKey()
   202  	require.NoError(t, err)
   203  	require.Equal(t, sikey, expectedSIKey)
   204  
   205  	inviteID, err := sikey.GenerateTeamInviteID()
   206  	require.NoError(t, err)
   207  	require.Equal(t, inviteID, expectedInviteID)
   208  
   209  	keyPair, err := sikey.generateKeyPair()
   210  	require.NoError(t, err)
   211  
   212  	expectedPKey, err := SeitanDecodePKey(pkeyBase64)
   213  	require.NoError(t, err)
   214  	require.EqualValues(t, 2, expectedPKey.Version)
   215  	require.EqualValues(t, 1, expectedPKey.TeamKeyGeneration)
   216  
   217  	keyAndLabel, err := expectedPKey.decryptKeyAndLabelWithSecretKey(secretKey)
   218  	require.NoError(t, err) // only encoded map or array can be decoded into a struct
   219  
   220  	keyAndLabelVersion, err := keyAndLabel.V()
   221  	require.NoError(t, err)
   222  	require.Equal(t, keybase1.SeitanKeyAndLabelVersion_V2, keyAndLabelVersion)
   223  	keyAndLabelV2 := keyAndLabel.V2()
   224  	pubKey := keyAndLabelV2.K
   225  
   226  	require.Equal(t, keybase1.SeitanPubKey(keyPair.GetKID().String()), pubKey)
   227  
   228  	label := keyAndLabelV2.L
   229  	labelType, err := label.T()
   230  	require.NoError(t, err)
   231  	require.Equal(t, keybase1.SeitanKeyLabelType_SMS, labelType)
   232  
   233  	labelSms := label.Sms()
   234  	require.Equal(t, "Alice", labelSms.F)
   235  	require.Equal(t, "111-555-222", labelSms.N)
   236  
   237  	pkey, _, err := sikey.generatePackedEncryptedKeyWithSecretKey(secretKey, keybase1.PerTeamKeyGeneration(1), expectedPKey.RandomNonce, keyAndLabelV2.L)
   238  	require.NoError(t, err)
   239  	require.Equal(t, expectedPKey.Version, pkey.Version)
   240  	require.Equal(t, expectedPKey.TeamKeyGeneration, pkey.TeamKeyGeneration)
   241  	require.Equal(t, expectedPKey.RandomNonce, pkey.RandomNonce)
   242  	require.Equal(t, expectedPKey.EncryptedKeyAndLabel, pkey.EncryptedKeyAndLabel)
   243  }
   244  
   245  // TestIsSeitanyAndAlphabetCoverage tests two unrelated things at once: (1) that
   246  // the IsSeitany function correctly identifies Seitan tokens; and (2) that all
   247  // letters of the Seitan alphabet are hit by generating a sufficient number of
   248  // tokens. It would be bad, for instance, if we only hit 10% of the characters.
   249  func TestIsSeitanyAndAlphabetCoverage(t *testing.T) {
   250  
   251  	ikeyV1Gen := func() (s string, err error) {
   252  		ikey, err := GenerateIKey()
   253  		return ikey.String(), err
   254  	}
   255  
   256  	ikeyV2Gen := func() (s string, err error) {
   257  		ikey, err := GenerateIKeyV2()
   258  		return ikey.String(), err
   259  	}
   260  
   261  	verifyCoverage := func(ikeyGen func() (s string, err error)) {
   262  		coverage := make(map[byte]bool)
   263  		for i := 0; i < 100; i++ {
   264  			s, err := ikeyGen()
   265  			require.NoError(t, err)
   266  			require.True(t, IsSeitany(s))
   267  			require.True(t, IsSeitany(s[2:10]))
   268  			require.True(t, IsSeitany(s[3:13]))
   269  			for _, b := range []byte(s) {
   270  				coverage[b] = true
   271  			}
   272  		}
   273  
   274  		// This test can fail with probability 1-(29/30)^(1800), which is
   275  		// approximately (1 - 2^-88)
   276  		for _, b := range []byte(KBase30EncodeStd) {
   277  			require.True(t, coverage[b], "covered all chars")
   278  		}
   279  	}
   280  	verifyCoverage(ikeyV1Gen)
   281  	verifyCoverage(ikeyV2Gen)
   282  }
   283  
   284  func TestTeamHandleMultipleSeitans(t *testing.T) {
   285  	tc := SetupTest(t, "team", 1)
   286  	defer tc.Cleanup()
   287  
   288  	tc.Tp.SkipSendingSystemChatMessages = true
   289  
   290  	users := make([]*kbtest.FakeUser, 4)
   291  	for i := range users {
   292  		u, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   293  		require.NoError(t, err)
   294  		kbtest.Logout(tc)
   295  		users[i] = u
   296  	}
   297  
   298  	ann, bee, dan, mel := users[0], users[1], users[2], users[3]
   299  	err := ann.Login(tc.G)
   300  	require.NoError(t, err)
   301  
   302  	teamName, teamID := createTeam2(tc)
   303  	t.Logf("Created team %s", teamName.String())
   304  
   305  	_, err = AddMember(context.TODO(), tc.G, teamName.String(), dan.Username, keybase1.TeamRole_WRITER, nil /* botSettings */)
   306  	require.NoError(t, err)
   307  
   308  	addSeitanV2 := func(F, N string, role keybase1.TeamRole) keybase1.SeitanIKeyV2 {
   309  		label := keybase1.NewSeitanKeyLabelWithSms(keybase1.SeitanKeyLabelSms{
   310  			F: F,
   311  			N: N,
   312  		})
   313  		ikeyV2, err := CreateSeitanTokenV2(context.TODO(), tc.G, teamName.String(), role, label)
   314  		require.NoError(t, err)
   315  		return ikeyV2
   316  	}
   317  
   318  	tokenForBee := addSeitanV2("bee", "123", keybase1.TeamRole_WRITER)
   319  	// tokenForDan := addSeitanV2("dan", "555", keybase1.TeamRole_READER)
   320  	anotherToken := addSeitanV2("someone", "666", keybase1.TeamRole_READER)
   321  
   322  	teamObj, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{
   323  		Name:      teamName.String(),
   324  		NeedAdmin: true,
   325  	})
   326  	require.NoError(t, err)
   327  
   328  	invites := teamObj.GetActiveAndObsoleteInvites()
   329  	require.Len(t, invites, 2)
   330  	for _, invite := range invites {
   331  		invtype, err := invite.Type.C()
   332  		require.NoError(t, err)
   333  		require.Equal(t, keybase1.TeamInviteCategory_SEITAN, invtype)
   334  	}
   335  
   336  	acceptSeitan := func(u *kbtest.FakeUser, ikey keybase1.SeitanIKeyV2, corrupt bool) keybase1.TeamSeitanRequest {
   337  		kbtest.LogoutAndLoginAs(tc, u)
   338  
   339  		uv := u.GetUserVersion()
   340  		now := keybase1.ToTime(time.Now())
   341  		accepted, err := generateAcceptanceSeitanV2(SeitanIKeyV2(ikey), uv, now)
   342  		require.NoError(t, err)
   343  
   344  		if corrupt {
   345  			// Ruin the acceptance sig so request is no longer valid
   346  			accepted.sig[0] ^= 0xF0
   347  			accepted.sig[1] ^= 0x0F
   348  			accepted.encoded = base64.StdEncoding.EncodeToString(accepted.sig[:])
   349  		}
   350  
   351  		// We need to send this request so HandleTeamSeitan links can
   352  		// do completed_invites, otherwise server will reject these.
   353  		err = postSeitanV2(tc.MetaContext(), accepted)
   354  		require.NoError(t, err)
   355  
   356  		return keybase1.TeamSeitanRequest{
   357  			InviteID:    keybase1.TeamInviteID(accepted.inviteID),
   358  			Uid:         uv.Uid,
   359  			EldestSeqno: uv.EldestSeqno,
   360  			Akey:        keybase1.SeitanAKey(accepted.encoded),
   361  			UnixCTime:   int64(now),
   362  		}
   363  	}
   364  
   365  	msg := keybase1.TeamSeitanMsg{
   366  		TeamID: teamID,
   367  		Seitans: []keybase1.TeamSeitanRequest{
   368  			acceptSeitan(bee, tokenForBee, false /* corrupt */),
   369  			// TODO: Accepting seitan while you are already in team is disabled because
   370  			// of Y2K-1898. Re-enable this after.
   371  			// acceptSeitan(dan, tokenForDan, false /* corrupt */),
   372  			acceptSeitan(mel, anotherToken, true /* corrupt */),
   373  		},
   374  	}
   375  
   376  	kbtest.LogoutAndLoginAs(tc, ann)
   377  
   378  	API := libkb.NewAPIArgRecorder(tc.G.API)
   379  	tc.G.API = API
   380  	err = HandleTeamSeitan(context.TODO(), tc.G, msg)
   381  	require.NoError(t, err)
   382  	records := API.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool {
   383  		return rec.Arg.Endpoint == "team/reject_invite_acceptance"
   384  	})
   385  	require.Len(t, records, 0, "no invite link acceptances were rejected")
   386  
   387  	teamObj, err = Load(context.TODO(), tc.G, keybase1.LoadTeamArg{
   388  		Name:      teamName.String(),
   389  		NeedAdmin: true,
   390  	})
   391  	require.NoError(t, err)
   392  
   393  	// Ann is still an owner
   394  	role, err := teamObj.MemberRole(context.Background(), ann.GetUserVersion())
   395  	require.NoError(t, err)
   396  	require.Equal(t, keybase1.TeamRole_OWNER, role)
   397  
   398  	// Bee got added as a writer
   399  	role, err = teamObj.MemberRole(context.Background(), bee.GetUserVersion())
   400  	require.NoError(t, err)
   401  	require.Equal(t, keybase1.TeamRole_WRITER, role)
   402  
   403  	// Dan stayed writer
   404  	role, err = teamObj.MemberRole(context.Background(), dan.GetUserVersion())
   405  	require.NoError(t, err)
   406  	require.Equal(t, keybase1.TeamRole_WRITER, role)
   407  
   408  	// Mel didn't get in
   409  	role, err = teamObj.MemberRole(context.Background(), mel.GetUserVersion())
   410  	require.NoError(t, err)
   411  	require.Equal(t, keybase1.TeamRole_NONE, role)
   412  
   413  	// And invite that Mel tried (and failed) to use is still there.
   414  	require.Equal(t, 1, teamObj.NumActiveInvites(), "NumActiveInvites")
   415  	allInvites := teamObj.GetActiveAndObsoleteInvites()
   416  	require.Len(t, allInvites, 1)
   417  	for _, invite := range allInvites {
   418  		// Ignore errors, we went through this path before in seitan
   419  		// processing and acceptance.
   420  		sikey, _ := SeitanIKeyV2(anotherToken).GenerateSIKey()
   421  		inviteID, _ := sikey.GenerateTeamInviteID()
   422  		require.EqualValues(t, inviteID, invite.Id)
   423  		invtype, err := invite.Type.C()
   424  		require.NoError(t, err)
   425  		require.Equal(t, keybase1.TeamInviteCategory_SEITAN, invtype)
   426  	}
   427  }
   428  
   429  func TestTeamInviteSeitanV2Failures(t *testing.T) {
   430  	tc := SetupTest(t, "team", 1)
   431  	defer tc.Cleanup()
   432  
   433  	tc.Tp.SkipSendingSystemChatMessages = true
   434  
   435  	user2, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   436  	require.NoError(t, err)
   437  	kbtest.Logout(tc)
   438  
   439  	admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   440  	require.NoError(t, err)
   441  
   442  	teamName, teamID := createTeam2(tc)
   443  	t.Logf("Created team %q", teamName.String())
   444  
   445  	token, err := CreateSeitanTokenV2(context.Background(), tc.G,
   446  		teamName.String(), keybase1.TeamRole_WRITER, keybase1.SeitanKeyLabel{})
   447  	require.NoError(t, err)
   448  
   449  	t.Logf("Created token %q", token)
   450  
   451  	kbtest.LogoutAndLoginAs(tc, user2)
   452  
   453  	// Generate invitation id, but make Signature with different IKey.
   454  	// Simulate "replay attack" or similar.
   455  	ikey, err := ParseIKeyV2FromString(string(token))
   456  	require.NoError(t, err)
   457  	sikey, err := ikey.GenerateSIKey()
   458  	require.NoError(t, err)
   459  	inviteID, err := sikey.GenerateTeamInviteID()
   460  	require.NoError(t, err)
   461  
   462  	ikey2, err := GenerateIKeyV2() // ikey2 is not the ikey from token.
   463  	require.NoError(t, err)
   464  	sikey2, err := ikey2.GenerateSIKey()
   465  	require.NoError(t, err)
   466  	now := keybase1.ToTime(time.Now())
   467  	badSig, badEncoded, err := sikey2.GenerateSignature(user2.GetUID(), user2.EldestSeqno, inviteID, now)
   468  	require.NoError(t, err)
   469  
   470  	err = postSeitanV2(tc.MetaContext(), acceptedSeitanV2{
   471  		sig:      badSig,
   472  		encoded:  badEncoded,
   473  		now:      now,
   474  		inviteID: inviteID,
   475  	})
   476  	require.NoError(t, err)
   477  
   478  	teamInviteID, err := inviteID.TeamInviteID()
   479  	require.NoError(t, err)
   480  
   481  	t.Logf("handle synthesized rekeyd command")
   482  	kbtest.LogoutAndLoginAs(tc, admin)
   483  
   484  	msg := keybase1.TeamSeitanMsg{
   485  		TeamID: teamID,
   486  		Seitans: []keybase1.TeamSeitanRequest{{
   487  			InviteID:    teamInviteID,
   488  			Uid:         user2.GetUID(),
   489  			EldestSeqno: user2.EldestSeqno,
   490  			Akey:        keybase1.SeitanAKey(badEncoded),
   491  			Role:        keybase1.TeamRole_WRITER,
   492  			UnixCTime:   int64(now),
   493  		}},
   494  	}
   495  	API := libkb.NewAPIArgRecorder(tc.G.API)
   496  	tc.G.API = API
   497  	err = HandleTeamSeitan(context.TODO(), tc.G, msg)
   498  	// Seitan handler does not fail, but ignores the request.
   499  	require.NoError(t, err)
   500  	records := API.GetFilteredRecordsAndReset(func(rec *libkb.APIRecord) bool {
   501  		return rec.Arg.Endpoint == "team/reject_invite_acceptance"
   502  	})
   503  	require.Len(t, records, 0, "no invite link acceptances were rejected")
   504  
   505  	t.Logf("invite should still be there")
   506  	t0, err := GetTeamByNameForTest(context.Background(), tc.G, teamName.String(), false /* public */, true /* needAdmin */)
   507  	require.NoError(t, err)
   508  	require.Equal(t, 1, t0.NumActiveInvites(), "invite should still be active")
   509  	require.EqualValues(t, t0.CurrentSeqno(), 2)
   510  
   511  	t.Logf("user should not be in team")
   512  	role, err := t0.MemberRole(context.Background(), user2.GetUserVersion())
   513  	require.NoError(t, err)
   514  	require.Equal(t, keybase1.TeamRole_NONE, role, "user role")
   515  }
   516  
   517  func TestSeitanPukless(t *testing.T) {
   518  	// Test what happens if client receives handle Seitan notification with an
   519  	// acceptance that's of a PUKless user. If a user can't be added as a
   520  	// crypto-member (using 'team.change_membership' link), they should not be
   521  	// added at all during Seitan resolution, because adding a type='keybase'
   522  	// invitation using 'team.invite' link cannot complete Seitan invite
   523  	// properly.
   524  
   525  	tc := SetupTest(t, "team", 1)
   526  	defer tc.Cleanup()
   527  
   528  	tc.Tp.SkipSendingSystemChatMessages = true
   529  
   530  	admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   531  	require.NoError(t, err)
   532  	t.Logf("Admin username: %s", admin.Username)
   533  
   534  	teamName, teamID := createTeam2(tc)
   535  	t.Logf("Created team %q", teamName.String())
   536  
   537  	token, err := CreateSeitanTokenV2(context.Background(), tc.G,
   538  		teamName.String(), keybase1.TeamRole_WRITER, keybase1.SeitanKeyLabel{})
   539  	require.NoError(t, err)
   540  
   541  	t.Logf("Created token %q", token)
   542  
   543  	kbtest.Logout(tc)
   544  
   545  	// Create a PUKless user
   546  	tc.Tp.DisableUpgradePerUserKey = true
   547  	user, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   548  	require.NoError(t, err)
   549  
   550  	t.Logf("User: %s", user.Username)
   551  
   552  	timeNow := keybase1.ToTime(tc.G.Clock().Now())
   553  	seitanRet, err := generateAcceptanceSeitanV2(SeitanIKeyV2(token), user.GetUserVersion(), timeNow)
   554  	require.NoError(t, err)
   555  
   556  	// Can't post this acceptance when we don't have a PUK.
   557  	err = postSeitanV2(tc.MetaContext(), seitanRet)
   558  	require.Error(t, err)
   559  	require.IsType(t, libkb.AppStatusError{}, err)
   560  	require.EqualValues(t, keybase1.StatusCode_SCTeamSeitanInviteNeedPUK, err.(libkb.AppStatusError).Code)
   561  
   562  	// But server could still send it to us, e.g. due to a bug.
   563  	kbtest.LogoutAndLoginAs(tc, admin)
   564  
   565  	inviteID, err := seitanRet.inviteID.TeamInviteID()
   566  	require.NoError(t, err)
   567  
   568  	msg := keybase1.TeamSeitanMsg{
   569  		TeamID: teamID,
   570  		Seitans: []keybase1.TeamSeitanRequest{{
   571  			InviteID:    inviteID,
   572  			Uid:         user.GetUID(),
   573  			EldestSeqno: user.EldestSeqno,
   574  			Akey:        keybase1.SeitanAKey(seitanRet.encoded),
   575  			Role:        keybase1.TeamRole_WRITER,
   576  			UnixCTime:   int64(timeNow),
   577  		}},
   578  	}
   579  	err = HandleTeamSeitan(context.Background(), tc.G, msg)
   580  	require.NoError(t, err)
   581  
   582  	// HandleTeamSeitan should not have added an invite for user. If it has, it
   583  	// also hasn't completed invite properly (`team.invite` link can't complete
   584  	// invite), which means the invite has been used but left active.
   585  	team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{
   586  		Name:        teamName.String(),
   587  		NeedAdmin:   true,
   588  		ForceRepoll: true,
   589  	})
   590  	require.NoError(t, err)
   591  
   592  	invite, _, found := team.FindActiveKeybaseInvite(user.GetUID())
   593  	require.False(t, found, "Expected not to find invite for user: %s", spew.Sdump(invite))
   594  }
   595  
   596  func TestSeitanMultipleRequestForOneInvite(t *testing.T) {
   597  	// Test server sending a Seitan notifications with multiple request for one
   598  	// Seitan invite. Seitan V1/V2 can never be multiple use, so at most one
   599  	// request should be handled.
   600  
   601  	tc := SetupTest(t, "team", 1)
   602  	defer tc.Cleanup()
   603  
   604  	tc.Tp.SkipSendingSystemChatMessages = true
   605  
   606  	admin, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   607  	require.NoError(t, err)
   608  
   609  	teamName, teamID := createTeam2(tc)
   610  
   611  	token, err := CreateSeitanTokenV2(context.Background(), tc.G,
   612  		teamName.String(), keybase1.TeamRole_WRITER, keybase1.SeitanKeyLabel{})
   613  	require.NoError(t, err)
   614  
   615  	// Create two users
   616  	var users [2]*kbtest.FakeUser
   617  	for i := range users {
   618  		kbtest.Logout(tc)
   619  
   620  		user, err := kbtest.CreateAndSignupFakeUser("team", tc.G)
   621  		require.NoError(t, err)
   622  		users[i] = user
   623  	}
   624  
   625  	timeNow := keybase1.ToTime(tc.G.Clock().Now())
   626  
   627  	var acceptances [2]acceptedSeitanV2
   628  	for i, user := range users {
   629  		kbtest.LogoutAndLoginAs(tc, user)
   630  		seitanRet, err := generateAcceptanceSeitanV2(SeitanIKeyV2(token), user.GetUserVersion(), timeNow)
   631  		require.NoError(t, err)
   632  		acceptances[i] = seitanRet
   633  
   634  		if i == 0 {
   635  			// First user has to PostSeitan so invite is changed to ACCEPTED on
   636  			// the server.
   637  			err = postSeitanV2(tc.MetaContext(), seitanRet)
   638  			require.NoError(t, err)
   639  		}
   640  	}
   641  
   642  	kbtest.LogoutAndLoginAs(tc, admin)
   643  
   644  	inviteID, err := acceptances[0].inviteID.TeamInviteID()
   645  	require.NoError(t, err)
   646  
   647  	var seitans [2]keybase1.TeamSeitanRequest
   648  	for i, user := range users {
   649  		seitans[i] = keybase1.TeamSeitanRequest{
   650  			InviteID:    inviteID,
   651  			Uid:         user.GetUID(),
   652  			EldestSeqno: user.EldestSeqno,
   653  			Akey:        keybase1.SeitanAKey(acceptances[i].encoded),
   654  			Role:        keybase1.TeamRole_WRITER,
   655  			UnixCTime:   int64(timeNow),
   656  		}
   657  	}
   658  	msg := keybase1.TeamSeitanMsg{
   659  		TeamID:  teamID,
   660  		Seitans: seitans[:],
   661  	}
   662  	err = HandleTeamSeitan(context.Background(), tc.G, msg)
   663  	if err != nil {
   664  		if err, ok := errors.Unwrap(err).(libkb.AppStatusError); ok {
   665  			// We are expecting no error, but if there's a specific bug that we can
   666  			// recognize, inform about it.
   667  			if err.Code == int(keybase1.StatusCode_SCTeamInviteCompletionMissing) {
   668  				require.FailNowf(t,
   669  					"Got error which suggests that bad change_membership was sent to the server.",
   670  					"%s", err.Error())
   671  			}
   672  		}
   673  	}
   674  	require.NoError(t, err)
   675  
   676  	// First request should have been fulfilled, so users[0] should have been
   677  	// added. Second request should have been ignored.
   678  	team, err := Load(context.TODO(), tc.G, keybase1.LoadTeamArg{
   679  		Name:        teamName.String(),
   680  		NeedAdmin:   true,
   681  		ForceRepoll: true,
   682  	})
   683  	require.NoError(t, err)
   684  
   685  	require.True(t, team.IsMember(context.TODO(), users[0].GetUserVersion()))
   686  	require.False(t, team.IsMember(context.TODO(), users[1].GetUserVersion()))
   687  }