Skip to content

Commit

Permalink
feat: new caps validation
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomrdias committed Feb 23, 2022
1 parent a502c14 commit 34e03d8
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 222 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"lint-staged": {
"*.{js,ts,d.ts,md,yml,json}": "prettier --write",
"*.{js}": "eslint --fix"
"*.js": "eslint --fix"
},
"devDependencies": {
"hd-scripts": "^1.1.0",
Expand Down
16 changes: 14 additions & 2 deletions packages/ucan-common/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,33 @@ export const storageSemantics = {
},

tryDelegating(parentCap, childCap) {
// check for unrelated caps

// console.log('\n', childCap, '\n', parentCap)
// must not escalate capability level
if (uploadLevels[childCap.can] > uploadLevels[parentCap.can]) {
// console.log('⚠️ Capability level escalation')
return {
escalation: 'Capability level escalation',
capability: childCap,
}
}

if (childCap.with.includes(parentCap.with)) {
if (!childCap.with.includes(parentCap.with)) {
// console.log('⚠️ Child resource does not match parent resource')
return {
escalation: 'Child resource must be under parent resource',
escalation: 'Child resource does not match parent resource',
capability: childCap,
}
}

// if (childCap.with.length <= parentCap.with.length) {
// return {
// escalation: 'Child resource must be under parent resource',
// capability: childCap,
// }
// }

return childCap
},
}
138 changes: 27 additions & 111 deletions packages/ucan-service/src/index.js
Original file line number Diff line number Diff line change
@@ -1,145 +1,61 @@
import * as ucan from 'ucan-storage'
import * as ucans from 'ucans'
import { storageSemantics } from 'ucan-common'
import { UcanChain } from './ucan-chain.js'

export class Service {
/**
* @param {any} privateKey
* @param {ucan.KeyPair} keypair
*/
constructor(privateKey) {
this.privateKey = privateKey
constructor(keypair) {
this.keypair = keypair
}

/**
* @param {any} did
*/
async ucan(did) {}

/**
* @param {string} encodedUcan
* @param {import('ucan-storage/dist/src/types').ValidateOptions} options
*/
static async verify(encodedUcan, options) {
const token = await UcanChained.fromToken(encodedUcan, options)

return token
}

/**
* @param {ucans.Chained} ucan
* @param {string} key
*/
static caps(ucan) {
return ucans.capabilities(ucan, storageSemantics)
static async fromPrivateKey(key) {
const kp = await ucan.KeyPair.fromExportedKey(key)
return new Service(kp)
}
}

export class UcanChained {
/**
* @param {string} encoded
* @param {import('ucan-storage/dist/src/types').Ucan<UcanChained>} decoded
*/
constructor(encoded, decoded) {
this._encoded = encoded
this._decoded = decoded
static async create() {
return new Service(await ucan.KeyPair.create())
}

/**
* @param {string} encodedUcan
* @param {import('ucan-storage/dist/src/types').ValidateOptions} options
* @returns {Promise<UcanChained>}
*/
static async fromToken(encodedUcan, options) {
const token = await ucan.validate(encodedUcan, options)

// parse proofs recursively
const proofs = await Promise.all(
token.payload.prf.map((encodedPrf) =>
UcanChained.fromToken(encodedPrf, options)
)
)

// check sender/receiver matchups. A parent ucan's audience must match the child ucan's issuer
const incorrectProof = proofs.find(
(proof) => proof.audience() !== token.payload.iss
)
if (incorrectProof) {
throw new Error(
`Invalid UCAN: Audience ${incorrectProof.audience()} doesn't match issuer ${
token.payload.iss
}`
)
}

const ucanTransformed = {
...token,
payload: {
...token.payload,
prf: proofs,
},
}

return new UcanChained(encodedUcan, ucanTransformed)
}

/**
* A representation of delegated capabilities throughout all ucan chains
*
* @template A
* @param {(ucan: import('ucan-storage/dist/src/types').Ucan, reducedProofs: () => Iterable<A>) => A} reduceLayer
* @returns {A}
*/
reduce(reduceLayer) {
// eslint-disable-next-line unicorn/no-this-assignment
const that = this
async validate(encodedUcan, options) {
const token = await UcanChain.fromToken(encodedUcan, options)

function* reduceProofs() {
for (const proof of that.proofs()) {
// eslint-disable-next-line unicorn/no-array-reduce
yield proof.reduce((accumulator, element) =>
reduceLayer(accumulator, element)
)
}
if (token.audience() !== this.did()) {
throw new Error('Invalid UCAN: Audience does not match this service.')
}

return reduceLayer(this.payload(), reduceProofs)
}

/**
* @returns { UcanChained[]} `prf`: Further UCANs possibly providing proof or origin for some capabilities in this UCAN.
*/
proofs() {
return this._decoded.payload.prf
return token
}

/**
*
* This is the identity this UCAN transfers rights to.
* It could e.g. be the DID of a service you're posting this UCAN as a JWT to,
* or it could be the DID of something that'll use this UCAN as a proof to
* continue the UCAN chain as an issuer.
* @param {ucans.Chained} ucan
*/
audience() {
return this._decoded.payload.aud
static caps(ucan) {
return ucans.capabilities(ucan, storageSemantics)
}

/**
* The UCAN must be signed with the private key of the issuer to be valid.
*/
issuer() {
return this._decoded.payload.iss
did() {
return this.keypair.did()
}

/**
* The payload the top level represented by this Chain element.
* Its proofs are omitted. To access proofs, use `.proofs()`
* @param {any} did
*/
payload() {
return {
...this._decoded,
payload: {
...this._decoded.payload,
prf: [],
},
}
ucan(did) {
return ucan.Ucan({
issuer: this.keypair,
audience: did,
capabilities: [{ with: `storage://${did}`, can: 'upload/*' }],
})
}
}
Loading

0 comments on commit 34e03d8

Please sign in to comment.