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: Add capability spec #6

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Changes from all 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
98 changes: 98 additions & 0 deletions formats/capabilities/SPEC_1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Capability format

<dl>
<dt>Namespace</dt><dd>`capabilities`</dd>
<dt>Version</dt><dd>1.0</dd>
</dl>

This is an application format for continuous sharing and minting of capability information

## Subspace

### Communal Namespaces

Capabilities are issued by an entity, and are therefore published under the issuing entities subspace.

### Owned Namespaces

The use of an owned Namespace depends on the owner - so they should set the rules on which subspace to use.
In practice - if you only have the ability to write in one subspace, you'd presumably publish the capabilities there.

## Public capabilities

The knowledge that you've issued capabilities to the receiver will be public for all. The contents, however, will be encrypted, so the capabilities / subspaces / paths they've been granted access to will not be public.

`/capabilities/1.0/public/{receiver-pubkey}/{id}`

## Private Capabilities

You may want to publish these entries to all (e.g. You'd like help from everyone in the network to distribute, because you don't have a direct peer with the recipient),
while not allowing people to know who you're actually granting these to.

More formally, given the issuer, a path a capability is published under, and a proposed receiver, it should not be possible for anyone else to know if that is the correct receiver.

For such cases instead of publishing these under the pubkey of the receiving entity, they are instead published under `encrypt(receiver, concat(receiver-pubkey, nonce))`. The receiver must attempt to decrypt all paths until they find their one. The issuer should save and use the same nonce in future so this only needs to happen once.

Choose a reason for hiding this comment

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

Counterproposal: publish under scalarmult(my_sk, their_pk). They can compute scalarmult(their_sk, mypk) (which yields the same value) to look up my capabilities for them.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, that's super neat, thanks :D

Copy link
Author

Choose a reason for hiding this comment

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

Question - does this pose an issue if any other app tries to use the public/secret combination as a shared secret, e.g. for encryption?

Just been reading a bunch of random stuff like this

Perhaps this should be HKDF-Expand(scalarmult(my_sk, their_pk), "capability-publication", 32), so we never disclose the original shared secret?

Choose a reason for hiding this comment

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

There are other issues as well: if you and I both write capabilities using the naive implementation, we both use the same curve point, so an observer actually sees that the two of us have exchanged capabilities. So there needs to be some further symmetry breaking. So see this "counterproposal" more as opening up some design space rather than as a serious suggestion.

Copy link

@AljoschaMeyer AljoschaMeyer Jan 22, 2024

Choose a reason for hiding this comment

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

But a definite "yes" to "Does this pose an issue if any other app tries to use the public/secret combination as a shared secret, e.g. for encryption?"

It might suffice to hash the shared secret (using a hash function that isn't used anywhere else - in practice, the same function you'd use elsewhere, but salted by hashing concat("CapabilityBuddies!", <shared secret>)) to work around problems with using the shared secret directly elsewhere? Could also break the symmetry for mutual capability-giving by using different hash functions depending on whether it is the peer with the greater or the lesser publickey issuing the capability.

edit: I basically redescribed hkdf, didn't I. I mean, not exactly, but pretty close in intent...

Take all of this with a large I-am-not-a-cryptographer grain of salt.


`/capabilities/1.0/private/{encrypt(receiver, concat(receiver-pubkey, nonce))}/{id}`

## Capability document IDs

You may want one, or many, depending on the application, if you want to coordinate between multiple client devices etc. These are opaque to the receiver, who should decode all documents under the path prefix.

Choose a reason for hiding this comment

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

The document exposition needs some clarification on whether we assume peers to reuse keypairs on multiple devices, or whether there should be at most one device per keypair. Do we wish to support both?


A suggested scheme for this would be using `/1`, `/2` etc. If you're merely extending the lifetime of an existing cap, then you can write to the same path. It doesn't matter if you have two clients that independently do this. If you want to change the contents, you should increment the number, and tombstone older entries. This behaviour doesn't handle the same user making changes to the same receiver's caps concurrently on two devices they own - only one will win.

Choose a reason for hiding this comment

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

Not quite getting either what exactly you describe here, nor which problem it solves =(
Which might very well be attributed to me being exhausted, but at the very least, it points to an opportunity to clarify the writing a bit.

Copy link
Author

Choose a reason for hiding this comment

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

Fair, will try and elaborate and reword. This relates to our discussion on Discord on multiple client apps using the same keypair to run this extension process, and what happens if they conflict. I think some more explicit examples would likely help. (Hmm, maybe a diagram? Will give it a go)


## Payload format

These should be encrypted under the pubkey of the receiver of the capability. This prevents anyone from knowing which Namespace/Subspace/Path the capability is for

This should be a list of `McCapability` (TODO whatever the standard encoding of this is)

## Use cases

### week-by-week caps

Imagine you want to grant someone access to your social media posts (e.g. a private Twitter account), but you want to be able to revoke this in future. Granting someone an infinite length of time in a capability prevents you from ever being able to revoke this access. But while giving someone a capability document once is feasible, being able to continually do this while you're both offline is a challenge.

The solution is to grant two capabilities, first:

```
CommunalCapability:
access_mode: read
namespace_key: gardening.xxx...
user_key: UserPublicKey
delegations:
- times: forever
path: /capabilities/1.0/private/{their-bit}...
subspace: you
```

This will allow them forever access - but only to capabilities specific to them.

Then you can put the actual week by week capability in there. Each time you make an entry, or regularly on expiry (whichever makes sense for your app), overwrite the entry at that path with a new capability, extending the time. This prevents there being lots of documents, and the first thing they can do on sync is get the latest capability, and then use that to sync all entries in the past. This could likely be done by the same client you're using to make the entries - if you aren't making you entries, the existing caps are sufficient. If you make an entry outside of the current end time, you'll want to update them all. Any peer will get the updated caps in the same sync as the new entries, whether that's a direct peer or via some pub/relay.

To stop someone accessing future Entries, stop updating the capability (and optionally tombstone the old one).

Choose a reason for hiding this comment

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

This section needs a discussion of alternatives. Of the top of my head, you could also post a new capability to /stuff-they-care-about/next-capability at the end of the week.

Also: recommendations for posting overlapping capabilities (chances are you want to issue week-long capabilities every 3.5 days or so).

Also: How about immediately merging caps? I.e. not giving one for week 1 first and week 2 second, but one for week 1 first, and one for (week1and wek2) second, and so on.

Copy link
Author

Choose a reason for hiding this comment

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

This section needs a discussion of alternatives. Of the top of my head, you could also post a new capability to /stuff-they-care-about/next-capability at the end of the week.

Also: recommendations for posting overlapping capabilities (chances are you want to issue week-long capabilities every 3.5 days or so).

🤔 I guess I was purely imagining this from a "giving out read-only caps" perspective. So, if you've not written anything new, no need for new caps, there'd be no entries anyway. But, for allowing writing - yeahh, you want overlap. I'll extend this section and cover some of these use-cases. Thanks :)

Also: How about immediately merging caps? I.e. not giving one for week 1 first and week 2 second, but one for week 1 first, and one for (week1and wek2) second, and so on.

This is what I was imagining. Will make this more explicit


### Granting public access

Choose a reason for hiding this comment

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

Can probably omit the details for this from the design process until everything else has settled.


For this to work, we generally want to get our capabilities into the hands of our desired recipient, but how do we bootstrap this? People can't request an entity without proving a right to receive it first. For online things, your website could mint the desired capabilitiy entry for their key - but we have the same problem as above for week-by-week caps, needing to update it continually. This means you need to know all your followers. For a generally-public account, this may be undesirable.

Grant a capability as follows:

```
CommunalCapability:
access_mode: read
namespace_key: gardening.xxx...
user_key: btnaix46fptu7nj4hwhkusutly6vhjgbigi6ewnapykza66ucf24a
delegations:
- times: forever
path: /capabilities/1.0/public/btnaix46fptu7nj4hwhkusutly6vhjgbigi6ewnapykza66ucf24a/default
subspace: you
```

Publish this on your website, in your bio, etc.

This public key matches the secret key `bhxh7emac7wtpbwqog4k24kfgmvjwoejexb5523g6mg7tzi7uyiwa`

This will allow anyone who knows the secret (i.e. everyone. This could well be encoded into client apps by default) to sync the public-access capability.

You can then put the week-by-week caps into `/capabilities/1.0/public/btnaix46fptu7nj4hwhkusutly6vhjgbigi6ewnapykza66ucf24a/default`, for all the content you want to make public. This will sync to everyone else who's opted-in to syncing the public capability Namespace.