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!: upgrade capabilities to latest ucanto #463

Merged
merged 8 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"prettier": "2.8.3",
"simple-git-hooks": "^2.8.1",
"typedoc-plugin-markdown": "^3.14.0",
"typescript": "4.9.4",
"typescript": "4.9.5",
"wrangler": "^2.8.0"
},
"simple-git-hooks": {
Expand Down
17 changes: 9 additions & 8 deletions packages/access-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@
"lint": "tsc --build && eslint '**/*.{js,ts}' && prettier --check '**/*.{js,ts,yml,json}' --ignore-path ../../.gitignore",
"dev": "scripts/cli.js dev",
"build": "scripts/cli.js build",
"check": "tsc --build",
"test": "pnpm build && mocha --bail --timeout 10s -n no-warnings -n experimental-vm-modules",
"test-watch": "pnpm build && mocha --bail --timeout 10s --watch --parallel -n no-warnings -n experimental-vm-modules --watch-files src,test"
},
"author": "Hugo Dias <hugomrdias@gmail.com> (hugodias.me)",
"license": "(Apache-2.0 OR MIT)",
"dependencies": {
"@ipld/dag-ucan": "^3.2.0",
"@ucanto/core": "^4.2.3",
"@ucanto/interface": "^4.2.3",
"@ucanto/principal": "^4.2.3",
"@ucanto/server": "^4.2.3",
"@ucanto/transport": "^4.2.3",
"@ucanto/validator": "^4.2.3",
"@ucanto/core": "^4.4.0",
"@ucanto/interface": "^4.4.1",
"@ucanto/principal": "^4.4.0",
"@ucanto/server": "^4.4.1",
"@ucanto/transport": "^4.4.0",
"@ucanto/validator": "^4.4.0",
"@web3-storage/access": "workspace:^",
"@web3-storage/capabilities": "workspace:^",
"@web3-storage/worker-utils": "0.4.3-dev",
Expand All @@ -45,7 +46,7 @@
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.18",
"@types/qrcode": "^1.5.0",
"@ucanto/client": "^4.2.3",
"@ucanto/client": "^4.4.0",
"better-sqlite3": "8.0.1",
"buffer": "^6.0.3",
"dotenv": "^16.0.3",
Expand All @@ -59,7 +60,7 @@
"process": "^0.11.10",
"readable-stream": "^4.2.0",
"sade": "^1.8.1",
"typescript": "4.9.4",
"typescript": "4.9.5",
"wrangler": "^2.8.0"
},
"eslintConfig": {
Expand Down
92 changes: 63 additions & 29 deletions packages/access-api/src/routes/validate-email.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable no-unused-vars */
import { stringToDelegation } from '@web3-storage/access/encoding'
import {
stringToDelegation,
delegationsToString,
} from '@web3-storage/access/encoding'
import * as Access from '@web3-storage/capabilities/access'
import QRCode from 'qrcode'
import { toEmail } from '../utils/did-mailto.js'
Expand All @@ -11,7 +14,7 @@ import {
} from '../utils/html.js'
import * as ucanto from '@ucanto/core'
import * as validator from '@ucanto/validator'
import { Verifier } from '@ucanto/principal/ed25519'
import { Verifier, Absentee } from '@ucanto/principal'

/**
* @param {import('@web3-storage/worker-utils/router').ParsedRequest} req
Expand Down Expand Up @@ -132,53 +135,84 @@ async function recover(req, env) {
* @param {import('../bindings.js').RouteContext} env
*/
async function session(req, env) {
/** @type {import('@ucanto/interface').Delegation<[import('@web3-storage/capabilities/src/types.js').AccessSession]>} */
/** @type {import('@ucanto/interface').Delegation<[import('@web3-storage/capabilities/src/types.js').AccessAuthorize]>} */
const delegation = stringToDelegation(req.query.ucan)
await env.models.validations.putSession(
req.query.ucan,
delegation.capabilities[0].nb.key
)

// ⚠️ This is not an ideal solution but we do need to ensure that attacker
// cannot simply send a valid `access/authorize` delegation to the service
// and get an attested session.
if (delegation.issuer.did() !== env.signer.did()) {
throw new Error('Delegation MUST be issued by the service')
}

// TODO: Figure when do we go through a post vs get request. WebSocket message
// was send regardless of the method, but delegations were only stored on post
// requests.
if (req.method.toLowerCase() === 'post') {
const accessSessionResult = await validator.access(delegation, {
capability: Access.session,
capability: Access.authorize,
principal: Verifier,
authority: env.signer,
})

if (accessSessionResult.error) {
throw new Error(
`unable to validate access session: ${accessSessionResult.error}`
)
}
const account = accessSessionResult.audience
const agentPubkey = accessSessionResult.capability.nb.key
const wrappedKeyCanAsignForAccount = await ucanto.delegate({

// Create a absentee signer for the account that authorized the delegation
const account = Absentee.from({ id: accessSessionResult.capability.nb.iss })
const agent = Verifier.parse(accessSessionResult.capability.with)

// It the future we should instead render a page and allow a user to select
// which delegations they wish to re-delegate. Right now we just re-delegate
// everything that was requested for all of the resources.
const capabilities =
/** @type {ucanto.UCAN.Capabilities} */
(
accessSessionResult.capability.nb.att.map(({ can }) => ({
can,
with: /** @type {ucanto.UCAN.Resource} */ ('ucan:*'),
}))
)

// create an authorization on behalf of the account with an absent
// signature.
const authorization = await ucanto.delegate({
issuer: account,
audience: agent,
capabilities,
expiration: Infinity,
// We should also include proofs with all the delegations we have for
// the account.
})

const attestation = await ucanto.delegate({
issuer: env.signer,
audience: { did: () => agentPubkey },
audience: agent,
capabilities: [
{
with: env.signer.did(),
can: 'access-api/delegation',
can: 'ucan/attest',
nb: { proof: authorization.cid },
},
],
proofs: [
await ucanto.delegate({
issuer: env.signer,
audience: account,
capabilities: [
{
with: env.signer.did(),
can: './update',
nb: {
key: agentPubkey,
},
},
],
}),
],
expiration: Infinity,
})
await env.models.delegations.putMany(wrappedKeyCanAsignForAccount)

// Store the delegations so that they can be pulled with access/claim
await env.models.delegations.putMany(authorization, attestation)

// Send delegations to the client through a websocket
await env.models.validations.putSession(
delegationsToString([authorization, attestation]),
agent.did()
)
}

// TODO: We clearly should not render that access/delegate in the QR code, but
// I'm not sure what this QR code is used for.
Copy link
Contributor

Choose a reason for hiding this comment

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

same

try {
return new HtmlResponse(
(
Expand Down
26 changes: 16 additions & 10 deletions packages/access-api/src/service/access-authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,27 @@ export function accessAuthorizeProvider(ctx) {
return Server.provide(
Access.authorize,
async ({ capability, invocation }) => {
const session = await Access.session
/**
* We re-delegate the capability to the account DID and limit it's
* lifetime to 15 minutes which should be enough time for the user to
* complete the authorization. We don't want to allow authorization for
* long time because it could be used by an attacker to gain authorization
* by sending second request misleading a user to click a wrong one.
*/
const authorization = await Access.authorize
.invoke({
issuer: ctx.signer,
audience: DID.parse(capability.nb.as),
with: ctx.signer.did(),
lifetimeInSeconds: 86_400 * 7, // 7 days
nb: {
key: capability.with,
},
audience: DID.parse(capability.nb.iss),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

information coming in is

{
   with: "did:key:zAlice",
   can: "access/authorize",  
   nb: {
      iss:  "did:malito:web.mail:alice"
      att: [{ can: "*" }]
  }
}

Copy link
Contributor Author

@Gozala Gozala Mar 1, 2023

Choose a reason for hiding this comment

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

{ 
   "iss": "did:web:web3.storage",
   "aud": "did:malito:web.mail:alice",
   "expiration": 15,
   "att": [{
        can: "access/confirm",
        with: "did:web:web.storage",
        nb: {
            aud: "did:key:zAlice",
            iss: "did:malito:web.mail:alice",
            att: [{ can: "*" }]
        }
    }]
}

with: capability.with,
lifetimeInSeconds: 60 * 15, // 15 minutes
nb: capability.nb,
proofs: [invocation],
})
.delegate()

const encoded = delegationToString(session)
const encoded = delegationToString(authorization)

await ctx.models.accounts.create(capability.nb.as)
await ctx.models.accounts.create(capability.nb.iss)

const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=session`
// For testing
Expand All @@ -37,7 +43,7 @@ export function accessAuthorizeProvider(ctx) {
}

await ctx.email.sendValidation({
to: Mailto.toEmail(capability.nb.as),
to: Mailto.toEmail(capability.nb.iss),
url,
})
}
Expand Down
3 changes: 2 additions & 1 deletion packages/access-api/src/service/access-claim.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export function accessClaimProvider(ctx) {
return Server.provide(claim, async ({ invocation }) => {
// disable until hardened in test/staging
if (ctx.config.ENV === 'production') {
throw new Error(`acccess/claim invocation handling is not enabled`)
throw new Error(`access/claim invocation handling is not enabled`)
}

return handleClaimInvocation(invocation)
})
}
Expand Down
Loading