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

     1  # Pseudocode rules
     2  #
     3  # - there are some basics types
     4  # - `func foo(int i, bar[] bars) baz` is a function that takes args of type `i`, an array of `bar`s and
     5  #   returns a baz.
     6  # - all calls by value, so no one ever modifies their arguments
     7  # - only "storage" should have any side effects
     8  #
     9  
    10  type teamSnapshot
    11  	- id: the teamID of this team
    12  	- fqName: fully qualified name
    13  	- seeds: Team key seeds for all generations
    14  	- Full Membership, without implied admins
    15  	- links: list of <seqno, linkHash, stubbed> triples for all links in the chain; if the 'stubbed' bool is set, then the link was stubbed, and we didn't actually verify the sig.
    16  	- adminBookends: the admins for this team; each admin can have multiple bookends if he/she was added/deleted multiple times
    17  	- parent: teamID of parent
    18  
    19  # Intended usage:
    20  #  - Chat encrypt: LoadTeam() then get the latest key
    21  #  - Chat decrypt: LoadTeam(NeedKeyGeneration: receivedKeyGen, WantMembers: [sender]) then checks membership.. um how will # it check membership for people who are no longer in the team?#
    22  #  - Chat UI: Load() then get whatever it needs cosmetically
    23  #  - CLI team management: LoadTeam(NeedAdmin: true, ForceSync: true)
    24  #  - KBFS: similar to chat, bust also wants cache busting notifications
    25  
    26  LoadArg
    27  	- teamID
    28  	- needAdmin: bool, whether we need to be an admin. Will fail unless we are an admin in the returned Team. It is unreasonable to look at invites and list subteams with this set to false.
    29  	- needKeyGen: int, Load at least up to the keygen. Returns an error if the keygen is not loaded.
    30  	- neededMembers: UIDs, Refresh if these members are not current members of the team in the cache. Does not guarantee these members will be present in the returned team.
    31  	- forceRepoll: bool, force a sync with merkle, if the caller knows they want this for some reason.
    32  
    33  func load(loadArg arg, storage storage)
    34  	ret := playchain(arg.teamID, arg.needAdmin, arg.forceRepoll, nil, storage)
    35  	if !arg.forceRepoll && (!hasAllMembers(ret, arg.neededMembers) || !hasKeyGeneration(ret, arg.neededKeyGen))
    36  		ret = playchain(arg.teamID, arg.needAdmin, true, nil, s)
    37  	return ret
    38  
    39  func assertProperlyStubbed(link link, seqno[] neededSeqnos, bool needAdmin)
    40  	if !isStubbed(link)
    41  		return
    42  	if (link.seqno in neededSeqnos) || needAdmin
    43  		throw "needed an unstubbed link"
    44  	if !linkCanBeStubbed(link)
    45  		throw "link type can't be stubbed"
    46  
    47  func playchain(teamID t, bool needAdmin, bool forceRepoll, seqno[] neededSeqnos, storage storage) teamSnaphot
    48  	# Load the team out of cold storage
    49  	ret, storedTime := get(storage, t)
    50  
    51  	if hasStubbedLinks(ret) && needAdmin
    52  		ret = nil
    53  
    54  	lastSeqno := nil
    55  	lastLinkID := nil
    56  	if (now() - storedTime > cacheTime) || ret.lastSeqno() < max(neededSeqnos) || forceRepoll
    57  		# Reference the merkle tree to fetch the last available sequence ID
    58  		# for the team in question.
    59  		lastSeqno, lastLinkID = fetchLastSeqnoFromServerMerkleTree(t)
    60  	else
    61  		lastSeqno = ret.lastSeqno()
    62          lastLinkID = ret.lastLinkID()
    63  
    64  	proofSet := newProofSet()
    65  	parentChildOperations := []
    66  
    67  	if neededSeqnos != nil
    68  		ret, proofSet, parentChildOperations = fillInStubbedLinks(ret, neededSeqnos, ret.lastSeqno(), proofSet, parentChildOperations, storage)
    69  
    70  	teamUpdate := nil
    71  	if ret == nil || ret.lastSeqno() < lastSeqno
    72  		teamUpdate = getNewLinksFromServer(t, ret.lastSeqno(), needAdmin)
    73  
    74  	prev := ret.links[-1].link
    75  	for link in teamUpdate.Links
    76  
    77  		assertProperlyStubbed(link, neededSeqnos, needAdmin)
    78  
    79  		assertHashEquality(link.prev, hash(prev))
    80  
    81  		proofSet = verifyLink(ret, link, proofSet, storage)
    82  
    83  		## ParentChildOperations affect a parent and child chain in lockstep.
    84  		## So far they are: subteam create, and subteam rename
    85  		## TODO need a new server ticket so that child chain changes on each rename too
    86  		if isParentChildOperation(link)
    87  			parentChildOperations.push(toParentChildOperation(link))
    88  
    89  		prev = link
    90  		ret = patchWithNewLink(ret, link)
    91  
    92  	if lastLinkID != ret.lastLinkID()
    93  		throw "link id mismatch"
    94  	checkParentChildOperations(ret.parent, parentChildOperations)
    95  	checkProofs(team, proofSet, storage)
    96  
    97  	ret = addSecrets(ret, teamUpdate.box, teamsUpdate.prevs, teamUpdate.readerKeyMasks)
    98  
    99  	put(storage, t, ret)
   100  
   101  	checkNeededSeqnos(ret, neededSeqnos)
   102  
   103  	return ret
   104  
   105  # An adminBookend on a sigchain that is delegated at the start, and maybe revoked
   106  # by the time we get to end.
   107  type adminBookend
   108  	- admin: userID
   109  	- start: link
   110  	- end: *link
   111  
   112  type teamUpdate
   113  	- links: list of links
   114  	- box: box for the current user
   115  	- prevs: prevs for previous shared keys
   116  	- readerKeyMasks: reader key masks for all apps for all generations for user
   117  
   118  type parentChildOperations
   119  	- childOperation: the actual operation that happened
   120  	- parent: Seqno where the operation appears in the parent
   121  
   122  type storage
   123  	- key -> value
   124  
   125  type leafID
   126  	- a UID or a teamID
   127  
   128  type proofTerm
   129  	- leafID: the UID or teamID for this proofTerm
   130  	- seqno: the seqno in the local chain
   131  	- linkHash: the hash of that link in the local chain
   132  	- merkleSeqno: the merkle seqno signed into the link
   133  	- merkleHashMeta: the meta hash signed into the link
   134  
   135  # We need proof that a happens before b, (i.e., a < b)
   136  type proof
   137  	- a: proofTerm
   138  	- b: proofTerm
   139  
   140  type proofSet
   141  	- proof[]: a list of proofs that are needed to be proven by keybase
   142  
   143  func verifyLink(teamSnapshot ts, link link, proofSet proofSet, storage storage) proofSet
   144  
   145  	// Note that it's possible to check this signature, but we'd need a way to lookup
   146  	// users from deviceIDs, and I'm not sure how much it's buying us.
   147  	if isStubbed(link)
   148  		return proofSet
   149  
   150  	// Check that inner and outer fields are in harmony
   151  	checkLinkOuterInnerMatch(link)
   152  
   153  	// just using what the signature says, verify it, and figure out
   154  	// which public key was used in the verification
   155  	kid := verifySignatureAndExtractKID(link)
   156  
   157  	// Load the user given the inner info in the sig's Body.Key
   158  	// section. Also load the key object specified there
   159  	user, key := loadUserAndKeyFromLinkInner(link, kid)
   160  
   161  	assert(key.KID == kid)
   162  
   163  	proofSet = verifyUserSignature(user, link, proofSet)
   164  
   165  	needsAdmin := linkNeedsAdmin(link)
   166  
   167  	if needsAdmin
   168  		# TODO needs a server change to be explicit about which implicit admin to use,
   169  		# and where on the chain this privilege was granted
   170  		ancestorTeamID, ancestorTeamSeqno := getTeamIdAndSeqnoOfAdminUsed(link)
   171  
   172  		if ancestorTeamID == nil
   173  			throw "need admin but link didn't specify which team admin to use"
   174  		tmp := ts
   175  		while tmp.id != ancestorTeamID
   176  			tmp = playchain(tmp.parent, false, false, {ancestorTeamSeqno}, storage)
   177  		if tmp == nil
   178  			throw "didn't find parent in chain"
   179  		proofSet = verifyUserIsAdmin(tmp, user, link, proofSet)
   180  	else
   181  		verifyUserIsWriter(ts, user, link)
   182  
   183  	return proofSet
   184  
   185  func verifyUserSignature(user user, link link, proofSet proofSet) proofSet
   186  	k := getKeyFromLink(link)
   187  	assertSigned(k, link)
   188  	a,b := findKeyInUserSigchain(user, k, link.merkleSeqno)
   189  	proofSet = happensBefore(proofSet, a, link)
   190  	if b != nil
   191  		proofSet = happensBefore(proofSet, link, b)
   192  	return proofSet
   193  
   194  func findKeyInUserSigchain(user user, key k, seqno merkleSeqno)
   195  	# iterate over all of the user's links, looking for the latest
   196  	# provisioning of k before merkleSeqo. Return that link, and
   197  	# also, if available, the next revocation of k after that link.
   198  
   199  func verifyUserIsAdmin(teamSnapshot ts, user user, link link, proofSet proofSet)
   200  	for pb in ts.adminBookends
   201  		if user.uid == pb.admin && (pb.start.merkleSeqno <= link.merkleSeqno) && (pb.end == nil && link.merklSeqno <= pb.end.merkleSeqno)
   202  			proofSet = happensBefore(proofSet, pb.start, link)
   203  			if pb.end != nil
   204  				proofSet = happensBefore(proofSet, link, pb.end)
   205  		return proofSet
   206  	throw "user wasn't admin in team"
   207  
   208  func checkParentChildOperations(teamID parentID, parentChildOperations[] parentChildOperations, storage stroage)
   209  	neededSeqnos := []
   210  	for pco in parentChildOperations
   211  		neededSeqnos.push(pco.parent)
   212  	parent := playchain(parentID, false, false, neededSeqnos, storage)
   213  	for pco in parentChildOperations
   214  		parentOp := linkToOperation(parent.links[pco.parent])
   215  		assertOperationEqual(parentOp, pco.childOperation)
   216  
   217  func checkProofs(teamSnapshot team, proofSet proofSet, storage storage)
   218  	for proof in proofSet
   219  		checkProof(team proof, storage)
   220  
   221  func checkProof(teamSnapshot team, proof proof, storage storage)
   222  	merklePath := getMerklePathFromRootToLeaf(proof.b.merkleSeqno, proof.a.leafID)
   223  	verifyMerklePath(merklePath, proof.a.leafID, proof.a.linkHash)
   224  	verifyMetaHash(merklePath.metaHash, proof.b.merkleMetaHash)
   225  	chainTail := merklePath[proof.a.leafID]
   226  	assert(chainTail.Seqno >= proof.a.seqno)
   227  	linkList = nil
   228  	if isUser(proof.a.leafID)
   229  		user := loadUPAK2(proof.a.leafID, storage)
   230  		# TODO this means we'll have to store all verified link hashes in a UPAK2
   231  		# including tracker link hashes.
   232  		linkList = user.links
   233  	else
   234  		if team.id != proof.a.leafID
   235  			team = playchain(proof.a.leafID, false, false, {proof.a.seqno}, storage)
   236  		linkList = team.links
   237  	assert(listList[chainTail.seqno] == chainTail.linkHash)
   238  
   239  func checkNeededSeqnos(teamSnapshot team, seqno[] neededSeqnos)
   240  	for seqno in neededSeqnos
   241  		if team.links[seqno].stubbed
   242  			throw "needed link not filled"
   243  
   244  func fillInStubbedLinks(teamSnapshot ret, seqno[] neededSeqnos, seqno upperLimit, proofSet proofSet, parentChildOperations, storage storage)
   245  			-> (teamSnapshot, proofSet, parentChildOperations)
   246  	# seqnos needed from the server
   247  	newLinkSeqnos := []
   248  	for seqno in neededSeqnos
   249  		if ret.links[seqno].stubbed && seqno <= upperLimit
   250  			newLinkSeqnos.push(seqno)
   251  
   252  	## TODO need a new server endpoint for this
   253  	newLinks := getLinksFromServer(ret.id, newLinkSeqnos)
   254  	for link in newLinks
   255  		assertIsntStubbed(link)
   256  		proofSet = verifyLink(pret, link, proofSet, storage)
   257  		ret.links[seqno].stubbed = false
   258  		ret = patchWithNewLink(ret, link)
   259  		if isParentChildOperation(link)
   260  			parentChildOperations = append(parentChildOperations, toParentChildOperation(link))
   261  
   262  	return ret, proofSet, parentChildOperations
   263  
   264  func happensBefore(proofSet proofSet, link a, link b) proofSet
   265  	a := toProofTerm(a)
   266  	b := toProofTerm(b)
   267  	for proof in proofSet by -1 # walk backwards to avoid O(n^2) hidden work factor
   268  		if a.leafID == proof.a.leafID && b.leafID == proof.b.leafID && proof.a.seqno <= a.seqno && b.seqno <= proof.b.seqno
   269  			proof.a = proofTermMax(proof.a, a)
   270  			proof.b = proofTermMin(proof.b, b)
   271  			return proofSet
   272  	proofSet.push(proof(a,b))
   273  	return proofSet
   274  
   275  func proofTermMax(proofTerm a, proofTerm b) proofTerm
   276  	if a.seqno > b.seqno
   277  		return a
   278  	else
   279  		return b
   280  
   281  func proofTermMin(proofTerm a, proofTerm b) proofTerm
   282  	if a.seqno < b.seqno
   283  		return a
   284  	else
   285  		return b
   286  
   287  func toProofTerm(link a) proofTerm
   288  	# for the given sigchain link, extract the seqno in the chain, the leafID of the chain
   289  	# (i.e., the UID or the teamID), the hash of the link, and the location in the merkle
   290  	# tree seen at the time the link was signed
   291  
   292  func addSecrets(teamSnapshot ts,....) teamSnapshot
   293  	# Update the proof set with the given secret values as fetched from the server
   294  	# output the new snapshot.
   295  	# Make sure the secrets are in sync and match the sigchain state.
   296  
   297  func patchWithNewLink(teamSnapshot ts, link link) teamSnapshot
   298  	# Update the teamSnapshot with the given link, and output a new teamSnapshot
   299  	# reflect the delta in the link. Should be idempotent, since we might call it twice
   300  	# in the case of fillInStubbedLinks.