-
Notifications
You must be signed in to change notification settings - Fork 39
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
base: master
Are you sure you want to change the base?
RFC: Identity Proofs #525
Conversation
6213f30
to
c28125a
Compare
There was a problem hiding this 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
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
docs/rfc/identity_proofs.md
Outdated
## 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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 🤔
docs/rfc/identity_proofs.md
Outdated
Seconds relative to `created`, after which the claim should no longer be | ||
considered. | ||
|
||
* `proof` (optional) |
There was a problem hiding this comment.
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? 🙃
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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 :)
docs/rfc/identity_proofs.md
Outdated
> might be undesirable for certain services. In this case, service-specific | ||
> post-validation is required. | ||
|
||
## Verification |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
b27d30b
to
c0e95d6
Compare
It is actually somewhat hilarious how inefficient this is in theory. Either I'm missing something, or life was a little easier on MySQL. |
There was a problem hiding this 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
"SERVICE": { | ||
"account": "STRING" | ||
"expiration": { | ||
"created": INTEGER, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
docs/rfc/identity_proofs.md
Outdated
"created": INTEGER, | ||
"expires": INTEGER | ||
}, | ||
"proof": "URL" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
is, future version MUST treat the absence of a disambiguating value as denoting | ||
"git". | ||
|
||
The values `root`, `revision`, and `public-key` are specified in |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 (aradicle-link
URN israd:git:<root>
). If all content was already available locally, we could find therevision
tree, inspect the document to find the parent, retrieve that, etc, until we reach theroot
-- and then start over and find a commit which includesrevision
, 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.
docs/rfc/identity_proofs.md
Outdated
|
||
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
80d9f85
to
82e7929
Compare
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 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 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 |
82e7929
to
fb3102b
Compare
fb3102b
to
ecb56f6
Compare
Signed-off-by: Kim Altintop <[email protected]>
ecb56f6
to
45d7336
Compare
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why after the fact?
There was a problem hiding this comment.
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 🤷
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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
- Post signature over
R || nonce
, storeURL
- Create
R+1
containingnonce
,SERVICE: {..., proof: Some(URL) }
- Sign
R+1
usingdelegations
to "finalise" it
Verify
- Check signature, abort if invalid
- Find
R+1
, check that- is valid
R
is parentR
has key in delegationsR+1
has claimR+1
claim is not expired
- Check
HEAD
still has the claim
vs.
Without Nonce
- Create
R
containingSERVICE: { ..., proof: None }
- Post signature over
R
, storeURL
- Create
R+1
withSERVICE: { ..., proof: Some(URL) }
- Sign
R
andR+1
usingdelegations
to finalise
Verify
- Check signature, abort if invalid
- Find
R
, check that- is valid
- has key in delegations
- has claim
- claim is not expired
- 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:
- there is only one key (the address)
- they basically store all claims in a central database (verified.json)
- 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 💭
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hahaha yes. mastodon, ssb?
Rendered