ucan-wg/ts-ucan

API for validation?

nichoth opened this issue ยท 13 comments

I was wondering what the API is for validating ucans. In my little demo app i used something ucan.isValid -- https://github.com/nichoth/ucan-demo/blob/main/src/index.js#L296 -- but I don't see it documented anywhere now.

There is this great example for creating a token -- https://github.com/ucan-wg/ts-ucan#example . it would be great to have something similar for validating the ucan chain.

What you need to do now that we have customizable capabilities is verify your UCAN in terms of some custom capability semantics.
I've got a PR up I need to get reviewed & merged, it defines hasCapability which can be used for exactly such purposes.

#53

Sorry that it's not as simple as isValid. The problem is that whether a UCAN is valid or not is - depending on what exactly is asked - not the right question or something that can't be answered easily. So what you essentially want to check is whether the given UCAN authorizes for some capability.

Ah ha thanks. I think I remember briefly chatting about this. The main thing being that there is no programmable way to check if a UCAN with a given cap is allowed to sign a new UCAN with another capability.

I suppose there are two types of validation then โ€” you could check if simply the merkle-list integrity of the UCAN and proof UCAN are valid (that the child has a reference to the parent), then you also need to check if the parent is able to create the given child capabilities.

Now that I think about it; the parent UCAN is just a string in the proof field, so there is nothing to check as far as a merkle-list is concerned; it's all about making sure the capabilities are ok with the parent's capabilities.

The main thing being that there is no programmable way to check if a UCAN with a given cap is allowed to sign a new UCAN with another capability

Oh well, there is - that's what we do with UCAN stores: We look through all the UCANs to find one that can grant given capability:

ts-ucan/src/store.ts

Lines 43 to 69 in 92d2281

findWithCapability<A>(
audience: string,
semantics: CapabilitySemantics<A>,
requirementsCap: A,
requirementsInfo: (info: CapabilityInfo) => boolean,
): { success: true; ucan: Chained } | FindFailure {
const ucans = this.index[audience]
if (ucans == null) {
return { success: false, reason: `Couldn't find any UCAN for audience ${audience}` }
}
for (const ucan of ucans) {
for (const result of capabilities(ucan, semantics)) {
if (isCapabilityEscalation(result)) continue
const { info, capability } = result
if (!requirementsInfo(info)) continue
const delegated = semantics.tryDelegating(capability, requirementsCap)
if (isCapabilityEscalation(delegated) || delegated == null) continue
return { success: true, ucan }
}
}
return { success: false, reason: `Couldn't find a UCAN with required capabilities` }
}
}

However, on the server side you'd still need to check the originator of your capabilities against your records, which is what the hasCapability function is for. (But also, you'd need to check that the last audience matches your DID).


you could check if simply the merkle-list integrity of the UCAN and proof UCAN are valid (that the child has a reference to the parent)

Now that I think about it; the parent UCAN is just a string in the proof field, so there is nothing to check as far as a merkle-list is concerned; it's all about making sure the capabilities are ok with the parent's capabilities.

Yeah - you're right. UCAN proofs can either be strings or CIDs (although CIDs aren't supported yet). So initially there's nothing to check before we've resolved them.

However, we can resolve/parse them recursively and check them while we're doing that. That's what happens in Chained.fromToken.

Initially I thought that that'd be the entry point to the library with the lower-level stuff (token.ts) exposed as well, but I realize that has become somewhat confusing.

It's great that you bring this stuff up. We'll want to revamp the public-facing API at some point, once we understand the whole problem space really well (we're in the process of figuring this out). This has given me some ideas.

Thanks for the explanation.

Now that I think about it; the parent UCAN is just a string in the proof field, so there is nothing to check as far as a merkle-list is concerned; it's all about making sure the capabilities are ok with the parent's capabilities.

Although, thinking more about this, we also have to check that the signatures are ok โ€” the parent needs to sign the child I think. But this should be checked by Chained.fromToken

So we need to use fromToken to make sure signatures are valid and also check that the capability semantics are ok.

For example, lets say that our application logic allows you to create a UCAN that gives read access to any subdirectory that you have access to.

my UCAN

capabilities: [
        {
          "wnfs": "example.fission.name/public/",
          "cap": "READ"
        },

a new UCAN I created

      capabilities: [
        {
          "wnfs": "example.fission.name/public/photos/",
          "cap": "READ"
        },

That's an example that is dependent on arbitrary application logic, as to whether that is allowed or not. I think that is the part that is confusing for me still. I've been looking at the test for attenuation โ€” https://github.com/ucan-wg/ts-ucan/blob/main/tests/attenuation.test.ts โ€” but I think the tests may be too low level for me in this case.

My plan was to write a test that demonstrates using hasCapability to check the validity of a UCAN, but I wasn't sure what that would look like.

There's a test that essentially tests the thing you've described. Maybe that helps?

it("works with a simple example", async () => {
const { leaf, ucan, chain } = await makeSimpleDelegation(
[{
wnfs: "boris.fission.name/public/Apps/",
cap: "OVERWRITE",
}],
[{
wnfs: "boris.fission.name/public/Apps/appinator/",
cap: "REVISE",
}]
)
expect(Array.from(wnfsPublicCapabilities(chain))).toEqual([
{
info: {
originator: alice.did(),
expiresAt: Math.min(leaf.payload.exp, ucan.payload.exp),
notBefore: maxNbf(leaf.payload.nbf, ucan.payload.nbf),
},
capability: {
user: "boris.fission.name",
publicPath: ["Apps", "appinator"],
cap: "REVISE",
}
}
])
})

(Also wanted to note that READ capabilities in WNFS are covered by sharing symmetric keys. UCANs are only for write permissions.)

Regarding everything else you wrote: It all seems consistent with my idea of how stuff works ๐Ÿ‘

Thanks for pointing me in that direction of the test. I will probably look at it later today

Thanks you for the help. Today I worked on writing a test, however I never did get typescript to stop underlining things ๐Ÿ˜ž Someday I need to learn what I'm doing.

https://github.com/nichoth/ts-ucan/blob/nichoth/hasCapability-test/tests/attenuation.test.ts#L213

Argument of type 'CapabilityWithInfo<EmailCapability> | CapabilityEscalation<EmailCapability>' is not assignable to parameter of type 'CapabilityWithInfo<Capability>'.
  Type 'CapabilityWithInfo<EmailCapability>' is not assignable to type 'CapabilityWithInfo<Capability>'.
    Type 'EmailCapability' is not assignable to type 'Capability'.
      Index signature for type 'string' is missing in type 'EmailCapability'.ts(2345)

I don't want to make extra work for anyone BTW, just thought I would share what I'm doing so far.

Hey! Don't feel bad โค๏ธ
This stuff is hard, and the interface is harder than it has to be right now, unfortunately. Through some other work I've been doing somewhere else right now & your feedback, I have some ideas on how to make it simpler!

I just created a PR for you that I hope helps figuring all of this out: nichoth#1

Thanks!

I merged and made a very small update so that the TS compiler is happy -- https://github.com/nichoth/ts-ucan/blob/nichoth/hasCapability-test/tests/attenuation.test.ts#L213

Another update -- i wrote a few tests that are useful for me -- https://github.com/nichoth/ts-ucan/blob/nichoth/hasCapability-test/tests/attenuation.test.ts#L229

Remaining questions --
The semantics API -- how is tryParsing used?

const testSemantics = {
  // ??? what is `tryParsing` used for?
  tryParsing(cap: Capability): Capability | null {
    return cap
  },

tryDelegating is where we check and see if a given parent UCAN is allowed to create a given child UCAN with some permissions.

So to check if a UCAN is allowed, you would want to use Chained.fromToken to check if the signatures are valid, then use hasCapability to check if the capabilities are allowed to be granted by the parent UCAN.

That will be my next task โ€” writing a test that demonstrates a good flow for checking if a UCAN is valid for allowing some arbitrary permission.

The semantics API -- how is tryParsing used?

In the example from your code it isn't used for anything.

But in general, it can be used to pre-process capabilities for (1) filtering out capabilities you don't know how to process (the UCAN might contain capabilities meant to be used for another service that you don't know it's capability semantics for) and (2) converting them into a custom type of your choice to make the implementation of tryDelegating easier.

See for example the implementation of the public WNFS capability, which essentially parses the capability that contains a path as a string into a path as an array of path segments:

/**
* Example valid public wnfs capability:
* ```js
* {
* wnfs: "boris.fission.name/public/path/to/dir/or/file",
* cap: "OVERWRITE"
* }
* ```
*/
tryParsing(cap: Capability): WnfsPublicCapability | null {
if (typeof cap.wnfs !== "string" || !isWnfsCap(cap.cap)) return null
// remove trailing slash
const trimmed = cap.wnfs.endsWith("/") ? cap.wnfs.slice(0, -1) : cap.wnfs
const split = trimmed.split("/")
const user = split[0]
const publicPath = split.slice(2) // drop first two: matheus23.fission.name/public/keep/this
if (user == null || split[1] !== "public") return null
return {
user,
publicPath,
cap: cap.cap,
}
},

I see that the current API is confusing. I want to reduce the API surface in the future by removing the need for the Chained.fromToken call and the need for specifying tryParsing :)