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

v1.0.0-rc.1 #2

Open
wants to merge 131 commits into
base: main
Choose a base branch
from
Open

v1.0.0-rc.1 #2

wants to merge 131 commits into from

Conversation

expede
Copy link
Member

@expede expede commented Oct 14, 2023

I may get pilloried for this version. WIP, obviously

Preview 📚

Okay, this version switches to IPLD. This makes it much easier to not need a IPLD version off to the side, and many teams that have adopted UCAN already use IPLD somewhere in their stack. There is a contingent of people that feel strongly in favour of JWT for a variety of reasons. I also defended the JWT strategy for a long time, beacuse I had many first-hand converstaions of "I need to sell this to management, please tell me it's a JWT and not some inscrutable binary format".

A few things have changed:

  • While Embedded IPLD hasn't merged yet, we know exactly how it works
    • Mostly quibbling about format right now, but I hope to close it soon
    • This makes IPLD it WAY friendlier for those not deeply familiar with IPLD
  • Batch signatures needed some upgrading
    • Noteworthy that W3C folks are working on this, but no major JWT implementation has this yet
  • We understand now Invocations can take over some of the heavy lifting that Delegations were doing previously

I believe that this proposal makes writing both UCAN Delegations and libraries much easier and more comprehensible. It also lowers our maintenance burden between multiple formats.

Changelog

Metadata

  • Bump version to 1.0.0-rc.1

Structure

  • Remove revocation section
  • Point at ucan-wg/revocation
  • Point at ucan-wg/invocation
  • Remove ucan/* (moving to ucan-wg/ucan-uri)
  • Point at ucan-wg/ucan-uri
  • Move prf field to ucan-wg/invocation + add to top-level ucan-wg/spec
  • Use DNF + compat form

Time

  • Restrict time bounds to 53-bits (because JS)
  • Explicit time bounds checking logic
  • Make exp nullable

Prose

  • Remove signer role (confusing to some people)
  • Remove confusing analogy about lanyards in section 1.2
  • Change term "discharge" to something clearer
  • Clarify outer/inner terminology (or better: change it)
  • Clarify in section 6 [now] 5, that you only invalidate single capabilities; not everything
  • Remove old session ID recommendation now that we have content addressing
  • Move FAQ to the high level spec
  • Rename "top" to "wildcard" because it's a more familiar term for normies
  • Remove bottom case entirely in new syntax

@expede
Copy link
Member Author

expede commented Aug 6, 2024

Thanks for the feedback @smoyer64 ! Responses inline:

Quickly jumping to the end first

  1. It's entirely possible that I'm reading the specification incorrectly -
    if so, please don't take the above input personally as I'm simply
    trying to adopt the awesome ideas presented in the UCAN specifications.

Thank you for the deep engagement with the material! I think other than the question about how strictly we adhere to jq, I'd like to implement the rest of your comments! 🙏

[...]

  1. Copy and slightly modify the policy ABNF from the delegation
    specification. I've submitted a PR
    with just enough changes to make the ABNF valid.

PR merged! Thanks :)

[...]

Result

  1. There are many inputs which are valid selectors per the ABNF but
    that are invalid or non-working filters in jq.

Yes, as Irakli responded earlier, we do have some divergences from jq. We think that they're well motivated ( #5 ) , but they have already bitten both you and me, so perhaps we should rethink that.

Recommendations

[...]

Therefore, I'd recommend that the word "MAY" in the above quoted text
be replaced with the word "MUST".

Agreed ✅ Will fix (DONE)

  1. The delegation specification includes .to[99]? as an example of
    using a try operator. The results of a jq command with an
    unknown object or array index is null, so stating that the ?
    operator turns what would otherwise be an error into a null
    isn't demonstrated by this example. This behavior can be demonstrated
    by the following shell commands:

This is one of the examples that we chose to diverge on. In short: jq is (largely) a parser that processes streams, and UCAN policies are a policy language that operate on static data. The security implications of permissive behaviour may be significant, and we can always get the null behaviour by using a try (?).

There's also a few cases of expressivity, e.g. distinguishing between a null value and a missing key.

Note that the examples in the jq manual don't show the utility of the Optional operator ? either.

Interesting! Can you expand on this?

  1. The delegation specification makes the following statements:

UCAN Delegation uses predicate logic statements extended with jq-style selectors as a policy language.

and

Selector syntax is closely based on jq's "filters".

After reading through this section, it seems like we should be able to say that the selector grammar is a proper subset of the jq filter grammar.

🤔 Perhaps. jq operates on streams, and is more permissive in how it treats things like assertion violation (e.g. when a field is not present), which may not be appropriate for an access control context.

I have mixed feelings about the name selector as we're actually filtering the invocation's args and then applying the predicate.

Could you expand? I'm not certain that ours is filtering in the same sense as jq filters on streams (or in the sense that you filter on arrays or collections). We have things lilke nested quantification to handle these cases instead. Arguably ours are "lenses" or "coarse parsers", but in these relatively simple cases that's often used interchangeably with "selector". I'm happy to call them something like IPFS Lenses if that's helpful, but it may get confused with the more common bidirectional lenses or profunctor lenses (with setters).

If Optional isn't the correct word, perhaps we can pick another without "baggage"?

Sure, let's switch it to "Optional" ✅ (DONE)

  1. The delegation specification states that the ABNF included in the
    Policy language
    section is the "formal syntax". In my opinion, this ABNF should be
    suitable for use when generating a parser in your language of choice.

If helpful, we could also switch to IPLD Schema if that's better for this purpose than ANBF. I was trying to avoid IPFS Schema since it has its own set of design issues.

In its current state, the selector syntax is too permissive and it
should be strengthened to limit selectors to those that are intended
throughout the rest of the document. This is obviously impacted by
the decisions about recommendations #1 and #3 above.

Yes, it does depend on the other design decisions for certain. I'm not as confident in the strategy of generating a parser directly since you'll need to reserialise the CBOR to JSON for this to work, right? That could be expensive on every request.

  1. I was going to compare my prototype policy code the the Rust
    implementation that was linked at the end of Brooklyn's IPFS Camp
    presentation (https://github.com/expede/rs-ucan.) Can someone
    provide the link to this code?

I'll flip the switch on the Rust implementation momentarily (DONE). It needs a few tweaks to come in line with your and Irakli's comments, but a version of it was being used right at the end of Fission.

Is it suitable to think of the Rust implementation as the RI?

We have two parallel implementations: JS and Rust.

@expede
Copy link
Member Author

expede commented Aug 6, 2024

Any jq filter that doesn't begin with a . is effectively a constant and applying that filter to ANY input value will result in the filter constant being sent to the output.

Indeed, that's how the Rust code works.

#[test_log::test]
fn test_fail_missing_leading_dot() -> TestResult {
    let got = Selector::from_str("[22]");
    assert!(got.is_err());
    Ok(())
}

Will update the spec ✅

@expede
Copy link
Member Author

expede commented Aug 6, 2024

@smoyer64 I pushed the branch directly to the ucan-wg fork, which is world-visible: https://github.com/ucan-wg/rs-ucan/tree/v1.0-rc.1 Wasm doesn't work yet, and some tweaks are needed to bring up to the above changes in the spec

@smoyer64
Copy link

I'm happy to concede that the deviations from strict compatibility with jq perhaps led the discussion in the wrong direction ... I hadn't seen #5 but that's exceedingly helpful in understanding the spec. There are examples of selectors that both pass the ABNF and that are invalid. One example that I can remember without consulting my notes is that, while the text states that .. is not allowed, code generated from the ABNF doesn't reject it. I'm still happy to do some fuzzing while considering #5.

Interesting! Can you expand on this?

This example from the jq manual works with and without the optional operator: https://jqplay.org/?q=.foo%3F&j=%7B%22notfoo%22%3A+true%2C+%22alsonotfoo%22%3A+false%7D

I'm not as confident in the strategy of generating a parser directly since you'll need to reserialise the CBOR to JSON for this to work.

Yes and I'm happy to see that @alanshaw has written a tokenizer/parser/matcher that works on the IPLD nodes.

I'll flip the switch on the Rust implementation momentarily (DONE).

Awesome! This will make it easier to decipher the spec - should I feed anything I think are ambiguities back into PRs?

README.md Outdated
| `iss` | `DID` | Yes | Issuer DID (sender) |
| `aud` | `DID` | Yes | Audience DID (receiver) |
| `sub` | `DID \| null` | Yes | Principal that the chain is about (the [Subject]) |
| `cmd` | `String` | Yes | The [Command] to eventually invoke |

Choose a reason for hiding this comment

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

Any chance to make that cmd a [String], so that multiple capability can be delegated with the same payload?

Unless I missed something, if a (larger) service wants to delegate a set of capabilities to a user, it might not be possible to express that with a nice wildcard. Instead, you'd have a list of command: /foo/*, /bar/baz, /bar/boz, ....

As I understand, with cmd being a String (and not allowing comma-separated value I assume), this has the following consequences:

  • a separate payload need to be issued and sign for each of the capabilities
  • the receiver (audience) need to handle multiple delegation: much more complex UX/DX and logic necessary on the client side
  • much larger token overall, especially as the signatures are duplicated.

Choose a reason for hiding this comment

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

Or maybe better, have it be a [Capability], so that one policy remains attached to one command neatly.

Copy link
Member Author

Choose a reason for hiding this comment

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

We did essentially [Capability] previously, and it had some ugly tradeoffs in practice that we heard complaints about for years until we removed. Some people liked it, but it made the token more complex, and saved way less space than you'd think. It also forces you to pass around (and leak!) a bunch of extra capabilities that don't apply to, it's way more complex to do revocations that target only one capability, etc.

The one thing that I do want to recover from [Capability] is fewer signatures (as always: crypto can be expensive). The solution there is to adopt batch signatures, which are flagged for a future version of Varsig. Basically build a Merkle tree of things to sign, sign the root, and include the Merkle proof to your token in the signature field.


Policies are structured as trees. With the exception of subtrees under `any`, `or`, and `not`, every leaf MUST evaluate to `true`.

A Policy is an array of statements. Every statement MUST take the form `[operator, selector, argument]` except for negation which MUST take the form `["not", statement]`.
Copy link

@MichaelMure MichaelMure Sep 1, 2024

Choose a reason for hiding this comment

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

Unless I'm missing something, and and or also have that 2-tuple form.

Choose a reason for hiding this comment

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

Hmm, those two are actually N-tuple, with N >= 2

Choose a reason for hiding this comment

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

No, they are 2-tuple, but the ABNF grammar is wrong


```js
// Data
{ name: "Katie", age: 35, nationalities: ["Canadian", "South African"] }

Choose a reason for hiding this comment

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

Suggested change
{ name: "Katie", age: 35, nationalities: ["Canadian", "South African"] }
{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }

// Data
{ name: "Katie", age: 35, nationalities: ["Canadian", "South African"] }

["and", []]

Choose a reason for hiding this comment

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

If I'm not mistaken, all those policy example should be wrapped with [ <example goes here> ], otherwise there are not legal as per the syntax

Choose a reason for hiding this comment

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

I suppose those are statements, not policies, but that's a bit confusing.

Comment on lines +403 to +407
["not",
["and", [
["==", ".name", "Katie"],
["==", ".nationalities", ["American"]] // ⬅️ false
]]

Choose a reason for hiding this comment

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

Suggested change
["not",
["and", [
["==", ".name", "Katie"],
["==", ".nationalities", ["American"]] // ⬅️ false
]]
["not",
["and", [
["==", ".name", "Katie"],
["==", ".nationalities", ["American"]] // ⬅️ false
]]
]

|----------|---------------------|---------------------------------------|
| `like` | `Selector, Pattern` | `["like", ".email", "*@example.com"]` |

Glob patterns MUST only include one specicial character: `*` ("wildcard"). There is no single character matcher. As many `*`s as desired MAY be used. Non-wildcard `*`-literals MUST be escaped (`"\*"`). Attempting to match on a non-string MUST return false and MUST NOT throw an exception.

Choose a reason for hiding this comment

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

is zero * allowed? Does that means exactly one * or at most one *?

Choose a reason for hiding this comment

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

+1

Choose a reason for hiding this comment

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

For the next person, I read that wrong:

Glob patterns MUST only include one special character

means: the grammar only has one special character (*), but that character is allowed multiple times in the same pattern: foo*bar*baz is valid.

@Peeja
Copy link

Peeja commented Sep 23, 2024

Something that's not clear to me (but maybe I missed it): is it valid to overdelegate abilities? Can A delegate /car/drive to B, and B then delegate /car to C, thereby effectively delegating only /car/drive, but also any other /car abilities B received later?

That is, in essence, does / behave like the Powerline, or do you have to actually prove you have all of / yourself to delegate it?

Operating like the Powerline would be a lot more powerful, but also puts a bit more burden on the implementer to calculate the correct effective capability.

@expede
Copy link
Member Author

expede commented Sep 23, 2024

@Peeja

but also any other /car abilities B received later?

This is a late binding decision — it happens at invocation time, not delegation time. You provide the path of delegations to the invocation, and it takes the minimal subset of all delegations by applying all policies to the invocation (you need to pass all policies in your stated proof chain)

Another way of thinking about this is that if you think of delegations as edges in an authority graph, where authority "flows" between agents. You can arrive at that edge from an of a number of earlier paths; and you have to state which one it is when invoking so that it's unambiguous what your intent is and how to prove that you can do that. If your upstream path is more powerful, then the less powerful edge attenuates it.

As a best practice, you SHOULD restate your intended authority level at each delegation. This is a tradeoff in the design that makes it significantly easier to recover if an upstream delegation (which may be n hops away from you) is revoked or expires. In that scenario, you're able to replace the broken upstream link as long as you have a valid path between whoever delegates to you and the subject.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.