Skip to content

Commit

Permalink
Test: cannot gain unattested access
Browse files Browse the repository at this point in the history
  • Loading branch information
Peeja committed Oct 2, 2024
1 parent dba4db6 commit 7f16b60
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 81 deletions.
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:*')),
derives: equalWith,
})
229 changes: 153 additions & 76 deletions packages/upload-api/test/access-client-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as API from './types.js'
import { Absentee } from '@ucanto/principal'
import * as delegationsResponse from '../src/utils/delegations-response.js'
import * as DidMailto from '@web3-storage/did-mailto'
import { Access, Space } from '@web3-storage/capabilities'
import { Access, Space, Top } from '@web3-storage/capabilities'
import { AgentData } from '@web3-storage/access'
import { alice } from './helpers/utils.js'
import { stringToDelegations } from '@web3-storage/access/encoding'
Expand All @@ -24,6 +24,80 @@ import {
} from '@web3-storage/access/agent'
import * as Provider from '@web3-storage/access/provider'

/**
* Create and return a space, delegated to the device agent and to the account,
* and provisioned for the account.
*
* @param {Agent} device
* @param {API.Principal<import('@ucanto/interface').DID<"mailto">>} account
* @param {API.Assert} assert
*/
const createSpace = async (device, account, assert) => {
const space = await device.createSpace(
`space-test-${Math.random().toString().slice(2)}`
)

assert.ok(space.did())
// provision space with an account so it can store delegations
const provisionResult = await Provider.add(device, {
account: account.did(),
consumer: space.did(),
})
assert.ok(provisionResult.ok)

// authorize device
const auth = await space.createAuthorization(device, {
access: AgentAccess.spaceAccess,
expiration: Infinity,
})
await device.importSpaceFromDelegation(auth)

// make space current
await device.setCurrentSpace(space.did())

const recovery = await space.createRecovery(account.did())
const delegateResult = await AgentAccess.delegate(device, {
delegations: [recovery],
})
assert.ok(delegateResult.ok)
return space
}

/**
* Claim (`access/claim`) delegations for the device agent, and add them to the
* agent's proofs.
*
* @param {Agent} device
*/
const claimDelegations = async (device) => {
await claimAccess(device, device.issuer.did(), {
addProofs: true,
nonce: Math.random().toString(),
})
}

/**
* Assert that the device agent can invoke `space/info` on the given space.
*
* @param {Agent} device
* @param {import('@web3-storage/access/agent').OwnedSpace} space
* @param {API.Assert} assert
*/
async function assertCanSpaceInfo(device, space, assert) {
const spaceInfoResult = await device.invokeAndExecute(Space.info, {
with: space.did(),
})

assert.equal(spaceInfoResult.out.error, undefined)

assert.ok(spaceInfoResult.out.ok)
const result =
/** @type {import('@web3-storage/access/types').SpaceInfoResult} */ (
spaceInfoResult.out.ok
)
assert.deepEqual(result.did, space.did())
}

/**
* @type {API.Tests}
*/
Expand Down Expand Up @@ -240,104 +314,107 @@ export const test = {
const account = Absentee.from({ id: DidMailto.fromEmail(email) })

// first device
const deviceAAgentData = await AgentData.create()
const deviceA = await Agent.create(deviceAAgentData, {
const deviceA = await Agent.create(await AgentData.create(), {
connection,
})
await requestAccess(deviceA, account, [{ can: '*' }])
await confirmConfirmationUrl(deviceA.connection, await mail.take())
await claimDelegations(deviceA)

/**
* @param {Agent} device
*/
const createSpace = async (device) => {
const space = await device.createSpace(
`space-test-${Math.random().toString().slice(2)}`
)

assert.ok(space.did())
// provision space with an account so it can store delegations
const provisionResult = await Provider.add(device, {
account: account.did(),
consumer: space.did(),
})
assert.ok(provisionResult.ok)

// authorize device
const auth = await space.createAuthorization(device, {
access: AgentAccess.spaceAccess,
expiration: Infinity,
})
await device.importSpaceFromDelegation(auth)

// make space current
await device.setCurrentSpace(space.did())
// deviceA creates a space
const space1 = await createSpace(deviceA, account, assert)

const recovery = await space.createRecovery(account.did())
const delegateResult = await AgentAccess.delegate(device, {
delegations: [recovery],
})
assert.ok(delegateResult.ok)
return space
}
// second device
const deviceB = await Agent.create(await AgentData.create(), {
connection,
})
await requestAccess(deviceB, account, [{ can: '*' }])
await confirmConfirmationUrl(deviceB.connection, await mail.take())
await claimDelegations(deviceB)

/**
* @param {Agent} device
*/
const claimDelegations = async (device) => {
await claimAccess(device, device.issuer.did(), {
addProofs: true,
nonce: Math.random().toString(),
})
}
// issuer + account proofs should authorize deviceB to invoke space/info
await assertCanSpaceInfo(deviceB, space1, assert)

/**
* @param {import('@web3-storage/access/agent').OwnedSpace} space
*/
async function assertDeviceBCanSpaceInfo(space) {
const spaceInfoResult = await deviceB.invokeAndExecute(Space.info, {
with: space.did(),
})
// deviceA creates another space
const space2 = await createSpace(deviceA, account, assert)

assert.equal(spaceInfoResult.out.error, undefined)
// deviceB claims delegations again
await claimDelegations(deviceB)

assert.ok(spaceInfoResult.out.ok)
const result =
/** @type {import('@web3-storage/access/types').SpaceInfoResult} */ (
spaceInfoResult.out.ok
)
assert.deepEqual(result.did, space.did())
}
// now deviceB should be able to invoke space/info on space2
await assertCanSpaceInfo(deviceB, space2, assert)
},
'cannot gain unattested access': async (assert, context) => {
const { connection, mail } = context
const email = '[email protected]'
const account = Absentee.from({ id: DidMailto.fromEmail(email) })

// deviceA authorization
// first device
const deviceA = await Agent.create(await AgentData.create(), {
connection,
})
await requestAccess(deviceA, account, [{ can: '*' }])
await confirmConfirmationUrl(deviceA.connection, await mail.take())
await claimDelegations(deviceA)

// deviceA creates a space
const space = await createSpace(deviceA)
const space = await createSpace(deviceA, account, assert)

// second device - deviceB
const deviceBData = await AgentData.create()
const accountCheater = Absentee.from({
id: DidMailto.fromEmail('[email protected]'),
})

const deviceB = await Agent.create(deviceBData, {
// second device is unrelated
const deviceCheater = await Agent.create(await AgentData.create(), {
connection,
})

// authorize deviceB
await requestAccess(deviceB, account, [{ can: '*' }])
await confirmConfirmationUrl(deviceB.connection, await mail.take())
await claimDelegations(deviceB)
await requestAccess(deviceCheater, accountCheater, [{ can: '*' }])
await confirmConfirmationUrl(deviceCheater.connection, await mail.take())
await claimDelegations(deviceCheater)

// issuer + account proofs should authorize deviceB to invoke space/info
await assertDeviceBCanSpaceInfo(space)
const spaceCheater = await createSpace(
deviceCheater,
accountCheater,
assert
)

// deviceA creates another space
const space2 = await createSpace(deviceA)
// deviceCheater shouldn't have access to the space
deviceCheater.setCurrentSpace(space.did())

// deviceB claims delegations again
await claimDelegations(deviceB)
// deviceCheater tries to craft a delegation to gain access to the account
const unattestedDelegation = await Top.top.delegate({
issuer: Absentee.from({ id: account.did() }),
audience: deviceCheater,
with: 'ucan:*',
expiration: Infinity,
proofs: [],
})

// now deviceB should be able to invoke space/info on space2
await assertDeviceBCanSpaceInfo(space2)
// Then they store the delegation on the server
const delegateResult = await AgentAccess.delegate(deviceCheater, {
delegations: [unattestedDelegation],
space: spaceCheater.did(),
})

assert.ok(
delegateResult.ok,
`delegateResult.error: ${delegateResult.error}`
)

// Then they claim their delegations. If their attack is successful, they,
// now have access to the account and all its spaces.
await claimDelegations(deviceCheater)

// Assert that they still can't do anything to the space.
await assert.rejects(
deviceCheater.invokeAndExecute(Space.info, {
with: space.did(),
}),
{
message: `no proofs available for resource ${space.did()} and ability space/info`,
}
)
},
'can addSpacesFromDelegations': async (assert, context) => {
const { agent } = await setup(context)
Expand Down
8 changes: 5 additions & 3 deletions packages/upload-api/test/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as API from '../src/types.js'
import assert from 'node:assert'

export * from '../src/types.js'

Expand All @@ -19,13 +20,14 @@ export interface Assert {
actual: Actual,
expected: Expected,
message?: string
) => unknown
) => void
deepEqual: <Actual, Expected extends Actual>(
actual: Actual,
expected: Expected,
message?: string
) => unknown
ok: <Actual>(actual: Actual, message?: string) => unknown
) => void
ok: <Actual>(actual: Actual, message?: string) => void
rejects: typeof assert.rejects
}

export type Test = (assert: Assert, context: TestContext) => unknown
Expand Down

0 comments on commit 7f16b60

Please sign in to comment.