Skip to content
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 11 commits into from
Oct 7, 2024
1 change: 0 additions & 1 deletion packages/access-client/src/agent-use-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export async function requestAccess(access, account, capabilities) {
* @param {object} opts
* @param {string} [opts.nonce] - nonce to use for the claim
* @param {boolean} [opts.addProofs] - whether to addProof to access agent
* @returns
*/
export async function claimAccess(
access,
Expand Down
4 changes: 2 additions & 2 deletions packages/capabilities/src/top.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @module
*/

import { capability, URI } from '@ucanto/validator'
import { capability, Schema } from '@ucanto/validator'
import { equalWith } from './utils.js'

/**
Expand All @@ -20,6 +20,6 @@ import { equalWith } from './utils.js'
*/
export const top = capability({
can: '*',
with: URI.match({ protocol: 'did:' }),
with: Schema.or(Schema.did(), Schema.literal('ucan:*')),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

derives: equalWith,
})
143 changes: 135 additions & 8 deletions packages/upload-api/src/access/claim.js
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,
}),
}
}
11 changes: 4 additions & 7 deletions packages/upload-api/src/access/confirm.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export async function confirm({ capability, invocation }, ctx) {
return delegationsResult
}

// Create session proofs, but containing no Space proofs. We'll store these,
// and generate the Space proofs on access/claim.
const [delegation, attestation] = await createSessionProofs({
service: ctx.signer,
account,
Expand All @@ -72,16 +74,11 @@ export async function confirm({ capability, invocation }, ctx) {
},
],
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: delegationsResult.ok,
delegationProofs: [],
expiration: Infinity,
})

// Store the delegations so that they can be pulled with access/claim.
// Store the delegations so that they can be pulled during access/claim.
// Since there is no invocation that contains these delegations, don't pass
// a `cause` parameter.
// TODO: we should invoke access/delegate here rather than interacting with
Expand Down
13 changes: 6 additions & 7 deletions packages/upload-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import type {
} from '@ucanto/interface'
import type { ProviderInput, ConnectionView } from '@ucanto/server'

import { Signer as EdSigner } from '@ucanto/principal/ed25519'
import { StorefrontService } from '@web3-storage/filecoin-api/types'
import { ServiceContext as FilecoinServiceContext } from '@web3-storage/filecoin-api/storefront/api'
import { DelegationsStorage as Delegations } from './types/delegations.js'
Expand Down Expand Up @@ -398,34 +397,34 @@ export type UploadServiceContext = ConsumerServiceContext &
SpaceServiceContext &
RevocationServiceContext &
ConcludeServiceContext & {
signer: EdSigner.Signer
signer: Signer
uploadTable: UploadTable
}

export interface AccessClaimContext {
signer: Signer
delegationsStorage: Delegations
}

export interface AccessServiceContext extends AccessClaimContext, AgentContext {
signer: EdSigner.Signer
email: Email
url: URL
provisionsStorage: Provisions
rateLimitsStorage: RateLimits
}

export interface ConsumerServiceContext {
signer: EdSigner.Signer
signer: Signer
provisionsStorage: Provisions
}

export interface CustomerServiceContext {
signer: EdSigner.Signer
signer: Signer
provisionsStorage: Provisions
}

export interface AdminServiceContext {
signer: EdSigner.Signer
signer: Signer
uploadTable: UploadTable
storeTable: StoreTable
}
Expand All @@ -446,7 +445,7 @@ export interface ProviderServiceContext {
}

export interface SubscriptionServiceContext {
signer: EdSigner.Signer
signer: Signer
provisionsStorage: Provisions
subscriptionsStorage: SubscriptionsStorage
}
Expand Down
Loading
Loading