-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
160 additions
and
81 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -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} | ||
*/ | ||
|
@@ -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) | ||
|
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