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

RFC: Identity Proofs #525

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

RFC: Identity Proofs #525

wants to merge 1 commit into from

Conversation

kim
Copy link
Contributor

@kim kim commented Feb 23, 2021

@kim kim added protocol Something concerning the core protocol rfc Request For Comments labels Feb 23, 2021
@kim kim requested a review from a team as a code owner February 23, 2021 17:05
NunoAlexandre
NunoAlexandre previously approved these changes Feb 24, 2021
Copy link
Contributor

@NunoAlexandre NunoAlexandre left a comment

Choose a reason for hiding this comment

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

🗻

docs/rfc/identity_proofs.md Outdated Show resolved Hide resolved
docs/rfc/identity_proofs.md Outdated Show resolved Hide resolved
Comment on lines 170 to 195
In order to prove that the(ir own) server is not lying by omission, Keybase
[anchors a merkle root][keybase-stellar] on a blockchain, which includes all
sigchains registered in the Keybase directory. Because `radicle-link` does not
have such a central directory, this approach could only be applied to a partial
view of the network.
Copy link
Contributor

Choose a reason for hiding this comment

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

Something for us to consider?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ceramic does something like this, where people can batch their transactions into a larger one (and share tx fees? Idk). Not sure where you get the merkle proof from.

Copy link
Contributor

@FintanH FintanH left a comment

Choose a reason for hiding this comment

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

This looks good to me. I'd just like to clarify some of my understanding -- which will likely lead to more questions -- before approving :)

I'd also like to point out, to anybody who's looking to develop on top of this on radicle-upstream, that we would need to have the identity document update and signing flows implemented. This is because we'll have the initial Person document with just the name and then the document would be updated with the claims, which in turn will need signing for others in the network to at least trust that.

## Claims

Claims can only be made by identities of kind `Person`, and claim a single
external account identifier. They are introduced by defining a new payload type,
Copy link
Contributor

Choose a reason for hiding this comment

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

For clarification, will this payload be an extension of Person or will it live as its own blob? If it's the latter then do we need to specify its git reference here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually a good point. I don't think there is a way this would be beneficial, though, unless we would also include the HASH(claim) in the tuple, or would be able to compute HASH(claim) (which would preclude time-based expiration). In this case, we could avoid having to read any data but the commit and immediate tree, and just compare hashes. There's no way around traversing the history backwards, though, because we need to check that public-key was in the delegations 🤔

Seconds relative to `created`, after which the claim should no longer be
considered.

* `proof` (optional)
Copy link
Contributor

Choose a reason for hiding this comment

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

Something I'm not clear on is whether this a proof from the service or is it the result of the ## Proof Generation, and then stored at the service URL? Or something entirely different? 🙃

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You know I like to pronounce the types in my head, so the name of an expression does not need to include it :) "proof URL"

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow 😅 But I suppose you've answered it here :)

> might be undesirable for certain services. In this case, service-specific
> post-validation is required.

## Verification
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this section also have the step of verifying the claim's proof, if present?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The proof URL is just a hint where to get the payload from. Say we'd support Twitter, then we can infer https://twitter.com/iamdevloper/status from what we have, but every client would need to scrape the timeline until they find the proof in https://twitter.com/iamdevloper/status/1361327114260271109. Same thing for blockchain proofs, although you could substitute the web3-provider.

Copy link
Contributor

Choose a reason for hiding this comment

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

But what I'm not understanding is where this comes into play as part of the attestation. Is it up to the malkovich to check this or up to the client program? Is that why there's no recommendation on what to do with it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There isn't really anything to check. Say all you have is the identity doc. Now you invoke rad sozial überprüfung, and that reads the payload, sees aha deep-link into twitter, let's go and get the 4-tuple from there. If you already have the 4-tuple, you can omit this step. If you don't have a 4-tuple, nor a deeplink, that nifty program needs to traverse the twitter timeline until it finds a tweet which looks like it could contain a 4-tuple.

The drawback is of course that adding this URL creates a new revision.

@kim kim force-pushed the kim/rfc-identity-proofs branch 2 times, most recently from b27d30b to c0e95d6 Compare February 26, 2021 12:05
@kim
Copy link
Contributor Author

kim commented Feb 26, 2021

It is actually somewhat hilarious how inefficient this is in theory. Either I'm missing something, or life was a little easier on MySQL.

Copy link
Contributor

@CodeSandwich CodeSandwich left a comment

Choose a reason for hiding this comment

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

Great document, kudos for putting huge effort into codifying all these details!

docs/rfc/identity_proofs.md Outdated Show resolved Hide resolved
"SERVICE": {
"account": "STRING"
"expiration": {
"created": INTEGER,
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is no expiration date, don't we want to know when the attestation has been created? Should created be optional in the first place?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure... a timestamp is always just a local timestamp, so unless some additional semantics are ascribed to it, there isn't so much one can do with it. Besides, there is also a redundancy: since we know the PeerId (== public-key), we can reify the commit which introduced the revision, which has a timestamp.

Copy link
Contributor

Choose a reason for hiding this comment

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

During our last chat you've explained, that the expiration date of a claim on link side is used for detection of abandoned identities and hidden updates. Do we need a separate expiration date for each service then? Could it be moved to the top level of the JSON and updated whenever a new version of identity is being prepared?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed.

If we treat social proofs different from radicleth attestations, we’ll still end up with two timestamps, though. Or we have the same namespace, but different variants (with very different semantics). Hm. JSON is hard.

Copy link
Contributor

@CodeSandwich CodeSandwich Mar 4, 2021

Choose a reason for hiding this comment

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

Yeah, we'd end up with two timestamp levels:

  • one for the whole document, mandatory and revoking everything if expired, even claims with no expiration date
  • one for the service, optional, may revoke only the said service if expired

Otherwise we'd need some logic deciding if the service's validity should be prolonged on each identity update, because in reality some claims never expire, like Ethereum.

"created": INTEGER,
"expires": INTEGER
},
"proof": "URL"
Copy link
Contributor

Choose a reason for hiding this comment

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

Will every proof scheme ever be sufficed with a single URL storage? Wouldn't it be better to make the proof type arbitrary, so a complex structure or an array can be stored natively? Its exact definition could be tied to the SERVICE value, so there's elasticity without ambiguity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that'd probably be a good idea. Although we can define the URL type as an enum in the host language, so as to make it extensible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We may also opt to just omit this -- it is probably a bit weird to manage, because you can only add the URL after the proof was submitted, creating a new revision. Tools may also want to check the plausibility of the combo SERVICE + URL. So perhaps just leaving it to the tool is also an option.

docs/rfc/identity_proofs.md Outdated Show resolved Hide resolved
is, future version MUST treat the absence of a disambiguating value as denoting
"git".

The values `root`, `revision`, and `public-key` are specified in
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to confirm: a revision is a hash of the file content, it does NOT matter which revision is the parent? Two coincidentally identical claim files of two different users can have the same revision despite having different roots, parent commits, etc.? Otherwise it wouldn't make much sense to include the root, would it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A revision is the hash of the tree object, where the document blob contains the parent tree hash. The commit hash is not relevant for verification, yet the commit message contains the signatures.

This means:

  • two entirely disjoint commit histories can describe the same document history
  • it is not possible to produce the same revision in a disjoint document history, unless a hash collision is found, such that a quorum of the key delegations will be controlled by an attacker. Of course, if one owns a quorum of the keys, a fork can be produced.
  • without the root, we wouldn't be able to address any particular commit history (a radicle-link URN is rad:git:<root>). If all content was already available locally, we could find the revision tree, inspect the document to find the parent, retrieve that, etc, until we reach the root -- and then start over and find a commit which includes revision, in order to verify the signature chain. That's not very efficient, but doable. The problem is when we don't have the content -- for example when we see this proof being posted on <social network of choice>, and now want to know what it is referring to.


A claim can be revoked by creating a new identity revision which simply does not
contain the claim payload. Likewise, a later claim describing the same `SERVICE`
invalidates an earlier one.
Copy link
Contributor

Choose a reason for hiding this comment

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

This will put a hard cap of having at most one connected account per service. Do we want this? Would it make sense for me to have 2 Twitter accounts claimed, e.g. a private one and a work related one or an old one and a new one? It could also simplify refreshing of the claims, in the current setup you need to 1. create a new revision 2. post a tweet/whatever with a revision signature 3. push the revision to the network. Otherwise you're risking temporary lack of validity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think temporary lack of validity is always the case -- you need to fulfill the signing obligation.

Whether or not we want to allow multiple accounts per service is an open question I'd say -- we can allow the payload type to be an array. I feel like the semantics of this is a bit wonky, though: if, for whatever reason, I choose to be one person on Radicle, but two persons on Twitter, then probably I'd like to include some indication of what's different (like "official", "shitposting" or something). But then, we'd also need to include this bit in the proof, don't we? Otherwise we'd need to try all matching.

Copy link
Contributor

@CodeSandwich CodeSandwich Mar 3, 2021

Choose a reason for hiding this comment

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

I think temporary lack of validity is always the case -- you need to fulfill the signing obligation.

I think that we can solve this problem easily if we allow the opposite claims to overlap in validity time ranges only partially.

Ethereum could be a good example. Let's say that today is day 80 (of whatever). I find a claim on Ethereum ranging from day 50 to 200, looks good. I go to link and find a counterpart claim from day 50 to 100, still good, now I can consider this attestation valid until day 100. A few days pass and I get an update from link: it claims the same attestation from days 50 to 200. This is great, now I know that the attestation is valid until day 200.

This schema allows seamless updates of attestations despite lack of synchronization and guarantees that on both sides we only need to know the single latest claim to have the full knowledge.

The only downside is that the validity ranges now must be solid: they have only start, end and no gaps inside. This is probably fine, I can't think of any use case where I would need to express something like "this Twitter account will be mine until April 1st, then it'll be somebody else's for a week and after that again mine for a year".

EDIT:

We're making an assumption here that there's no need to put a specific link revision in the external service claim. But it looks like everybody should always be looking only at the tip of link identity chain, so this assumption seems correct. On the other hand link will be usually storing a direct link to the counter-claim in the external service. This means that an attestation or a prolongation must be first published in an external service and only then on link.

Copy link
Contributor

Choose a reason for hiding this comment

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

I choose to be one person on Radicle, but two persons on Twitter, then probably I'd like to include some indication of what's different

If my previous comment is correct, we are guaranteed to only need 1 entry in the identity JSON per account. We could bypass the multiple accounts problem by duplicating the services, e.g. "twitter", "twitter_2" and "twitter_3". This will make the number of accounts strictly limited, but it'll be trivial to build a UI to display them and keep track of which account is which. Another advantage is that for now we can stick with having only "twitter" and add support for "twitter_2" when we fell like doing so. The JSON will stay unambiguous to interpret and easy to navigate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure I'm following. You must create a new revision always, but it may remain invalid until it was signed by the delegate quorum. There is also no guarantee that the tip will be seen by others -- that's why the revision is important: if you come across a proof, but are not able to retrieve the revision it is talking about, there is something funky going on.

It is impossible to talk about time without a reference. Arguably, when you want to have expiration, wall-clock time is probably good enough -- after a week, most people's computers will agree that created + expires is in the past. You can't safely base something like handover on this, though: if I publish such a proof on my homepage, but later sell the domain, the buyer can keep the proof up, but it would be compromised. If your time unit is block height, that problem goes away.

Copy link
Contributor

Choose a reason for hiding this comment

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

You must create a new revision always, but it may remain invalid until it was signed by the delegate quorum.

I really don't think that the quorum signing process matters here. Let's assume that all revisions not quorum-signed are invalid and thus will be ignored by everyone except the quorum members.

the revision is important

It seems that you envision a system where a link identity revision contains a link to proof on an external service (transaction hash) and that proof (transaction content) contains a reference to said identity revision leading to a chicken and egg problem.

We've discussed that yesterday and I think that we came to a conclusion, that there are means of deciding, which link revision is the tip (from best effort perspective, there may always be a better one floating on the network). This would break the cycle and allow looser and simpler relation between claims.

if you come across a proof, but are not able to retrieve the revision it is talking about, there is something funky going on

Does any revision except the tip matter? Even if a 3rd party claim contains a proper revision, it doesn't matter if the tip doesn't recognize the said 3rd party.

wall-clock time is probably good enough

Absolutely. You can't verify any security without a decent clock synchronization. If it's off by a few days and the user uses it to decide if they trust something on the internet, it's their own fault.

if I publish such a proof on my homepage, but later sell the domain, the buyer can keep the proof up, but it would be compromised

I think that there is a missing part in this example: after selling a domain I buy a new one and hand over the attestation to it, right? In that case there's nothing we can do about it, if after handing over there are still people who are looking at a claim on a compromised 3rd party and hold an old link state, they will always trust the new owner. It doesn't matter how we measure time.

If your time unit is block height, that problem goes away.

How? BTW every block has a timestamp with precision of a few seconds, but it probably doesn't matter if the claim on Ethereum contains the validity time range submitted as a parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don’t understand what you are debating, so I think we may just stop here.

What we’ve discussed works pretty much exactly like the ACME protocol (RFC 8555), with comparable guarantees. This is enough for the purpose, but not enough for other security aspects of a radicle-link identity. Therefore, this RFC stands on its own.

You are welcome to suggest improvements to this proposal, or ask to clarify aspects of it, but I would like to ask you to do so in a more systematic way — just stating that this or that doesn’t matter, or that something clearly works without explaining your axioms makes it really hard to figure out how to respond.

So, if you forget about blockchains — I have removed everything except for merkle root anchoring — do the questions you raised in your last comment persist?

Copy link
Contributor

@CodeSandwich CodeSandwich Mar 3, 2021

Choose a reason for hiding this comment

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

I'm trying to figure out how is the handover supposed to work? I haven't found it in the RFC, so I've proposed a solution, but you seem to have a different solution already in your mind.

I've proposed a synchronous meeting, I think that it'll be way more productive to just have a chat, I'm missing some crucial parts from the picture.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aha, ok cool. I brought up this example trying to guess whether you are trying to find some flaw in this RFC, or considering if there needs to be anything else for the planned contract.

Maybe let’s look at the contract first: we said it’s just register(URN), unregister(URN), and we store the corresponding tx hashes in link. Let’s say the link revision which contains the unregister- (or simply removal of the register-, I’m not sure) tx gets lost. When I follow the register tx, I will find that it’s valid, but without an extra step I don’t know if later it was unregistered (and maybe someone else registered). However, we know that it is virtually impossible to hide the chain prefix, so the window in which someone else who registered id X can pretend to own it is a couple of confirmations.

So I would say this is fairly safe without auto-expiration, assuming that it’s not hard to check if the current address holding the link ID is the same as the one in the tx we know.

Let’s take away the contract: I post a proof somewhere where there is a risk of takeover (or just handover), like a DNS name. Let’s say I’m using LetsEncrypt. The is no way to perform the online check we just introduced, because there is no information about who owns the domain, or if that changed (that is, we cannot ask “who owned the domain at time X”). Because we could also be missing the revocation revision from link, it would be wise to specify an expiration time in the claim (preferably set to some time before the domain expires).

Finally, we want to defend against account compromise (whether that’s social media or Ethereum). The only option we have is to revoke on the link side, but we cannot be sure that this is received. That’s where anchoring (or: checkpointing) the latest revision somewhere else comes in: if I see a revision there which I don’t have, I should reject any proofs until I have it. Without this, an expiration time will also do, but with larger uncertainty.

Does this make sense?

docs/rfc/identity_proofs.md Outdated Show resolved Hide resolved
docs/rfc/identity_proofs.md Outdated Show resolved Hide resolved
@kim kim force-pushed the kim/rfc-identity-proofs branch from 80d9f85 to 82e7929 Compare March 2, 2021 13:28
@kim
Copy link
Contributor Author

kim commented Mar 2, 2021

It has been concluded that for the use-case of radicle-dev/radicle-upstream#965, a simple one-way attestation in the other direction suffices, and can be expressed with a much more compact proof:

Consider a contract which allows to claim (and unclaim) some Radicle ID (URN). No two claims for the same ID at the same time are permitted. After a successful claim, the transaction ID is included in the claimed URN's identity history.

If either the Ethereum address or the radicle-link identity is trusted, only the inclusion of the transaction in a block on the main chain needs to be verified. Navigation from address to URN entails inspecting the transaction payload. Navigating from radicle-link identity to address entails inspecting the transaction sender.

Note that no elevation of trust results from this, nor assurance of an association with a "real" person. Neither does it improve the fork resiliency of the radicle-link identity, nor defend against hiding of updates. I therefore think that this RFC has a justification in its own right.

Since the identity document payload can be freely extended by library users, and both the payload and the verification semantics are entirely defined by the external system, I don't think a subsequent RFC is required. I also don't think any changes to radicle-link are required, except perhaps for convenience or code organisation reasons. #464 would not hurt, though.

@kim kim force-pushed the kim/rfc-identity-proofs branch from 82e7929 to fb3102b Compare March 3, 2021 11:21
@kim kim changed the base branch from master to kim/mutch March 17, 2021 08:14
Signed-off-by: Kim Altintop <[email protected]>
@kim kim changed the base branch from kim/mutch to master March 17, 2021 08:20

A URL to assist verification tooling in retrieving the proof from the external
system. This is mainly a convenience, and obviously requires creation of a new
revision after the fact.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why after the fact?

Copy link
Contributor

Choose a reason for hiding this comment

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

This almost seems like a non-feature, since it requires a new identity revision, I'm not sure if clients will deem it worthwhile to support 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since there’s no central database, this would mean that you can only navigate from the external system to link, but not the other way round — which would be a bit lame I think.

Perhaps the proof signature could be over the previous revision + a random nonce 🤔 If space is constrained, maybe the preimage could be omitted entirely (except the root hash)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, true.

Signing the previous revision + account + nonce is not a bad idea 🤔 but seems like it may complicate implementations slightly due to the verification requiring two revisions as input instead of one?

I'm wondering in what scenarios the proof URL will be used? Because if there are legit use-cases for it, then it probably shouldn't be optional, and we should go with your idea.

So the typical use-case is that of sybil.org for example, where you can verify the statement: "I am @cloudhead and address 0xabcdef123 belongs to me". They must store the tweet id somewhere, since that's how they verify the signature. So yeah it seems kind of important 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the verification requiring two revisions as input instead of one?

Myeah, so:

With Nonce

  1. Post signature over R || nonce, store URL
  2. Create R+1 containing nonce, SERVICE: {..., proof: Some(URL) }
  3. Sign R+1 using delegations to "finalise" it

Verify

  1. Check signature, abort if invalid
  2. Find R+1, check that
    1. is valid
    2. R is parent
    3. R has key in delegations
    4. R+1 has claim
    5. R+1 claim is not expired
  3. Check HEAD still has the claim

vs.

Without Nonce

  1. Create R containing SERVICE: { ..., proof: None }
  2. Post signature over R, store URL
  3. Create R+1 with SERVICE: { ..., proof: Some(URL) }
  4. Sign R and R+1 using delegations to finalise

Verify

  1. Check signature, abort if invalid
  2. Find R, check that
    1. is valid
    2. has key in delegations
    3. has claim
    4. claim is not expired
  3. Check HEAD still has the claim

So the issue with the nonce is that you need to publish the nonce, or that you can only verify the signature if you know R+1's payload. The issue without the nonce is that you need to sign two successive revisions for finalisation.

I'm wondering in what scenarios the proof URL will be used?

It's when you start from link -- you'll want an O(1) method of getting at the proof payload.

then it probably shouldn't be optional

Without a nonce, it's a chicken-and-egg problem :)

sybil.org for example

sybil.org is simpler, because:

  1. there is only one key (the address)
  2. they basically store all claims in a central database (verified.json)
  3. there is nothing to verify about the database itself (because 1.)

I mean, the proposed proof URL doesn't technically need to be signed, either. But if it's not in the identity document, then where else could we put it? Annotate R with a git-notes note?

[identities][ids], and it is RECOMMENDED to follow the serialisation formats
devised there. `signature` is the Ed25519 signature over `revision`, in much the
same way as the actual revision is signed. All values can thus be obtained by
inspecting the identity storage.
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps the answer to the above question is missing here: the exact revision to be signed is the one which contains the claim, correct? Perhaps worth mentioning if that is the case.

inspecting the identity storage.

It is beyond the scope of this document to devise the exact external format to
serialise the tuple into, as this is expected to vary from service to service.
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be a good candidate for a separate RFC with some proposed plain-text formats 💭

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wondering if we should do something fun instead of the usual ones. Matrix? lobste.rs?

Copy link
Contributor

Choose a reason for hiding this comment

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

hahaha yes. mastodon, ssb?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
protocol Something concerning the core protocol rfc Request For Comments
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants