-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Generate Space proofs on the fly, on access/claim
#1555
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
ed9771d
Generate Space proofs on the fly, on `access/claim`
Peeja 4e48393
Test claiming access to recent spaces
Peeja 31ee9fb
Remove rogue `@returns`
Peeja dba4db6
Add `signer` to the `account/claim` handler context
Peeja 7f16b60
Test: cannot gain unattested access
Peeja 9c94daa
Allow non-`ucan:*` delegations to still be claimed
Peeja fb13d85
Only refresh *attested* session proofs
Peeja 6c7a438
Ensure attacker can't use a fake attestation
Peeja 8bf7036
Refactor by flattening out conditionals
Peeja 5abf1ac
Keep capitalization consistent
Peeja 584d350
Use generic `Signer` interface, not `EdSigner`
Peeja File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,164 @@ | ||
import * as Server from '@ucanto/server' | ||
import * as Access from '@web3-storage/capabilities/access' | ||
import * as UCAN from '@ipld/dag-ucan' | ||
import * as API from '../types.js' | ||
import * as delegationsResponse from '../utils/delegations-response.js' | ||
import { createSessionProofs } from './confirm.js' | ||
|
||
/** | ||
* @param {API.AccessClaimContext} ctx | ||
*/ | ||
export const provide = (ctx) => | ||
Server.provide(Access.claim, (input) => claim(input, ctx)) | ||
|
||
/** | ||
* Checks if the given Principal is an Account. | ||
* @param {API.Principal} principal | ||
* @returns {principal is API.Principal<API.DID<'mailto'>>} | ||
*/ | ||
const isAccount = (principal) => principal.did().startsWith('did:mailto:') | ||
|
||
/** | ||
* Returns true when the delegation has a `ucan:*` capability. | ||
* @param {API.Delegation} delegation | ||
* @returns boolean | ||
*/ | ||
const isUCANStar = (delegation) => | ||
delegation.capabilities.some((capability) => capability.with === 'ucan:*') | ||
|
||
/** | ||
* Returns true when the capability is a `ucan/attest` capability for the given | ||
* signer. | ||
* | ||
* @param {API.Capability} capability | ||
* @returns {capability is API.UCANAttest} | ||
*/ | ||
const isUCANAttest = (capability) => capability.can === 'ucan/attest' | ||
|
||
/** | ||
* @param {API.Input<Access.claim>} input | ||
* @param {API.AccessClaimContext} ctx | ||
* @returns {Promise<API.Result<API.AccessClaimSuccess, API.AccessClaimFailure>>} | ||
*/ | ||
export const claim = async ( | ||
{ invocation }, | ||
{ delegationsStorage: delegations } | ||
) => { | ||
export const claim = async ({ invocation }, { delegationsStorage, signer }) => { | ||
const claimedAudience = invocation.capabilities[0].with | ||
const claimedResult = await delegations.find({ audience: claimedAudience }) | ||
if (claimedResult.error) { | ||
const storedDelegationsResult = await delegationsStorage.find({ | ||
audience: claimedAudience, | ||
}) | ||
|
||
if (storedDelegationsResult.error) { | ||
return { | ||
error: { | ||
name: 'AccessClaimFailure', | ||
message: 'error finding delegations', | ||
cause: claimedResult.error, | ||
cause: storedDelegationsResult.error, | ||
}, | ||
} | ||
} | ||
|
||
const delegationsToReturnByCid = Object.fromEntries( | ||
storedDelegationsResult.ok.map((delegation) => [delegation.cid, delegation]) | ||
) | ||
|
||
// Find any attested ucan:* delegations and replace them with fresh ones. | ||
for (const delegation of storedDelegationsResult.ok) { | ||
// Ignore delegations that aren't attestations, and ours. | ||
const attestCap = delegation.capabilities.find(isUCANAttest) | ||
if (!(attestCap && attestCap.with === signer.did())) continue | ||
|
||
// Ignore invalid attestations. | ||
const valid = | ||
(await UCAN.verifySignature(delegation.data, signer)) && | ||
!UCAN.isTooEarly(delegation.data) && | ||
!UCAN.isExpired(delegation.data) | ||
if (!valid) continue | ||
|
||
// Ignore attestations of delegations we don't have. | ||
const attestedCid = attestCap.nb.proof | ||
const attestedDelegation = delegationsToReturnByCid[attestedCid.toString()] | ||
if (!(attestedDelegation && isUCANStar(attestedDelegation))) continue | ||
|
||
// Create new session proofs for the attested delegation. | ||
const sessionProofsResult = await createSessionProofsForLogin( | ||
attestedDelegation, | ||
delegationsStorage, | ||
signer | ||
) | ||
|
||
// If something went wrong, bail on the entire invocation with the error. | ||
// NB: This breaks out of the loop, because if this fails at all, we don't | ||
// need to keep looking. | ||
if (sessionProofsResult.error) { | ||
return { | ||
error: { | ||
name: 'AccessClaimFailure', | ||
message: 'error creating session proofs', | ||
cause: sessionProofsResult.error, | ||
}, | ||
} | ||
} | ||
|
||
// Delete the ones we're replacing... | ||
delete delegationsToReturnByCid[delegation.cid.toString()] | ||
delete delegationsToReturnByCid[attestedCid.toString()] | ||
|
||
// ...and add the new ones. | ||
for (const proof of sessionProofsResult.ok) { | ||
delegationsToReturnByCid[proof.cid.toString()] = proof | ||
} | ||
} | ||
|
||
return { | ||
ok: { | ||
delegations: delegationsResponse.encode(claimedResult.ok), | ||
delegations: delegationsResponse.encode( | ||
Object.values(delegationsToReturnByCid) | ||
), | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* @param {API.Delegation} loginDelegation | ||
* @param {API.DelegationsStorage} delegationsStorage | ||
* @param {API.Signer} signer | ||
* @returns {Promise<API.Result<API.Delegation[], API.AccessClaimFailure>>} | ||
*/ | ||
async function createSessionProofsForLogin( | ||
loginDelegation, | ||
delegationsStorage, | ||
signer | ||
) { | ||
// These should always be Accounts (did:mailto:), but if one's not, skip it. | ||
if (!isAccount(loginDelegation.issuer)) return { ok: [] } | ||
|
||
const accountDelegationsResult = await delegationsStorage.find({ | ||
audience: loginDelegation.issuer.did(), | ||
}) | ||
|
||
if (accountDelegationsResult.error) { | ||
return { | ||
error: { | ||
name: 'AccessClaimFailure', | ||
message: 'error finding delegations', | ||
cause: accountDelegationsResult.error, | ||
}, | ||
} | ||
} | ||
|
||
return { | ||
ok: await createSessionProofs({ | ||
service: signer, | ||
account: loginDelegation.issuer, | ||
agent: loginDelegation.audience, | ||
facts: loginDelegation.facts, | ||
capabilities: loginDelegation.capabilities, | ||
// We include all the delegations to the account so that the agent will | ||
// have delegation chains to all the delegated resources. | ||
// We should actually filter out only delegations that support delegated | ||
// capabilities, but for now we just include all of them since we only | ||
// implement sudo access anyway. | ||
delegationProofs: accountDelegationsResult.ok, | ||
expiration: Infinity, | ||
}), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌