feat: clients can find proof chains as servers do#85
Conversation
alanshaw
left a comment
There was a problem hiding this comment.
Urgh, I cannot wait for us to get to UCAN 1.0.
| Proofs() []Authorization[Caveats] | ||
| // Attestations returns ucan/attest delegations that were used to authorize | ||
| // non-did:key issuers (e.g. did:mailto accounts) in this authorization. | ||
| Attestations() []Authorization[any] |
There was a problem hiding this comment.
Why not?
| Attestations() []Authorization[any] | |
| Attestations() []Authorization[Caveats] |
There was a problem hiding this comment.
it's not Caveats because these are ucan/attest delegations, as opposed to the other ones, which refer to the claimed capability.
| // delegation being built by a client to send over a size-constrained channel | ||
| // (e.g. an HTTP header). | ||
| // | ||
| // dlg must be built with the full candidate proof pool — all proof blocks |
There was a problem hiding this comment.
This is a little odd - why would you go through the process of creating and signing a delegation that will subsequently be thrown away?
I'd perhaps frame this as pruning proofs from a delegation whose proof set may contain additional delegations that are not necessary to prove the delegation is valid.
There was a problem hiding this comment.
I changed the wording in the comment in an attempt to clarify and cover what I explained in my comment below, let me know if that works better.
| walk(attest) | ||
| } | ||
| for _, prf := range a.Proofs() { | ||
| walk(prf) |
There was a problem hiding this comment.
This will hoist all proofs in the chain and they will end up listed as proofs of the root delegation, even if they do not directly prove it.
i.e. this shouldn't be necessary.
There was a problem hiding this comment.
ah, yeah, good catch, thanks!
| // Note: the capability in vctx must match the capability in dlg, or | ||
| // PruneProofs will return [Unauthorized]. If dlg contains multiple | ||
| // capabilities, only the chain for the first matching one is discovered. | ||
| func PruneProofs[Caveats any](ctx context.Context, dlg delegation.Delegation, vctx ValidationContext[Caveats]) ([]delegation.Proof, Unauthorized) { |
There was a problem hiding this comment.
I think it's okay for this to exist, but ideally you want to create a delegation with only the proofs that are necessary. If you have the ability to prune, then you have the ability to select the minimal set of proofs for your delegation in the first place.
This is perhaps useful when you are given a random delegation and want to send it somewhere for storage, and you want to minimise the amount of data stored/transferred. In the case where you use said delegation (in an invocation) then you can select the relevant proofs and you wouldn't use this function...right?
There was a problem hiding this comment.
yeah, I admit is a bit awkward. The thing is that principal alignment is checked per "level". If there is a root delegation, Claim will attempt to find a valid chain that aligns with the issuer of the delegation. However, if given a list of delegations at the same level, the first one that is self-issued (such as one from the space to the account) would work as a valid chain.
This is really an artefact of me trying to re-use Claim, and Claim being tailored towards verifying proof chains in the server, where everything emanates from a single invocation. In the client, we need a way to tell Claim what's the intended principal we want to build a chain for. Encapsulating that in a delegation is what allows re-using Claim as is. We could also modify it to get that target principal, but I'd rather touching that code as little as possible. Besides, Claim is called recursively in a bunch of places where an additional target principal doesn't really make sense.
I do see building delegations from scratch as a main use case of this function. You can also prune random delegations, but you'll only be able to properly sign the pruned version if you were the original issuer. Alternatively, you can use the output of PruneProofs to filter blocks from the delegation's blockstore without actually rebuilding it. It will still link to proofs whose blocks are not attached anymore, but that shouldn't break anything, I think.
Maybe having SelectProofs take a single delegation instead of a list would make this more clear. I used a list to mirror the Access+Claims pattern, but it might just be confusing.
|
|
||
| // Re-export the delegation into a fresh blockstore so it only carries | ||
| // the blocks from its own proof chain, not the full candidate pool that | ||
| // was inherited from the draft's blockstore. |
There was a problem hiding this comment.
Right, but Export() doesn't prune proofs of proofs. i.e. what if the proof has a bunch of candidates?
...I guess you can't really do this since you can't re-sign a proof....
There was a problem hiding this comment.
yeah, if they contain unnecessary proofs themselves there's not much we can do
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
Offer proof chain optimization (proof pruning) as an option when creating delegations. Removes the need of callers having to roll their own logic to implement the `draft delegation -> proof pruning -> final delegation` pattern and encapsulates it in a single place so that it's easier to refactor in the future. The `ProofChainOptimizer` type is required to prevent an import cycle issue, because the `validator` package imports `delegation`, so we cannot use `validator` directly here. Callers will need to call `validator.NewProofChainOptimizer` themselves and pass the result in the option. Not the most straightforward API, but it's a good trade-off. An alternative would be to extract the shared types into a third package, but that would require a larger refactor (we might consider doing it in the future).
Ref. storacha/guppy#378
This PR adds
validator.PruneProofsandvalidator.SelectProofs. They are for clients whatvalidator.Accessandvalidator.Claimare for servers:SelectProofswalks a successful Claim authorization tree and returns the minimal flat set ofdelegation.Delegationsneeded to prove the claimed capability, including anyucan/attestdelegations used along the way.PruneProofsis the client-side counterpart toAccess: given a draft delegation carrying the full candidate proof pool, returns only thedelegation.Proofsactually needed to form a valid chain. Each returned proof is re-exported into a fresh blockstore (viaExport()) so it carries only its own chain blocks rather than the entire inherited pool.These functions enable a client to confirm the proof chain is correct before sending it to the server and also reduce the amount of data sent. The latter is especially interesting when these delegations need to travel in HTTP headers.
Implementation details
Changes to existing functions have the goal of surfacing
ucan/attestdelegations as part of the chain. The current implementation checks them (VerifySession) but doesn't add them to the returnedAuthorizationobject. This works because servers only useAccessto confirm the chain is valid, but rarely use the actual proofs in the chain. The client, however, needs all the relevant proofs in the chain if it wants an invocation to succeed.To do so,
ValidateandVerifyAuthorizationnow return theucan/attestauthorization used when verifying a non-did:key issuer. This is threaded throughResolveSources,ResolveMatch,Authorize, andClaim, and exposed via a newAttestations() []Authorization[any]method on theAuthorizationinterface.Authorizeuses anattestationsForhelper to filter attestations down to only those whosenb.prooflink matches delegations that match.